allframe_core/cache/
memory.rs

1//! In-memory cache implementation
2
3use std::{
4    collections::HashMap,
5    future::Future,
6    pin::Pin,
7    sync::RwLock,
8    time::{Duration, Instant},
9};
10
11use serde::{de::DeserializeOwned, Serialize};
12
13use super::{Cache, CacheConfig};
14
15/// An entry in the memory cache
16struct CacheEntry {
17    /// Serialized value
18    data: Vec<u8>,
19    /// Expiration time (if any)
20    expires_at: Option<Instant>,
21}
22
23impl CacheEntry {
24    fn new(data: Vec<u8>, ttl: Option<Duration>) -> Self {
25        Self {
26            data,
27            expires_at: ttl.map(|d| Instant::now() + d),
28        }
29    }
30
31    fn is_expired(&self) -> bool {
32        self.expires_at.map(|t| Instant::now() > t).unwrap_or(false)
33    }
34}
35
36/// In-memory cache with TTL support
37///
38/// This cache stores values in memory using a `HashMap` with optional
39/// time-to-live (TTL) for entries. It's suitable for single-process
40/// applications or as a fallback cache.
41///
42/// # Example
43///
44/// ```rust,ignore
45/// use allframe_core::cache::{Cache, MemoryCache, CacheConfig};
46/// use std::time::Duration;
47///
48/// let cache = MemoryCache::with_config(
49///     CacheConfig::new()
50///         .prefix("myapp")
51///         .default_ttl(Duration::from_secs(300))
52/// );
53///
54/// cache.set("key", &"value", None).await;
55/// let value: Option<String> = cache.get("key").await;
56/// ```
57pub struct MemoryCache {
58    entries: RwLock<HashMap<String, CacheEntry>>,
59    config: CacheConfig,
60}
61
62impl MemoryCache {
63    /// Create a new memory cache with default configuration
64    pub fn new() -> Self {
65        Self::with_config(CacheConfig::default())
66    }
67
68    /// Create a new memory cache with custom configuration
69    pub fn with_config(config: CacheConfig) -> Self {
70        Self {
71            entries: RwLock::new(HashMap::new()),
72            config,
73        }
74    }
75
76    /// Create a builder for the memory cache
77    pub fn builder() -> MemoryCacheBuilder {
78        MemoryCacheBuilder::new()
79    }
80
81    /// Remove expired entries from the cache
82    ///
83    /// This is called automatically on get operations, but can be called
84    /// manually if needed.
85    pub fn cleanup_expired(&self) {
86        let mut entries = self.entries.write().unwrap();
87        entries.retain(|_, entry| !entry.is_expired());
88    }
89
90    /// Get the effective TTL for an entry
91    fn effective_ttl(&self, ttl: Option<Duration>) -> Option<Duration> {
92        ttl.or(self.config.default_ttl)
93    }
94
95    /// Build the full key with prefix
96    fn full_key(&self, key: &str) -> String {
97        self.config.build_key(key)
98    }
99}
100
101impl Default for MemoryCache {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107impl Cache for MemoryCache {
108    fn get<T: DeserializeOwned + Send>(
109        &self,
110        key: &str,
111    ) -> Pin<Box<dyn Future<Output = Option<T>> + Send + '_>> {
112        let key = self.full_key(key);
113        Box::pin(async move {
114            let entries = self.entries.read().unwrap();
115            entries.get(&key).and_then(|entry| {
116                if entry.is_expired() {
117                    None
118                } else {
119                    serde_json::from_slice(&entry.data).ok()
120                }
121            })
122        })
123    }
124
125    fn set<T: Serialize + Send + Sync>(
126        &self,
127        key: &str,
128        value: &T,
129        ttl: Option<Duration>,
130    ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
131        let key = self.full_key(key);
132        let ttl = self.effective_ttl(ttl);
133
134        // Serialize outside the lock
135        let data = match serde_json::to_vec(value) {
136            Ok(d) => d,
137            Err(_) => return Box::pin(async {}),
138        };
139
140        Box::pin(async move {
141            let mut entries = self.entries.write().unwrap();
142
143            // Check max entries limit
144            if let Some(max) = self.config.max_entries {
145                if entries.len() >= max && !entries.contains_key(&key) {
146                    // Remove expired entries first
147                    entries.retain(|_, entry| !entry.is_expired());
148
149                    // If still at limit, remove oldest entry (simple LRU approximation)
150                    if entries.len() >= max {
151                        if let Some(oldest_key) = entries.keys().next().cloned() {
152                            entries.remove(&oldest_key);
153                        }
154                    }
155                }
156            }
157
158            entries.insert(key, CacheEntry::new(data, ttl));
159        })
160    }
161
162    fn delete(&self, key: &str) -> Pin<Box<dyn Future<Output = bool> + Send + '_>> {
163        let key = self.full_key(key);
164        Box::pin(async move {
165            let mut entries = self.entries.write().unwrap();
166            entries.remove(&key).is_some()
167        })
168    }
169
170    fn exists(&self, key: &str) -> Pin<Box<dyn Future<Output = bool> + Send + '_>> {
171        let key = self.full_key(key);
172        Box::pin(async move {
173            let entries = self.entries.read().unwrap();
174            entries
175                .get(&key)
176                .map(|entry| !entry.is_expired())
177                .unwrap_or(false)
178        })
179    }
180
181    fn clear(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
182        Box::pin(async move {
183            let mut entries = self.entries.write().unwrap();
184            entries.clear();
185        })
186    }
187
188    fn len(&self) -> Pin<Box<dyn Future<Output = Option<usize>> + Send + '_>> {
189        Box::pin(async move {
190            let entries = self.entries.read().unwrap();
191            // Count non-expired entries
192            let count = entries.values().filter(|e| !e.is_expired()).count();
193            Some(count)
194        })
195    }
196}
197
198/// Builder for MemoryCache
199pub struct MemoryCacheBuilder {
200    config: CacheConfig,
201}
202
203impl MemoryCacheBuilder {
204    /// Create a new builder
205    pub fn new() -> Self {
206        Self {
207            config: CacheConfig::default(),
208        }
209    }
210
211    /// Set the key prefix
212    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
213        self.config.prefix = Some(prefix.into());
214        self
215    }
216
217    /// Set the default TTL
218    pub fn default_ttl(mut self, ttl: Duration) -> Self {
219        self.config.default_ttl = Some(ttl);
220        self
221    }
222
223    /// Set the maximum number of entries
224    pub fn max_entries(mut self, max: usize) -> Self {
225        self.config.max_entries = Some(max);
226        self
227    }
228
229    /// Build the cache
230    pub fn build(self) -> MemoryCache {
231        MemoryCache::with_config(self.config)
232    }
233}
234
235impl Default for MemoryCacheBuilder {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[tokio::test]
246    async fn test_memory_cache_basic() {
247        let cache = MemoryCache::new();
248
249        cache.set("key", &"value", None).await;
250        let value: Option<String> = cache.get("key").await;
251        assert_eq!(value, Some("value".to_string()));
252    }
253
254    #[tokio::test]
255    async fn test_memory_cache_missing_key() {
256        let cache = MemoryCache::new();
257        let value: Option<String> = cache.get("nonexistent").await;
258        assert_eq!(value, None);
259    }
260
261    #[tokio::test]
262    async fn test_memory_cache_delete() {
263        let cache = MemoryCache::new();
264
265        cache.set("key", &"value", None).await;
266        assert!(cache.exists("key").await);
267
268        let deleted = cache.delete("key").await;
269        assert!(deleted);
270        assert!(!cache.exists("key").await);
271    }
272
273    #[tokio::test]
274    async fn test_memory_cache_delete_nonexistent() {
275        let cache = MemoryCache::new();
276        let deleted = cache.delete("nonexistent").await;
277        assert!(!deleted);
278    }
279
280    #[tokio::test]
281    async fn test_memory_cache_clear() {
282        let cache = MemoryCache::new();
283
284        cache.set("key1", &"value1", None).await;
285        cache.set("key2", &"value2", None).await;
286
287        cache.clear().await;
288
289        assert!(!cache.exists("key1").await);
290        assert!(!cache.exists("key2").await);
291    }
292
293    #[tokio::test]
294    async fn test_memory_cache_len() {
295        let cache = MemoryCache::new();
296
297        assert_eq!(cache.len().await, Some(0));
298
299        cache.set("key1", &"value1", None).await;
300        cache.set("key2", &"value2", None).await;
301
302        assert_eq!(cache.len().await, Some(2));
303    }
304
305    #[tokio::test]
306    async fn test_memory_cache_ttl_expired() {
307        let cache = MemoryCache::new();
308
309        // Set with very short TTL
310        cache
311            .set("key", &"value", Some(Duration::from_millis(1)))
312            .await;
313
314        // Wait for expiration
315        tokio::time::sleep(Duration::from_millis(10)).await;
316
317        let value: Option<String> = cache.get("key").await;
318        assert_eq!(value, None);
319    }
320
321    #[tokio::test]
322    async fn test_memory_cache_ttl_not_expired() {
323        let cache = MemoryCache::new();
324
325        cache
326            .set("key", &"value", Some(Duration::from_secs(60)))
327            .await;
328
329        let value: Option<String> = cache.get("key").await;
330        assert_eq!(value, Some("value".to_string()));
331    }
332
333    #[tokio::test]
334    async fn test_memory_cache_with_prefix() {
335        let cache = MemoryCache::builder().prefix("test").build();
336
337        cache.set("key", &"value", None).await;
338
339        // The internal key should have the prefix
340        let value: Option<String> = cache.get("key").await;
341        assert_eq!(value, Some("value".to_string()));
342    }
343
344    #[tokio::test]
345    async fn test_memory_cache_max_entries() {
346        let cache = MemoryCache::builder().max_entries(2).build();
347
348        cache.set("key1", &"value1", None).await;
349        cache.set("key2", &"value2", None).await;
350        cache.set("key3", &"value3", None).await;
351
352        // Should have at most 2 entries
353        assert!(cache.len().await.unwrap() <= 2);
354    }
355
356    #[tokio::test]
357    async fn test_memory_cache_complex_type() {
358        #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
359        struct User {
360            id: u64,
361            name: String,
362        }
363
364        let cache = MemoryCache::new();
365        let user = User {
366            id: 1,
367            name: "Alice".to_string(),
368        };
369
370        cache.set("user:1", &user, None).await;
371
372        let retrieved: Option<User> = cache.get("user:1").await;
373        assert_eq!(retrieved, Some(user));
374    }
375
376    #[tokio::test]
377    async fn test_memory_cache_builder_default_ttl() {
378        let cache = MemoryCache::builder()
379            .default_ttl(Duration::from_millis(1))
380            .build();
381
382        // Set without explicit TTL - should use default
383        cache.set("key", &"value", None).await;
384
385        // Wait for expiration
386        tokio::time::sleep(Duration::from_millis(10)).await;
387
388        let value: Option<String> = cache.get("key").await;
389        assert_eq!(value, None);
390    }
391}