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}