beck_core/
delta.rs

1//! The data patch: what a Mode B subscription carries instead of DOM patches.
2//!
3//! [`docs/05-tier-lowering.md`](../../../../../docs/05-tier-lowering.md) §5.1's table has one row
4//! that decides the whole mode — "Wire carries: **DOM patches** | **data patches (state diffs)**".
5//! [`mod@crate::diff`] is the first half — the DOM patch — and this is the second.
6//!
7//! # Why a diff rather than the value
8//!
9//! Sending the accumulator on every event would make each event cost the size of the *state*
10//! rather than the size of the *change*, which is the asymptote
11//! [`docs/24-incremental-views-report.md`](../../../../../docs/24-incremental-views-report.md)
12//! spent a whole report removing from the view path. A card moved on a thousand-card board is one
13//! [`Op::Put`], and it stays one as the board grows: cost per event is a function of the change,
14//! not of the collection.
15//!
16//! # Paths
17//!
18//! A [`Path`] is how to get from the root of the accumulator to the value that changed — a field
19//! of a record, an index of a list, a key of a map. Records and maps are addressed by *name* and
20//! by *key*, so an op stays applicable when its neighbours move; only a list is addressed by
21//! position, which is why the list rules below are the conservative ones.
22//!
23//! # What this is not
24//!
25//! It is not a merge. Ops are produced by one writer against a state the reader has, and applied
26//! in order at a known `seq` — the same discipline the DOM patch stream already has (§4.4). Two
27//! writers would need [`docs/10-decisions.md`](../../../../../docs/10-decisions.md) D7's
28//! CRDT-valued types, which are v1.x and are not this.
29
30use serde::{Deserialize, Serialize};
31
32use crate::core::{Fields, Value};
33use crate::repr::Repr;
34
35/// One step from a value to a value inside it.
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37pub enum Step {
38    /// A record field, by name.
39    Field(String),
40    /// A list element, by position.
41    Index(u32),
42    /// A map entry, by key. The key is a whole value, because a Beck map's keys are.
43    Key(Repr),
44}
45
46/// Where in the accumulator an op applies.
47pub type Path = Vec<Step>;
48
49/// One change to the state a client holds.
50#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
51pub enum Op {
52    /// Replace whatever is at `path`. The fallback for a scalar, and for any two values whose
53    /// shapes differ enough that describing the difference would cost more than the value.
54    Set { path: Path, value: Repr },
55    /// Insert into the list at `path`, before `index`.
56    Insert { path: Path, index: u32, value: Repr },
57    /// Remove element `index` of the list at `path`.
58    Remove { path: Path, index: u32 },
59    /// Put an entry in the map at `path`.
60    Put { path: Path, key: Repr, value: Repr },
61    /// Drop an entry from the map at `path`.
62    Drop { path: Path, key: Repr },
63}
64
65/// Why a patch could not be applied.
66#[derive(Clone, Debug, PartialEq, Eq)]
67pub struct BadPatch {
68    pub why: String,
69}
70
71impl std::fmt::Display for BadPatch {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        f.write_str(&self.why)
74    }
75}
76
77impl std::error::Error for BadPatch {}
78
79fn bad(why: impl Into<String>) -> BadPatch {
80    BadPatch { why: why.into() }
81}
82
83/// The ops that turn `old` into `new`.
84///
85/// Empty when they are equal, which is the common case for a subscriber whose corner of the state
86/// did not change — and the reason an idle client costs nothing.
87pub fn diff(old: &Value, new: &Value) -> Vec<Op> {
88    let mut ops = Vec::new();
89    walk(&mut Vec::new(), old, new, &mut ops);
90    ops
91}
92
93fn walk(path: &mut Path, old: &Value, new: &Value, ops: &mut Vec<Op>) {
94    if old == new {
95        return;
96    }
97    match (old, new) {
98        // A record's fields are fixed by its type, so two records of the same shape differ only in
99        // their values — and each field is its own path.
100        (Value::Data(a), Value::Data(b)) if a.ty == b.ty && a.variant == b.variant => {
101            for (name, av) in a.fields.iter() {
102                let Some(bv) = b.fields.get(name) else {
103                    // A field one side does not have is a different shape wearing the same name.
104                    set(path, new, ops);
105                    return;
106                };
107                path.push(Step::Field(name.to_string()));
108                walk(path, av, bv, ops);
109                path.pop();
110            }
111        }
112        (Value::Map(a), Value::Map(b)) => {
113            // Both iterate in key order, so this is one merge rather than two lookups per key.
114            let mut left = a.iter().peekable();
115            let mut right = b.iter().peekable();
116            loop {
117                match (left.peek(), right.peek()) {
118                    (None, None) => break,
119                    (Some((k, _)), None) => {
120                        drop_key(path, k, ops);
121                        left.next();
122                    }
123                    (None, Some((k, v))) => {
124                        put(path, k, v, ops);
125                        right.next();
126                    }
127                    (Some((lk, lv)), Some((rk, rv))) => match lk.cmp(rk) {
128                        std::cmp::Ordering::Less => {
129                            drop_key(path, lk, ops);
130                            left.next();
131                        }
132                        std::cmp::Ordering::Greater => {
133                            put(path, rk, rv, ops);
134                            right.next();
135                        }
136                        std::cmp::Ordering::Equal => {
137                            if lv != rv {
138                                // Descend: a card whose text changed is one `Set` at that card's
139                                // field, not a whole card on the wire.
140                                if let Ok(key) = Repr::of(lk) {
141                                    path.push(Step::Key(key));
142                                    walk(path, lv, rv, ops);
143                                    path.pop();
144                                } else {
145                                    put(path, rk, rv, ops);
146                                }
147                            }
148                            left.next();
149                            right.next();
150                        }
151                    },
152                }
153            }
154        }
155        (Value::List(a), Value::List(b)) => {
156            // A list is addressed by position, so the rules are the conservative ones: a shared
157            // prefix and a shared suffix are left alone and the middle is rewritten. That is exact
158            // for the shapes a fold produces — an append, a prepend, an element replaced — and it
159            // degrades to `Set` rather than to a wrong patch for anything else.
160            let common = a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count();
161            let tail = a
162                .iter()
163                .rev()
164                .zip(b.iter().rev())
165                .take_while(|(x, y)| x == y)
166                .count()
167                .min(a.len() - common)
168                .min(b.len() - common);
169            // Exactly one element differs and the rest match: describe *it* rather than the pair.
170            if a.len() == b.len() && common + tail + 1 == a.len() {
171                path.push(Step::Index(common as u32));
172                walk(path, &a[common], &b[common], ops);
173                path.pop();
174                return;
175            }
176            // Removals from the back, so the indices ahead of each one are still the client's.
177            for i in (common..a.len() - tail).rev() {
178                ops.push(Op::Remove {
179                    path: path.clone(),
180                    index: i as u32,
181                });
182            }
183            for (offset, v) in b[common..b.len() - tail].iter().enumerate() {
184                let Ok(value) = Repr::of(v) else {
185                    return set(path, new, ops);
186                };
187                ops.push(Op::Insert {
188                    path: path.clone(),
189                    index: (common + offset) as u32,
190                    value,
191                });
192            }
193        }
194        _ => set(path, new, ops),
195    }
196}
197
198fn set(path: &Path, new: &Value, ops: &mut Vec<Op>) {
199    // A value that is not storable is not sendable either, and the checker refuses a state that
200    // contains one (`B0413`). Nothing is emitted rather than something wrong: a client that
201    // received no op for a change it cannot represent would be stale, and a client sent a
202    // fabricated one would be wrong.
203    if let Ok(value) = Repr::of(new) {
204        ops.push(Op::Set {
205            path: path.clone(),
206            value,
207        });
208    }
209}
210
211fn put(path: &Path, key: &Value, value: &Value, ops: &mut Vec<Op>) {
212    if let (Ok(key), Ok(value)) = (Repr::of(key), Repr::of(value)) {
213        ops.push(Op::Put {
214            path: path.clone(),
215            key,
216            value,
217        });
218    }
219}
220
221fn drop_key(path: &Path, key: &Value, ops: &mut Vec<Op>) {
222    if let Ok(key) = Repr::of(key) {
223        ops.push(Op::Drop {
224            path: path.clone(),
225            key,
226        });
227    }
228}
229
230/// Apply a patch, in order.
231///
232/// Fails rather than guesses: a path that does not exist means the client and the server disagree
233/// about the state, and continuing from a state neither of them has is how a browser ends up
234/// rendering something that never happened. The subscription's answer to a failure is the same as
235/// the log's — ask for the whole value again ([`crate::render`]).
236pub fn apply(state: &Value, ops: &[Op]) -> Result<Value, BadPatch> {
237    let mut out = state.clone();
238    for op in ops {
239        out = apply_one(&out, op)?;
240    }
241    Ok(out)
242}
243
244fn apply_one(state: &Value, op: &Op) -> Result<Value, BadPatch> {
245    let (path, edit): (&Path, Edit) = match op {
246        Op::Set { path, value } => (path, Edit::Set(value.to_value())),
247        Op::Insert { path, index, value } => (path, Edit::Insert(*index, value.to_value())),
248        Op::Remove { path, index } => (path, Edit::Remove(*index)),
249        Op::Put { path, key, value } => (path, Edit::Put(key.to_value(), value.to_value())),
250        Op::Drop { path, key } => (path, Edit::Drop(key.to_value())),
251    };
252    edit_at(state, path, &edit)
253}
254
255enum Edit {
256    Set(Value),
257    Insert(u32, Value),
258    Remove(u32),
259    Put(Value, Value),
260    Drop(Value),
261}
262
263fn edit_at(state: &Value, path: &[Step], edit: &Edit) -> Result<Value, BadPatch> {
264    let Some((step, rest)) = path.split_first() else {
265        return here(state, edit);
266    };
267    match (step, state) {
268        (Step::Field(name), Value::Data(d)) => {
269            let old = d
270                .fields
271                .get(name.as_str())
272                .ok_or_else(|| bad(format!("no field `{name}` here")))?;
273            let next = edit_at(old, rest, edit)?;
274            let mut fields = Fields::new();
275            for (k, v) in d.fields.iter() {
276                fields.insert(
277                    k.clone(),
278                    if k.as_ref() == name.as_str() {
279                        next.clone()
280                    } else {
281                        v.clone()
282                    },
283                );
284            }
285            Ok(Value::data(d.ty.clone(), d.variant.clone(), fields))
286        }
287        (Step::Index(i), Value::List(xs)) => {
288            let i = *i as usize;
289            let old = xs
290                .get(i)
291                .ok_or_else(|| bad(format!("this list has no element {i}")))?;
292            let next = edit_at(old, rest, edit)?;
293            let mut items = xs.as_ref().clone();
294            items[i] = next;
295            Ok(Value::List(std::sync::Arc::new(items)))
296        }
297        (Step::Key(k), Value::Map(m)) => {
298            let key = k.to_value();
299            let old = m.get(&key).ok_or_else(|| bad("no such key here"))?;
300            let next = edit_at(old, rest, edit)?;
301            Ok(Value::Map(m.insert(key, next)))
302        }
303        (step, other) => Err(bad(format!(
304            "cannot follow {} into a {}",
305            match step {
306                Step::Field(n) => format!("`.{n}`"),
307                Step::Index(i) => format!("`[{i}]`"),
308                Step::Key(_) => "a key".to_string(),
309            },
310            other.display()
311        ))),
312    }
313}
314
315fn here(state: &Value, edit: &Edit) -> Result<Value, BadPatch> {
316    match edit {
317        Edit::Set(v) => Ok(v.clone()),
318        Edit::Insert(i, v) => {
319            let Value::List(xs) = state else {
320                return Err(bad("insert applies to a list"));
321            };
322            let i = *i as usize;
323            if i > xs.len() {
324                return Err(bad(format!(
325                    "cannot insert at {i} in a list of {}",
326                    xs.len()
327                )));
328            }
329            let mut items = xs.as_ref().clone();
330            items.insert(i, v.clone());
331            Ok(Value::List(std::sync::Arc::new(items)))
332        }
333        Edit::Remove(i) => {
334            let Value::List(xs) = state else {
335                return Err(bad("remove applies to a list"));
336            };
337            let i = *i as usize;
338            if i >= xs.len() {
339                return Err(bad(format!("this list has no element {i}")));
340            }
341            let mut items = xs.as_ref().clone();
342            items.remove(i);
343            Ok(Value::List(std::sync::Arc::new(items)))
344        }
345        Edit::Put(k, v) => {
346            let Value::Map(m) = state else {
347                return Err(bad("put applies to a map"));
348            };
349            Ok(Value::Map(m.insert(k.clone(), v.clone())))
350        }
351        Edit::Drop(k) => {
352            let Value::Map(m) = state else {
353                return Err(bad("drop applies to a map"));
354            };
355            Ok(Value::Map(m.remove(k)))
356        }
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use crate::pmap::PMap;
364    use std::sync::Arc;
365
366    fn card(id: &str, text: &str, column: i64) -> Value {
367        Value::data(
368            Arc::from("Card"),
369            None,
370            Fields::from_iter([
371                (Arc::from("id"), Value::str_(id)),
372                (Arc::from("text"), Value::str_(text)),
373                (Arc::from("column"), Value::Int(column)),
374            ]),
375        )
376    }
377
378    fn board(cards: &[(&str, &str, i64)]) -> Value {
379        let mut m = PMap::new();
380        for (id, text, column) in cards {
381            m = m.insert(Value::str_(id), card(id, text, *column));
382        }
383        Value::data(
384            Arc::from("Board"),
385            None,
386            Fields::from_iter([(Arc::from("cards"), Value::Map(m))]),
387        )
388    }
389
390    /// The round trip is the whole contract: whatever the ops are, applying them has to produce
391    /// the value they were derived from.
392    fn round_trip(old: &Value, new: &Value) -> Vec<Op> {
393        let ops = diff(old, new);
394        assert_eq!(&apply(old, &ops).expect("applies"), new, "ops: {ops:?}");
395        ops
396    }
397
398    #[test]
399    fn an_unchanged_state_is_no_ops() {
400        assert!(round_trip(&board(&[("1", "a", 0)]), &board(&[("1", "a", 0)])).is_empty());
401    }
402
403    #[test]
404    fn a_card_moved_on_a_large_board_is_one_op() {
405        let many: Vec<(String, String, i64)> = (0..500)
406            .map(|i| (format!("{i:03}"), format!("card {i}"), 0))
407            .collect();
408        let before: Vec<(&str, &str, i64)> = many
409            .iter()
410            .map(|(a, b, c)| (a.as_str(), b.as_str(), *c))
411            .collect();
412        let mut after = before.clone();
413        after[250].2 = 1;
414
415        let ops = round_trip(&board(&before), &board(&after));
416        assert_eq!(ops.len(), 1, "{ops:?}");
417        // And it names the path rather than carrying the board: field, key, field.
418        match &ops[0] {
419            Op::Set { path, value } => {
420                assert_eq!(path.len(), 3, "{path:?}");
421                assert_eq!(*value, Repr::Int(1));
422            }
423            other => panic!("expected a set, got {other:?}"),
424        }
425    }
426
427    #[test]
428    fn an_added_card_is_one_put_and_a_dropped_card_is_one_drop() {
429        let ops = round_trip(
430            &board(&[("1", "a", 0)]),
431            &board(&[("1", "a", 0), ("2", "b", 0)]),
432        );
433        assert!(matches!(ops.as_slice(), [Op::Put { .. }]), "{ops:?}");
434
435        let ops = round_trip(
436            &board(&[("1", "a", 0), ("2", "b", 0)]),
437            &board(&[("1", "a", 0)]),
438        );
439        assert!(matches!(ops.as_slice(), [Op::Drop { .. }]), "{ops:?}");
440    }
441
442    #[test]
443    fn a_list_appends_removes_and_replaces() {
444        let list = |xs: &[i64]| Value::List(Arc::new(xs.iter().copied().map(Value::Int).collect()));
445
446        let ops = round_trip(&list(&[1, 2, 3]), &list(&[1, 2, 3, 4]));
447        assert!(
448            matches!(ops.as_slice(), [Op::Insert { index: 3, .. }]),
449            "{ops:?}"
450        );
451
452        let ops = round_trip(&list(&[1, 2, 3]), &list(&[1, 3]));
453        assert!(
454            matches!(ops.as_slice(), [Op::Remove { index: 1, .. }]),
455            "{ops:?}"
456        );
457
458        let ops = round_trip(&list(&[1, 2, 3]), &list(&[1, 9, 3]));
459        assert!(matches!(ops.as_slice(), [Op::Set { .. }]), "{ops:?}");
460
461        round_trip(&list(&[1, 2, 3]), &list(&[]));
462        round_trip(&list(&[]), &list(&[7, 8]));
463        round_trip(&list(&[1, 2, 3]), &list(&[3, 2, 1]));
464    }
465
466    #[test]
467    fn a_patch_against_the_wrong_state_fails_rather_than_guesses() {
468        let ops = diff(&board(&[("1", "a", 0)]), &board(&[("1", "b", 0)]));
469        // A client that never saw card 1 cannot apply an op about card 1's text.
470        assert!(apply(&board(&[]), &ops).is_err());
471    }
472
473    #[test]
474    fn a_variant_change_replaces_rather_than_descends() {
475        let some = Value::data(
476            Arc::from("Option"),
477            Some(Arc::from("Some")),
478            Fields::from_iter([(Arc::from("value"), Value::Int(1))]),
479        );
480        let none = Value::data(Arc::from("Option"), Some(Arc::from("None")), Fields::new());
481        let ops = round_trip(&some, &none);
482        assert!(matches!(ops.as_slice(), [Op::Set { .. }]), "{ops:?}");
483    }
484}