1use indexmap::IndexSet;
2use serde_json::Value;
3use std::collections::{HashMap, HashSet};
4
5#[derive(Default, Clone)]
7pub struct VersionTracker {
8 versions: HashMap<String, u64>,
9}
10
11impl VersionTracker {
12 pub fn new() -> Self {
13 Self {
14 versions: HashMap::new(),
15 }
16 }
17
18 #[inline]
19 pub fn get(&self, path: &str) -> u64 {
20 self.versions.get(path).copied().unwrap_or(0)
21 }
22
23 #[inline]
24 pub fn bump(&mut self, path: &str, source: &str) {
25 let current = self.get(path);
26 if crate::utils::is_debug_cache_enabled() {
27 println!(
28 "[store_cache] BUMPING for {} -> {} ({})",
29 path,
30 current + 1,
31 source
32 );
33 }
34 self.versions.insert(path.to_string(), current + 1);
36 }
37
38 pub fn merge_from(&mut self, other: &VersionTracker) {
42 for (k, v) in &other.versions {
43 let current = self.versions.get(k).copied().unwrap_or(0);
44 self.versions.insert(k.clone(), current.max(*v));
45 }
46 }
47
48 pub fn merge_from_params(&mut self, other: &VersionTracker) {
52 for (k, v) in &other.versions {
53 if k.starts_with("/$params") {
54 let current = self.versions.get(k).copied().unwrap_or(0);
55 self.versions.insert(k.clone(), current.max(*v));
56 }
57 }
58 }
59
60 pub(crate) fn merge_excluding_prefix(&mut self, other: &VersionTracker, excluded_prefix: &str) {
62 for (k, v) in &other.versions {
63 if !k.starts_with(excluded_prefix) {
64 let current = self.versions.get(k).copied().unwrap_or(0);
65 self.versions.insert(k.clone(), current.max(*v));
66 }
67 }
68 }
69
70 pub fn any_bumped_with_prefix(&self, prefix: &str) -> bool {
73 self.versions
74 .iter()
75 .any(|(k, &v)| k.starts_with(prefix) && v > 0)
76 }
77
78 pub fn any_newly_bumped_with_prefix(&self, prefix: &str, baseline: &VersionTracker) -> bool {
82 self.versions
83 .iter()
84 .any(|(k, &v)| k.starts_with(prefix) && v > baseline.get(k))
85 }
86
87 pub fn versions(&self) -> impl Iterator<Item = (&str, &u64)> {
89 self.versions.iter().map(|(k, v)| (k.as_str(), v))
90 }
91}
92
93#[derive(Clone)]
95pub struct CacheEntry {
96 pub dep_versions: HashMap<String, u64>,
97 pub result: Value,
98 pub computed_for_item: Option<usize>,
103}
104
105#[derive(Default, Clone)]
107pub struct SubformItemCache {
108 pub data_versions: VersionTracker,
109 pub entries: HashMap<String, CacheEntry>,
110 pub item_snapshot: Value,
111 pub evaluated_schema: Option<Value>,
115}
116
117impl SubformItemCache {
118 pub fn new() -> Self {
119 Self {
120 data_versions: VersionTracker::new(),
121 entries: HashMap::new(),
122 item_snapshot: Value::Null,
123 evaluated_schema: None,
124 }
125 }
126}
127
128#[derive(Clone)]
130pub struct EvalCache {
131 pub data_versions: VersionTracker,
132 pub params_versions: VersionTracker,
133 pub entries: HashMap<String, CacheEntry>,
134
135 pub active_item_index: Option<usize>,
136 pub subform_caches: HashMap<usize, SubformItemCache>,
137
138 pub eval_generation: u64,
142 pub last_evaluated_generation: u64,
143
144 pub main_form_snapshot: Option<Value>,
148}
149
150impl Default for EvalCache {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl EvalCache {
157 pub fn new() -> Self {
158 Self {
159 data_versions: VersionTracker::new(),
160 params_versions: VersionTracker::new(),
161 entries: HashMap::new(),
162 active_item_index: None,
163 subform_caches: HashMap::new(),
164 eval_generation: 0,
165 last_evaluated_generation: u64::MAX, main_form_snapshot: None,
167 }
168 }
169
170 pub fn clear(&mut self) {
171 self.data_versions = VersionTracker::new();
172 self.params_versions = VersionTracker::new();
173 self.entries.clear();
174 self.active_item_index = None;
175 self.subform_caches.clear();
176 self.eval_generation = 0;
177 self.last_evaluated_generation = u64::MAX;
178 self.main_form_snapshot = None;
179 }
180
181 pub fn prune_subform_caches(&mut self, current_count: usize) {
185 self.subform_caches.retain(|&idx, _| idx < current_count);
186 }
187
188 pub fn invalidate_params_tables_for_item(&mut self, idx: usize, table_keys: &[String]) {
195 for key in table_keys {
197 let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(key);
198 self.params_versions
199 .bump(&data_path, "invalidate_params_tables_for_item");
200 self.eval_generation += 1;
201 }
202
203 if let Some(item_cache) = self.subform_caches.get_mut(&idx) {
205 for key in table_keys {
206 item_cache.entries.remove(key);
207 }
208 }
209 }
210
211 pub fn needs_full_evaluation(&self) -> bool {
213 self.eval_generation != self.last_evaluated_generation
214 }
215
216 pub fn mark_evaluated(&mut self) {
218 self.last_evaluated_generation = self.eval_generation;
219 }
220
221 pub(crate) fn ensure_active_item_cache(&mut self, idx: usize) {
222 self.subform_caches
223 .entry(idx)
224 .or_insert_with(SubformItemCache::new);
225 }
226
227 pub fn set_active_item(&mut self, idx: usize) {
228 self.active_item_index = Some(idx);
229 self.ensure_active_item_cache(idx);
230 }
231
232 pub fn clear_active_item(&mut self) {
233 self.active_item_index = None;
234 }
235
236 pub fn store_snapshot_and_diff_versions(&mut self, old: &Value, new: &Value) {
238 if let Some(idx) = self.active_item_index {
239 self.ensure_active_item_cache(idx);
240 let sub_cache = self.subform_caches.get_mut(&idx).unwrap();
241 diff_and_update_versions(
242 &mut sub_cache.data_versions,
243 "",
244 old,
245 new,
246 "subform store_snapshot_and_diff_versions",
247 );
248 sub_cache.item_snapshot = new.clone();
249 } else {
250 diff_and_update_versions(
251 &mut self.data_versions,
252 "",
253 old,
254 new,
255 "store_snapshot_and_diff_versions",
256 );
257 }
258 }
259
260 pub fn get_active_snapshot(&self) -> Value {
261 if let Some(idx) = self.active_item_index {
262 self.subform_caches
263 .get(&idx)
264 .map(|c| c.item_snapshot.clone())
265 .unwrap_or(Value::Null)
266 } else {
267 Value::Null
268 }
269 }
270
271 pub fn diff_active_item(
272 &mut self,
273 field_key: &str,
274 old_sub_data: &Value,
275 new_sub_data: &Value,
276 ) {
277 if let Some(idx) = self.active_item_index {
278 self.ensure_active_item_cache(idx);
279 let sub_cache = self.subform_caches.get_mut(&idx).unwrap();
280
281 let empty = Value::Null;
283 let old_item = old_sub_data.get(field_key).unwrap_or(&empty);
284 let new_item = new_sub_data.get(field_key).unwrap_or(&empty);
285
286 diff_and_update_versions(
287 &mut sub_cache.data_versions,
288 &format!("/{}", field_key),
289 old_item,
290 new_item,
291 format!("diff_active_item {}", field_key).as_str(),
292 );
293 sub_cache.item_snapshot = new_sub_data.clone();
294 }
295 }
296
297 pub fn bump_data_version(&mut self, data_path: &str) {
298 self.eval_generation += 1;
301 if let Some(idx) = self.active_item_index {
302 if let Some(cache) = self.subform_caches.get_mut(&idx) {
303 cache.data_versions.bump(data_path, "bump_data_version1");
304 }
305 } else {
306 self.data_versions.bump(data_path, "bump_data_version2");
307 }
308 }
309
310 pub fn bump_params_version(&mut self, data_path: &str) {
311 self.params_versions.bump(data_path, "bump_params_version");
312 self.eval_generation += 1;
313 }
314
315 pub fn check_cache(&self, eval_key: &str, deps: &IndexSet<String>) -> Option<Value> {
321 if let Some(idx) = self.active_item_index {
322 if let Some(cache) = self.subform_caches.get(&idx) {
324 if let Some(hit) =
325 self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
326 {
327 if crate::utils::is_debug_cache_enabled() {
328 println!("Cache HIT [T1 idx={}] {}", idx, eval_key);
329 }
330 return Some(hit);
331 }
332 }
333
334 let item_data_versions = self
340 .subform_caches
341 .get(&idx)
342 .map(|c| &c.data_versions)
343 .unwrap_or(&self.data_versions);
344
345 if let Some(entry) = self.entries.get(eval_key) {
346 let index_safe = match entry.computed_for_item {
347 None => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
352 Some(stored_idx) if stored_idx == idx => true,
353 _ => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
354 };
355 if index_safe {
356 let result =
357 self.validate_entry(eval_key, deps, &self.entries, item_data_versions);
358 if result.is_some() {
359 if crate::utils::is_debug_cache_enabled() {
360 println!(
361 "Cache HIT [T2 idx={} for={:?}] {}",
362 idx, entry.computed_for_item, eval_key
363 );
364 }
365 }
366 return result;
367 }
368 }
369
370 None
371 } else {
372 self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
373 }
374 }
375
376 pub fn check_table_cache(&self, eval_key: &str, deps: &IndexSet<String>) -> Option<Value> {
388 if let Some(idx) = self.active_item_index {
389 if let Some(cache) = self.subform_caches.get(&idx) {
391 if let Some(hit) =
392 self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
393 {
394 if crate::utils::is_debug_cache_enabled() {
395 println!("Cache HIT [T1 table idx={}] {}", idx, eval_key);
396 }
397 return Some(hit);
398 }
399 }
400
401 let result = self.validate_entry(eval_key, deps, &self.entries, &self.data_versions);
410 if result.is_some() {
411 if crate::utils::is_debug_cache_enabled() {
412 println!("Cache HIT [T2 table idx={}] {}", idx, eval_key);
413 }
414 }
415 result
416 } else {
417 self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
418 }
419 }
420
421 fn validate_entry(
422 &self,
423 eval_key: &str,
424 deps: &IndexSet<String>,
425 entries: &HashMap<String, CacheEntry>,
426 data_versions: &VersionTracker,
427 ) -> Option<Value> {
428 let entry = entries.get(eval_key)?;
429 for dep in deps {
430 let data_dep_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
431
432 let current_ver = if data_dep_path.starts_with("/$params") {
433 self.params_versions.get(&data_dep_path)
434 } else {
435 data_versions.get(&data_dep_path)
436 };
437
438 if let Some(&cached_ver) = entry.dep_versions.get(data_dep_path.as_ref()) {
439 if current_ver != cached_ver {
440 if crate::utils::is_debug_cache_enabled() {
441 println!(
442 "Cache MISS {}: dep {} changed ({} -> {})",
443 eval_key, data_dep_path, cached_ver, current_ver
444 );
445 }
446 return None;
447 }
448 } else {
449 if crate::utils::is_debug_cache_enabled() {
450 println!(
451 "Cache MISS {}: dep {} missing from cache entry",
452 eval_key, data_dep_path
453 );
454 }
455 return None;
456 }
457 }
458 if crate::utils::is_debug_cache_enabled() {
459 println!("Cache HIT {}", eval_key);
460 }
461 Some(entry.result.clone())
462 }
463
464 pub fn store_cache(&mut self, eval_key: &str, deps: &IndexSet<String>, result: Value) {
472 let mut dep_versions = HashMap::with_capacity(deps.len());
476 {
477 let data_versions = if let Some(idx) = self.active_item_index {
478 self.ensure_active_item_cache(idx);
479 &self.subform_caches[&idx].data_versions
480 } else {
481 &self.data_versions
482 };
483
484 for dep in deps {
485 let data_dep_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
486 let ver = if data_dep_path.starts_with("/$params") {
487 self.params_versions.get(&data_dep_path)
488 } else {
489 data_versions.get(&data_dep_path)
490 };
491 dep_versions.insert(data_dep_path.into_owned(), ver);
492 }
493 }
494
495 let computed_for_item = self.active_item_index;
497
498 if eval_key.starts_with("#/$params") {
507 let existing_result: Option<&Value> = if let Some(idx) = self.active_item_index {
508 self.entries.get(eval_key).map(|e| &e.result).or_else(|| {
510 self.subform_caches
511 .get(&idx)
512 .and_then(|c| c.entries.get(eval_key))
513 .map(|e| &e.result)
514 })
515 } else {
516 self.entries.get(eval_key).map(|e| &e.result)
517 };
518
519 let value_changed = existing_result.map_or(true, |r| r != &result);
520
521 if value_changed {
522 let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(eval_key);
523
524 let mut current_path = data_path.as_ref();
527 let mut slash_count = current_path.matches('/').count();
528
529 while slash_count >= 3 {
530 self.params_versions.bump(current_path, "store_cache");
531 if let Some(last_slash) = current_path.rfind('/') {
532 current_path = ¤t_path[..last_slash];
533 slash_count -= 1;
534 } else {
535 break;
536 }
537 }
538
539 self.eval_generation += 1;
540 }
541 }
542
543 let entry = CacheEntry {
544 dep_versions,
545 result,
546 computed_for_item,
547 };
548
549 if let Some(idx) = self.active_item_index {
550 self.subform_caches
552 .get_mut(&idx)
553 .unwrap()
554 .entries
555 .insert(eval_key.to_string(), entry.clone());
556
557 if eval_key.starts_with("#/$params") {
566 let t2_dep_versions: HashMap<String, u64> = entry
567 .dep_versions
568 .iter()
569 .map(|(path, &item_ver)| {
570 let parent_ver = if path.starts_with("/$params") {
571 item_ver } else {
573 self.data_versions.get(path)
575 };
576 (path.clone(), parent_ver)
577 })
578 .collect();
579
580 let t2_entry = CacheEntry {
581 dep_versions: t2_dep_versions,
582 result: entry.result.clone(),
583 computed_for_item,
584 };
585 self.entries.insert(eval_key.to_string(), t2_entry);
586 }
587 } else {
588 self.entries.insert(eval_key.to_string(), entry);
589 }
590 }
591}
592
593pub(crate) fn diff_and_update_versions(
595 tracker: &mut VersionTracker,
596 pointer: &str,
597 old: &Value,
598 new: &Value,
599 source: &str,
600) {
601 let mut pointer_buf = String::with_capacity(128);
602 pointer_buf.push_str(pointer);
603 diff_and_update_versions_internal(tracker, &mut pointer_buf, old, new, source);
604}
605
606fn diff_and_update_versions_internal(
607 tracker: &mut VersionTracker,
608 pointer: &mut String,
609 old: &Value,
610 new: &Value,
611 source: &str,
612) {
613 if old == new {
614 return;
615 }
616
617 if crate::utils::is_debug_cache_enabled() {
618 println!(
619 "[diff_and_update_versions_internal] {} pointer={}, old={:?}, new={:?}",
620 source, pointer, old, new
621 );
622 }
623
624 match (old, new) {
625 (Value::Object(a), Value::Object(b)) => {
626 let mut keys = HashSet::new();
627 for k in a.keys() {
628 keys.insert(k.as_str());
629 }
630 for k in b.keys() {
631 keys.insert(k.as_str());
632 }
633
634 for key in keys {
635 if key == "$params" {
639 continue;
640 }
641
642 let a_val = a.get(key).unwrap_or(&Value::Null);
643 let b_val = b.get(key).unwrap_or(&Value::Null);
644
645 let escaped_key = key.replace('~', "~0").replace('/', "~1");
646 let old_len = pointer.len();
647 pointer.push('/');
648 pointer.push_str(&escaped_key);
649 diff_and_update_versions_internal(tracker, pointer, a_val, b_val, source);
650 pointer.truncate(old_len);
651 }
652 }
653 (Value::Array(a), Value::Array(b)) => {
654 let max_len = a.len().max(b.len());
655 for i in 0..max_len {
656 let a_val = a.get(i).unwrap_or(&Value::Null);
657 let b_val = b.get(i).unwrap_or(&Value::Null);
658 let old_len = pointer.len();
659 use std::fmt::Write;
660 write!(pointer, "/{}", i).unwrap();
661 diff_and_update_versions_internal(tracker, pointer, a_val, b_val, source);
662 pointer.truncate(old_len);
663 }
664 }
665 (old_val, new_val) => {
666 if old_val != new_val {
667 if crate::utils::is_debug_cache_enabled() {
668 println!(
669 "[store_cache] Catch-all for {}: old={}, new={}",
670 pointer,
671 match old_val {
672 Value::Null => "Null",
673 Value::Bool(_) => "Bool",
674 Value::Number(_) => "Number",
675 Value::String(_) => "String",
676 Value::Array(_) => "Array",
677 Value::Object(_) => "Object",
678 },
679 match new_val {
680 Value::Null => "Null",
681 Value::Bool(_) => "Bool",
682 Value::Number(_) => "Number",
683 Value::String(_) => "String",
684 Value::Array(_) => "Array",
685 Value::Object(_) => "Object",
686 }
687 );
688 }
689 tracker.bump(pointer, "diff_and_update_versions_internal");
690
691 if old_val.is_object() || old_val.is_array() {
694 traverse_and_bump(tracker, pointer, old_val);
695 }
696 if new_val.is_object() || new_val.is_array() {
697 traverse_and_bump(tracker, pointer, new_val);
698 }
699 }
700 }
701 }
702}
703
704#[cfg(test)]
708mod tests {
709 use super::VersionTracker;
710
711 #[test]
712 fn merge_excluding_prefix_keeps_item_versions_isolated() {
713 let mut item = VersionTracker::new();
714 item.bump("/riders/wop_flag", "test");
715
716 let mut parent = VersionTracker::new();
717 parent.bump("/illustration/insured/phins_relation", "test");
718 parent.bump("/riders/wop_flag", "test");
719
720 item.merge_excluding_prefix(&parent, "/riders/");
721
722 assert_eq!(item.get("/illustration/insured/phins_relation"), 1);
723 assert_eq!(
724 item.get("/riders/wop_flag"),
725 1,
726 "another rider's parent-tracker bump must not alter this item's version"
727 );
728 }
729}
730
731fn traverse_and_bump(tracker: &mut VersionTracker, pointer: &mut String, val: &Value) {
732 match val {
733 Value::Object(map) => {
734 for (key, v) in map {
735 if key == "$params" {
736 continue; }
738 let escaped_key = key.replace('~', "~0").replace('/', "~1");
739 let old_len = pointer.len();
740 pointer.push('/');
741 pointer.push_str(&escaped_key);
742 tracker.bump(pointer, "traverse_and_bump1");
743 traverse_and_bump(tracker, pointer, v);
744 pointer.truncate(old_len);
745 }
746 }
747 Value::Array(arr) => {
748 for (i, v) in arr.iter().enumerate() {
749 let old_len = pointer.len();
750 use std::fmt::Write;
751 write!(pointer, "/{}", i).unwrap();
752 tracker.bump(pointer, "traverse_and_bump2");
753 traverse_and_bump(tracker, pointer, v);
754 pointer.truncate(old_len);
755 }
756 }
757 _ => {}
758 }
759}