Skip to main content

rh_foundation/
memory.rs

1//! In-memory storage utilities for WASM-compatible caching.
2//!
3//! This module provides a generic `MemoryStore` that can be used to cache
4//! data in memory, particularly useful for WASM environments where filesystem
5//! access is limited or unavailable.
6//!
7//! # Example
8//!
9//! ```
10//! use rh_foundation::memory::{MemoryStore, MemoryStoreConfig};
11//!
12//! // Create a store with default config
13//! let store: MemoryStore<String> = MemoryStore::new(MemoryStoreConfig::default());
14//!
15//! // Insert and retrieve values
16//! store.insert("key1".to_string(), "value1".to_string());
17//! assert_eq!(store.get(&"key1".to_string()), Some("value1".to_string()));
18//!
19//! // Check if key exists
20//! assert!(store.contains(&"key1".to_string()));
21//!
22//! // Remove a value
23//! store.remove(&"key1".to_string());
24//! assert!(!store.contains(&"key1".to_string()));
25//! ```
26
27use std::collections::HashMap;
28use std::hash::Hash;
29use std::sync::{Arc, RwLock};
30
31/// Configuration for a memory store.
32#[derive(Debug, Clone, Default)]
33pub struct MemoryStoreConfig {
34    /// Maximum number of entries to store (0 = unlimited).
35    pub max_entries: usize,
36    /// Whether to track access statistics.
37    pub track_stats: bool,
38}
39
40impl MemoryStoreConfig {
41    /// Create a config with a maximum entry limit.
42    pub fn with_max_entries(max_entries: usize) -> Self {
43        Self {
44            max_entries,
45            ..Default::default()
46        }
47    }
48
49    /// Enable statistics tracking.
50    pub fn with_stats(mut self) -> Self {
51        self.track_stats = true;
52        self
53    }
54}
55
56/// Statistics for a memory store.
57#[derive(Debug, Clone, Default)]
58pub struct MemoryStoreStats {
59    /// Number of cache hits.
60    pub hits: u64,
61    /// Number of cache misses.
62    pub misses: u64,
63    /// Number of insertions.
64    pub insertions: u64,
65    /// Number of removals.
66    pub removals: u64,
67    /// Number of evictions due to capacity limits.
68    pub evictions: u64,
69}
70
71impl MemoryStoreStats {
72    /// Calculate the hit rate (0.0 to 1.0).
73    pub fn hit_rate(&self) -> f64 {
74        let total = self.hits + self.misses;
75        if total == 0 {
76            0.0
77        } else {
78            self.hits as f64 / total as f64
79        }
80    }
81}
82
83/// A thread-safe in-memory key-value store.
84///
85/// This store is designed to be WASM-compatible and can be used to cache
86/// any type of data that implements `Clone`. It uses interior mutability
87/// via `RwLock` to allow concurrent read access.
88///
89/// # Type Parameters
90///
91/// - `K`: The key type, must implement `Eq + Hash + Clone`
92/// - `V`: The value type, must implement `Clone`
93#[derive(Debug)]
94pub struct MemoryStore<V, K = String>
95where
96    K: Eq + Hash + Clone,
97    V: Clone,
98{
99    data: Arc<RwLock<HashMap<K, V>>>,
100    config: MemoryStoreConfig,
101    stats: Arc<RwLock<MemoryStoreStats>>,
102}
103
104impl<V, K> Clone for MemoryStore<V, K>
105where
106    K: Eq + Hash + Clone,
107    V: Clone,
108{
109    fn clone(&self) -> Self {
110        Self {
111            data: Arc::clone(&self.data),
112            config: self.config.clone(),
113            stats: Arc::clone(&self.stats),
114        }
115    }
116}
117
118impl<V> Default for MemoryStore<V, String>
119where
120    V: Clone,
121{
122    fn default() -> Self {
123        Self::new(MemoryStoreConfig::default())
124    }
125}
126
127impl<V, K> MemoryStore<V, K>
128where
129    K: Eq + Hash + Clone,
130    V: Clone,
131{
132    /// Create a new memory store with the given configuration.
133    pub fn new(config: MemoryStoreConfig) -> Self {
134        Self {
135            data: Arc::new(RwLock::new(HashMap::new())),
136            config,
137            stats: Arc::new(RwLock::new(MemoryStoreStats::default())),
138        }
139    }
140
141    /// Insert a value into the store.
142    ///
143    /// If the store has a maximum entry limit and is at capacity,
144    /// an arbitrary entry will be evicted to make room.
145    pub fn insert(&self, key: K, value: V) {
146        let mut data = self.data.write().unwrap();
147
148        // Check capacity and evict if necessary
149        if self.config.max_entries > 0 && data.len() >= self.config.max_entries {
150            // Simple eviction: remove first entry (not LRU, but simple and fast)
151            if let Some(first_key) = data.keys().next().cloned() {
152                data.remove(&first_key);
153                if self.config.track_stats {
154                    self.stats.write().unwrap().evictions += 1;
155                }
156            }
157        }
158
159        data.insert(key, value);
160
161        if self.config.track_stats {
162            self.stats.write().unwrap().insertions += 1;
163        }
164    }
165
166    /// Get a value from the store.
167    ///
168    /// Returns `Some(value)` if the key exists, `None` otherwise.
169    pub fn get(&self, key: &K) -> Option<V> {
170        let data = self.data.read().unwrap();
171        let result = data.get(key).cloned();
172
173        if self.config.track_stats {
174            let mut stats = self.stats.write().unwrap();
175            if result.is_some() {
176                stats.hits += 1;
177            } else {
178                stats.misses += 1;
179            }
180        }
181
182        result
183    }
184
185    /// Check if a key exists in the store.
186    pub fn contains(&self, key: &K) -> bool {
187        self.data.read().unwrap().contains_key(key)
188    }
189
190    /// Remove a value from the store.
191    ///
192    /// Returns the removed value if the key existed.
193    pub fn remove(&self, key: &K) -> Option<V> {
194        let result = self.data.write().unwrap().remove(key);
195
196        if self.config.track_stats && result.is_some() {
197            self.stats.write().unwrap().removals += 1;
198        }
199
200        result
201    }
202
203    /// Clear all entries from the store.
204    pub fn clear(&self) {
205        self.data.write().unwrap().clear();
206    }
207
208    /// Get the number of entries in the store.
209    pub fn len(&self) -> usize {
210        self.data.read().unwrap().len()
211    }
212
213    /// Check if the store is empty.
214    pub fn is_empty(&self) -> bool {
215        self.data.read().unwrap().is_empty()
216    }
217
218    /// Get all keys in the store.
219    pub fn keys(&self) -> Vec<K> {
220        self.data.read().unwrap().keys().cloned().collect()
221    }
222
223    /// Get statistics for the store.
224    ///
225    /// Only meaningful if `track_stats` was enabled in the config.
226    pub fn stats(&self) -> MemoryStoreStats {
227        self.stats.read().unwrap().clone()
228    }
229
230    /// Reset statistics.
231    pub fn reset_stats(&self) {
232        *self.stats.write().unwrap() = MemoryStoreStats::default();
233    }
234
235    /// Get or insert a value using a factory function.
236    ///
237    /// If the key exists, returns the existing value.
238    /// If the key doesn't exist, calls the factory function to create
239    /// a value, inserts it, and returns it.
240    pub fn get_or_insert_with<F>(&self, key: K, factory: F) -> V
241    where
242        F: FnOnce() -> V,
243    {
244        // Try to get first (read lock only)
245        if let Some(value) = self.get(&key) {
246            return value;
247        }
248
249        // Need to insert (write lock)
250        let mut data = self.data.write().unwrap();
251
252        // Double-check after acquiring write lock
253        if let Some(value) = data.get(&key) {
254            if self.config.track_stats {
255                self.stats.write().unwrap().hits += 1;
256            }
257            return value.clone();
258        }
259
260        // Create and insert
261        let value = factory();
262
263        // Check capacity and evict if necessary
264        if self.config.max_entries > 0 && data.len() >= self.config.max_entries {
265            if let Some(first_key) = data.keys().next().cloned() {
266                data.remove(&first_key);
267                if self.config.track_stats {
268                    self.stats.write().unwrap().evictions += 1;
269                }
270            }
271        }
272
273        data.insert(key, value.clone());
274
275        if self.config.track_stats {
276            let mut stats = self.stats.write().unwrap();
277            stats.misses += 1;
278            stats.insertions += 1;
279        }
280
281        value
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_basic_operations() {
291        let store: MemoryStore<String> = MemoryStore::default();
292
293        store.insert("key1".to_string(), "value1".to_string());
294        assert_eq!(store.get(&"key1".to_string()), Some("value1".to_string()));
295        assert!(store.contains(&"key1".to_string()));
296        assert_eq!(store.len(), 1);
297
298        store.remove(&"key1".to_string());
299        assert!(!store.contains(&"key1".to_string()));
300        assert!(store.is_empty());
301    }
302
303    #[test]
304    fn test_max_entries() {
305        let config = MemoryStoreConfig::with_max_entries(2).with_stats();
306        let store: MemoryStore<i32> = MemoryStore::new(config);
307
308        store.insert("a".to_string(), 1);
309        store.insert("b".to_string(), 2);
310        assert_eq!(store.len(), 2);
311
312        // This should evict one entry
313        store.insert("c".to_string(), 3);
314        assert_eq!(store.len(), 2);
315        assert_eq!(store.stats().evictions, 1);
316    }
317
318    #[test]
319    fn test_stats_tracking() {
320        let config = MemoryStoreConfig::default().with_stats();
321        let store: MemoryStore<String> = MemoryStore::new(config);
322
323        store.insert("key1".to_string(), "value1".to_string());
324        assert_eq!(store.stats().insertions, 1);
325
326        store.get(&"key1".to_string());
327        assert_eq!(store.stats().hits, 1);
328
329        store.get(&"nonexistent".to_string());
330        assert_eq!(store.stats().misses, 1);
331
332        let rate = store.stats().hit_rate();
333        assert!((rate - 0.5).abs() < 0.01);
334    }
335
336    #[test]
337    fn test_get_or_insert_with() {
338        let store: MemoryStore<i32> = MemoryStore::default();
339
340        let value = store.get_or_insert_with("key1".to_string(), || 42);
341        assert_eq!(value, 42);
342
343        // Should return cached value, not call factory
344        let value = store.get_or_insert_with("key1".to_string(), || 100);
345        assert_eq!(value, 42);
346    }
347
348    #[test]
349    fn test_clear() {
350        let store: MemoryStore<String> = MemoryStore::default();
351
352        store.insert("a".to_string(), "1".to_string());
353        store.insert("b".to_string(), "2".to_string());
354        assert_eq!(store.len(), 2);
355
356        store.clear();
357        assert!(store.is_empty());
358    }
359
360    #[test]
361    fn test_keys() {
362        let store: MemoryStore<i32> = MemoryStore::default();
363
364        store.insert("a".to_string(), 1);
365        store.insert("b".to_string(), 2);
366
367        let mut keys = store.keys();
368        keys.sort();
369        assert_eq!(keys, vec!["a".to_string(), "b".to_string()]);
370    }
371
372    #[test]
373    fn test_clone_shares_data() {
374        let store1: MemoryStore<String> = MemoryStore::default();
375        store1.insert("key".to_string(), "value".to_string());
376
377        let store2 = store1.clone();
378        assert_eq!(store2.get(&"key".to_string()), Some("value".to_string()));
379
380        // Changes in store2 should be visible in store1
381        store2.insert("key2".to_string(), "value2".to_string());
382        assert_eq!(store1.get(&"key2".to_string()), Some("value2".to_string()));
383    }
384
385    #[test]
386    fn test_custom_key_type() {
387        let store: MemoryStore<String, i32> = MemoryStore::new(MemoryStoreConfig::default());
388
389        store.insert(1, "one".to_string());
390        store.insert(2, "two".to_string());
391
392        assert_eq!(store.get(&1), Some("one".to_string()));
393        assert_eq!(store.get(&2), Some("two".to_string()));
394    }
395}