Skip to main content

rust_webx_host/
memory_cache.rs

1//! In-process memory cache —matches ASP.NET Core's `MemoryCache`.
2//!
3//! Implements [`IDistributedCache`] and provides typed access
4//! via [`DistributedCacheExtensions`].
5
6use rust_webx_core::cache::options::DistributedCacheEntryOptions;
7use rust_webx_core::cache::trait_def::{CacheError, IDistributedCache, Result};
8use std::collections::{HashMap, VecDeque};
9use std::time::Instant;
10use tokio::sync::RwLock;
11
12struct CacheEntry {
13    data: Vec<u8>,
14    expires_at: Option<Instant>,
15    sliding_ttl: Option<std::time::Duration>,
16}
17
18impl CacheEntry {
19    fn new(data: Vec<u8>, options: &DistributedCacheEntryOptions) -> Self {
20        let expires_at = options
21            .absolute_expiration_relative_to_now
22            .map(|d| Instant::now() + d);
23        let sliding_ttl = options.sliding_expiration;
24        let expires_at = if expires_at.is_some() {
25            expires_at
26        } else {
27            sliding_ttl.map(|d| Instant::now() + d)
28        };
29        Self {
30            data,
31            expires_at,
32            sliding_ttl,
33        }
34    }
35    fn is_expired(&self) -> bool {
36        self.expires_at.is_some_and(|t| Instant::now() >= t)
37    }
38    fn refresh(&mut self) {
39        if let Some(ttl) = self.sliding_ttl {
40            self.expires_at = Some(Instant::now() + ttl);
41        }
42    }
43}
44
45struct CacheInner {
46    entries: HashMap<String, CacheEntry>,
47    insertion_order: VecDeque<String>,
48}
49
50impl CacheInner {
51    fn evict_expired(&mut self) {
52        let expired: Vec<String> = self
53            .entries
54            .iter()
55            .filter(|(_, v)| v.is_expired())
56            .map(|(k, _)| k.clone())
57            .collect();
58        for k in expired {
59            self.entries.remove(&k);
60            self.insertion_order.retain(|x| x != &k);
61        }
62    }
63}
64
65pub struct MemoryCache {
66    inner: RwLock<CacheInner>,
67    max_entries: usize,
68}
69
70impl MemoryCache {
71    pub fn new() -> Self {
72        Self {
73            inner: RwLock::new(CacheInner {
74                entries: HashMap::new(),
75                insertion_order: VecDeque::new(),
76            }),
77            max_entries: 0,
78        }
79    }
80    pub fn with_max_entries(mut self, n: usize) -> Self {
81        self.max_entries = n;
82        self
83    }
84    pub async fn compact(&self) {
85        let mut inner = self.inner.write().await;
86        inner.evict_expired();
87    }
88    pub async fn count(&self) -> usize {
89        self.inner.read().await.entries.len()
90    }
91}
92
93impl Default for MemoryCache {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99#[async_trait::async_trait]
100impl IDistributedCache for MemoryCache {
101    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
102        // Fast path: read lock — clone data, check freshness
103        {
104            let inner = self.inner.read().await;
105            match inner.entries.get(key) {
106                Some(e) if !e.is_expired() => {
107                    let data = e.data.clone();
108                    let needs_refresh = e.sliding_ttl.is_some();
109                    drop(inner);
110                    if needs_refresh {
111                        let mut inner = self.inner.write().await;
112                        if let Some(e) = inner.entries.get_mut(key) {
113                            e.refresh();
114                        }
115                    }
116                    return Ok(Some(data));
117                }
118                Some(_) => {}
119                None => return Ok(None),
120            }
121        }
122        // Slow path: expired entry — remove under write lock
123        let mut inner = self.inner.write().await;
124        match inner.entries.get_mut(key) {
125            Some(e) if !e.is_expired() => {
126                e.refresh();
127                Ok(Some(e.data.clone()))
128            }
129            Some(_) => {
130                inner.entries.remove(key);
131                inner.insertion_order.retain(|x| x != key);
132                Ok(None)
133            }
134            None => Ok(None),
135        }
136    }
137
138    async fn set(
139        &self,
140        key: &str,
141        value: Vec<u8>,
142        options: Option<&DistributedCacheEntryOptions>,
143    ) -> Result<()> {
144        let opts = options.cloned().unwrap_or_default();
145        if opts.size_limit > 0 && value.len() > opts.size_limit {
146            return Err(CacheError::Message(format!(
147                "size {} exceeds limit {}",
148                value.len(),
149                opts.size_limit
150            )));
151        }
152        let mut inner = self.inner.write().await;
153        let is_new = !inner.entries.contains_key(key);
154
155        if is_new && self.max_entries > 0 && inner.entries.len() >= self.max_entries {
156            inner.evict_expired();
157            while inner.entries.len() >= self.max_entries {
158                match inner.insertion_order.pop_front() {
159                    Some(k) => {
160                        inner.entries.remove(&k);
161                    }
162                    None => break,
163                }
164            }
165        }
166
167        inner
168            .entries
169            .insert(key.to_string(), CacheEntry::new(value, &opts));
170        if is_new {
171            inner.insertion_order.push_back(key.to_string());
172        }
173        Ok(())
174    }
175
176    async fn remove(&self, key: &str) -> Result<()> {
177        let mut inner = self.inner.write().await;
178        inner.entries.remove(key);
179        inner.insertion_order.retain(|x| x != key);
180        Ok(())
181    }
182
183    async fn refresh(&self, key: &str) -> Result<()> {
184        let mut inner = self.inner.write().await;
185        if let Some(e) = inner.entries.get_mut(key) {
186            e.refresh();
187        }
188        Ok(())
189    }
190
191    async fn exists(&self, key: &str) -> Result<bool> {
192        // Fast path: read lock
193        {
194            let inner = self.inner.read().await;
195            match inner.entries.get(key) {
196                Some(e) if !e.is_expired() => {
197                    let needs_refresh = e.sliding_ttl.is_some();
198                    drop(inner);
199                    if needs_refresh {
200                        let mut inner = self.inner.write().await;
201                        if let Some(e) = inner.entries.get_mut(key) {
202                            e.refresh();
203                        }
204                    }
205                    return Ok(true);
206                }
207                Some(_) => {}
208                None => return Ok(false),
209            }
210        }
211        // Slow path: expired entry — remove under write lock
212        let mut inner = self.inner.write().await;
213        match inner.entries.get_mut(key) {
214            Some(e) if !e.is_expired() => {
215                e.refresh();
216                Ok(true)
217            }
218            Some(_) => {
219                inner.entries.remove(key);
220                inner.insertion_order.retain(|x| x != key);
221                Ok(false)
222            }
223            None => Ok(false),
224        }
225    }
226
227    async fn clear(&self) -> Result<()> {
228        let mut inner = self.inner.write().await;
229        inner.entries.clear();
230        inner.insertion_order.clear();
231        Ok(())
232    }
233}