reflex/cache/
l1.rs

1//! L1 exact-match cache (in-memory).
2//!
3//! L1 uses a BLAKE3 hash of the prompt (plus tenant prefix) as the key and stores an
4//! [`crate::storage::mmap::MmapFileHandle`] to the persisted entry payload.
5
6use moka::sync::Cache;
7use std::sync::Arc;
8
9use super::types::ReflexStatus;
10use crate::hashing::hash_prompt;
11use crate::storage::mmap::MmapFileHandle;
12
13/// Result of an L1 lookup (exact hash match).
14#[derive(Debug, Clone)]
15pub struct L1LookupResult {
16    handle: MmapFileHandle,
17    hash: [u8; 32],
18}
19
20impl L1LookupResult {
21    /// Returns the [`ReflexStatus`] for this lookup result.
22    #[inline]
23    pub fn status(&self) -> ReflexStatus {
24        ReflexStatus::HitL1Exact
25    }
26
27    /// Returns the underlying mmap handle.
28    #[inline]
29    pub fn handle(&self) -> &MmapFileHandle {
30        &self.handle
31    }
32
33    /// Consumes the result and returns the underlying mmap handle.
34    #[inline]
35    pub fn into_handle(self) -> MmapFileHandle {
36        self.handle
37    }
38
39    /// Returns the 32-byte BLAKE3 prompt hash used as the key.
40    #[inline]
41    pub fn hash(&self) -> &[u8; 32] {
42        &self.hash
43    }
44
45    /// Returns the raw bytes of the mmap'd payload.
46    #[inline]
47    pub fn as_slice(&self) -> &[u8] {
48        self.handle.as_slice()
49    }
50}
51
52/// In-memory exact-match cache keyed by prompt hash.
53pub struct L1Cache {
54    entries: Cache<[u8; 32], MmapFileHandle>,
55}
56
57impl L1Cache {
58    const DEFAULT_CAPACITY: u64 = 10_000;
59
60    /// Creates a cache with the default capacity.
61    #[inline]
62    pub fn new() -> Self {
63        Self::with_capacity(Self::DEFAULT_CAPACITY)
64    }
65
66    /// Creates a cache with a max entry capacity (LRU eviction).
67    #[inline]
68    pub fn with_capacity(capacity: u64) -> Self {
69        Self {
70            entries: Cache::builder().max_capacity(capacity).build(),
71        }
72    }
73
74    /// Looks up a prompt by hashing it with [`hash_prompt`].
75    #[inline]
76    pub fn lookup(&self, prompt: &str) -> Option<L1LookupResult> {
77        let hash = hash_prompt(prompt);
78        self.lookup_by_hash(&hash)
79    }
80
81    /// Looks up an entry by a precomputed 32-byte hash.
82    #[inline]
83    pub fn lookup_by_hash(&self, hash: &[u8; 32]) -> Option<L1LookupResult> {
84        self.entries.get(hash).map(|handle| L1LookupResult {
85            handle,
86            hash: *hash,
87        })
88    }
89
90    /// Inserts a prompt → handle mapping and returns the computed hash.
91    #[inline]
92    pub fn insert(&self, prompt: &str, handle: MmapFileHandle) -> [u8; 32] {
93        let hash = hash_prompt(prompt);
94        self.entries.insert(hash, handle);
95        hash
96    }
97
98    /// Inserts a precomputed hash → handle mapping.
99    #[inline]
100    pub fn insert_by_hash(&self, hash: [u8; 32], handle: MmapFileHandle) {
101        self.entries.insert(hash, handle);
102    }
103
104    /// Removes an entry by hash.
105    #[inline]
106    pub fn remove(&self, hash: &[u8; 32]) -> Option<MmapFileHandle> {
107        self.entries.remove(hash)
108    }
109
110    /// Removes an entry by prompt (hashing it first).
111    #[inline]
112    pub fn remove_prompt(&self, prompt: &str) -> Option<MmapFileHandle> {
113        let hash = hash_prompt(prompt);
114        self.remove(&hash)
115    }
116
117    /// Returns the number of cached entries.
118    #[inline]
119    pub fn len(&self) -> u64 {
120        self.entries.entry_count()
121    }
122
123    /// Returns `true` if the cache is empty.
124    #[inline]
125    pub fn is_empty(&self) -> bool {
126        self.entries.entry_count() == 0
127    }
128
129    /// Clears all entries.
130    #[inline]
131    pub fn clear(&self) {
132        self.entries.invalidate_all();
133    }
134
135    /// Returns `true` if the cache contains the given hash.
136    #[inline]
137    pub fn contains_hash(&self, hash: &[u8; 32]) -> bool {
138        self.entries.contains_key(hash)
139    }
140
141    /// Returns `true` if the cache contains the given prompt.
142    #[inline]
143    pub fn contains_prompt(&self, prompt: &str) -> bool {
144        let hash = hash_prompt(prompt);
145        self.contains_hash(&hash)
146    }
147
148    /// Runs any pending maintenance tasks in the underlying cache.
149    #[inline]
150    pub fn run_pending_tasks(&self) {
151        self.entries.run_pending_tasks();
152    }
153
154    /// Returns an iterator of currently stored hashes.
155    pub fn hashes(&self) -> impl Iterator<Item = [u8; 32]> {
156        self.entries.iter().map(|(k, _)| *k)
157    }
158}
159
160impl Default for L1Cache {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166impl std::fmt::Debug for L1Cache {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("L1Cache")
169            .field("entries", &self.entries.entry_count())
170            .finish()
171    }
172}
173
174#[derive(Clone)]
175/// Shared handle to an [`L1Cache`].
176pub struct L1CacheHandle {
177    inner: Arc<L1Cache>,
178}
179
180impl L1CacheHandle {
181    /// Creates a new handle with default capacity.
182    #[inline]
183    pub fn new() -> Self {
184        Self {
185            inner: Arc::new(L1Cache::new()),
186        }
187    }
188
189    /// Creates a new handle with a specific capacity.
190    #[inline]
191    pub fn with_capacity(capacity: usize) -> Self {
192        Self {
193            inner: Arc::new(L1Cache::with_capacity(capacity as u64)),
194        }
195    }
196
197    /// Looks up a prompt.
198    #[inline]
199    pub fn lookup(&self, prompt: &str) -> Option<L1LookupResult> {
200        self.inner.lookup(prompt)
201    }
202
203    /// Looks up by precomputed hash.
204    #[inline]
205    pub fn lookup_by_hash(&self, hash: &[u8; 32]) -> Option<L1LookupResult> {
206        self.inner.lookup_by_hash(hash)
207    }
208
209    /// Inserts a prompt → handle mapping and returns the computed hash.
210    #[inline]
211    pub fn insert(&self, prompt: &str, handle: MmapFileHandle) -> [u8; 32] {
212        self.inner.insert(prompt, handle)
213    }
214
215    /// Inserts a hash → handle mapping.
216    #[inline]
217    pub fn insert_by_hash(&self, hash: [u8; 32], handle: MmapFileHandle) {
218        self.inner.insert_by_hash(hash, handle)
219    }
220
221    /// Removes an entry by hash.
222    #[inline]
223    pub fn remove(&self, hash: &[u8; 32]) -> Option<MmapFileHandle> {
224        self.inner.remove(hash)
225    }
226
227    /// Removes an entry by prompt.
228    #[inline]
229    pub fn remove_prompt(&self, prompt: &str) -> Option<MmapFileHandle> {
230        self.inner.remove_prompt(prompt)
231    }
232
233    /// Returns the number of entries.
234    #[inline]
235    pub fn len(&self) -> usize {
236        self.inner.len() as usize
237    }
238
239    /// Returns `true` if empty.
240    #[inline]
241    pub fn is_empty(&self) -> bool {
242        self.inner.is_empty()
243    }
244
245    /// Clears all entries.
246    #[inline]
247    pub fn clear(&self) {
248        self.inner.clear();
249    }
250
251    /// Returns `true` if the cache contains the given hash.
252    #[inline]
253    pub fn contains_hash(&self, hash: &[u8; 32]) -> bool {
254        self.inner.contains_hash(hash)
255    }
256
257    /// Returns `true` if the cache contains the given prompt.
258    #[inline]
259    pub fn contains_prompt(&self, prompt: &str) -> bool {
260        self.inner.contains_prompt(prompt)
261    }
262
263    /// Runs any pending maintenance tasks.
264    #[inline]
265    pub fn run_pending_tasks(&self) {
266        self.inner.run_pending_tasks();
267    }
268
269    /// Returns the number of strong references to the underlying cache.
270    #[inline]
271    pub fn strong_count(&self) -> usize {
272        Arc::strong_count(&self.inner)
273    }
274}
275
276impl Default for L1CacheHandle {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282impl std::fmt::Debug for L1CacheHandle {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        f.debug_struct("L1CacheHandle")
285            .field("strong_count", &self.strong_count())
286            .finish()
287    }
288}