Skip to main content

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}