Skip to main content

replicated_notes/
replicated_notes.rs

1//! Replicated documents on one device: open a store, edit, persist, reopen.
2//!
3//! Run it with:
4//!
5//! ```text
6//! cargo run --package offline-protocol --example replicated_notes
7//! ```
8//!
9//! What it shows, in order: that a document needs no storage setup, that the
10//! four collection types behave the way an application expects, that a flush
11//! is what makes a change durable, and that the same records reopen into the
12//! same document after the engine is rebuilt.
13//!
14//! What it deliberately does not show is replication, which needs a second
15//! device, a confirmed MLS session, and a transport. The merge semantics that
16//! makes replication safe is a separate example that needs none of those:
17//! `cargo run --package offline-protocol-data --example offline_merge`.
18//!
19//! The backend below is an in-memory `ProtocolStateStorage`, written out in
20//! full because it is also the shape of the bring-your-own-storage seam: an
21//! application swaps SQLite, files, or its own encrypted store in at exactly
22//! this point, and sealing sits above it, so what `store` receives here is
23//! already ciphertext.
24
25use std::collections::HashMap;
26use std::sync::{Arc, Mutex};
27
28use offline_protocol::{
29    DataValue, OfflineProtocol, ProtocolConfig, ProtocolStateResult, ProtocolStateStorage,
30};
31use offline_protocol_mls::storage::InMemoryStorage;
32
33/// A protocol-state backend that keeps records in a map.
34///
35/// Four methods over `(key_type, key_id, bytes)`. The rules a real adapter
36/// has to honour are the ones the conformance suite checks: bytes are stored
37/// verbatim, a second write to one key replaces the first, and a key that was
38/// never written loads as `None` rather than as an error.
39#[derive(Default)]
40struct MemoryStateStorage {
41    records: Mutex<HashMap<(String, String), Vec<u8>>>,
42}
43
44impl ProtocolStateStorage for MemoryStateStorage {
45    fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> ProtocolStateResult<()> {
46        let mut records = self.records.lock().expect("storage mutex");
47        records.insert((key_type.to_string(), key_id.to_string()), data.to_vec());
48        Ok(())
49    }
50
51    fn load(&self, key_type: &str, key_id: &str) -> ProtocolStateResult<Option<Vec<u8>>> {
52        let records = self.records.lock().expect("storage mutex");
53        Ok(records
54            .get(&(key_type.to_string(), key_id.to_string()))
55            .cloned())
56    }
57
58    fn delete(&self, key_type: &str, key_id: &str) -> ProtocolStateResult<()> {
59        let mut records = self.records.lock().expect("storage mutex");
60        records.remove(&(key_type.to_string(), key_id.to_string()));
61        Ok(())
62    }
63
64    fn list_keys(&self, key_type: &str) -> ProtocolStateResult<Vec<String>> {
65        let records = self.records.lock().expect("storage mutex");
66        Ok(records
67            .keys()
68            .filter(|(stored_type, _)| stored_type == key_type)
69            .map(|(_, key_id)| key_id.clone())
70            .collect())
71    }
72}
73
74fn main() -> Result<(), Box<dyn std::error::Error>> {
75    // Both stores outlive the engine below, which is what makes the reopen at
76    // the end a reopen rather than a fresh start. The secure store matters as
77    // much as the record store: documents are sealed with a key that lives in
78    // it, so dropping only that half looks exactly like data loss.
79    let secure = Arc::new(InMemoryStorage::default());
80    let records = Arc::new(MemoryStateStorage::default());
81
82    // `data.enabled` defaults to true, so nothing here switches the layer on.
83    let config = ProtocolConfig::builder("com.example.notes", "default").build()?;
84    let mut protocol = OfflineProtocol::new(config)?;
85
86    // Documents are sealed at rest and the record key is minted here, so this
87    // call is the prerequisite for every method below.
88    protocol.initialize_mls(secure.clone(), records.clone())?;
89
90    // A space is an MLS scope: a peer's address for a 1:1 space, or the group
91    // id for a group. Nothing replicates in this example, so the name only
92    // has to be a valid one.
93    let space = "demo-space";
94
95    protocol.data_create_doc(space, "trip")?;
96
97    // A map: last writer wins per key, and different keys never conflict.
98    protocol.data_map_set(
99        space,
100        "trip",
101        "meta",
102        "title",
103        DataValue::Text {
104            value: "Coast road".to_string(),
105        },
106    )?;
107
108    // A list: concurrent insertions both survive, in a deterministic order.
109    for item in ["water", "map", "torch"] {
110        protocol.data_list_push(
111            space,
112            "trip",
113            "packing",
114            DataValue::Text {
115                value: item.to_string(),
116            },
117        )?;
118    }
119
120    // Text: character-level merge, so two people typing in one paragraph keep
121    // both sets of words. Positions are character offsets, not byte offsets.
122    protocol.data_text_insert(space, "trip", "notes", 0, "Meet at the bridge")?;
123
124    // A counter: increments add up rather than overwriting each other.
125    protocol.data_counter_increment(space, "trip", "opened", 1.0)?;
126
127    // Edits batch before they reach storage. This is the call that makes them
128    // durable, and `data_changed` fires after the same point, never before.
129    protocol.data_flush(space, "trip")?;
130
131    println!("spaces:    {:?}", protocol.data_list_spaces()?);
132    println!("documents: {:?}", protocol.data_list_docs(space)?);
133    println!(
134        "compacted: {} bytes",
135        protocol.data_doc_size(space, "trip")?
136    );
137    println!(
138        "history:   {} bytes",
139        protocol.data_export_raw(space, "trip")?.len()
140    );
141    println!("json:      {}", protocol.data_doc_json(space, "trip")?);
142
143    // Both halves of the escape hatch are above: `data_doc_json` is the plain
144    // JSON of the current state, `data_export_raw` the engine-native history.
145    // An application can always take its data and leave.
146
147    // Reopen: a new engine over the same two stores. The records were sealed
148    // by the first one and are read back by the second, which is the property
149    // an application depends on across a restart.
150    drop(protocol);
151    let config = ProtocolConfig::builder("com.example.notes", "default").build()?;
152    let mut reopened = OfflineProtocol::new(config)?;
153    reopened.initialize_mls(secure, records)?;
154
155    println!("after reopen");
156    println!(
157        "  title:   {:?}",
158        reopened.data_map_get(space, "trip", "meta", "title")?
159    );
160    println!(
161        "  packing: {}",
162        reopened.data_list_len(space, "trip", "packing")?
163    );
164    println!(
165        "  notes:   {:?}",
166        reopened.data_text_value(space, "trip", "notes")?
167    );
168    println!(
169        "  opened:  {}",
170        reopened.data_counter_value(space, "trip", "opened")?
171    );
172
173    Ok(())
174}