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}