Skip to main content

ferroui_skia/
two_level_cache.rs

1/// Compares cache keys.
2type KeyComparer<TKey> = Box<dyn Fn(&TKey, &TKey) -> bool + Send>;
3
4/// Receives the values that leave the cache.
5type EvictionAction<TValue> = Box<dyn Fn(&TValue) + Send>;
6
7/// Provides a lightweight two-level cache for storing key-value pairs,
8/// supporting fast retrieval and optional eviction handling.
9///
10/// The cache maintains a primary entry for the most recently added item and a
11/// secondary array for additional items, up to the specified capacity. When
12/// the cache exceeds its capacity, evicted values are passed to the eviction
13/// action. It is optimized for scenarios where a small number of items are
14/// frequently accessed.
15pub struct TwoLevelCache<TKey, TValue: Clone> {
16    secondary_size: usize,
17    primary: Option<(TKey, TValue)>,
18    secondary: Option<Vec<(TKey, TValue)>>,
19    eviction_action: Option<EvictionAction<TValue>>,
20    comparer: KeyComparer<TKey>,
21}
22
23impl<TKey: PartialEq + Clone + 'static, TValue: Clone> TwoLevelCache<TKey, TValue> {
24    /// Creates a cache with the default secondary capacity (3), no eviction
25    /// action and the default comparer.
26    pub fn new() -> Self {
27        Self::with_options(3, None, None)
28    }
29
30    /// Creates a cache with the given secondary capacity.
31    ///
32    /// # Panics
33    ///
34    /// When `secondary_size` is negative (upstream throws
35    /// `ArgumentOutOfRangeException`).
36    pub fn with_secondary_size(secondary_size: i32) -> Self {
37        Self::with_options(secondary_size, None, None)
38    }
39
40    /// Creates a cache with the given secondary capacity, eviction action and
41    /// key comparer (`None` compares with `==`).
42    ///
43    /// # Panics
44    ///
45    /// When `secondary_size` is negative (upstream throws
46    /// `ArgumentOutOfRangeException`).
47    pub fn with_options(
48        secondary_size: i32,
49        eviction_action: Option<EvictionAction<TValue>>,
50        comparer: Option<KeyComparer<TKey>>,
51    ) -> Self {
52        let Ok(secondary_size) = usize::try_from(secondary_size) else {
53            panic!("secondary_size is out of range: {secondary_size}");
54        };
55
56        Self {
57            secondary_size,
58            primary: None,
59            secondary: None,
60            eviction_action,
61            comparer: comparer.unwrap_or_else(|| Box::new(|a, b| a == b)),
62        }
63    }
64}
65
66impl<TKey: PartialEq + Clone + 'static, TValue: Clone> Default for TwoLevelCache<TKey, TValue> {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl<TKey: Clone, TValue: Clone> TwoLevelCache<TKey, TValue> {
73    /// Looks up the value stored for `key`.
74    pub fn try_get(&self, key: &TKey) -> Option<TValue> {
75        if let Some((primary_key, primary_value)) = &self.primary {
76            if (self.comparer)(primary_key, key) {
77                return Some(primary_value.clone());
78            }
79        }
80
81        if let Some(secondary) = &self.secondary {
82            for (secondary_key, value) in secondary {
83                if (self.comparer)(secondary_key, key) {
84                    return Some(value.clone());
85                }
86            }
87        }
88
89        None
90    }
91
92    /// Returns the value stored for `key`, creating and storing it with
93    /// `factory` when the key is not in the cache.
94    pub fn get_or_add(&mut self, key: TKey, factory: impl FnOnce(&TKey) -> TValue) -> TValue {
95        // Check if the key already exists.
96        if let Some(existing) = self.try_get(&key) {
97            return existing;
98        }
99
100        // The key doesn't exist: create a new value.
101        let value = factory(&key);
102
103        // Primary is empty: store in primary.
104        if self.primary.is_none() {
105            self.primary = Some((key, value.clone()));
106            return value;
107        }
108
109        // No secondary cache configured: replace primary.
110        if self.secondary_size == 0 {
111            if let Some((_, evicted)) = self.primary.replace((key, value.clone())) {
112                self.evict(&evicted);
113            }
114            return value;
115        }
116
117        // Insert the new entry at the front. This maintains insertion order
118        // and evicts the oldest (last) entry when full.
119        let secondary_size = self.secondary_size;
120        let secondary = self.secondary.get_or_insert_with(|| Vec::with_capacity(secondary_size));
121        let evicted = if secondary.len() == secondary_size { secondary.pop() } else { None };
122        secondary.insert(0, (key, value.clone()));
123
124        if let Some((_, evicted)) = evicted {
125            self.evict(&evicted);
126        }
127
128        value
129    }
130
131    /// Removes every entry, passing each value to the eviction action.
132    pub fn clear_and_dispose(&mut self) {
133        if let Some((_, value)) = self.primary.take() {
134            self.evict(&value);
135        }
136
137        if let Some(secondary) = self.secondary.take() {
138            for (_, value) in &secondary {
139                self.evict(value);
140            }
141        }
142    }
143
144    fn evict(&self, value: &TValue) {
145        if let Some(action) = &self.eviction_action {
146            action(value);
147        }
148    }
149}