1use std::collections::{BTreeMap, VecDeque};
14use std::sync::Arc;
15
16use crate::{
17 DocumentFingerprint, ErrorCode, FileMakerError, ResolvedScene, ResourceLimits, Result,
18};
19
20pub struct SceneCache {
22 capacity: usize,
23 max_bytes: usize,
24 used_bytes: usize,
25 entries: BTreeMap<DocumentFingerprint, CacheEntry>,
26 insertion_order: VecDeque<DocumentFingerprint>,
27}
28
29struct CacheEntry {
30 scene: Arc<ResolvedScene>,
31 bytes: usize,
32}
33
34impl SceneCache {
35 pub const DEFAULT_MAX_BYTES: usize = 256 * 1024 * 1024;
37
38 pub fn new(capacity: usize) -> Result<Self> {
40 Self::with_byte_capacity(capacity, Self::DEFAULT_MAX_BYTES)
41 }
42
43 pub fn with_byte_capacity(capacity: usize, max_bytes: usize) -> Result<Self> {
45 if capacity == 0 || max_bytes == 0 {
46 return Err(FileMakerError::new(
47 ErrorCode::LimitExceeded,
48 "scene cache entry and byte bounds must be non-zero",
49 ));
50 }
51 Ok(Self {
52 capacity,
53 max_bytes,
54 used_bytes: 0,
55 entries: BTreeMap::new(),
56 insertion_order: VecDeque::new(),
57 })
58 }
59
60 #[must_use]
62 pub fn get(&self, key: &DocumentFingerprint) -> Option<Arc<ResolvedScene>> {
63 self.entries.get(key).map(|entry| entry.scene.clone())
64 }
65
66 pub fn get_or_try_insert_with<F>(
68 &mut self,
69 key: DocumentFingerprint,
70 limits: &ResourceLimits,
71 resolve: F,
72 ) -> Result<Arc<ResolvedScene>>
73 where
74 F: FnOnce() -> Result<ResolvedScene>,
75 {
76 if let Some(scene) = self.get(&key) {
77 return Ok(scene);
78 }
79 self.insert(key, resolve()?, limits)
80 }
81
82 pub fn insert(
84 &mut self,
85 key: DocumentFingerprint,
86 scene: ResolvedScene,
87 limits: &ResourceLimits,
88 ) -> Result<Arc<ResolvedScene>> {
89 if scene.engine_version != crate::ENGINE_VERSION {
90 return Err(cache_error(
91 "cached scene engine version does not match the active engine",
92 ));
93 }
94 let elements = scene
95 .pages
96 .iter()
97 .try_fold(0_usize, |total, page| {
98 total.checked_add(page.elements.len())
99 })
100 .ok_or_else(|| cache_error("cached scene element count overflow"))?;
101 if scene.pages.len() > limits.max_pages || elements > limits.max_elements {
102 return Err(FileMakerError::new(
103 ErrorCode::LimitExceeded,
104 "cached scene exceeds configured page or element budget",
105 ));
106 }
107 if let Some(existing) = self.entries.get(&key) {
108 return Ok(existing.scene.clone());
109 }
110 let bytes = crate::memory::serialized_size(&scene)?;
111 if bytes > self.max_bytes {
112 return Err(FileMakerError::new(
113 ErrorCode::LimitExceeded,
114 "resolved scene exceeds the cache byte budget",
115 ));
116 }
117 while self.entries.len() >= self.capacity
118 || self.used_bytes.saturating_add(bytes) > self.max_bytes
119 {
120 if let Some(oldest) = self.insertion_order.pop_front() {
121 if let Some(entry) = self.entries.remove(&oldest) {
122 self.used_bytes = self.used_bytes.saturating_sub(entry.bytes);
123 }
124 }
125 }
126 let scene = Arc::new(scene);
127 self.entries.insert(
128 key,
129 CacheEntry {
130 scene: scene.clone(),
131 bytes,
132 },
133 );
134 self.used_bytes += bytes;
135 self.insertion_order.push_back(key);
136 Ok(scene)
137 }
138
139 #[must_use]
141 pub fn len(&self) -> usize {
142 self.entries.len()
143 }
144
145 #[must_use]
147 pub fn is_empty(&self) -> bool {
148 self.entries.is_empty()
149 }
150
151 #[must_use]
153 pub const fn used_bytes(&self) -> usize {
154 self.used_bytes
155 }
156
157 #[must_use]
159 pub const fn max_bytes(&self) -> usize {
160 self.max_bytes
161 }
162}
163
164fn cache_error(message: impl Into<String>) -> FileMakerError {
165 FileMakerError::new(ErrorCode::Validation, message)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn fingerprint(byte: u8) -> DocumentFingerprint {
173 let mut builder = crate::FingerprintBuilder::new();
174 builder.field("test", &[byte]).unwrap();
175 builder.finish()
176 }
177
178 #[test]
179 fn evicts_oldest_without_unbounded_growth() {
180 let mut cache = SceneCache::new(1).unwrap();
181 let scene = ResolvedScene {
182 template_id: "cache".to_owned(),
183 pages: Vec::new(),
184 engine_version: crate::ENGINE_VERSION.to_owned(),
185 };
186 cache
187 .insert(fingerprint(1), scene.clone(), &ResourceLimits::default())
188 .unwrap();
189 cache
190 .insert(fingerprint(2), scene, &ResourceLimits::default())
191 .unwrap();
192 assert!(cache.get(&fingerprint(1)).is_none());
193 assert_eq!(cache.len(), 1);
194 }
195
196 #[test]
197 fn resolves_only_once_for_a_repeated_fingerprint() {
198 let mut cache = SceneCache::new(1).unwrap();
199 let key = fingerprint(1);
200 let mut calls = 0;
201 for _ in 0..2 {
202 cache
203 .get_or_try_insert_with(key, &ResourceLimits::default(), || {
204 calls += 1;
205 Ok(ResolvedScene {
206 template_id: "cache".to_owned(),
207 pages: Vec::new(),
208 engine_version: crate::ENGINE_VERSION.to_owned(),
209 })
210 })
211 .unwrap();
212 }
213 assert_eq!(calls, 1);
214 }
215
216 #[test]
217 fn rejects_a_scene_from_another_engine_version() {
218 let mut cache = SceneCache::new(1).unwrap();
219 let error = cache
220 .insert(
221 fingerprint(1),
222 ResolvedScene {
223 template_id: "stale".to_owned(),
224 pages: Vec::new(),
225 engine_version: "other".to_owned(),
226 },
227 &ResourceLimits::default(),
228 )
229 .unwrap_err();
230 assert_eq!(error.code(), ErrorCode::Validation);
231 assert!(cache.is_empty());
232 }
233
234 #[test]
235 fn rejects_one_scene_larger_than_the_byte_budget() {
236 let mut cache = SceneCache::with_byte_capacity(2, 1).unwrap();
237 let error = cache
238 .insert(
239 fingerprint(1),
240 ResolvedScene {
241 template_id: "oversized".to_owned(),
242 pages: Vec::new(),
243 engine_version: crate::ENGINE_VERSION.to_owned(),
244 },
245 &ResourceLimits::default(),
246 )
247 .unwrap_err();
248 assert_eq!(error.code(), ErrorCode::LimitExceeded);
249 assert_eq!(cache.used_bytes(), 0);
250 }
251
252 #[test]
253 fn byte_budget_evicts_even_below_the_entry_capacity() {
254 let scene = ResolvedScene {
255 template_id: "bounded".to_owned(),
256 pages: Vec::new(),
257 engine_version: crate::ENGINE_VERSION.to_owned(),
258 };
259 let bytes = crate::memory::serialized_size(&scene).unwrap();
260 let mut cache = SceneCache::with_byte_capacity(4, bytes).unwrap();
261 cache
262 .insert(fingerprint(1), scene.clone(), &ResourceLimits::default())
263 .unwrap();
264 cache
265 .insert(fingerprint(2), scene, &ResourceLimits::default())
266 .unwrap();
267 assert!(cache.get(&fingerprint(1)).is_none());
268 assert_eq!(cache.len(), 1);
269 assert!(cache.used_bytes() <= cache.max_bytes());
270 }
271}