oxistore_cache/lru.rs
1/// Pure-Rust LRU cache backed by `hashlink::LinkedHashMap`.
2///
3/// The map treats the *back* of the linked list as most-recently-used (MRU)
4/// and the *front* as least-recently-used (LRU), following `LinkedHashMap`'s
5/// ordering convention (back = most recently inserted/promoted).
6///
7/// On `get` the entry is moved to the back (MRU) so frequently accessed items
8/// are protected from eviction. On `put`, if the cache is already at capacity
9/// the front entry (LRU) is evicted before inserting the new item.
10///
11/// TTL support is provided via [`LruCache::put_with_ttl`]. Expiry is checked
12/// lazily on every access — no background thread is needed.
13use std::hash::Hash;
14
15use hashlink::LinkedHashMap;
16
17use crate::{Cache, CacheEntry};
18
19/// An LRU cache with a fixed capacity and optional per-entry TTL.
20///
21/// # Type parameters
22///
23/// - `K`: key type -- must be `Eq + Hash`.
24/// - `V`: value type.
25pub struct LruCache<K, V> {
26 map: LinkedHashMap<K, CacheEntry<V>, ahash::RandomState>,
27 cap: usize,
28}
29
30impl<K, V> LruCache<K, V>
31where
32 K: Eq + Hash,
33{
34 /// Create a new `LruCache` with the given capacity.
35 ///
36 /// A capacity of `0` is valid but every `put` will immediately evict the
37 /// newly inserted entry on the next insertion.
38 #[must_use]
39 pub fn new(cap: usize) -> Self {
40 LruCache {
41 map: LinkedHashMap::with_hasher(ahash::RandomState::default()),
42 cap,
43 }
44 }
45
46 /// Insert `key` -> `value` with an expiry time derived from the raw entry.
47 ///
48 /// Returns the evicted value (for new-key insertions at capacity) or `None`
49 /// (for key updates — the old value is not returned, matching original behavior).
50 fn insert_entry(&mut self, key: K, entry: CacheEntry<V>) -> Option<V> {
51 if self.map.contains_key(&key) {
52 // Key update: replace in place and promote to MRU. No eviction.
53 self.map.insert(key, entry);
54 return None;
55 }
56
57 // New key: evict LRU if at capacity.
58 let evicted = if self.cap > 0 && self.map.len() >= self.cap {
59 self.map.pop_front().map(|(_, e)| e.value)
60 } else {
61 None
62 };
63
64 self.map.insert(key, entry);
65 evicted
66 }
67
68 /// Return the value associated with `key`, moving it to the MRU position.
69 ///
70 /// If the entry has expired, it is removed and `None` is returned.
71 pub fn get(&mut self, key: &K) -> Option<&V> {
72 // First check expiry without promoting.
73 let expired = self.map.get(key).map(|e| e.is_expired()).unwrap_or(false);
74
75 if expired {
76 self.map.remove(key);
77 return None;
78 }
79
80 // Move the entry to back (MRU) using the raw entry API.
81 match self.map.raw_entry_mut().from_key(key) {
82 hashlink::linked_hash_map::RawEntryMut::Occupied(mut occ) => {
83 occ.to_back();
84 Some(&occ.into_mut().value)
85 }
86 hashlink::linked_hash_map::RawEntryMut::Vacant(_) => None,
87 }
88 }
89
90 /// Insert or update `key` -> `value` (no TTL), evicting the LRU entry if at capacity.
91 ///
92 /// - If `key` already exists: updates the value and promotes to MRU; no eviction.
93 /// - If `key` is new and the cache is at capacity: evicts the LRU entry first.
94 ///
95 /// Returns the evicted value (not the replaced value on key updates).
96 pub fn put(&mut self, key: K, value: V) -> Option<V> {
97 self.insert_entry(key, CacheEntry::new(value))
98 }
99
100 /// Insert or update `key` -> `value` with a TTL.
101 pub fn put_with_ttl(&mut self, key: K, value: V, ttl: std::time::Duration) -> Option<V> {
102 self.insert_entry(key, CacheEntry::with_ttl(value, ttl))
103 }
104
105 /// Return the number of entries currently in the cache (including unexpired).
106 #[must_use]
107 pub fn len(&self) -> usize {
108 self.map.len()
109 }
110
111 /// Return `true` if the cache contains no entries.
112 #[must_use]
113 pub fn is_empty(&self) -> bool {
114 self.map.is_empty()
115 }
116
117 /// Return the maximum capacity of the cache.
118 #[must_use]
119 pub fn cap(&self) -> usize {
120 self.cap
121 }
122
123 /// Return `true` if `key` is present and not expired (without promoting).
124 #[must_use]
125 pub fn contains(&self, key: &K) -> bool {
126 match self.map.get(key) {
127 Some(e) => !e.is_expired(),
128 None => false,
129 }
130 }
131
132 /// Read the value for `key` without promoting it to MRU.
133 ///
134 /// Returns `None` if the key is not present or has expired.
135 /// Note: this method takes `&self`, so it cannot remove the expired entry;
136 /// the removal happens lazily on the next mutable access.
137 #[must_use]
138 pub fn peek(&self, key: &K) -> Option<&V> {
139 self.map
140 .get(key)
141 .and_then(|e| if e.is_expired() { None } else { Some(&e.value) })
142 }
143
144 /// Remove the entry for `key`, returning its value if present and not expired.
145 pub fn remove(&mut self, key: &K) -> Option<V> {
146 self.map.remove(key).map(|e| e.value)
147 }
148
149 /// Remove all entries from the cache.
150 pub fn clear(&mut self) {
151 self.map.clear();
152 }
153
154 /// Dynamically resize the cache capacity.
155 ///
156 /// If `new_cap` is smaller than the current length, LRU entries are
157 /// evicted until the length fits.
158 pub fn resize(&mut self, new_cap: usize) {
159 self.cap = new_cap;
160 while self.cap > 0 && self.map.len() > self.cap {
161 self.map.pop_front();
162 }
163 }
164
165 /// Return an iterator over `(&K, &V)` pairs in LRU-to-MRU order
166 /// (front to back, oldest to newest).
167 ///
168 /// Expired entries are included in the iterator; callers should check
169 /// expiry if needed.
170 pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
171 self.map.iter().map(|(k, e)| (k, &e.value))
172 }
173
174 /// Return an entry handle for `key`.
175 ///
176 /// - [`Entry::Occupied`] if the key is present (and not expired).
177 /// - [`Entry::Vacant`] otherwise.
178 ///
179 /// The entry API mirrors `HashMap::entry`, enabling efficient
180 /// insert-or-modify patterns without double hash lookups in most cases.
181 pub fn entry(&mut self, key: K) -> Entry<'_, K, V>
182 where
183 K: Clone,
184 {
185 if self.contains(&key) {
186 Entry::Occupied(OccupiedEntry { cache: self, key })
187 } else {
188 Entry::Vacant(VacantEntry { cache: self, key })
189 }
190 }
191}
192
193impl<K: std::fmt::Debug, V> std::fmt::Debug for LruCache<K, V> {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.debug_struct("LruCache")
196 .field("capacity", &self.cap)
197 .field("length", &self.map.len())
198 .finish_non_exhaustive()
199 }
200}
201
202impl<K: Eq + Hash, V> From<Vec<(K, V)>> for LruCache<K, V> {
203 /// Build an `LruCache` from a vector of `(key, value)` pairs.
204 ///
205 /// The cache capacity is set to `pairs.len().max(1)` so that all pairs
206 /// fit without eviction.
207 fn from(pairs: Vec<(K, V)>) -> Self {
208 let cap = pairs.len().max(1);
209 let mut cache = LruCache::new(cap);
210 for (k, v) in pairs {
211 cache.put(k, v);
212 }
213 cache
214 }
215}
216
217// ---------------------------------------------------------------------------
218// Entry API
219// ---------------------------------------------------------------------------
220
221/// A view into a single entry in an [`LruCache`], either occupied or vacant.
222///
223/// Created by [`LruCache::entry`].
224pub enum Entry<'a, K, V> {
225 /// The cache has an entry for the key.
226 Occupied(OccupiedEntry<'a, K, V>),
227 /// The cache does not have an entry for the key.
228 Vacant(VacantEntry<'a, K, V>),
229}
230
231/// A handle to an occupied entry in an [`LruCache`].
232pub struct OccupiedEntry<'a, K, V> {
233 cache: &'a mut LruCache<K, V>,
234 key: K,
235}
236
237/// A handle to a vacant entry in an [`LruCache`].
238pub struct VacantEntry<'a, K, V> {
239 cache: &'a mut LruCache<K, V>,
240 key: K,
241}
242
243impl<'a, K: Eq + Hash + Clone, V> OccupiedEntry<'a, K, V> {
244 /// Return a reference to the entry's key.
245 pub fn key(&self) -> &K {
246 &self.key
247 }
248
249 /// Return a reference to the entry's value without promoting to MRU.
250 ///
251 /// # Panics
252 ///
253 /// Panics if the entry is no longer present (which cannot happen under
254 /// normal use, since the entry was checked before constructing this handle).
255 pub fn get(&self) -> &V {
256 self.cache.peek(&self.key).expect("occupied entry exists")
257 }
258
259 /// Remove the entry and return its value.
260 ///
261 /// # Panics
262 ///
263 /// Panics if the entry is no longer present (which cannot happen under
264 /// normal use).
265 pub fn remove(self) -> V {
266 self.cache.remove(&self.key).expect("occupied entry exists")
267 }
268}
269
270impl<'a, K: Eq + Hash + Clone, V> VacantEntry<'a, K, V> {
271 /// Return a reference to the key that would be inserted.
272 pub fn key(&self) -> &K {
273 &self.key
274 }
275
276 /// Insert `value` under this key and return a reference to the stored value.
277 pub fn insert(self, value: V) -> &'a V {
278 let VacantEntry { cache, key } = self;
279 cache.put(key.clone(), value);
280 cache.peek(&key).expect("key was just inserted")
281 }
282}
283
284impl<K, V> Cache<K, V> for LruCache<K, V>
285where
286 K: Eq + Hash,
287{
288 fn get(&mut self, key: &K) -> Option<&V> {
289 LruCache::get(self, key)
290 }
291
292 fn put(&mut self, key: K, value: V) -> Option<V> {
293 LruCache::put(self, key, value)
294 }
295
296 fn put_with_ttl(&mut self, key: K, value: V, ttl: std::time::Duration) -> Option<V> {
297 LruCache::put_with_ttl(self, key, value, ttl)
298 }
299
300 fn len(&self) -> usize {
301 self.map.len()
302 }
303
304 fn cap(&self) -> usize {
305 self.cap
306 }
307
308 fn remove(&mut self, key: &K) -> Option<V> {
309 LruCache::remove(self, key)
310 }
311
312 fn clear(&mut self) {
313 LruCache::clear(self);
314 }
315
316 fn peek(&self, key: &K) -> Option<&V> {
317 LruCache::peek(self, key)
318 }
319
320 fn contains_key(&self, key: &K) -> bool {
321 LruCache::contains(self, key)
322 }
323
324 fn resize(&mut self, new_cap: usize) {
325 LruCache::resize(self, new_cap);
326 }
327
328 fn values(&self) -> Vec<&V> {
329 self.map
330 .values()
331 .filter(|e| !e.is_expired())
332 .map(|e| &e.value)
333 .collect()
334 }
335}