chartml_core/resolver/
cache.rs1use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13use std::time::Duration;
19use web_time::SystemTime;
20
21use async_trait::async_trait;
22use thiserror::Error;
23
24use crate::data::DataTable;
25
26#[derive(Debug, Clone)]
29pub struct CachedEntry {
30 pub data: DataTable,
31 pub fetched_at: SystemTime,
32 pub ttl: Duration,
33 pub tags: Vec<String>,
36 pub metadata: HashMap<String, serde_json::Value>,
40}
41
42impl CachedEntry {
43 pub fn is_expired(&self) -> bool {
47 SystemTime::now()
48 .duration_since(self.fetched_at)
49 .map(|age| age > self.ttl)
50 .unwrap_or(true)
51 }
52
53 pub fn age(&self) -> Duration {
56 SystemTime::now()
57 .duration_since(self.fetched_at)
58 .unwrap_or(Duration::ZERO)
59 }
60}
61
62#[derive(Debug, Error, Clone)]
65pub enum CacheError {
66 #[error("cache backend error: {0}")]
69 Backend(String),
70}
71
72#[cfg(not(target_arch = "wasm32"))]
85#[async_trait]
86pub trait CacheBackend: Send + Sync {
87 async fn get(&self, key: u64) -> Option<CachedEntry>;
91
92 async fn put(&self, key: u64, entry: CachedEntry) -> Result<(), CacheError>;
96
97 async fn invalidate(&self, key: u64) -> Result<(), CacheError>;
99
100 async fn invalidate_by_tag(&self, tag: &str) -> Result<(), CacheError>;
103
104 async fn clear(&self) -> Result<(), CacheError>;
106
107 async fn shutdown(&self) {}
110}
111
112#[cfg(target_arch = "wasm32")]
117#[async_trait(?Send)]
118pub trait CacheBackend {
119 async fn get(&self, key: u64) -> Option<CachedEntry>;
123
124 async fn put(&self, key: u64, entry: CachedEntry) -> Result<(), CacheError>;
128
129 async fn invalidate(&self, key: u64) -> Result<(), CacheError>;
131
132 async fn invalidate_by_tag(&self, tag: &str) -> Result<(), CacheError>;
135
136 async fn clear(&self) -> Result<(), CacheError>;
138
139 async fn shutdown(&self) {}
142}
143
144#[derive(Debug, Default, Clone)]
148pub struct MemoryBackend {
149 inner: Arc<Mutex<HashMap<u64, CachedEntry>>>,
150}
151
152impl MemoryBackend {
153 pub fn new() -> Self {
154 Self {
155 inner: Arc::new(Mutex::new(HashMap::new())),
156 }
157 }
158
159 pub fn len(&self) -> usize {
161 self.inner
162 .lock()
163 .expect("memory cache lock poisoned")
164 .len()
165 }
166
167 pub fn is_empty(&self) -> bool {
168 self.len() == 0
169 }
170}
171
172#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
173#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
174impl CacheBackend for MemoryBackend {
175 async fn get(&self, key: u64) -> Option<CachedEntry> {
176 let guard = self.inner.lock().expect("memory cache lock poisoned");
177 guard.get(&key).cloned()
178 }
179
180 async fn put(&self, key: u64, entry: CachedEntry) -> Result<(), CacheError> {
181 let mut guard = self.inner.lock().expect("memory cache lock poisoned");
182 guard.insert(key, entry);
183 Ok(())
184 }
185
186 async fn invalidate(&self, key: u64) -> Result<(), CacheError> {
187 let mut guard = self.inner.lock().expect("memory cache lock poisoned");
188 guard.remove(&key);
189 Ok(())
190 }
191
192 async fn invalidate_by_tag(&self, tag: &str) -> Result<(), CacheError> {
193 let mut guard = self.inner.lock().expect("memory cache lock poisoned");
194 guard.retain(|_, entry| !entry.tags.iter().any(|t| t == tag));
195 Ok(())
196 }
197
198 async fn clear(&self) -> Result<(), CacheError> {
199 let mut guard = self.inner.lock().expect("memory cache lock poisoned");
200 guard.clear();
201 Ok(())
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 #![allow(clippy::unwrap_used)]
208 use super::*;
209 use crate::data::Row;
210 use serde_json::json;
211
212 fn make_entry(tags: Vec<&str>) -> CachedEntry {
213 let row: Row = [("x".to_string(), json!(1.0))].into_iter().collect();
214 CachedEntry {
215 data: DataTable::from_rows(&[row]).unwrap(),
216 fetched_at: SystemTime::now(),
217 ttl: Duration::from_secs(60),
218 tags: tags.into_iter().map(String::from).collect(),
219 metadata: HashMap::new(),
220 }
221 }
222
223 #[tokio::test]
224 async fn memory_backend_get_put_roundtrip() {
225 let backend = MemoryBackend::new();
226 backend.put(1, make_entry(vec![])).await.unwrap();
227 let got = backend.get(1).await;
228 assert!(got.is_some());
229 assert_eq!(backend.len(), 1);
230 }
231
232 #[tokio::test]
233 async fn memory_backend_invalidate_single() {
234 let backend = MemoryBackend::new();
235 backend.put(1, make_entry(vec![])).await.unwrap();
236 backend.put(2, make_entry(vec![])).await.unwrap();
237 backend.invalidate(1).await.unwrap();
238 assert!(backend.get(1).await.is_none());
239 assert!(backend.get(2).await.is_some());
240 }
241
242 #[tokio::test]
243 async fn memory_backend_invalidate_by_tag() {
244 let backend = MemoryBackend::new();
245 backend.put(1, make_entry(vec!["slug:foo"])).await.unwrap();
246 backend.put(2, make_entry(vec!["slug:foo"])).await.unwrap();
247 backend.put(3, make_entry(vec!["slug:bar"])).await.unwrap();
248 backend.invalidate_by_tag("slug:foo").await.unwrap();
249 assert!(backend.get(1).await.is_none());
250 assert!(backend.get(2).await.is_none());
251 assert!(backend.get(3).await.is_some());
252 }
253
254 #[tokio::test]
255 async fn memory_backend_clear() {
256 let backend = MemoryBackend::new();
257 backend.put(1, make_entry(vec![])).await.unwrap();
258 backend.put(2, make_entry(vec![])).await.unwrap();
259 backend.clear().await.unwrap();
260 assert_eq!(backend.len(), 0);
261 }
262
263 #[tokio::test]
264 async fn cached_entry_expiry() {
265 let mut entry = make_entry(vec![]);
266 entry.ttl = Duration::from_millis(0);
267 std::thread::sleep(Duration::from_millis(2));
269 assert!(entry.is_expired());
270 }
271}