Skip to main content

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}