Skip to main content

termwright_protocol/
diffing.rs

1//! Turning two consecutive trees into the delta between them.
2//!
3//! Producing a delta is the mirror of composing one, and it has to agree with
4//! [`crate::apply_tree_delta`] exactly: whatever this emits, the driver
5//! applies, and any disagreement shows up as a tree that silently drifts from
6//! the screen.
7//!
8//! Two rules here are easy to get wrong and both are load-bearing:
9//!
10//! * a node that *survives* under a parent being removed must be re-sent in
11//!   `changed`, even when nothing about it changed, because the removal
12//!   cascades through it first;
13//! * `rootIds` must be sent whenever the inherited list — the base's roots
14//!   minus whatever the removals took — is not the list the new tree wants.
15
16use std::collections::{HashMap, HashSet};
17
18use serde_json::{Map, Value};
19
20/// The point past which a delta stops paying for itself: beyond roughly half
21/// the tree, the whole snapshot is cheaper to send and far cheaper to reason
22/// about.
23pub const DELTA_SHARE_CEILING: f64 = 0.5;
24
25fn nodes_of(tree: &Value) -> &[Value] {
26    tree.get("nodes")
27        .and_then(Value::as_array)
28        .map_or(&[], Vec::as_slice)
29}
30
31fn id_of(node: &Value) -> &str {
32    node.get("id").and_then(Value::as_str).unwrap_or_default()
33}
34
35/// Build the `tree-delta` body, or `None` when a whole snapshot is the better
36/// answer.
37///
38/// Returning `None` is not a failure: past roughly half the tree a delta costs
39/// more than the snapshot it replaces, and a cursor that disappears cannot be
40/// expressed as a delta at all.
41pub fn build_delta(base: &Value, next: &Value) -> Option<Value> {
42    let (changed, removed, root_ids, cursor_changed) = diff_trees(base, next);
43
44    let count = nodes_of(next).len().max(1);
45    if changed.len() as f64 > count as f64 * DELTA_SHARE_CEILING {
46        return None;
47    }
48    if base.get("cursor").is_some() && next.get("cursor").is_none() {
49        // A delta can replace a cursor but never remove one, and an absent
50        // cursor is inherited — so the only honest way to drop it is a whole
51        // tree. Sending the delta anyway would leave the driver holding a
52        // cursor the application no longer reports.
53        return None;
54    }
55
56    let mut delta = Map::new();
57    delta.insert("type".into(), Value::from("tree-delta"));
58    delta.insert("baseRevision".into(), base.get("revision").cloned()?);
59    delta.insert("revision".into(), next.get("revision").cloned()?);
60    delta.insert("changed".into(), Value::Array(changed));
61    delta.insert("removed".into(), Value::Array(removed));
62    if let Some(root_ids) = root_ids {
63        delta.insert("rootIds".into(), Value::Array(root_ids));
64    }
65    // An absent cursor means unchanged, so it travels only when it moved.
66    if cursor_changed {
67        if let Some(cursor) = next.get("cursor") {
68            delta.insert("cursor".into(), cursor.clone());
69        }
70    }
71    Some(Value::Object(delta))
72}
73
74/// Report what changed, what was removed, the root list when it can no longer
75/// be inherited, and whether the cursor moved.
76pub fn diff_trees(
77    base: &Value,
78    next: &Value,
79) -> (Vec<Value>, Vec<Value>, Option<Vec<Value>>, bool) {
80    let base_nodes = nodes_of(base);
81    let next_nodes = nodes_of(next);
82
83    let mut base_by_id: HashMap<&str, &Value> = HashMap::with_capacity(base_nodes.len());
84    let mut children_of: HashMap<&str, Vec<&str>> = HashMap::new();
85    for node in base_nodes {
86        base_by_id.insert(id_of(node), node);
87        if let Some(parent) = node.get("parentId").and_then(Value::as_str) {
88            children_of.entry(parent).or_default().push(id_of(node));
89        }
90    }
91    let next_by_id: HashMap<&str, &Value> =
92        next_nodes.iter().map(|node| (id_of(node), node)).collect();
93
94    let gone: HashSet<&str> = base_by_id
95        .keys()
96        .copied()
97        .filter(|id| !next_by_id.contains_key(id))
98        .collect();
99
100    // Only the topmost id of each removed subtree needs sending: the cascade
101    // takes the rest, which is what makes a delta small.
102    let mut removal_roots: Vec<&str> = gone
103        .iter()
104        .copied()
105        .filter(
106            |id| match base_by_id[id].get("parentId").and_then(Value::as_str) {
107                Some(parent) => !gone.contains(parent),
108                None => true,
109            },
110        )
111        .collect();
112    removal_roots.sort_unstable();
113
114    // Everything the cascade will take, so survivors underneath can be re-sent.
115    let mut swept: HashSet<&str> = HashSet::new();
116    let mut pending: Vec<&str> = removal_roots.clone();
117    while let Some(current) = pending.pop() {
118        if !swept.insert(current) {
119            continue;
120        }
121        if let Some(children) = children_of.get(current) {
122            pending.extend(children.iter().copied());
123        }
124    }
125
126    // Walk `next` in its own order so the delta is deterministic.
127    let changed: Vec<Value> = next_nodes
128        .iter()
129        .filter(|node| {
130            let id = id_of(node);
131            match base_by_id.get(id) {
132                None => true,
133                Some(previous) => swept.contains(id) || *previous != *node,
134            }
135        })
136        .cloned()
137        .collect();
138
139    let removed: Vec<Value> = removal_roots.iter().map(|id| Value::from(*id)).collect();
140
141    let survivors: HashSet<&str> = base_by_id
142        .keys()
143        .copied()
144        .filter(|id| !swept.contains(id))
145        .chain(next_by_id.keys().copied())
146        .collect();
147    let inherited: Vec<&str> =
148        base.get("rootIds")
149            .and_then(Value::as_array)
150            .map_or(Vec::new(), |roots| {
151                roots
152                    .iter()
153                    .filter_map(Value::as_str)
154                    .filter(|id| survivors.contains(id))
155                    .collect()
156            });
157    let wanted: Vec<&str> = next
158        .get("rootIds")
159        .and_then(Value::as_array)
160        .map_or(Vec::new(), |roots| {
161            roots.iter().filter_map(Value::as_str).collect()
162        });
163    let root_ids = if inherited == wanted {
164        None
165    } else {
166        Some(wanted.iter().map(|id| Value::from(*id)).collect())
167    };
168
169    let cursor_changed = base.get("cursor") != next.get("cursor");
170    (changed, removed, root_ids, cursor_changed)
171}