json_eval_rs/jsoneval/cache.rs
1use super::JSONEval;
2use crate::jsoneval::eval_cache::{CacheKey, CacheStats};
3use crate::jsoneval::eval_data::EvalData;
4use crate::is_timing_enabled;
5use crate::jsoneval::path_utils;
6
7
8use indexmap::IndexSet;
9use serde_json::Value;
10use std::time::Instant;
11
12impl JSONEval {
13 /// Check if a dependency should be part of the cache key
14 pub fn should_cache_dependency(&self, dep_path: &str) -> bool {
15 // Cache based on:
16 // 1. Data paths (starting with /)
17 // 2. Context paths (starting with /) - treated same as data
18 // 3. Schema paths that point to data (e.g. #/properties/foo/value)
19
20 // Don't cache structural dependencies like loop indices or temporary variables if we can identify them
21 // For now, cache everything that looks like a path
22 if dep_path.starts_with('/') || dep_path.starts_with("#/") {
23 return true;
24 }
25 false
26 }
27
28 /// Try to get a result from cache
29 pub(crate) fn try_get_cached(
30 &self,
31 eval_key: &str,
32 eval_data_snapshot: &EvalData,
33 ) -> Option<Value> {
34 if !self.cache_enabled {
35 return None;
36 }
37
38 let deps = self.dependencies.get(eval_key)?;
39
40 // Buffer to hold pairs of (dep_key, value_ref)
41 let mut key_values = Vec::with_capacity(deps.len());
42
43 for dep in deps {
44 if self.should_cache_dependency(dep) {
45 // Get value from snapshot
46 // Normalize path first
47 let pointer_path = path_utils::normalize_to_json_pointer(dep);
48 let value = if pointer_path.starts_with("#/") {
49 // It's a schema path - check if it points to a value we track
50 // For caching purposes, we care about the DATA value at that schema path
51 // So convert to data path
52 let data_path = pointer_path.replace("/properties/", "/").replace("#", "");
53 eval_data_snapshot
54 .data()
55 .pointer(&data_path)
56 .unwrap_or(&Value::Null)
57 } else {
58 // Direct data path
59 eval_data_snapshot
60 .data()
61 .pointer(&pointer_path)
62 .unwrap_or(&Value::Null)
63 };
64
65 key_values.push((dep.clone(), value));
66 }
67 }
68
69 let key = CacheKey::new(eval_key.to_string(), deps, &key_values);
70
71 self.eval_cache.get(&key).map(|v| v.as_ref().clone())
72 }
73
74 /// Cache a result
75 pub(crate) fn cache_result(
76 &self,
77 eval_key: &str,
78 _result: Value,
79 eval_data_snapshot: &EvalData,
80 ) {
81 if !self.cache_enabled {
82 return;
83 }
84
85 if let Some(deps) = self.dependencies.get(eval_key) {
86 // Buffer to hold pairs of (dep_key, value_ref)
87 let mut key_values = Vec::with_capacity(deps.len());
88
89 for dep in deps {
90 if self.should_cache_dependency(dep) {
91 let pointer_path = path_utils::normalize_to_json_pointer(dep);
92 let value = if pointer_path.starts_with("#/") {
93 let data_path = pointer_path.replace("/properties/", "/").replace("#", "");
94 eval_data_snapshot
95 .data()
96 .pointer(&data_path)
97 .unwrap_or(&Value::Null)
98 } else {
99 eval_data_snapshot
100 .data()
101 .pointer(&pointer_path)
102 .unwrap_or(&Value::Null)
103 };
104
105 key_values.push((dep.clone(), value));
106 }
107 }
108
109 let key = CacheKey::new(eval_key.to_string(), deps, &key_values);
110
111 self.eval_cache.insert(key, _result);
112 }
113 }
114
115 /// Purge cache entries affected by changed data paths, comparing old and new values
116 pub fn purge_cache_for_changed_data_with_comparison(
117 &self,
118 changed_paths: &[String],
119 old_data: &Value,
120 new_data: &Value,
121 ) {
122 // Collect actual changed paths by comparing values
123 let mut actual_changes = Vec::new();
124
125 for path in changed_paths {
126 let pointer = if path.starts_with('/') {
127 path.clone()
128 } else {
129 format!("/{}", path)
130 };
131
132 let old_val = old_data.pointer(&pointer).unwrap_or(&Value::Null);
133 let new_val = new_data.pointer(&pointer).unwrap_or(&Value::Null);
134
135 if old_val != new_val {
136 actual_changes.push(path.clone());
137 }
138 }
139
140 if !actual_changes.is_empty() {
141 self.purge_cache_for_changed_data(&actual_changes);
142 }
143 }
144
145 /// Purge cache entries affected by changed data paths
146 pub fn purge_cache_for_changed_data(&self, changed_paths: &[String]) {
147 if changed_paths.is_empty() {
148 return;
149 }
150
151 // We need to find cache entries that depend on these paths.
152 // Since we don't have a reverse mapping from dependency -> cache keys readily available for specific values,
153 // we iterate the cache.
154 // IMPROVEMENT: Maintain a dependency graph for cache invalidation?
155 // Current implementation: Iterate all cache keys and check if they depend on changed paths.
156
157 // Collect keys to remove to avoid borrowing issues
158 // EvalCache internal structure (DashMap) allows concurrent removal, but we don't have direct access here easily w/o iterating
159 // `eval_cache` in struct is `EvalCache`.
160
161 let start = Instant::now();
162 let initial_size = self.eval_cache.len();
163
164 // Convert changed paths to a format easier to match against dependencies
165 // Cache dependencies are stored as original strings from logic (e.g. "path/to/field", "#/path", "/path")
166 // We need flexible matching.
167 let paths_set: IndexSet<String> = changed_paths.iter().cloned().collect();
168
169 self.eval_cache.retain(|key, _| {
170 // key.dependencies is Vec<(String, Value)> (Wait, CacheKey deps logic might be different?)
171 // Actually CacheKey doesn't expose dependencies easily?
172 // Ah, CacheKey struct: pub eval_key: String. It does NOT store dependencies list publically?
173 // checking eval_cache.rs: struct CacheKey { pub eval_key: String, pub deps_hash: u64 }.
174 // It does NOT store the dependencies themselves!
175 // So we CANNOT check dependencies from key!
176
177 // This implies my purge logic is BROKEN if I can't access dependencies.
178 // But strict adherence to `lib.rs`: How did `lib.rs` do it?
179 // `lib.rs` view 1600+?
180 // If CacheKey doesn't store dependencies, then we can't iterate dependencies.
181 // But `JSONEval` has `dependencies: Arc<IndexMap<String, IndexSet<String>>>`.
182 // We can look up dependencies using `key.eval_key`!
183
184 if let Some(deps) = self.dependencies.get(&key.eval_key) {
185 !deps.iter().any(|dep_path| {
186 self.paths_match_flexible(dep_path, &paths_set)
187 })
188 } else {
189 // No dependencies recorded? Keep it.
190 true
191 }
192 });
193
194 if is_timing_enabled() {
195 let _duration = start.elapsed();
196 let removed = initial_size - self.eval_cache.len();
197 if removed > 0 {
198 // Record timing if needed, or just debug log
199 // println!("Purged {} cache entries in {:?}", removed, duration);
200 }
201 }
202 }
203
204 /// Helper to check if a dependency path matches any of the changed paths
205 pub(crate) fn paths_match_flexible(
206 &self,
207 dep_path: &str,
208 changed_paths: &IndexSet<String>,
209 ) -> bool {
210 // Normalize dep_path to slash format for comparison
211 let normalized_dep = path_utils::normalize_to_json_pointer(dep_path);
212 let normalized_dep_slash = normalized_dep.replace("#", ""); // e.g. /properties/foo
213
214 for changed in changed_paths {
215 // changed is usually like "/foo" or "/foo/bar"
216 // normalized_dep like "/properties/foo" or "/foo"
217
218 // 1. Exact match (ignoring /properties/ noise)
219 // stripped_dep: /foo
220 let stripped_dep = normalized_dep_slash.replace("/properties/", "/");
221
222 if stripped_dep == *changed {
223 return true;
224 }
225
226 // 2. Ancestor/Descendant check
227 // If data at "/foo" changed, then dependency on "/foo/bar" is invalid
228 if stripped_dep.starts_with(changed) && stripped_dep.chars().nth(changed.len()) == Some('/') {
229 return true;
230 }
231
232 // If data at "/foo/bar" changed, then dependency on "/foo" is invalid (if it evaluates object)
233 // But we don't know if it evaluates object or is just a structural parent.
234 // Safe bet: invalidate.
235 if changed.starts_with(&stripped_dep) && changed.chars().nth(stripped_dep.len()) == Some('/') {
236 // EXCEPTION: If dep is purely structural (e.g. existence check), might be fine?
237 // But generally safe to invalidate.
238 return true;
239 }
240 }
241
242 false
243 }
244
245 /// Purge cache entries affected by context changes
246 pub fn purge_cache_for_context_change(&self) {
247 // Invalidate anything that depends on context
248 // Context dependencies usually start with "/" but point to context?
249 // Or they are special variables?
250 // For now, invalidate all keys that have dependencies NOT starting with # (assuming # is schema/internal)
251 // AND not starting with / (data).
252 // Actually, context is accessed via /variables etc?
253 // If we can't distinguish, we might need to clear all?
254 // Or check if dependency is in context?
255
256 // Safer approach: Clear everything if context changes?
257 // Or iterate and check if dependency is NOT found in data?
258
259 // Current implementation in lib.rs purges everything if context provided?
260 // Line 1160 in lib.rs: "if context_provided { self.purge_cache_for_context_change(); }"
261 // And implementation?
262
263 // Let's implement based on checking dependencies for context-like paths
264 // Assuming context paths start with / and data paths also start with /.
265 // If we can't distinguish, we have to clear all entries with / dependencies.
266
267 self.eval_cache.retain(|key, _| {
268 if let Some(deps) = self.dependencies.get(&key.eval_key) {
269 !deps.iter().any(|dep_path| {
270 dep_path.starts_with('/') && !dep_path.starts_with("#")
271 })
272 } else {
273 true
274 }
275 });
276 }
277
278 /// Get cache statistics
279 pub fn cache_stats(&self) -> CacheStats {
280 self.eval_cache.stats()
281 }
282
283 /// Clear the cache manually
284 pub fn clear_cache(&self) {
285 self.eval_cache.clear();
286 }
287
288 /// Enable caching
289 pub fn enable_cache(&mut self) {
290 self.cache_enabled = true;
291 for subform in self.subforms.values_mut() {
292 subform.enable_cache();
293 }
294 }
295
296 /// Disable caching
297 pub fn disable_cache(&mut self) {
298 self.cache_enabled = false;
299 self.eval_cache.clear();
300 for subform in self.subforms.values_mut() {
301 subform.disable_cache();
302 }
303 }
304
305 /// Check if cache is enabled
306 pub fn is_cache_enabled(&self) -> bool {
307 self.cache_enabled
308 }
309
310 /// Get cache size
311 pub fn cache_len(&self) -> usize {
312 self.eval_cache.len()
313 }
314}