use indexmap::IndexSet;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Default, Clone)]
pub struct VersionTracker {
versions: HashMap<String, u64>,
}
impl VersionTracker {
pub fn new() -> Self {
Self {
versions: HashMap::new(),
}
}
#[inline]
pub fn get(&self, path: &str) -> u64 {
self.versions.get(path).copied().unwrap_or(0)
}
#[inline]
pub fn bump(&mut self, path: &str, source: &str) {
let current = self.get(path);
if crate::utils::is_debug_cache_enabled() {
println!(
"[store_cache] BUMPING for {} -> {} ({})",
path,
current + 1,
source
);
}
self.versions.insert(path.to_string(), current + 1);
}
pub fn merge_from(&mut self, other: &VersionTracker) {
for (k, v) in &other.versions {
let current = self.versions.get(k).copied().unwrap_or(0);
self.versions.insert(k.clone(), current.max(*v));
}
}
pub fn merge_from_params(&mut self, other: &VersionTracker) {
for (k, v) in &other.versions {
if k.starts_with("/$params") {
let current = self.versions.get(k).copied().unwrap_or(0);
self.versions.insert(k.clone(), current.max(*v));
}
}
}
pub(crate) fn merge_excluding_prefix(&mut self, other: &VersionTracker, excluded_prefix: &str) {
for (k, v) in &other.versions {
if !k.starts_with(excluded_prefix) {
let current = self.versions.get(k).copied().unwrap_or(0);
self.versions.insert(k.clone(), current.max(*v));
}
}
}
pub fn any_bumped_with_prefix(&self, prefix: &str) -> bool {
self.versions
.iter()
.any(|(k, &v)| k.starts_with(prefix) && v > 0)
}
pub fn any_newly_bumped_with_prefix(&self, prefix: &str, baseline: &VersionTracker) -> bool {
self.versions
.iter()
.any(|(k, &v)| k.starts_with(prefix) && v > baseline.get(k))
}
pub fn versions(&self) -> impl Iterator<Item = (&str, &u64)> {
self.versions.iter().map(|(k, v)| (k.as_str(), v))
}
}
#[derive(Clone)]
pub struct CacheEntry {
pub dep_versions: HashMap<String, u64>,
pub result: std::sync::Arc<Value>,
pub computed_for_item: Option<usize>,
}
#[derive(Default, Clone)]
pub struct SubformItemCache {
pub data_versions: VersionTracker,
pub entries: HashMap<String, CacheEntry>,
pub item_snapshot: Value,
pub evaluated_schema: Option<Value>,
}
impl SubformItemCache {
pub fn new() -> Self {
Self {
data_versions: VersionTracker::new(),
entries: HashMap::new(),
item_snapshot: Value::Null,
evaluated_schema: None,
}
}
}
#[derive(Clone)]
pub struct EvalCache {
pub data_versions: VersionTracker,
pub params_versions: VersionTracker,
pub entries: HashMap<String, CacheEntry>,
pub active_item_index: Option<usize>,
pub subform_caches: HashMap<usize, SubformItemCache>,
pub eval_generation: u64,
pub last_evaluated_generation: u64,
pub main_form_snapshot: Option<std::sync::Arc<Value>>,
}
impl Default for EvalCache {
fn default() -> Self {
Self::new()
}
}
impl EvalCache {
pub fn new() -> Self {
Self {
data_versions: VersionTracker::new(),
params_versions: VersionTracker::new(),
entries: HashMap::new(),
active_item_index: None,
subform_caches: HashMap::new(),
eval_generation: 0,
last_evaluated_generation: u64::MAX, main_form_snapshot: None,
}
}
pub fn clear(&mut self) {
self.data_versions = VersionTracker::new();
self.params_versions = VersionTracker::new();
self.entries.clear();
self.active_item_index = None;
self.subform_caches.clear();
self.eval_generation = 0;
self.last_evaluated_generation = u64::MAX;
self.main_form_snapshot = None;
}
pub fn prune_subform_caches(&mut self, current_count: usize) {
self.subform_caches.retain(|&idx, _| idx < current_count);
}
pub fn invalidate_params_tables_for_item(&mut self, idx: usize, table_keys: &[String]) {
for key in table_keys {
let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(key);
self.params_versions
.bump(&data_path, "invalidate_params_tables_for_item");
self.eval_generation += 1;
}
if let Some(item_cache) = self.subform_caches.get_mut(&idx) {
for key in table_keys {
item_cache.entries.remove(key);
}
}
}
pub fn needs_full_evaluation(&self) -> bool {
self.eval_generation != self.last_evaluated_generation
}
pub fn mark_evaluated(&mut self) {
self.last_evaluated_generation = self.eval_generation;
}
pub(crate) fn ensure_active_item_cache(&mut self, idx: usize) {
self.subform_caches
.entry(idx)
.or_insert_with(SubformItemCache::new);
}
pub fn set_active_item(&mut self, idx: usize) {
self.active_item_index = Some(idx);
self.ensure_active_item_cache(idx);
}
pub fn clear_active_item(&mut self) {
self.active_item_index = None;
}
pub fn store_snapshot_and_diff_versions(&mut self, old: &Value, new: &Value) {
if let Some(idx) = self.active_item_index {
self.ensure_active_item_cache(idx);
let sub_cache = self.subform_caches.get_mut(&idx).unwrap();
diff_and_update_versions(
&mut sub_cache.data_versions,
"",
old,
new,
"subform store_snapshot_and_diff_versions",
);
sub_cache.item_snapshot = new.clone();
} else {
diff_and_update_versions(
&mut self.data_versions,
"",
old,
new,
"store_snapshot_and_diff_versions",
);
}
}
pub fn get_active_snapshot(&self) -> Value {
if let Some(idx) = self.active_item_index {
self.subform_caches
.get(&idx)
.map(|c| c.item_snapshot.clone())
.unwrap_or(Value::Null)
} else {
Value::Null
}
}
pub fn diff_active_item(
&mut self,
field_key: &str,
old_sub_data: &Value,
new_sub_data: &Value,
) {
if let Some(idx) = self.active_item_index {
self.ensure_active_item_cache(idx);
let sub_cache = self.subform_caches.get_mut(&idx).unwrap();
let empty = Value::Null;
let old_item = old_sub_data.get(field_key).unwrap_or(&empty);
let new_item = new_sub_data.get(field_key).unwrap_or(&empty);
diff_and_update_versions(
&mut sub_cache.data_versions,
&format!("/{}", field_key),
old_item,
new_item,
format!("diff_active_item {}", field_key).as_str(),
);
sub_cache.item_snapshot = new_sub_data.clone();
}
}
pub fn bump_data_version(&mut self, data_path: &str) {
self.eval_generation += 1;
if let Some(idx) = self.active_item_index {
if let Some(cache) = self.subform_caches.get_mut(&idx) {
cache.data_versions.bump(data_path, "bump_data_version1");
}
} else {
self.data_versions.bump(data_path, "bump_data_version2");
}
}
pub fn bump_params_version(&mut self, data_path: &str) {
self.params_versions.bump(data_path, "bump_params_version");
self.eval_generation += 1;
}
pub fn check_cache(&self, eval_key: &str, deps: &IndexSet<String>) -> Option<Value> {
self.check_cache_arc(eval_key, deps)
.map(|arc| (*arc).clone())
}
pub fn check_cache_arc(
&self,
eval_key: &str,
deps: &IndexSet<String>,
) -> Option<std::sync::Arc<Value>> {
if let Some(idx) = self.active_item_index {
if let Some(cache) = self.subform_caches.get(&idx) {
if let Some(hit) =
self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
{
if crate::utils::is_debug_cache_enabled() {
println!("Cache HIT [T1 idx={}] {}", idx, eval_key);
}
return Some(hit);
}
}
let item_data_versions = self
.subform_caches
.get(&idx)
.map(|c| &c.data_versions)
.unwrap_or(&self.data_versions);
if let Some(entry) = self.entries.get(eval_key) {
let index_safe = match entry.computed_for_item {
None => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
Some(stored_idx) if stored_idx == idx => true,
_ => entry.dep_versions.keys().all(|p| p.starts_with("/$params")),
};
if index_safe {
let result =
self.validate_entry(eval_key, deps, &self.entries, item_data_versions);
if result.is_some() {
if crate::utils::is_debug_cache_enabled() {
println!(
"Cache HIT [T2 idx={} for={:?}] {}",
idx, entry.computed_for_item, eval_key
);
}
}
return result;
}
}
None
} else {
self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
}
}
pub fn check_table_cache(
&self,
eval_key: &str,
deps: &IndexSet<String>,
) -> Option<std::sync::Arc<Value>> {
if let Some(idx) = self.active_item_index {
if let Some(cache) = self.subform_caches.get(&idx) {
if let Some(hit) =
self.validate_entry(eval_key, deps, &cache.entries, &cache.data_versions)
{
if crate::utils::is_debug_cache_enabled() {
println!("Cache HIT [T1 table idx={}] {}", idx, eval_key);
}
return Some(hit);
}
}
let has_changed_item_dep = self.subform_caches.get(&idx).is_some_and(|cache| {
deps.iter().any(|dep| {
let p = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
if p.starts_with("/$params") {
false
} else {
cache.data_versions.get(&p) > self.data_versions.get(&p)
}
})
});
if has_changed_item_dep {
return None;
}
let result = self.validate_entry(eval_key, deps, &self.entries, &self.data_versions);
if result.is_some() {
if crate::utils::is_debug_cache_enabled() {
println!("Cache HIT [T2 table idx={}] {}", idx, eval_key);
}
}
result
} else {
self.validate_entry(eval_key, deps, &self.entries, &self.data_versions)
}
}
fn validate_entry(
&self,
eval_key: &str,
deps: &IndexSet<String>,
entries: &HashMap<String, CacheEntry>,
data_versions: &VersionTracker,
) -> Option<std::sync::Arc<Value>> {
let entry = entries.get(eval_key)?;
for dep in deps {
let data_dep_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
let current_ver = if data_dep_path.starts_with("/$params") {
self.params_versions.get(&data_dep_path)
} else if let Some(idx) = self.active_item_index {
self.subform_caches
.get(&idx)
.map(|c| c.data_versions.get(&data_dep_path))
.filter(|&v| v > 0)
.unwrap_or_else(|| data_versions.get(&data_dep_path))
} else {
data_versions.get(&data_dep_path)
};
if let Some(&cached_ver) = entry.dep_versions.get(data_dep_path.as_ref()) {
if current_ver != cached_ver {
if crate::utils::is_debug_cache_enabled() {
println!(
"Cache MISS {}: dep {} changed ({} -> {})",
eval_key, data_dep_path, cached_ver, current_ver
);
}
return None;
}
} else {
if crate::utils::is_debug_cache_enabled() {
println!(
"Cache MISS {}: dep {} missing from cache entry",
eval_key, data_dep_path
);
}
return None;
}
}
if crate::utils::is_debug_cache_enabled() {
println!("Cache HIT {}", eval_key);
}
Some(std::sync::Arc::clone(&entry.result))
}
pub fn store_cache(&mut self, eval_key: &str, deps: &IndexSet<String>, result: Value) {
self.store_cache_arc(eval_key, deps, std::sync::Arc::new(result));
}
pub fn store_cache_arc(
&mut self,
eval_key: &str,
deps: &IndexSet<String>,
result: std::sync::Arc<Value>,
) {
let mut dep_versions = HashMap::with_capacity(deps.len());
{
let data_versions = if let Some(idx) = self.active_item_index {
self.ensure_active_item_cache(idx);
&self.subform_caches[&idx].data_versions
} else {
&self.data_versions
};
for dep in deps {
let data_dep_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
let ver = if data_dep_path.starts_with("/$params") {
self.params_versions.get(&data_dep_path)
} else {
data_versions.get(&data_dep_path)
};
dep_versions.insert(data_dep_path.into_owned(), ver);
}
}
let computed_for_item = self.active_item_index;
if eval_key.starts_with("#/$params") {
let existing_result: Option<&Value> = if let Some(idx) = self.active_item_index {
self.entries
.get(eval_key)
.map(|e| e.result.as_ref())
.or_else(|| {
self.subform_caches
.get(&idx)
.and_then(|c| c.entries.get(eval_key))
.map(|e| e.result.as_ref())
})
} else {
self.entries.get(eval_key).map(|e| e.result.as_ref())
};
let value_changed = existing_result.map_or(true, |r| r != result.as_ref());
if value_changed {
let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(eval_key);
let mut current_path = data_path.as_ref();
let mut slash_count = current_path.matches('/').count();
while slash_count >= 3 {
self.params_versions.bump(current_path, "store_cache");
if let Some(last_slash) = current_path.rfind('/') {
current_path = ¤t_path[..last_slash];
slash_count -= 1;
} else {
break;
}
}
self.eval_generation += 1;
}
}
let entry = CacheEntry {
dep_versions,
result: std::sync::Arc::clone(&result),
computed_for_item,
};
if let Some(idx) = self.active_item_index {
self.subform_caches
.get_mut(&idx)
.unwrap()
.entries
.insert(eval_key.to_string(), entry.clone());
if eval_key.starts_with("#/$params") {
let t2_dep_versions: HashMap<String, u64> = entry
.dep_versions
.iter()
.map(|(path, &item_ver)| {
let parent_ver = if path.starts_with("/$params") {
item_ver } else {
self.data_versions.get(path)
};
(path.clone(), parent_ver)
})
.collect();
let t2_entry = CacheEntry {
dep_versions: t2_dep_versions,
result: std::sync::Arc::clone(&entry.result),
computed_for_item,
};
self.entries.insert(eval_key.to_string(), t2_entry);
}
} else {
self.entries.insert(eval_key.to_string(), entry);
}
}
}
pub(crate) fn diff_and_update_versions(
tracker: &mut VersionTracker,
pointer: &str,
old: &Value,
new: &Value,
source: &str,
) {
let mut pointer_buf = String::with_capacity(128);
pointer_buf.push_str(pointer);
diff_and_update_versions_internal(tracker, &mut pointer_buf, old, new, source);
}
#[cfg(test)]
mod cache_tests {
use super::{CacheEntry, EvalCache};
use indexmap::IndexSet;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
#[test]
fn unchanged_active_item_reuses_global_table_with_item_dependency() {
let mut cache = EvalCache::new();
cache.set_active_item(1);
let eval_key = "#/$params/references/RIDER_RATE";
let deps = IndexSet::from_iter(["#/riders/properties/benefit".to_string()]);
cache.entries.insert(
eval_key.to_string(),
CacheEntry {
dep_versions: HashMap::from([("/riders/benefit".to_string(), 0)]),
result: Arc::new(json!([{"rate": 97}])),
computed_for_item: None,
},
);
assert_eq!(
cache.check_table_cache(eval_key, &deps),
Some(Arc::new(json!([{"rate": 97}]))),
"a scoped alias may reuse the parent result for its unchanged canonical rider"
);
}
#[test]
fn changed_active_item_does_not_reuse_global_table_with_item_dependency() {
let mut cache = EvalCache::new();
cache.set_active_item(1);
cache
.subform_caches
.get_mut(&1)
.expect("active item cache must exist")
.data_versions
.bump("/riders/benefit", "test rider input change");
let eval_key = "#/$params/references/RIDER_RATE";
let deps = IndexSet::from_iter(["#/riders/properties/benefit".to_string()]);
cache.entries.insert(
eval_key.to_string(),
CacheEntry {
dep_versions: HashMap::from([("/riders/benefit".to_string(), 0)]),
result: Arc::new(json!([{"rate": 97}])),
computed_for_item: None,
},
);
assert!(
cache.check_table_cache(eval_key, &deps).is_none(),
"a changed rider input must force item-scoped table recomputation"
);
}
#[test]
fn active_item_reuses_global_table_with_only_params_dependencies() {
let mut cache = EvalCache::new();
cache.set_active_item(1);
let eval_key = "#/$params/references/SHARED_RATE";
let deps = IndexSet::from_iter(["#/$params/others/currency".to_string()]);
cache.entries.insert(
eval_key.to_string(),
CacheEntry {
dep_versions: HashMap::from([("/$params/others/currency".to_string(), 0)]),
result: Arc::new(json!([{"rate": 10}])),
computed_for_item: None,
},
);
assert_eq!(
cache.check_table_cache(eval_key, &deps),
Some(Arc::new(json!([{"rate": 10}])))
);
}
}
fn diff_and_update_versions_internal(
tracker: &mut VersionTracker,
pointer: &mut String,
old: &Value,
new: &Value,
source: &str,
) {
if old == new {
return;
}
if crate::utils::is_debug_cache_enabled() {
println!(
"[diff_and_update_versions_internal] {} pointer={}, old={:?}, new={:?}",
source, pointer, old, new
);
}
match (old, new) {
(Value::Object(a), Value::Object(b)) => {
for (key, a_val) in a {
if key == "$params" {
continue;
}
let b_val = b.get(key).unwrap_or(&Value::Null);
if a_val == b_val {
continue;
}
let old_len = pointer.len();
pointer.push('/');
if key.contains('~') || key.contains('/') {
let escaped_key = key.replace('~', "~0").replace('/', "~1");
pointer.push_str(&escaped_key);
} else {
pointer.push_str(key);
}
diff_and_update_versions_internal(tracker, pointer, a_val, b_val, source);
pointer.truncate(old_len);
}
for (key, b_val) in b {
if key == "$params" || a.contains_key(key) {
continue;
}
if b_val.is_null() {
continue;
}
let old_len = pointer.len();
pointer.push('/');
if key.contains('~') || key.contains('/') {
let escaped_key = key.replace('~', "~0").replace('/', "~1");
pointer.push_str(&escaped_key);
} else {
pointer.push_str(key);
}
diff_and_update_versions_internal(tracker, pointer, &Value::Null, b_val, source);
pointer.truncate(old_len);
}
}
(Value::Array(a), Value::Array(b)) => {
if a != b {
tracker.bump(pointer, source);
}
let max_len = a.len().max(b.len());
for i in 0..max_len {
let a_val = a.get(i).unwrap_or(&Value::Null);
let b_val = b.get(i).unwrap_or(&Value::Null);
if a_val == b_val {
continue;
}
let old_len = pointer.len();
use std::fmt::Write;
write!(pointer, "/{}", i).unwrap();
diff_and_update_versions_internal(tracker, pointer, a_val, b_val, source);
pointer.truncate(old_len);
}
}
(old_val, new_val) => {
if old_val != new_val {
if crate::utils::is_debug_cache_enabled() {
println!(
"[store_cache] Catch-all for {}: old={}, new={}",
pointer,
match old_val {
Value::Null => "Null",
Value::Bool(_) => "Bool",
Value::Number(_) => "Number",
Value::String(_) => "String",
Value::Array(_) => "Array",
Value::Object(_) => "Object",
},
match new_val {
Value::Null => "Null",
Value::Bool(_) => "Bool",
Value::Number(_) => "Number",
Value::String(_) => "String",
Value::Array(_) => "Array",
Value::Object(_) => "Object",
}
);
}
tracker.bump(pointer, "diff_and_update_versions_internal");
if old_val.is_object() || old_val.is_array() {
traverse_and_bump(tracker, pointer, old_val);
}
if new_val.is_object() || new_val.is_array() {
traverse_and_bump(tracker, pointer, new_val);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::VersionTracker;
#[test]
fn merge_excluding_prefix_keeps_item_versions_isolated() {
let mut item = VersionTracker::new();
item.bump("/riders/wop_flag", "test");
let mut parent = VersionTracker::new();
parent.bump("/illustration/insured/phins_relation", "test");
parent.bump("/riders/wop_flag", "test");
item.merge_excluding_prefix(&parent, "/riders/");
assert_eq!(item.get("/illustration/insured/phins_relation"), 1);
assert_eq!(
item.get("/riders/wop_flag"),
1,
"another rider's parent-tracker bump must not alter this item's version"
);
}
}
fn traverse_and_bump(tracker: &mut VersionTracker, pointer: &mut String, val: &Value) {
match val {
Value::Object(map) => {
for (key, v) in map {
if key == "$params" {
continue; }
let escaped_key = key.replace('~', "~0").replace('/', "~1");
let old_len = pointer.len();
pointer.push('/');
pointer.push_str(&escaped_key);
tracker.bump(pointer, "traverse_and_bump1");
traverse_and_bump(tracker, pointer, v);
pointer.truncate(old_len);
}
}
Value::Array(arr) => {
for (i, v) in arr.iter().enumerate() {
let old_len = pointer.len();
use std::fmt::Write;
write!(pointer, "/{}", i).unwrap();
tracker.bump(pointer, "traverse_and_bump2");
traverse_and_bump(tracker, pointer, v);
pointer.truncate(old_len);
}
}
_ => {}
}
}