use core::fmt;
use std::collections::HashMap;
use std::sync::{PoisonError, RwLock};
use std::time::{Duration, Instant};
use crate::did::{Did, ResolutionResult};
pub struct DocumentCache {
ttl: Duration,
capacity: usize,
entries: RwLock<HashMap<Did, Entry>>,
}
struct Entry {
result: ResolutionResult,
expires_at: Option<Instant>,
}
impl Entry {
fn is_live(&self, now: Instant) -> bool {
self.expires_at.is_none_or(|expires_at| expires_at > now)
}
}
impl DocumentCache {
pub const DEFAULT_CAPACITY: usize = 1024;
pub fn new(ttl: Duration) -> Self {
Self::with_capacity(ttl, Self::DEFAULT_CAPACITY)
}
pub fn with_capacity(ttl: Duration, capacity: usize) -> Self {
Self {
ttl,
capacity: capacity.max(1),
entries: RwLock::new(HashMap::new()),
}
}
pub fn ttl(&self) -> Duration {
self.ttl
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn get(&self, did: &Did) -> Option<ResolutionResult> {
let now = Instant::now();
let entries = self.entries.read().unwrap_or_else(PoisonError::into_inner);
entries
.get(did)
.filter(|entry| entry.is_live(now))
.map(|entry| entry.result.clone())
}
pub fn insert(&self, did: Did, result: ResolutionResult) {
let now = Instant::now();
let mut entries = self.entries.write().unwrap_or_else(PoisonError::into_inner);
if !entries.contains_key(&did) && entries.len() >= self.capacity {
entries.retain(|_, entry| entry.is_live(now));
if entries.len() >= self.capacity {
let soonest = entries
.iter()
.min_by_key(|(_, entry)| entry.expires_at)
.map(|(did, _)| did.clone());
if let Some(soonest) = soonest {
entries.remove(&soonest);
}
}
}
entries.insert(
did,
Entry {
result,
expires_at: now.checked_add(self.ttl),
},
);
}
pub fn remove(&self, did: &Did) -> Option<ResolutionResult> {
let now = Instant::now();
let mut entries = self.entries.write().unwrap_or_else(PoisonError::into_inner);
entries
.remove(did)
.filter(|entry| entry.is_live(now))
.map(|entry| entry.result)
}
pub fn clear(&self) {
self.entries
.write()
.unwrap_or_else(PoisonError::into_inner)
.clear();
}
pub fn purge_expired(&self) -> usize {
let now = Instant::now();
let mut entries = self.entries.write().unwrap_or_else(PoisonError::into_inner);
let before = entries.len();
entries.retain(|_, entry| entry.is_live(now));
before - entries.len()
}
pub fn len(&self) -> usize {
let now = Instant::now();
self.entries
.read()
.unwrap_or_else(PoisonError::into_inner)
.values()
.filter(|entry| entry.is_live(now))
.count()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl fmt::Debug for DocumentCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DocumentCache")
.field("ttl", &self.ttl)
.field("capacity", &self.capacity)
.field("len", &self.len())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::did::DidDocument;
#[test]
fn an_entry_expires_at_its_instant() {
let did: Did = "did:example:123".parse().unwrap();
let now = Instant::now();
let entry = Entry {
result: ResolutionResult::new(DidDocument::new(did)),
expires_at: Some(now),
};
assert!(entry.is_live(now.checked_sub(Duration::from_nanos(1)).unwrap()));
assert!(!entry.is_live(now));
}
}