gestura_core_context/
cache.rs

1//! Smart caching for context data
2//!
3//! Provides LRU-style caching with TTL for context data that can be
4//! expensive to compute or fetch.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::RwLock;
9use std::time::{Duration, Instant};
10
11/// Default cache TTL in seconds
12const DEFAULT_TTL_SECS: u64 = 300; // 5 minutes
13/// Maximum cache entries
14const MAX_CACHE_SIZE: usize = 100;
15
16/// A cached entry with TTL
17#[derive(Debug, Clone)]
18pub struct CacheEntry<T> {
19    /// The cached value
20    pub value: T,
21    /// When this entry was created
22    pub created_at: Instant,
23    /// Time-to-live for this entry
24    pub ttl: Duration,
25    /// Number of times this entry was accessed
26    pub access_count: u32,
27    /// Last access time
28    pub last_accessed: Instant,
29}
30
31impl<T: Clone> CacheEntry<T> {
32    /// Create a new cache entry
33    pub fn new(value: T, ttl: Duration) -> Self {
34        let now = Instant::now();
35        Self {
36            value,
37            created_at: now,
38            ttl,
39            access_count: 0,
40            last_accessed: now,
41        }
42    }
43
44    /// Check if this entry is expired
45    pub fn is_expired(&self) -> bool {
46        self.created_at.elapsed() > self.ttl
47    }
48
49    /// Mark as accessed
50    pub fn touch(&mut self) {
51        self.access_count += 1;
52        self.last_accessed = Instant::now();
53    }
54}
55
56/// Smart cache for context data
57pub struct ContextCache<T> {
58    /// Internal storage
59    entries: RwLock<HashMap<String, CacheEntry<T>>>,
60    /// Default TTL for entries
61    default_ttl: Duration,
62    /// Maximum number of entries
63    max_size: usize,
64}
65
66impl<T: Clone> ContextCache<T> {
67    /// Create a new cache with default settings
68    pub fn new() -> Self {
69        Self {
70            entries: RwLock::new(HashMap::new()),
71            default_ttl: Duration::from_secs(DEFAULT_TTL_SECS),
72            max_size: MAX_CACHE_SIZE,
73        }
74    }
75
76    /// Create with custom TTL
77    pub fn with_ttl(ttl_secs: u64) -> Self {
78        Self {
79            entries: RwLock::new(HashMap::new()),
80            default_ttl: Duration::from_secs(ttl_secs),
81            max_size: MAX_CACHE_SIZE,
82        }
83    }
84
85    /// Get a value from the cache
86    pub fn get(&self, key: &str) -> Option<T> {
87        let mut entries = self.entries.write().ok()?;
88        if let Some(entry) = entries.get_mut(key) {
89            if entry.is_expired() {
90                entries.remove(key);
91                return None;
92            }
93            entry.touch();
94            return Some(entry.value.clone());
95        }
96        None
97    }
98
99    /// Insert a value into the cache
100    pub fn insert(&self, key: impl Into<String>, value: T) {
101        self.insert_with_ttl(key, value, self.default_ttl);
102    }
103
104    /// Insert with custom TTL
105    pub fn insert_with_ttl(&self, key: impl Into<String>, value: T, ttl: Duration) {
106        if let Ok(mut entries) = self.entries.write() {
107            // Evict if needed
108            if entries.len() >= self.max_size {
109                self.evict_oldest(&mut entries);
110            }
111            entries.insert(key.into(), CacheEntry::new(value, ttl));
112        }
113    }
114
115    /// Remove a value from the cache
116    pub fn remove(&self, key: &str) -> Option<T> {
117        self.entries
118            .write()
119            .ok()
120            .and_then(|mut e| e.remove(key))
121            .map(|e| e.value)
122    }
123
124    /// Clear all entries
125    pub fn clear(&self) {
126        if let Ok(mut entries) = self.entries.write() {
127            entries.clear();
128        }
129    }
130
131    /// Get the number of entries
132    pub fn len(&self) -> usize {
133        self.entries.read().map(|e| e.len()).unwrap_or(0)
134    }
135
136    /// Check if empty
137    pub fn is_empty(&self) -> bool {
138        self.len() == 0
139    }
140
141    /// Evict expired entries
142    pub fn evict_expired(&self) {
143        if let Ok(mut entries) = self.entries.write() {
144            entries.retain(|_, entry| !entry.is_expired());
145        }
146    }
147
148    /// Evict the oldest entry
149    fn evict_oldest(&self, entries: &mut HashMap<String, CacheEntry<T>>) {
150        // Find the oldest entry by last access time
151        if let Some(oldest_key) = entries
152            .iter()
153            .min_by_key(|(_, e)| e.last_accessed)
154            .map(|(k, _)| k.clone())
155        {
156            entries.remove(&oldest_key);
157        }
158    }
159
160    /// Get cache statistics
161    pub fn stats(&self) -> CacheStats {
162        let entries = self.entries.read().ok();
163        let (size, total_accesses, expired) = entries
164            .map(|e| {
165                let expired = e.values().filter(|v| v.is_expired()).count();
166                let total: u32 = e.values().map(|v| v.access_count).sum();
167                (e.len(), total, expired)
168            })
169            .unwrap_or((0, 0, 0));
170
171        CacheStats {
172            size,
173            max_size: self.max_size,
174            total_accesses,
175            expired_count: expired,
176        }
177    }
178}
179
180impl<T: Clone> Default for ContextCache<T> {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186/// Cache statistics
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct CacheStats {
189    /// Current number of entries
190    pub size: usize,
191    /// Maximum allowed entries
192    pub max_size: usize,
193    /// Total access count across all entries
194    pub total_accesses: u32,
195    /// Number of currently expired entries (pending cleanup)
196    pub expired_count: usize,
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use std::thread;
203    use std::time::Duration;
204
205    #[test]
206    fn test_cache_basic() {
207        let cache: ContextCache<String> = ContextCache::new();
208        cache.insert("key1", "value1".to_string());
209        cache.insert("key2", "value2".to_string());
210
211        assert_eq!(cache.get("key1"), Some("value1".to_string()));
212        assert_eq!(cache.get("key2"), Some("value2".to_string()));
213        assert_eq!(cache.get("key3"), None);
214    }
215
216    #[test]
217    fn test_cache_expiry() {
218        let cache: ContextCache<String> = ContextCache::with_ttl(1);
219        cache.insert("key1", "value1".to_string());
220
221        assert_eq!(cache.get("key1"), Some("value1".to_string()));
222
223        // Wait for expiry
224        thread::sleep(Duration::from_millis(1100));
225
226        assert_eq!(cache.get("key1"), None);
227    }
228
229    #[test]
230    fn test_cache_remove() {
231        let cache: ContextCache<String> = ContextCache::new();
232        cache.insert("key1", "value1".to_string());
233
234        assert_eq!(cache.remove("key1"), Some("value1".to_string()));
235        assert_eq!(cache.get("key1"), None);
236    }
237
238    #[test]
239    fn test_cache_clear() {
240        let cache: ContextCache<String> = ContextCache::new();
241        cache.insert("key1", "value1".to_string());
242        cache.insert("key2", "value2".to_string());
243
244        cache.clear();
245        assert!(cache.is_empty());
246    }
247
248    #[test]
249    fn test_cache_stats() {
250        let cache: ContextCache<String> = ContextCache::new();
251        cache.insert("key1", "value1".to_string());
252        cache.get("key1");
253        cache.get("key1");
254
255        let stats = cache.stats();
256        assert_eq!(stats.size, 1);
257        assert_eq!(stats.total_accesses, 2);
258    }
259}