did_hosting_server/cache.rs
1//! Simple in-memory content cache with TTL-based eviction.
2//!
3//! Used to cache DID document content (`did.jsonl`) to reduce store lookups
4//! on the hot DID resolution path. Entries are evicted on read after TTL expires.
5//! Uses `Arc<Vec<u8>>` to avoid cloning large documents on cache hits.
6
7use std::collections::HashMap;
8use std::sync::{Arc, RwLock};
9use std::time::{Duration, Instant};
10
11struct CacheEntry {
12 data: Arc<Vec<u8>>,
13 inserted_at: Instant,
14}
15
16/// Thread-safe content cache with TTL-based eviction.
17///
18/// Uses `RwLock<HashMap>` — reads only acquire a read lock (no contention
19/// between concurrent resolves). Writes acquire a write lock (infrequent:
20/// only on cache miss or invalidation).
21///
22/// Data is stored behind `Arc` so cache hits return a cheap reference-counted
23/// pointer instead of cloning the entire document.
24pub struct ContentCache {
25 entries: RwLock<HashMap<String, CacheEntry>>,
26 ttl: Duration,
27}
28
29impl ContentCache {
30 /// Create a new cache with the given TTL.
31 pub fn new(ttl: Duration) -> Self {
32 Self {
33 entries: RwLock::new(HashMap::new()),
34 ttl,
35 }
36 }
37
38 /// Get cached content if it exists and hasn't expired.
39 /// Returns an `Arc<Vec<u8>>` — cheap clone (reference count bump only).
40 pub fn get(&self, key: &str) -> Option<Arc<Vec<u8>>> {
41 let entries = self.entries.read().ok()?;
42 let entry = entries.get(key)?;
43 if entry.inserted_at.elapsed() < self.ttl {
44 Some(entry.data.clone()) // Arc clone = atomic ref count increment
45 } else {
46 None // Expired — caller should fetch from store and re-insert
47 }
48 }
49
50 /// Insert or update a cache entry.
51 pub fn insert(&self, key: String, data: Vec<u8>) {
52 if let Ok(mut entries) = self.entries.write() {
53 entries.insert(
54 key,
55 CacheEntry {
56 data: Arc::new(data),
57 inserted_at: Instant::now(),
58 },
59 );
60 }
61 }
62
63 /// Invalidate a cache entry (call on publish/delete).
64 pub fn invalidate(&self, key: &str) {
65 if let Ok(mut entries) = self.entries.write() {
66 entries.remove(key);
67 }
68 }
69
70 /// Remove all expired entries (call periodically from cleanup thread).
71 pub fn evict_expired(&self) {
72 if let Ok(mut entries) = self.entries.write() {
73 entries.retain(|_, entry| entry.inserted_at.elapsed() < self.ttl);
74 }
75 }
76}