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