use serde_json::{Map, Value};
const LITERAL: &str = "$v";
const KEYED: &str = "$k";
const REMOVED: &str = "$d";
const ORDER: &str = "$o";
fn element_key(value: &Value) -> Option<String> {
let object = value.as_object()?;
if let Some(id) = object.get("id").and_then(Value::as_str) {
return Some(id.to_string());
}
match (
object.get("zone").and_then(Value::as_str),
object.get("ownerId").and_then(Value::as_str),
) {
(Some(zone), Some(owner)) => Some(format!("{zone}/{owner}")),
_ => None,
}
}
fn keyed_elements(values: &[Value]) -> Option<Vec<(String, &Value)>> {
let mut keys = Vec::with_capacity(values.len());
for value in values {
keys.push((element_key(value)?, value));
}
let mut seen: Vec<&str> = keys.iter().map(|(k, _)| k.as_str()).collect();
seen.sort_unstable();
let unique = seen.len();
seen.dedup();
(seen.len() == unique).then_some(keys)
}
fn find<'a>(entries: &'a [(String, &'a Value)], key: &str) -> Option<&'a Value> {
entries.iter().find_map(|(k, v)| (k == key).then_some(*v))
}
pub fn diff(previous: &Value, next: &Value) -> Option<Value> {
if previous == next {
return None;
}
match (previous, next) {
(Value::Object(before), Value::Object(after)) => {
let mut patch = Map::new();
for (key, value) in after {
match before.get(key) {
Some(old) => {
if let Some(inner) = diff(old, value) {
patch.insert(key.clone(), inner);
}
}
None => {
patch.insert(key.clone(), literal(value));
}
}
}
let removed: Vec<Value> = before
.keys()
.filter(|key| !after.contains_key(*key))
.map(|key| Value::String(key.clone()))
.collect();
if !removed.is_empty() {
patch.insert(REMOVED.to_string(), Value::Array(removed));
}
Some(Value::Object(patch))
}
(Value::Array(before), Value::Array(after)) => {
match (keyed_elements(before), keyed_elements(after)) {
(Some(before_keyed), Some(after_keyed)) => {
Some(keyed_diff(&before_keyed, &after_keyed))
}
_ => Some(literal(next)),
}
}
_ => Some(literal(next)),
}
}
fn keyed_diff(before: &[(String, &Value)], after: &[(String, &Value)]) -> Value {
let mut changed = Map::new();
for (key, value) in after {
match find(before, key) {
Some(old) => {
if let Some(inner) = diff(old, value) {
changed.insert(key.clone(), inner);
}
}
None => {
changed.insert(key.clone(), literal(value));
}
}
}
let removed: Vec<Value> = before
.iter()
.filter(|(key, _)| find(after, key).is_none())
.map(|(key, _)| Value::String(key.clone()))
.collect();
let mut patch = Map::new();
if !changed.is_empty() {
patch.insert(KEYED.to_string(), Value::Object(changed));
}
if !removed.is_empty() {
patch.insert(REMOVED.to_string(), Value::Array(removed));
}
let before_order: Vec<&str> = before.iter().map(|(k, _)| k.as_str()).collect();
let after_order: Vec<&str> = after.iter().map(|(k, _)| k.as_str()).collect();
if before_order != after_order {
patch.insert(
ORDER.to_string(),
Value::Array(
after_order
.iter()
.map(|k| Value::String((*k).into()))
.collect(),
),
);
} else if patch.is_empty() {
patch.insert(KEYED.to_string(), Value::Object(Map::new()));
}
Value::Object(patch)
}
fn literal(value: &Value) -> Value {
match value {
Value::Object(_) => {
let mut wrapper = Map::new();
wrapper.insert(LITERAL.to_string(), value.clone());
Value::Object(wrapper)
}
other => other.clone(),
}
}
fn is_keyed_patch(patch: &Map<String, Value>) -> bool {
patch.contains_key(KEYED) || patch.contains_key(ORDER)
}
pub fn apply(previous: &Value, patch: &Value) -> Value {
let Value::Object(fields) = patch else {
return patch.clone();
};
if let Some(value) = fields.get(LITERAL) {
return value.clone();
}
if is_keyed_patch(fields) {
return apply_keyed(previous, fields);
}
let mut result = previous.as_object().cloned().unwrap_or_default();
if let Some(Value::Array(removed)) = fields.get(REMOVED) {
for key in removed.iter().filter_map(Value::as_str) {
result.remove(key);
}
}
for (key, inner) in fields {
if key == REMOVED {
continue;
}
let base = result.get(key).cloned().unwrap_or(Value::Null);
result.insert(key.clone(), apply(&base, inner));
}
Value::Object(result)
}
fn apply_keyed(previous: &Value, fields: &Map<String, Value>) -> Value {
let empty = Vec::new();
let before = previous.as_array().unwrap_or(&empty);
let mut order: Vec<String> = Vec::with_capacity(before.len());
let mut elements: Vec<(String, Value)> = Vec::with_capacity(before.len());
for value in before {
let key = element_key(value).unwrap_or_default();
order.push(key.clone());
elements.push((key, value.clone()));
}
if let Some(Value::Array(removed)) = fields.get(REMOVED) {
for key in removed.iter().filter_map(Value::as_str) {
elements.retain(|(k, _)| k != key);
order.retain(|k| k != key);
}
}
if let Some(Value::Object(changed)) = fields.get(KEYED) {
for (key, inner) in changed {
match elements.iter_mut().find(|(k, _)| k == key) {
Some((_, value)) => *value = apply(value, inner),
None => {
elements.push((key.clone(), apply(&Value::Null, inner)));
order.push(key.clone());
}
}
}
}
if let Some(Value::Array(new_order)) = fields.get(ORDER) {
order = new_order
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect();
}
let mut result = Vec::with_capacity(order.len());
for key in &order {
if let Some((_, value)) = elements.iter().find(|(k, _)| k == key) {
result.push(value.clone());
}
}
Value::Array(result)
}
pub fn fingerprint(state: &Value) -> String {
let mut canonical = String::new();
write_canonical(state, &mut canonical);
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in canonical.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("{hash:016x}")
}
fn write_canonical(value: &Value, out: &mut String) {
match value {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable();
out.push('{');
for (index, key) in keys.iter().enumerate() {
if index > 0 {
out.push(',');
}
out.push_str(&Value::String((*key).clone()).to_string());
out.push(':');
write_canonical(&map[*key], out);
}
out.push('}');
}
Value::Array(items) => {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_canonical(item, out);
}
out.push(']');
}
other => out.push_str(&other.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn round_trip(before: Value, after: Value) {
match diff(&before, &after) {
Some(patch) => assert_eq!(apply(&before, &patch), after, "patch {patch}"),
None => assert_eq!(before, after),
}
}
#[test]
fn null_fields_survive() {
round_trip(
json!({"power": 2, "toughness": null}),
json!({"power": null, "toughness": 3}),
);
}
#[test]
fn removed_fields_are_explicit() {
round_trip(json!({"a": 1, "b": 2}), json!({"a": 1}));
}
#[test]
fn card_moving_zones_does_not_resend_the_battlefield() {
let before = json!({"zones": [
{"zone": "battlefield", "ownerId": "player-0", "count": 2, "cards": [
{"id": "c1", "tapped": false, "text": "a long rules text"},
{"id": "c2", "tapped": false, "text": "another long rules text"}]},
{"zone": "graveyard", "ownerId": "player-0", "count": 0, "cards": []}]});
let mut after = before.clone();
after["zones"][0]["cards"][1]["tapped"] = json!(true);
let patch = diff(&before, &after).expect("changed");
assert_eq!(apply(&before, &patch), after);
let encoded = patch.to_string();
assert!(
!encoded.contains("long rules text"),
"resent card text: {encoded}"
);
assert!(encoded.len() < 120, "patch larger than expected: {encoded}");
}
#[test]
fn reordering_is_carried() {
let before = json!([{"id": "a"}, {"id": "b"}, {"id": "c"}]);
let after = json!([{"id": "c"}, {"id": "a"}]);
round_trip(before, after);
}
#[test]
fn arrays_without_stable_keys_are_replaced() {
round_trip(
json!({"types": ["Land"]}),
json!({"types": ["Creature", "Artifact"]}),
);
}
#[test]
fn object_replacing_a_scalar_is_literal() {
round_trip(json!({"a": 1}), json!({"a": {"b": 2}}));
}
#[test]
fn fingerprint_ignores_key_order() {
assert_eq!(
fingerprint(&json!({"a": 1, "b": {"c": 2, "d": 3}})),
fingerprint(&json!({"b": {"d": 3, "c": 2}, "a": 1}))
);
}
#[test]
fn recorded_states_round_trip() {
let Ok(path) = std::env::var("STATE_DELTA_FIXTURE") else {
return;
};
let raw = std::fs::read_to_string(path).expect("fixture");
let sequences: Vec<Vec<Value>> = serde_json::from_str(&raw).expect("fixture json");
let (mut full, mut delta, mut pairs) = (0usize, 0usize, 0usize);
let mut worst = 0usize;
for sequence in &sequences {
for pair in sequence.windows(2) {
let patch = diff(&pair[0], &pair[1]);
let rebuilt = match &patch {
Some(patch) => apply(&pair[0], patch),
None => pair[0].clone(),
};
assert_eq!(rebuilt, pair[1], "delta did not reproduce the state");
let size = patch.map_or(2, |p| p.to_string().len());
full += pair[1].to_string().len();
delta += size;
worst = worst.max(size);
pairs += 1;
}
}
println!(
"pairs {pairs}: full {:.1}MB, delta {:.1}MB, {:.0}x smaller, worst patch {worst}B",
full as f64 / 1e6,
delta as f64 / 1e6,
full as f64 / delta.max(1) as f64
);
}
#[test]
fn shared_cases_match_the_typescript_implementation() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../../src/lib/stateDelta.cases.json"
);
let Ok(raw) = std::fs::read_to_string(path) else {
return;
};
let cases: Vec<Value> = serde_json::from_str(&raw).expect("cases json");
assert!(!cases.is_empty());
for case in &cases {
let name = case["name"].as_str().unwrap_or_default();
let (before, after, patch) = (&case["before"], &case["after"], &case["patch"]);
assert_eq!(diff(before, after).as_ref(), Some(patch), "{name}: patch");
assert_eq!(&apply(before, patch), after, "{name}: apply");
}
}
#[test]
fn fingerprint_separates_different_states() {
assert_ne!(fingerprint(&json!({"a": 1})), fingerprint(&json!({"a": 2})));
}
}