fifo_cache/lib.rs
1//! A minimalist FIFO (First In, First Out) cache with TTL (Time To Live) support.
2//!
3//! # Example
4//!
5//! ```
6//! use fifo_cache::FifoCache;
7//! use std::time::Duration;
8//!
9//! let mut cache = FifoCache::new(100, Duration::from_secs(60));
10//! cache.insert("key1", "value1");
11//!
12//! if let Some(value) = cache.get(&"key1") {
13//! println!("Found: {}", value);
14//! }
15//! ```
16
17use std::borrow::Borrow;
18use std::collections::{hash_map, HashMap, VecDeque};
19use std::time::{Duration, Instant};
20
21/// A cache entry that stores a value along with its expiration time.
22#[derive(Debug, Clone)]
23struct CacheEntry<V> {
24 value: V,
25 expires_at: Instant,
26}
27
28/// A FIFO cache with TTL support.
29///
30/// This cache maintains insertion order and evicts the oldest entries when
31/// the maximum size is reached. Entries also expire after the specified TTL.
32///
33/// Note that:
34/// - reinserting an existing entry will not move it back to the front
35/// - the maximum capacity may *very briefly* be exceeded by 1
36#[derive(Debug)]
37pub struct FifoCache<K, V> {
38 map: HashMap<K, CacheEntry<V>>,
39 order: VecDeque<K>,
40 max_size: usize,
41 default_ttl: Duration,
42}
43
44impl<K, V> FifoCache<K, V>
45where
46 K: Clone + Eq + std::hash::Hash,
47 V: Clone,
48{
49 /// Creates a new FIFO cache with the specified maximum size and default TTL.
50 ///
51 /// # Arguments
52 ///
53 /// * `max_size` - Maximum number of entries the cache can hold
54 /// * `default_ttl` - Default time-to-live for cache entries
55 pub fn new(max_size: usize, default_ttl: Duration) -> Self {
56 Self {
57 map: HashMap::with_capacity(max_size + 1),
58 order: VecDeque::with_capacity(max_size + 1),
59 max_size,
60 default_ttl,
61 }
62 }
63
64 /// Retrieves a value from the cache if it exists and hasn't expired.
65 ///
66 /// # Arguments
67 ///
68 /// * `key` - The key to look up
69 ///
70 /// # Returns
71 ///
72 /// `Some(&V)` if the key exists and hasn't expired, `None` otherwise.
73 ///
74 /// # Example
75 ///
76 /// ```
77 /// use fifo_cache::FifoCache;
78 /// use std::time::Duration;
79 ///
80 /// let mut cache = FifoCache::new(100, Duration::from_secs(60));
81 /// cache.insert("my_key", "my_value");
82 /// let value = cache.get(&"my_key");
83 /// assert_eq!(value, Some(&"my_value"));
84 /// ```
85 pub fn get<Q>(&self, key: &Q) -> Option<&V>
86 where
87 K: Borrow<Q>,
88 Q: ?Sized + std::hash::Hash + Eq,
89 {
90 let now = Instant::now();
91 self.map.get(key)
92 .filter(|entry| entry.expires_at > now)
93 .map(|entry| &entry.value)
94 }
95
96 /// Inserts a key-value pair into the cache.
97 ///
98 /// If the key already exists, its value is updated and TTL is refreshed.
99 /// If the cache is at capacity, the oldest entry is evicted.
100 ///
101 /// # Arguments
102 ///
103 /// * `key` - The key to insert
104 /// * `value` - The value to associate with the key
105 pub fn insert(&mut self, key: K, value: V) {
106 let expires_at = Instant::now() + self.default_ttl;
107
108 match self.map.entry(key.clone()) {
109 hash_map::Entry::Occupied(mut entry) => {
110 // Entry exists, just update it
111 entry.insert(CacheEntry { value, expires_at });
112 }
113 hash_map::Entry::Vacant(entry) => {
114 // Entry doesn't exist, insert it then prune
115 entry.insert(CacheEntry { value, expires_at });
116 self.order.push_back(key);
117 self.prune();
118 }
119 }
120 }
121
122 /// Inserts a key-value pair into the cache using types that can be converted into the key and value types.
123 ///
124 /// This is a convenience wrapper around [`insert`](Self::insert) that accepts any types implementing
125 /// `Into<K>` and `Into<V>`. Note that using only `insert_lazy` prevents type inference, so you'll
126 /// need to explicitly specify the cache types:
127 ///
128 /// ```
129 /// use fifo_cache::FifoCache;
130 /// use std::time::Duration;
131 ///
132 /// // With insert - types are inferred
133 /// let mut cache = FifoCache::new(100, Duration::from_secs(60));
134 /// cache.insert("key", "value"); // FifoCache<&str, &str>
135 ///
136 /// // With insert_lazy - types must be specified
137 /// let mut cache: FifoCache<String, String> = FifoCache::new(100, Duration::from_secs(60));
138 /// cache.insert_lazy("key", "value"); // &str -> String conversion
139 /// ```
140 pub fn insert_lazy<Kinto: Into<K>, Vinto: Into<V>>(&mut self, key: Kinto, value: Vinto) {
141 self.insert(key.into(), value.into())
142 }
143
144 /// Removes a key from the cache.
145 ///
146 /// # Arguments
147 ///
148 /// * `key` - The key to remove
149 ///
150 /// # Returns
151 ///
152 /// `Some(V)` if the key existed, `None` otherwise.
153 pub fn remove<Q>(&mut self, key: &Q) -> Option<V>
154 where
155 K: Borrow<Q>,
156 Q: ?Sized + std::hash::Hash + Eq,
157 {
158 if let Some(entry) = self.map.remove(key) {
159 self.order.retain(|k| k.borrow() != key);
160 Some(entry.value)
161 } else {
162 None
163 }
164 }
165
166 /// Returns the current number of entries in the cache.
167 pub fn len(&self) -> usize {
168 self.map.len()
169 }
170
171 /// Returns `true` if the cache is empty.
172 pub fn is_empty(&self) -> bool {
173 self.map.is_empty()
174 }
175
176 /// Removes all expired entries from the cache.
177 pub fn cleanup_expired(&mut self) {
178 let now = Instant::now();
179 self.order.retain(|key| {
180 if let Some(entry) = self.map.get(key) {
181 if entry.expires_at <= now {
182 self.map.remove(key);
183 false
184 } else {
185 true
186 }
187 } else {
188 false
189 }
190 });
191 }
192
193 /// Clears all entries from the cache.
194 pub fn clear(&mut self) {
195 self.map.clear();
196 self.order.clear();
197 }
198
199 /// Returns the maximum capacity of the cache.
200 pub fn max_size(&self) -> usize {
201 self.max_size
202 }
203
204 /// Sets the maximum capacity of the cache.
205 ///
206 /// # Arguments
207 ///
208 /// * `max_size` - The new maximum number of entries the cache can hold
209 /// * `prune` - Whether or not to immediately prune the excess number of entries, in case the update causes
210 /// the cache to be above capacity
211 pub fn set_max_size(&mut self, max_size: usize, prune: bool) {
212 self.max_size = max_size;
213 if prune {
214 self.prune();
215 }
216 }
217
218 /// Returns the default TTL for cache entries.
219 pub fn default_ttl(&self) -> Duration {
220 self.default_ttl
221 }
222
223 /// Sets the default TTL for cache entries.
224 /// Note that this will only affect entries that get inserted or updated after the change.
225 /// Existing entries will keep their TTL until they expire.
226 ///
227 /// # Arguments
228 ///
229 /// * `default_ttl` - The new default time-to-live for cache entries
230 pub fn set_default_ttl(&mut self, default_ttl: Duration) {
231 self.default_ttl = default_ttl;
232 }
233
234 // Evicts oldest entries if at capacity
235 fn prune(&mut self) {
236 while self.order.len() > self.max_size {
237 if let Some(old_key) = self.order.pop_front() {
238 self.map.remove(&old_key);
239 }
240 }
241 }
242}
243
244impl<K, V> Default for FifoCache<K, V>
245where
246 K: Clone + Eq + std::hash::Hash,
247 V: Clone,
248{
249 /// Creates a cache with capacity 1000 and TTL of 5 minutes.
250 /// This is *extremely* arbitrary and you most likely want to use your own, use-case-adjusted settings rather than this default.
251 fn default() -> Self {
252 Self::new(1000, Duration::from_secs(300))
253 }
254}