Skip to main content

offline_merge/
offline_merge.rs

1//! What happens when two people edit the same document while apart.
2//!
3//! Run it with:
4//!
5//! ```text
6//! cargo run --package offline-protocol-data --example offline_merge
7//! ```
8//!
9//! Two replicas start from one shared state, lose contact, both keep editing,
10//! and then exchange what each committed while away. The point of the example
11//! is the last line: after the exchange both replicas hold the same state, and
12//! neither of them ran a conflict resolver an application had to write.
13//!
14//! This is the engine seam rather than the application API. An application
15//! calls `data_map_set` and friends on `OfflineProtocol` and never sees a
16//! delta, because the protocol carries them; see
17//! `cargo run --package offline-protocol --example replicated_notes` for that
18//! side. What this example is for is the question the other one cannot answer:
19//! which edit wins, and what an application should therefore put in one key.
20
21use offline_protocol_data::{DataDoc, DataValue};
22
23fn text(value: &str) -> DataValue {
24    DataValue::Text {
25        value: value.to_string(),
26    }
27}
28
29fn main() -> Result<(), Box<dyn std::error::Error>> {
30    // One shared starting point: Ada writes, and Bo receives that change.
31    let mut ada = DataDoc::new();
32    ada.map_set("meta", "title", text("Coast road"))?;
33    ada.list_push("packing", text("water"))?;
34    let shared = ada.commit()?.expect("a change to share");
35
36    let mut bo = DataDoc::new();
37    bo.import(&shared.bytes)?;
38
39    println!("both start at: {}", bo.export_json()?);
40
41    // The link goes away here. Neither side can see the other's edits, and
42    // neither side blocks on that.
43    ada.map_set("meta", "title", text("Coast road, Friday"))?;
44    ada.list_push("packing", text("map"))?;
45    ada.text_insert("notes", 0, "leave at six")?;
46    ada.counter_increment("edits", 1.0)?;
47    let from_ada = ada.commit()?.expect("Ada's offline work");
48
49    bo.map_set("meta", "title", text("Coast road, Saturday"))?;
50    bo.list_push("packing", text("torch"))?;
51    bo.text_insert("notes", 0, "bring the tent, ")?;
52    bo.counter_increment("edits", 1.0)?;
53    let from_bo = bo.commit()?.expect("Bo's offline work");
54
55    println!("apart, Ada has: {}", ada.export_json()?);
56    println!("apart, Bo has:  {}", bo.export_json()?);
57
58    // The link comes back. Each side imports what the other committed. Order
59    // does not matter and neither does arriving twice: importing the same
60    // bytes again is a no-op, which is why at-least-once delivery is enough.
61    ada.import(&from_bo.bytes)?;
62    bo.import(&from_ada.bytes)?;
63    bo.import(&from_ada.bytes)?;
64
65    let ada_state = ada.export_json()?;
66    let bo_state = bo.export_json()?;
67
68    println!("together, Ada: {ada_state}");
69    println!("together, Bo:  {bo_state}");
70    println!("identical:     {}", ada_state == bo_state);
71
72    // Read that state against the four collection types:
73    //
74    // - `meta.title` is a map key two people wrote. One value wins, both
75    //   replicas pick the same one, and neither ends up holding half of each.
76    //   That is the reason to put independently editable fields in separate
77    //   keys rather than in one JSON blob.
78    // - `packing` kept every insertion. A list loses nothing to a concurrent
79    //   push.
80    // - `notes` merged at character level, so both sentences survive.
81    // - `edits` is 2. Two offline increments of 1 add up rather than
82    //   overwriting each other, which is what makes a counter a counter and
83    //   not an integer in a map.
84    println!("packing:       {} entries", ada.list_len("packing")?);
85    println!("notes:         {:?}", ada.text_value("notes")?);
86    println!("edits:         {}", ada.counter_value("edits")?);
87
88    Ok(())
89}