use dashmap::DashMap; use std::sync::Arc; use std::time::{Duration, Instant}; #[derive(Clone, Debug)] struct CachedHandle { handle: String, cached_at: Instant, } /// A thread-safe cache for DID-to-handle resolutions with TTL expiration. #[derive(Clone)] pub struct HandleCache { cache: Arc>, ttl: Duration, } impl Default for HandleCache { fn default() -> Self { Self::new() } } impl HandleCache { /// Creates a new HandleCache with a default TTL of 1 hour. pub fn new() -> Self { Self::with_ttl(Duration::from_secs(3600)) } /// Creates a new HandleCache with a custom TTL. pub fn with_ttl(ttl: Duration) -> Self { Self { cache: Arc::new(DashMap::new()), ttl, } } /// Gets a cached handle for the given DID, if it exists and hasn't expired. pub fn get(&self, did: &str) -> Option { let entry = self.cache.get(did)?; if entry.cached_at.elapsed() > self.ttl { drop(entry); self.cache.remove(did); return None; } Some(entry.handle.clone()) } /// Inserts a DID-to-handle mapping into the cache. pub fn insert(&self, did: String, handle: String) { self.cache.insert( did, CachedHandle { handle, cached_at: Instant::now(), }, ); } /// Removes expired entries from the cache. /// Call this periodically to prevent memory growth. pub fn cleanup(&self) { self.cache.retain(|_, v| v.cached_at.elapsed() <= self.ttl); } /// Returns the number of entries in the cache. pub fn len(&self) -> usize { self.cache.len() } /// Returns true if the cache is empty. pub fn is_empty(&self) -> bool { self.cache.is_empty() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_cache_insert_and_get() { let cache = HandleCache::new(); cache.insert("did:plc:test".into(), "test.handle.com".into()); assert_eq!(cache.get("did:plc:test"), Some("test.handle.com".into())); } #[test] fn test_cache_miss() { let cache = HandleCache::new(); assert_eq!(cache.get("did:plc:nonexistent"), None); } #[test] fn test_cache_expiration() { let cache = HandleCache::with_ttl(Duration::from_millis(1)); cache.insert("did:plc:test".into(), "test.handle.com".into()); std::thread::sleep(Duration::from_millis(10)); assert_eq!(cache.get("did:plc:test"), None); } }