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/23-incremental-views-report.md`](../../../../../docs/23-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                let (Some(x), Some(y)) = (a.get(common), b.get(common)) else {
172                    return set(path, new, ops);
173                };
174                path.push(Step::Index(common as u32));
175                walk(path, &x, &y, ops);
176                path.pop();
177                return;
178            }
179            // Removals from the back, so the indices ahead of each one are still the client's.
180            for i in (common..a.len() - tail).rev() {
181                ops.push(Op::Remove {
182                    path: path.clone(),
183                    index: i as u32,
184                });
185            }
186            for (offset, v) in b.slice(common, b.len() - tail).iter().enumerate() {
187                let Ok(value) = Repr::of(&v) else {
188                    return set(path, new, ops);
189                };
190                ops.push(Op::Insert {
191                    path: path.clone(),
192                    index: (common + offset) as u32,
193                    value,
194                });
195            }
196        }
197        _ => set(path, new, ops),
198    }
199}
200
201fn set(path: &Path, new: &Value, ops: &mut Vec<Op>) {
202    // A value that is not storable is not sendable either, and the checker refuses a state that
203    // contains one (`B0413`). Nothing is emitted rather than something wrong: a client that
204    // received no op for a change it cannot represent would be stale, and a client sent a
205    // fabricated one would be wrong.
206    if let Ok(value) = Repr::of(new) {
207        ops.push(Op::Set {
208            path: path.clone(),
209            value,
210        });
211    }
212}
213
214fn put(path: &Path, key: &Value, value: &Value, ops: &mut Vec<Op>) {
215    if let (Ok(key), Ok(value)) = (Repr::of(key), Repr::of(value)) {
216        ops.push(Op::Put {
217            path: path.clone(),
218            key,
219            value,
220        });
221    }
222}
223
224fn drop_key(path: &Path, key: &Value, ops: &mut Vec<Op>) {
225    if let Ok(key) = Repr::of(key) {
226        ops.push(Op::Drop {
227            path: path.clone(),
228            key,
229        });
230    }
231}
232
233/// Apply a patch, in order.
234///
235/// Fails rather than guesses: a path that does not exist means the client and the server disagree
236/// about the state, and continuing from a state neither of them has is how a browser ends up
237/// rendering something that never happened. The subscription's answer to a failure is the same as
238/// the log's — ask for the whole value again ([`crate::render`]).
239pub fn apply(state: &Value, ops: &[Op]) -> Result<Value, BadPatch> {
240    let mut out = state.clone();
241    for op in ops {
242        out = apply_one(&out, op)?;
243    }
244    Ok(out)
245}
246
247fn apply_one(state: &Value, op: &Op) -> Result<Value, BadPatch> {
248    let (path, edit): (&Path, Edit) = match op {
249        Op::Set { path, value } => (path, Edit::Set(value.to_value())),
250        Op::Insert { path, index, value } => (path, Edit::Insert(*index, value.to_value())),
251        Op::Remove { path, index } => (path, Edit::Remove(*index)),
252        Op::Put { path, key, value } => (path, Edit::Put(key.to_value(), value.to_value())),
253        Op::Drop { path, key } => (path, Edit::Drop(key.to_value())),
254    };
255    edit_at(state, path, &edit)
256}
257
258enum Edit {
259    Set(Value),
260    Insert(u32, Value),
261    Remove(u32),
262    Put(Value, Value),
263    Drop(Value),
264}
265
266fn edit_at(state: &Value, path: &[Step], edit: &Edit) -> Result<Value, BadPatch> {
267    let Some((step, rest)) = path.split_first() else {
268        return here(state, edit);
269    };
270    match (step, state) {
271        (Step::Field(name), Value::Data(d)) => {
272            let old = d
273                .fields
274                .get(name.as_str())
275                .ok_or_else(|| bad(format!("no field `{name}` here")))?;
276            let next = edit_at(old, rest, edit)?;
277            let mut fields = Fields::new();
278            for (k, v) in d.fields.iter() {
279                fields.insert(
280                    k.clone(),
281                    if k.as_ref() == name.as_str() {
282                        next.clone()
283                    } else {
284                        v.clone()
285                    },
286                );
287            }
288            Ok(Value::data(d.ty.clone(), d.variant.clone(), fields))
289        }
290        (Step::Index(i), Value::List(xs)) => {
291            let i = *i as usize;
292            let old = xs
293                .get(i)
294                .ok_or_else(|| bad(format!("this list has no element {i}")))?;
295            let next = edit_at(&old, rest, edit)?;
296            // Through `Seq::set` rather than a `Vec`, so a column that a patch edits one element of
297            // stays a column instead of being taken apart and put back together.
298            let mut items = (**xs).clone();
299            items.set(i, next);
300            Ok(Value::of_seq(items))
301        }
302        (Step::Key(k), Value::Map(m)) => {
303            let key = k.to_value();
304            let old = m.get(&key).ok_or_else(|| bad("no such key here"))?;
305            let next = edit_at(old, rest, edit)?;
306            Ok(Value::Map(m.insert(key, next)))
307        }
308        (step, other) => Err(bad(format!(
309            "cannot follow {} into a {}",
310            match step {
311                Step::Field(n) => format!("`.{n}`"),
312                Step::Index(i) => format!("`[{i}]`"),
313                Step::Key(_) => "a key".to_string(),
314            },
315            other.display()
316        ))),
317    }
318}
319
320fn here(state: &Value, edit: &Edit) -> Result<Value, BadPatch> {
321    match edit {
322        Edit::Set(v) => Ok(v.clone()),
323        Edit::Insert(i, v) => {
324            let Value::List(xs) = state else {
325                return Err(bad("insert applies to a list"));
326            };
327            let i = *i as usize;
328            if i > xs.len() {
329                return Err(bad(format!(
330                    "cannot insert at {i} in a list of {}",
331                    xs.len()
332                )));
333            }
334            let mut items = xs.to_vec();
335            items.insert(i, v.clone());
336            Ok(Value::list(items))
337        }
338        Edit::Remove(i) => {
339            let Value::List(xs) = state else {
340                return Err(bad("remove applies to a list"));
341            };
342            let i = *i as usize;
343            if i >= xs.len() {
344                return Err(bad(format!("this list has no element {i}")));
345            }
346            let mut items = xs.to_vec();
347            items.remove(i);
348            Ok(Value::list(items))
349        }
350        Edit::Put(k, v) => {
351            let Value::Map(m) = state else {
352                return Err(bad("put applies to a map"));
353            };
354            Ok(Value::Map(m.insert(k.clone(), v.clone())))
355        }
356        Edit::Drop(k) => {
357            let Value::Map(m) = state else {
358                return Err(bad("drop applies to a map"));
359            };
360            Ok(Value::Map(m.remove(k)))
361        }
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::pmap::PMap;
369    use std::sync::Arc;
370
371    fn card(id: &str, text: &str, column: i64) -> Value {
372        Value::data(
373            Arc::from("Card"),
374            None,
375            Fields::from_iter([
376                (Arc::from("id"), Value::str_(id)),
377                (Arc::from("text"), Value::str_(text)),
378                (Arc::from("column"), Value::Int(column)),
379            ]),
380        )
381    }
382
383    fn board(cards: &[(&str, &str, i64)]) -> Value {
384        let mut m = PMap::new();
385        for (id, text, column) in cards {
386            m = m.insert(Value::str_(id), card(id, text, *column));
387        }
388        Value::data(
389            Arc::from("Board"),
390            None,
391            Fields::from_iter([(Arc::from("cards"), Value::Map(m))]),
392        )
393    }
394
395    /// The round trip is the whole contract: whatever the ops are, applying them has to produce
396    /// the value they were derived from.
397    fn round_trip(old: &Value, new: &Value) -> Vec<Op> {
398        let ops = diff(old, new);
399        assert_eq!(&apply(old, &ops).expect("applies"), new, "ops: {ops:?}");
400        ops
401    }
402
403    #[test]
404    fn an_unchanged_state_is_no_ops() {
405        assert!(round_trip(&board(&[("1", "a", 0)]), &board(&[("1", "a", 0)])).is_empty());
406    }
407
408    #[test]
409    fn a_card_moved_on_a_large_board_is_one_op() {
410        let many: Vec<(String, String, i64)> = (0..500)
411            .map(|i| (format!("{i:03}"), format!("card {i}"), 0))
412            .collect();
413        let before: Vec<(&str, &str, i64)> = many
414            .iter()
415            .map(|(a, b, c)| (a.as_str(), b.as_str(), *c))
416            .collect();
417        let mut after = before.clone();
418        after[250].2 = 1;
419
420        let ops = round_trip(&board(&before), &board(&after));
421        assert_eq!(ops.len(), 1, "{ops:?}");
422        // And it names the path rather than carrying the board: field, key, field.
423        match &ops[0] {
424            Op::Set { path, value } => {
425                assert_eq!(path.len(), 3, "{path:?}");
426                assert_eq!(*value, Repr::Int(1));
427            }
428            other => panic!("expected a set, got {other:?}"),
429        }
430    }
431
432    #[test]
433    fn an_added_card_is_one_put_and_a_dropped_card_is_one_drop() {
434        let ops = round_trip(
435            &board(&[("1", "a", 0)]),
436            &board(&[("1", "a", 0), ("2", "b", 0)]),
437        );
438        assert!(matches!(ops.as_slice(), [Op::Put { .. }]), "{ops:?}");
439
440        let ops = round_trip(
441            &board(&[("1", "a", 0), ("2", "b", 0)]),
442            &board(&[("1", "a", 0)]),
443        );
444        assert!(matches!(ops.as_slice(), [Op::Drop { .. }]), "{ops:?}");
445    }
446
447    #[test]
448    fn a_list_appends_removes_and_replaces() {
449        let list = |xs: &[i64]| Value::list(xs.iter().copied().map(Value::Int).collect());
450
451        let ops = round_trip(&list(&[1, 2, 3]), &list(&[1, 2, 3, 4]));
452        assert!(
453            matches!(ops.as_slice(), [Op::Insert { index: 3, .. }]),
454            "{ops:?}"
455        );
456
457        let ops = round_trip(&list(&[1, 2, 3]), &list(&[1, 3]));
458        assert!(
459            matches!(ops.as_slice(), [Op::Remove { index: 1, .. }]),
460            "{ops:?}"
461        );
462
463        let ops = round_trip(&list(&[1, 2, 3]), &list(&[1, 9, 3]));
464        assert!(matches!(ops.as_slice(), [Op::Set { .. }]), "{ops:?}");
465
466        round_trip(&list(&[1, 2, 3]), &list(&[]));
467        round_trip(&list(&[]), &list(&[7, 8]));
468        round_trip(&list(&[1, 2, 3]), &list(&[3, 2, 1]));
469    }
470
471    #[test]
472    fn a_patch_against_the_wrong_state_fails_rather_than_guesses() {
473        let ops = diff(&board(&[("1", "a", 0)]), &board(&[("1", "b", 0)]));
474        // A client that never saw card 1 cannot apply an op about card 1's text.
475        assert!(apply(&board(&[]), &ops).is_err());
476    }
477
478    #[test]
479    fn a_variant_change_replaces_rather_than_descends() {
480        let some = Value::data(
481            Arc::from("Option"),
482            Some(Arc::from("Some")),
483            Fields::from_iter([(Arc::from("value"), Value::Int(1))]),
484        );
485        let none = Value::data(Arc::from("Option"), Some(Arc::from("None")), Fields::new());
486        let ops = round_trip(&some, &none);
487        assert!(matches!(ops.as_slice(), [Op::Set { .. }]), "{ops:?}");
488    }
489}