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}