television_utils/
cache.rs

1use rustc_hash::{FxBuildHasher, FxHashSet};
2use std::collections::{HashSet, VecDeque};
3use tracing::debug;
4
5/// A ring buffer that also keeps track of the keys it contains to avoid duplicates.
6///
7/// This serves as a backend for the preview cache.
8/// Basic idea:
9/// - When a new key is pushed, if it's already in the buffer, do nothing.
10/// - If the buffer is full, remove the oldest key and push the new key.
11///
12/// # Example
13/// ```rust
14/// use television_utils::cache::RingSet;
15///
16/// let mut ring_set = RingSet::with_capacity(3);
17/// // push 3 values into the ringset
18/// assert_eq!(ring_set.push(1), None);
19/// assert_eq!(ring_set.push(2), None);
20/// assert_eq!(ring_set.push(3), None);
21///
22/// // check that the values are in the buffer
23/// assert!(ring_set.contains(&1));
24/// assert!(ring_set.contains(&2));
25/// assert!(ring_set.contains(&3));
26///
27/// // push an existing value (should do nothing)
28/// assert_eq!(ring_set.push(1), None);
29///
30/// // entries should still be there
31/// assert!(ring_set.contains(&1));
32/// assert!(ring_set.contains(&2));
33/// assert!(ring_set.contains(&3));
34///
35/// // push a new value, should remove the oldest value (1)
36/// assert_eq!(ring_set.push(4), Some(1));
37///
38/// // 1 is no longer there but 2 and 3 remain
39/// assert!(!ring_set.contains(&1));
40/// assert!(ring_set.contains(&2));
41/// assert!(ring_set.contains(&3));
42/// assert!(ring_set.contains(&4));
43/// ```
44#[derive(Debug)]
45pub struct RingSet<T> {
46    ring_buffer: VecDeque<T>,
47    known_keys: FxHashSet<T>,
48    capacity: usize,
49}
50
51const DEFAULT_CAPACITY: usize = 20;
52
53impl<T> Default for RingSet<T>
54where
55    T: Eq + std::hash::Hash + Clone + std::fmt::Debug,
56{
57    fn default() -> Self {
58        RingSet::with_capacity(DEFAULT_CAPACITY)
59    }
60}
61
62impl<T> RingSet<T>
63where
64    T: Eq + std::hash::Hash + Clone + std::fmt::Debug,
65{
66    /// Create a new `RingSet` with the given capacity.
67    pub fn with_capacity(capacity: usize) -> Self {
68        RingSet {
69            ring_buffer: VecDeque::with_capacity(capacity),
70            known_keys: HashSet::with_capacity_and_hasher(
71                capacity,
72                FxBuildHasher,
73            ),
74            capacity,
75        }
76    }
77
78    /// Push a new item to the back of the buffer, removing the oldest item if the buffer is full.
79    /// Returns the item that was removed, if any.
80    /// If the item is already in the buffer, do nothing and return None.
81    pub fn push(&mut self, item: T) -> Option<T> {
82        // If the key is already in the buffer, do nothing
83        if self.contains(&item) {
84            debug!("Key already in ring buffer: {:?}", item);
85            return None;
86        }
87        let mut popped_key = None;
88        // If the buffer is full, remove the oldest key (e.g. pop from the front of the buffer)
89        if self.ring_buffer.len() >= self.capacity {
90            popped_key = self.pop();
91        }
92        // finally, push the new key to the back of the buffer
93        self.ring_buffer.push_back(item.clone());
94        self.known_keys.insert(item);
95        popped_key
96    }
97
98    fn pop(&mut self) -> Option<T> {
99        if let Some(item) = self.ring_buffer.pop_front() {
100            debug!("Removing key from ring buffer: {:?}", item);
101            self.known_keys.remove(&item);
102            Some(item)
103        } else {
104            None
105        }
106    }
107
108    pub fn contains(&self, key: &T) -> bool {
109        self.known_keys.contains(key)
110    }
111
112    /// Returns an iterator that goes from the back to the front of the buffer.
113    pub fn back_to_front(&self) -> impl Iterator<Item = T> {
114        self.ring_buffer.clone().into_iter().rev()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_ring_set() {
124        let mut ring_set = RingSet::with_capacity(3);
125        // push 3 values into the ringset
126        assert_eq!(ring_set.push(1), None);
127        assert_eq!(ring_set.push(2), None);
128        assert_eq!(ring_set.push(3), None);
129
130        // check that the values are in the buffer
131        assert!(ring_set.contains(&1));
132        assert!(ring_set.contains(&2));
133        assert!(ring_set.contains(&3));
134
135        // push an existing value (should do nothing)
136        assert_eq!(ring_set.push(1), None);
137
138        // entries should still be there
139        assert!(ring_set.contains(&1));
140        assert!(ring_set.contains(&2));
141        assert!(ring_set.contains(&3));
142
143        // push a new value, should remove the oldest value (1)
144        assert_eq!(ring_set.push(4), Some(1));
145
146        // 1 is no longer there but 2 and 3 remain
147        assert!(!ring_set.contains(&1));
148        assert!(ring_set.contains(&2));
149        assert!(ring_set.contains(&3));
150        assert!(ring_set.contains(&4));
151
152        // push two new values, should remove 2 and 3
153        assert_eq!(ring_set.push(5), Some(2));
154        assert_eq!(ring_set.push(6), Some(3));
155
156        // 2 and 3 are no longer there but 4, 5 and 6 remain
157        assert!(!ring_set.contains(&2));
158        assert!(!ring_set.contains(&3));
159        assert!(ring_set.contains(&4));
160        assert!(ring_set.contains(&5));
161        assert!(ring_set.contains(&6));
162    }
163}