gestura_core_context/
cache.rs1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::RwLock;
9use std::time::{Duration, Instant};
10
11const DEFAULT_TTL_SECS: u64 = 300; const MAX_CACHE_SIZE: usize = 100;
15
16#[derive(Debug, Clone)]
18pub struct CacheEntry<T> {
19 pub value: T,
21 pub created_at: Instant,
23 pub ttl: Duration,
25 pub access_count: u32,
27 pub last_accessed: Instant,
29}
30
31impl<T: Clone> CacheEntry<T> {
32 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 pub fn is_expired(&self) -> bool {
46 self.created_at.elapsed() > self.ttl
47 }
48
49 pub fn touch(&mut self) {
51 self.access_count += 1;
52 self.last_accessed = Instant::now();
53 }
54}
55
56pub struct ContextCache<T> {
58 entries: RwLock<HashMap<String, CacheEntry<T>>>,
60 default_ttl: Duration,
62 max_size: usize,
64}
65
66impl<T: Clone> ContextCache<T> {
67 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 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 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 pub fn insert(&self, key: impl Into<String>, value: T) {
101 self.insert_with_ttl(key, value, self.default_ttl);
102 }
103
104 pub fn insert_with_ttl(&self, key: impl Into<String>, value: T, ttl: Duration) {
106 if let Ok(mut entries) = self.entries.write() {
107 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 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 pub fn clear(&self) {
126 if let Ok(mut entries) = self.entries.write() {
127 entries.clear();
128 }
129 }
130
131 pub fn len(&self) -> usize {
133 self.entries.read().map(|e| e.len()).unwrap_or(0)
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.len() == 0
139 }
140
141 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 fn evict_oldest(&self, entries: &mut HashMap<String, CacheEntry<T>>) {
150 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct CacheStats {
189 pub size: usize,
191 pub max_size: usize,
193 pub total_accesses: u32,
195 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 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}