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