Skip to main content

rh_foundation/memory/
mod.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//! let store: MemoryStore<String> = MemoryStore::new(MemoryStoreConfig::default());
13//!
14//! store.insert("key1".to_string(), "value1".to_string());
15//! assert_eq!(store.get(&"key1".to_string()), Some("value1".to_string()));
16//! assert!(store.contains(&"key1".to_string()));
17//!
18//! store.remove(&"key1".to_string());
19//! assert!(!store.contains(&"key1".to_string()));
20//! ```
21
22mod state;
23mod stats;
24
25use std::hash::Hash;
26use std::sync::Arc;
27
28use parking_lot::RwLock;
29
30use state::StoreState;
31pub use stats::MemoryStoreStats;
32use stats::StatsRecorder;
33
34/// Configuration for a memory store.
35#[derive(Debug, Clone, Default)]
36pub struct MemoryStoreConfig {
37    /// Maximum number of entries to store (0 = unlimited).
38    pub max_entries: usize,
39    /// Whether to track access statistics.
40    pub track_stats: bool,
41}
42
43impl MemoryStoreConfig {
44    /// Create a config with a maximum entry limit.
45    pub fn with_max_entries(max_entries: usize) -> Self {
46        Self {
47            max_entries,
48            ..Default::default()
49        }
50    }
51
52    /// Enable statistics tracking.
53    pub fn with_stats(mut self) -> Self {
54        self.track_stats = true;
55        self
56    }
57}
58
59/// A thread-safe in-memory key-value store.
60///
61/// This store is designed to be WASM-compatible and can be used to cache
62/// any type of data that implements `Clone`. It uses interior mutability
63/// via `RwLock` to allow concurrent read access.
64///
65/// # Type Parameters
66///
67/// - `K`: The key type, must implement `Eq + Hash + Clone`
68/// - `V`: The value type, must implement `Clone`
69#[derive(Debug)]
70pub struct MemoryStore<V, K = String>
71where
72    K: Eq + Hash + Clone,
73    V: Clone,
74{
75    state: Arc<RwLock<StoreState<K, V>>>,
76    config: MemoryStoreConfig,
77    stats: StatsRecorder,
78}
79
80impl<V, K> Clone for MemoryStore<V, K>
81where
82    K: Eq + Hash + Clone,
83    V: Clone,
84{
85    fn clone(&self) -> Self {
86        Self {
87            state: Arc::clone(&self.state),
88            config: self.config.clone(),
89            stats: self.stats.clone(),
90        }
91    }
92}
93
94impl<V> Default for MemoryStore<V, String>
95where
96    V: Clone,
97{
98    fn default() -> Self {
99        Self::new(MemoryStoreConfig::default())
100    }
101}
102
103impl<V, K> MemoryStore<V, K>
104where
105    K: Eq + Hash + Clone,
106    V: Clone,
107{
108    /// Create a new memory store with the given configuration.
109    pub fn new(config: MemoryStoreConfig) -> Self {
110        Self {
111            state: Arc::new(RwLock::new(StoreState::new(config.max_entries))),
112            stats: StatsRecorder::new(config.track_stats),
113            config,
114        }
115    }
116
117    /// Insert a value into the store.
118    ///
119    /// If the store has a maximum entry limit and is at capacity,
120    /// an arbitrary entry will be evicted to make room.
121    pub fn insert(&self, key: K, value: V) {
122        let outcome = self.state.write().insert(key, value);
123        if outcome.evicted {
124            self.stats.record_eviction();
125        }
126        self.stats.record_insertion();
127    }
128
129    /// Get a value from the store.
130    ///
131    /// Returns `Some(value)` if the key exists, `None` otherwise.
132    pub fn get(&self, key: &K) -> Option<V> {
133        let result = self.state.read().get_cloned(key);
134
135        if result.is_some() {
136            self.stats.record_hit();
137        } else {
138            self.stats.record_miss();
139        }
140
141        result
142    }
143
144    /// Check if a key exists in the store.
145    pub fn contains(&self, key: &K) -> bool {
146        self.state.read().contains(key)
147    }
148
149    /// Remove a value from the store.
150    ///
151    /// Returns the removed value if the key existed.
152    pub fn remove(&self, key: &K) -> Option<V> {
153        let result = self.state.write().remove(key);
154        if result.is_some() {
155            self.stats.record_removal();
156        }
157        result
158    }
159
160    /// Clear all entries from the store.
161    pub fn clear(&self) {
162        self.state.write().clear();
163    }
164
165    /// Get the number of entries in the store.
166    pub fn len(&self) -> usize {
167        self.state.read().len()
168    }
169
170    /// Check if the store is empty.
171    pub fn is_empty(&self) -> bool {
172        self.state.read().is_empty()
173    }
174
175    /// Get all keys in the store.
176    pub fn keys(&self) -> Vec<K> {
177        self.state.read().keys()
178    }
179
180    /// Get statistics for the store.
181    ///
182    /// Only meaningful if `track_stats` was enabled in the config.
183    pub fn stats(&self) -> MemoryStoreStats {
184        self.stats.snapshot()
185    }
186
187    /// Reset statistics.
188    pub fn reset_stats(&self) {
189        self.stats.reset();
190    }
191
192    /// Get or insert a value using a factory function.
193    ///
194    /// If the key exists, returns the existing value.
195    /// If the key doesn't exist, calls the factory function to create
196    /// a value, inserts it, and returns it.
197    pub fn get_or_insert_with<F>(&self, key: K, factory: F) -> V
198    where
199        F: FnOnce() -> V,
200    {
201        if let Some(value) = self.state.read().get_cloned(&key) {
202            self.stats.record_hit();
203            return value;
204        }
205
206        let outcome = self.state.write().get_or_insert_with(key, factory);
207
208        if outcome.inserted {
209            self.stats.record_miss();
210            if outcome.evicted {
211                self.stats.record_eviction();
212            }
213            self.stats.record_insertion();
214        } else {
215            self.stats.record_miss();
216            self.stats.record_hit();
217        }
218
219        outcome.value
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_basic_operations() {
229        let store: MemoryStore<String> = MemoryStore::default();
230
231        store.insert("key1".to_string(), "value1".to_string());
232        assert_eq!(store.get(&"key1".to_string()), Some("value1".to_string()));
233        assert!(store.contains(&"key1".to_string()));
234        assert_eq!(store.len(), 1);
235
236        store.remove(&"key1".to_string());
237        assert!(!store.contains(&"key1".to_string()));
238        assert!(store.is_empty());
239    }
240
241    #[test]
242    fn test_max_entries() {
243        let config = MemoryStoreConfig::with_max_entries(2).with_stats();
244        let store: MemoryStore<i32> = MemoryStore::new(config);
245
246        store.insert("a".to_string(), 1);
247        store.insert("b".to_string(), 2);
248        assert_eq!(store.len(), 2);
249
250        store.insert("c".to_string(), 3);
251        assert_eq!(store.len(), 2);
252        assert_eq!(store.stats().evictions, 1);
253    }
254
255    #[test]
256    fn test_stats_tracking() {
257        let config = MemoryStoreConfig::default().with_stats();
258        let store: MemoryStore<String> = MemoryStore::new(config);
259
260        store.insert("key1".to_string(), "value1".to_string());
261        assert_eq!(store.stats().insertions, 1);
262
263        store.get(&"key1".to_string());
264        assert_eq!(store.stats().hits, 1);
265
266        store.get(&"nonexistent".to_string());
267        assert_eq!(store.stats().misses, 1);
268
269        let rate = store.stats().hit_rate();
270        assert!((rate - 0.5).abs() < 0.01);
271    }
272
273    #[test]
274    fn test_get_or_insert_with() {
275        let store: MemoryStore<i32> = MemoryStore::default();
276
277        let value = store.get_or_insert_with("key1".to_string(), || 42);
278        assert_eq!(value, 42);
279
280        let value = store.get_or_insert_with("key1".to_string(), || 100);
281        assert_eq!(value, 42);
282    }
283
284    #[test]
285    fn test_clear() {
286        let store: MemoryStore<String> = MemoryStore::default();
287
288        store.insert("a".to_string(), "1".to_string());
289        store.insert("b".to_string(), "2".to_string());
290        assert_eq!(store.len(), 2);
291
292        store.clear();
293        assert!(store.is_empty());
294    }
295
296    #[test]
297    fn test_keys() {
298        let store: MemoryStore<i32> = MemoryStore::default();
299
300        store.insert("a".to_string(), 1);
301        store.insert("b".to_string(), 2);
302
303        let mut keys = store.keys();
304        keys.sort();
305        assert_eq!(keys, vec!["a".to_string(), "b".to_string()]);
306    }
307
308    #[test]
309    fn test_clone_shares_data() {
310        let store1: MemoryStore<String> = MemoryStore::default();
311        store1.insert("key".to_string(), "value".to_string());
312
313        let store2 = store1.clone();
314        assert_eq!(store2.get(&"key".to_string()), Some("value".to_string()));
315
316        store2.insert("key2".to_string(), "value2".to_string());
317        assert_eq!(store1.get(&"key2".to_string()), Some("value2".to_string()));
318    }
319
320    #[test]
321    fn test_custom_key_type() {
322        let store: MemoryStore<String, i32> = MemoryStore::new(MemoryStoreConfig::default());
323
324        store.insert(1, "one".to_string());
325        store.insert(2, "two".to_string());
326
327        assert_eq!(store.get(&1), Some("one".to_string()));
328        assert_eq!(store.get(&2), Some("two".to_string()));
329    }
330}