identity_kit/resolver/
cache.rs1use core::fmt;
4use std::collections::HashMap;
5use std::sync::{PoisonError, RwLock};
6use std::time::{Duration, Instant};
7
8use crate::did::{Did, ResolutionResult};
9
10pub struct DocumentCache {
39 ttl: Duration,
40 capacity: usize,
41 entries: RwLock<HashMap<Did, Entry>>,
42}
43
44struct Entry {
45 result: ResolutionResult,
46 expires_at: Option<Instant>,
48}
49
50impl Entry {
51 fn is_live(&self, now: Instant) -> bool {
52 self.expires_at.is_none_or(|expires_at| expires_at > now)
53 }
54}
55
56impl DocumentCache {
57 pub const DEFAULT_CAPACITY: usize = 1024;
59
60 pub fn new(ttl: Duration) -> Self {
63 Self::with_capacity(ttl, Self::DEFAULT_CAPACITY)
64 }
65
66 pub fn with_capacity(ttl: Duration, capacity: usize) -> Self {
68 Self {
69 ttl,
70 capacity: capacity.max(1),
71 entries: RwLock::new(HashMap::new()),
72 }
73 }
74
75 pub fn ttl(&self) -> Duration {
77 self.ttl
78 }
79
80 pub fn capacity(&self) -> usize {
82 self.capacity
83 }
84
85 pub fn get(&self, did: &Did) -> Option<ResolutionResult> {
87 let now = Instant::now();
88 let entries = self.entries.read().unwrap_or_else(PoisonError::into_inner);
89 entries
90 .get(did)
91 .filter(|entry| entry.is_live(now))
92 .map(|entry| entry.result.clone())
93 }
94
95 pub fn insert(&self, did: Did, result: ResolutionResult) {
97 let now = Instant::now();
98 let mut entries = self.entries.write().unwrap_or_else(PoisonError::into_inner);
99 if !entries.contains_key(&did) && entries.len() >= self.capacity {
100 entries.retain(|_, entry| entry.is_live(now));
101 if entries.len() >= self.capacity {
102 let soonest = entries
104 .iter()
105 .min_by_key(|(_, entry)| entry.expires_at)
106 .map(|(did, _)| did.clone());
107 if let Some(soonest) = soonest {
108 entries.remove(&soonest);
109 }
110 }
111 }
112 entries.insert(
113 did,
114 Entry {
115 result,
116 expires_at: now.checked_add(self.ttl),
117 },
118 );
119 }
120
121 pub fn remove(&self, did: &Did) -> Option<ResolutionResult> {
123 let now = Instant::now();
124 let mut entries = self.entries.write().unwrap_or_else(PoisonError::into_inner);
125 entries
126 .remove(did)
127 .filter(|entry| entry.is_live(now))
128 .map(|entry| entry.result)
129 }
130
131 pub fn clear(&self) {
133 self.entries
134 .write()
135 .unwrap_or_else(PoisonError::into_inner)
136 .clear();
137 }
138
139 pub fn purge_expired(&self) -> usize {
141 let now = Instant::now();
142 let mut entries = self.entries.write().unwrap_or_else(PoisonError::into_inner);
143 let before = entries.len();
144 entries.retain(|_, entry| entry.is_live(now));
145 before - entries.len()
146 }
147
148 pub fn len(&self) -> usize {
150 let now = Instant::now();
151 self.entries
152 .read()
153 .unwrap_or_else(PoisonError::into_inner)
154 .values()
155 .filter(|entry| entry.is_live(now))
156 .count()
157 }
158
159 pub fn is_empty(&self) -> bool {
161 self.len() == 0
162 }
163}
164
165impl fmt::Debug for DocumentCache {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 f.debug_struct("DocumentCache")
168 .field("ttl", &self.ttl)
169 .field("capacity", &self.capacity)
170 .field("len", &self.len())
171 .finish_non_exhaustive()
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::did::DidDocument;
179
180 #[test]
181 fn an_entry_expires_at_its_instant() {
182 let did: Did = "did:example:123".parse().unwrap();
183 let now = Instant::now();
184 let entry = Entry {
185 result: ResolutionResult::new(DidDocument::new(did)),
186 expires_at: Some(now),
187 };
188 assert!(entry.is_live(now.checked_sub(Duration::from_nanos(1)).unwrap()));
189 assert!(!entry.is_live(now));
190 }
191}