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}