1use indexmap::IndexSet;
2use serde_json::Value;
3use std::collections::HashMap;
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: std::sync::Arc<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<std::sync::Arc<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 self.check_cache_arc(eval_key, deps)
322 .map(|arc| (*arc).clone())
323 }
324
325 pub fn check_cache_arc(
326 &self,
327 eval_key: &str,
328 deps: &IndexSet<String>,
329 ) -> Option<std::sync::Arc<Value>> {
330 if let Some(idx) = self.active_item_index {
331 if let Some(cache) = self.subform_caches.get(&idx) {
333 if let Some(hit) =
334 self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
335 {
336 if crate::utils::is_debug_cache_enabled() {
337 println!("Cache HIT [T1 idx={}] {}", idx, eval_key);
338 }
339 return Some(hit);
340 }
341 }
342
343 let item_data_versions = self
345 .subform_caches
346 .get(&idx)
347 .map(|c| &c.data_versions)
348 .unwrap_or(&self.data_versions);
349
350 if let Some(entry) = self.entries.get(eval_key) {
351 let index_safe = match entry.computed_for_item {
352 None => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
354 Some(stored_idx) if stored_idx == idx => true,
355 _ => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
356 };
357 if index_safe {
358 let result =
359 self.validate_entry(eval_key, deps, &self.entries, item_data_versions);
360 if result.is_some() {
361 if crate::utils::is_debug_cache_enabled() {
362 println!(
363 "Cache HIT [T2 idx={} for={:?}] {}",
364 idx, entry.computed_for_item, eval_key
365 );
366 }
367 }
368 return result;
369 }
370 }
371
372 None
373 } else {
374 self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
375 }
376 }
377
378 pub fn check_table_cache(
382 &self,
383 eval_key: &str,
384 deps: &IndexSet<String>,
385 ) -> Option<std::sync::Arc<Value>> {
386 if let Some(idx) = self.active_item_index {
387 if let Some(cache) = self.subform_caches.get(&idx) {
389 if let Some(hit) =
390 self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
391 {
392 if crate::utils::is_debug_cache_enabled() {
393 println!("Cache HIT [T1 table idx={}] {}", idx, eval_key);
394 }
395 return Some(hit);
396 }
397 }
398
399 let has_item_data_dependency = deps.iter().any(|dep| {
401 !crate::jsoneval::path_utils::schema_path_to_data_pointer(dep)
402 .starts_with("/$params")
403 });
404 let active_item_changed = self.subform_caches.get(&idx).is_some_and(|cache| {
405 cache
406 .data_versions
407 .versions()
408 .any(|(path, version)| path.starts_with("/riders/") && *version > 0)
409 });
410 if has_item_data_dependency && active_item_changed {
411 return None;
412 }
413
414 let result = self.validate_entry(eval_key, deps, &self.entries, &self.data_versions);
415 if result.is_some() {
416 if crate::utils::is_debug_cache_enabled() {
417 println!("Cache HIT [T2 table idx={}] {}", idx, eval_key);
418 }
419 }
420 result
421 } else {
422 self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
423 }
424 }
425
426 fn validate_entry(
427 &self,
428 eval_key: &str,
429 deps: &IndexSet<String>,
430 entries: &HashMap<String, CacheEntry>,
431 data_versions: &VersionTracker,
432 ) -> Option<std::sync::Arc<Value>> {
433 let entry = entries.get(eval_key)?;
434 for dep in deps {
435 let data_dep_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
436
437 let current_ver = if data_dep_path.starts_with("/$params") {
438 self.params_versions.get(&data_dep_path)
439 } else {
440 data_versions.get(&data_dep_path)
441 };
442
443 if let Some(&cached_ver) = entry.dep_versions.get(data_dep_path.as_ref()) {
444 if current_ver != cached_ver {
445 if crate::utils::is_debug_cache_enabled() {
446 println!(
447 "Cache MISS {}: dep {} changed ({} -> {})",
448 eval_key, data_dep_path, cached_ver, current_ver
449 );
450 }
451 return None;
452 }
453 } else {
454 if crate::utils::is_debug_cache_enabled() {
455 println!(
456 "Cache MISS {}: dep {} missing from cache entry",
457 eval_key, data_dep_path
458 );
459 }
460 return None;
461 }
462 }
463 if crate::utils::is_debug_cache_enabled() {
464 println!("Cache HIT {}", eval_key);
465 }
466 Some(std::sync::Arc::clone(&entry.result))
467 }
468
469 pub fn store_cache(&mut self, eval_key: &str, deps: &IndexSet<String>, result: Value) {
473 self.store_cache_arc(eval_key, deps, std::sync::Arc::new(result));
474 }
475
476 pub fn store_cache_arc(
478 &mut self,
479 eval_key: &str,
480 deps: &IndexSet<String>,
481 result: std::sync::Arc<Value>,
482 ) {
483 let mut dep_versions = HashMap::with_capacity(deps.len());
485 {
486 let data_versions = if let Some(idx) = self.active_item_index {
487 self.ensure_active_item_cache(idx);
488 &self.subform_caches[&idx].data_versions
489 } else {
490 &self.data_versions
491 };
492
493 for dep in deps {
494 let data_dep_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
495 let ver = if data_dep_path.starts_with("/$params") {
496 self.params_versions.get(&data_dep_path)
497 } else {
498 data_versions.get(&data_dep_path)
499 };
500 dep_versions.insert(data_dep_path.into_owned(), ver);
501 }
502 }
503
504 let computed_for_item = self.active_item_index;
506
507 if eval_key.starts_with("#/$params") {
509 let existing_result: Option<&Value> = if let Some(idx) = self.active_item_index {
510 self.entries
512 .get(eval_key)
513 .map(|e| e.result.as_ref())
514 .or_else(|| {
515 self.subform_caches
516 .get(&idx)
517 .and_then(|c| c.entries.get(eval_key))
518 .map(|e| e.result.as_ref())
519 })
520 } else {
521 self.entries.get(eval_key).map(|e| e.result.as_ref())
522 };
523
524 let value_changed = existing_result.map_or(true, |r| r != result.as_ref());
525
526 if value_changed {
527 let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(eval_key);
528
529 let mut current_path = data_path.as_ref();
532 let mut slash_count = current_path.matches('/').count();
533
534 while slash_count >= 3 {
535 self.params_versions.bump(current_path, "store_cache");
536 if let Some(last_slash) = current_path.rfind('/') {
537 current_path = ¤t_path[..last_slash];
538 slash_count -= 1;
539 } else {
540 break;
541 }
542 }
543
544 self.eval_generation += 1;
545 }
546 }
547
548 let entry = CacheEntry {
549 dep_versions,
550 result: std::sync::Arc::clone(&result),
551 computed_for_item,
552 };
553
554 if let Some(idx) = self.active_item_index {
555 self.subform_caches
557 .get_mut(&idx)
558 .unwrap()
559 .entries
560 .insert(eval_key.to_string(), entry.clone());
561
562 if eval_key.starts_with("#/$params") {
564 let t2_dep_versions: HashMap<String, u64> = entry
565 .dep_versions
566 .iter()
567 .map(|(path, &item_ver)| {
568 let parent_ver = if path.starts_with("/$params") {
569 item_ver } else {
571 self.data_versions.get(path)
573 };
574 (path.clone(), parent_ver)
575 })
576 .collect();
577
578 let t2_entry = CacheEntry {
579 dep_versions: t2_dep_versions,
580 result: std::sync::Arc::clone(&entry.result),
581 computed_for_item,
582 };
583 self.entries.insert(eval_key.to_string(), t2_entry);
584 }
585 } else {
586 self.entries.insert(eval_key.to_string(), entry);
587 }
588 }
589}
590
591pub(crate) fn diff_and_update_versions(
593 tracker: &mut VersionTracker,
594 pointer: &str,
595 old: &Value,
596 new: &Value,
597 source: &str,
598) {
599 let mut pointer_buf = String::with_capacity(128);
600 pointer_buf.push_str(pointer);
601 diff_and_update_versions_internal(tracker, &mut pointer_buf, old, new, source);
602}
603
604#[cfg(test)]
605mod cache_tests {
606 use super::{CacheEntry, EvalCache};
607 use indexmap::IndexSet;
608 use serde_json::json;
609 use std::collections::HashMap;
610 use std::sync::Arc;
611
612 #[test]
613 fn unchanged_active_item_reuses_global_table_with_item_dependency() {
614 let mut cache = EvalCache::new();
615 cache.set_active_item(1);
616
617 let eval_key = "#/$params/references/RIDER_RATE";
618 let deps = IndexSet::from_iter(["#/riders/properties/benefit".to_string()]);
619 cache.entries.insert(
620 eval_key.to_string(),
621 CacheEntry {
622 dep_versions: HashMap::from([("/riders/benefit".to_string(), 0)]),
623 result: Arc::new(json!([{"rate": 97}])),
624 computed_for_item: None,
625 },
626 );
627
628 assert_eq!(
629 cache.check_table_cache(eval_key, &deps),
630 Some(Arc::new(json!([{"rate": 97}]))),
631 "a scoped alias may reuse the parent result for its unchanged canonical rider"
632 );
633 }
634
635 #[test]
636 fn changed_active_item_does_not_reuse_global_table_with_item_dependency() {
637 let mut cache = EvalCache::new();
638 cache.set_active_item(1);
639 cache
640 .subform_caches
641 .get_mut(&1)
642 .expect("active item cache must exist")
643 .data_versions
644 .bump("/riders/benefit", "test rider input change");
645
646 let eval_key = "#/$params/references/RIDER_RATE";
647 let deps = IndexSet::from_iter(["#/riders/properties/benefit".to_string()]);
648 cache.entries.insert(
649 eval_key.to_string(),
650 CacheEntry {
651 dep_versions: HashMap::from([("/riders/benefit".to_string(), 0)]),
652 result: Arc::new(json!([{"rate": 97}])),
653 computed_for_item: None,
654 },
655 );
656
657 assert!(
658 cache.check_table_cache(eval_key, &deps).is_none(),
659 "a changed rider input must force item-scoped table recomputation"
660 );
661 }
662
663 #[test]
664 fn active_item_reuses_global_table_with_only_params_dependencies() {
665 let mut cache = EvalCache::new();
666 cache.set_active_item(1);
667
668 let eval_key = "#/$params/references/SHARED_RATE";
669 let deps = IndexSet::from_iter(["#/$params/others/currency".to_string()]);
670 cache.entries.insert(
671 eval_key.to_string(),
672 CacheEntry {
673 dep_versions: HashMap::from([("/$params/others/currency".to_string(), 0)]),
674 result: Arc::new(json!([{"rate": 10}])),
675 computed_for_item: None,
676 },
677 );
678
679 assert_eq!(
680 cache.check_table_cache(eval_key, &deps),
681 Some(Arc::new(json!([{"rate": 10}])))
682 );
683 }
684}
685
686fn diff_and_update_versions_internal(
687 tracker: &mut VersionTracker,
688 pointer: &mut String,
689 old: &Value,
690 new: &Value,
691 source: &str,
692) {
693 if old == new {
694 return;
695 }
696
697 if crate::utils::is_debug_cache_enabled() {
698 println!(
699 "[diff_and_update_versions_internal] {} pointer={}, old={:?}, new={:?}",
700 source, pointer, old, new
701 );
702 }
703
704 match (old, new) {
705 (Value::Object(a), Value::Object(b)) => {
706 for (key, a_val) in a {
707 if key == "$params" {
708 continue;
709 }
710 let b_val = b.get(key).unwrap_or(&Value::Null);
711 if a_val == b_val {
712 continue;
713 }
714
715 let old_len = pointer.len();
716 pointer.push('/');
717 if key.contains('~') || key.contains('/') {
718 let escaped_key = key.replace('~', "~0").replace('/', "~1");
719 pointer.push_str(&escaped_key);
720 } else {
721 pointer.push_str(key);
722 }
723 diff_and_update_versions_internal(tracker, pointer, a_val, b_val, source);
724 pointer.truncate(old_len);
725 }
726
727 for (key, b_val) in b {
728 if key == "$params" || a.contains_key(key) {
729 continue;
730 }
731 if b_val.is_null() {
732 continue;
733 }
734
735 let old_len = pointer.len();
736 pointer.push('/');
737 if key.contains('~') || key.contains('/') {
738 let escaped_key = key.replace('~', "~0").replace('/', "~1");
739 pointer.push_str(&escaped_key);
740 } else {
741 pointer.push_str(key);
742 }
743 diff_and_update_versions_internal(tracker, pointer, &Value::Null, b_val, source);
744 pointer.truncate(old_len);
745 }
746 }
747 (Value::Array(a), Value::Array(b)) => {
748 let max_len = a.len().max(b.len());
749 for i in 0..max_len {
750 let a_val = a.get(i).unwrap_or(&Value::Null);
751 let b_val = b.get(i).unwrap_or(&Value::Null);
752 if a_val == b_val {
753 continue;
754 }
755 let old_len = pointer.len();
756 use std::fmt::Write;
757 write!(pointer, "/{}", i).unwrap();
758 diff_and_update_versions_internal(tracker, pointer, a_val, b_val, source);
759 pointer.truncate(old_len);
760 }
761 }
762 (old_val, new_val) => {
763 if old_val != new_val {
764 if crate::utils::is_debug_cache_enabled() {
765 println!(
766 "[store_cache] Catch-all for {}: old={}, new={}",
767 pointer,
768 match old_val {
769 Value::Null => "Null",
770 Value::Bool(_) => "Bool",
771 Value::Number(_) => "Number",
772 Value::String(_) => "String",
773 Value::Array(_) => "Array",
774 Value::Object(_) => "Object",
775 },
776 match new_val {
777 Value::Null => "Null",
778 Value::Bool(_) => "Bool",
779 Value::Number(_) => "Number",
780 Value::String(_) => "String",
781 Value::Array(_) => "Array",
782 Value::Object(_) => "Object",
783 }
784 );
785 }
786 tracker.bump(pointer, "diff_and_update_versions_internal");
787
788 if old_val.is_object() || old_val.is_array() {
791 traverse_and_bump(tracker, pointer, old_val);
792 }
793 if new_val.is_object() || new_val.is_array() {
794 traverse_and_bump(tracker, pointer, new_val);
795 }
796 }
797 }
798 }
799}
800
801#[cfg(test)]
805mod tests {
806 use super::VersionTracker;
807
808 #[test]
809 fn merge_excluding_prefix_keeps_item_versions_isolated() {
810 let mut item = VersionTracker::new();
811 item.bump("/riders/wop_flag", "test");
812
813 let mut parent = VersionTracker::new();
814 parent.bump("/illustration/insured/phins_relation", "test");
815 parent.bump("/riders/wop_flag", "test");
816
817 item.merge_excluding_prefix(&parent, "/riders/");
818
819 assert_eq!(item.get("/illustration/insured/phins_relation"), 1);
820 assert_eq!(
821 item.get("/riders/wop_flag"),
822 1,
823 "another rider's parent-tracker bump must not alter this item's version"
824 );
825 }
826}
827
828fn traverse_and_bump(tracker: &mut VersionTracker, pointer: &mut String, val: &Value) {
829 match val {
830 Value::Object(map) => {
831 for (key, v) in map {
832 if key == "$params" {
833 continue; }
835 let escaped_key = key.replace('~', "~0").replace('/', "~1");
836 let old_len = pointer.len();
837 pointer.push('/');
838 pointer.push_str(&escaped_key);
839 tracker.bump(pointer, "traverse_and_bump1");
840 traverse_and_bump(tracker, pointer, v);
841 pointer.truncate(old_len);
842 }
843 }
844 Value::Array(arr) => {
845 for (i, v) in arr.iter().enumerate() {
846 let old_len = pointer.len();
847 use std::fmt::Write;
848 write!(pointer, "/{}", i).unwrap();
849 tracker.bump(pointer, "traverse_and_bump2");
850 traverse_and_bump(tracker, pointer, v);
851 pointer.truncate(old_len);
852 }
853 }
854 _ => {}
855 }
856}