pub struct Document { /* private fields */ }Expand description
A unified CRDT document container.
The Document is the main entry point for Diamond Types Extended. It wraps an OpLog and provides:
- A Map root for organizing data
- Transaction-based mutations (solves Rust borrow issues)
- Consistent API across all CRDT types
- Clean serialization/replication
§Conflict Resolution
Diamond Types Extended uses deterministic conflict resolution that guarantees all peers converge to the same state:
-
Maps: Each key is an LWW (Last-Writer-Wins) register. Concurrent writes are resolved by
(lamport_timestamp, agent_id)ordering - higher timestamp wins, with agent_id as tiebreaker. Useget_conflicted()to see losing values. -
Sets: OR-Set (Observed-Remove) with add-wins semantics. If one peer adds a value while another removes it concurrently, the add wins.
-
Text: Operations are interleaved based on causal ordering. Concurrent inserts at the same position are ordered deterministically.
§Transaction Isolation
Transactions provide a consistent view of the document. All reads within a transaction see the state as of transaction start - they do NOT see writes made earlier in the same transaction. This matches typical CRDT semantics where operations are applied to the log immediately.
use diamond_types_extended::{Document, Uuid};
let mut doc = Document::new();
let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
doc.transact(alice, |tx| {
tx.root().set("key", "value");
// Note: get() here WILL see "value" because we read from the oplog
// which has the operation applied
});§Example
use diamond_types_extended::{Document, Uuid};
let mut doc = Document::new();
let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
// Mutations happen in transactions
doc.transact(alice, |tx| {
tx.root().set("title", "My Document");
tx.root().create_text("content");
});
// Reading is direct
let title = doc.root().get("title");Implementations§
Source§impl Document
impl Document
Sourcepub fn create_agent(&mut self, id: Uuid) -> AgentId
pub fn create_agent(&mut self, id: Uuid) -> AgentId
Create or retrieve an agent ID from a UUID.
Agents represent participants in collaborative editing. Each agent should have a unique UUID per editing session.
Sourcepub fn version(&self) -> &Frontier
pub fn version(&self) -> &Frontier
Get the current version of the document.
This can be used for:
- Checking if documents are at the same version
- Creating patches since a version with
encode_since
Sourcepub fn remote_version(&self) -> RemoteFrontier
pub fn remote_version(&self) -> RemoteFrontier
Get the document’s current version as a portable RemoteFrontier.
Unlike version(), which returns local-only version numbers,
the remote frontier uses (Uuid, seq) pairs that are safe to send to other
peers for incremental sync.
use diamond_types_extended::{Document, Uuid};
let mut doc = Document::new();
let agent = doc.create_agent(Uuid::from_u128(0x1));
doc.transact(agent, |tx| { tx.root().set("k", "v"); });
let remote_v = doc.remote_version();
assert_eq!(remote_v.len(), 1);Sourcepub fn ops_since_remote(
&self,
remote_frontier: &RemoteFrontier,
) -> SerializedOps<'_>
pub fn ops_since_remote( &self, remote_frontier: &RemoteFrontier, ) -> SerializedOps<'_>
Get operations since a remote frontier received from another peer.
This is the safe alternative to ops_since() for
cross-document sync. It resolves the remote frontier to local versions,
gracefully handling unknown agents by returning more operations rather
than panicking.
use diamond_types_extended::{Document, Uuid, Frontier};
let mut doc_a = Document::new();
let mut doc_b = Document::new();
let alice = doc_a.create_agent(Uuid::from_u128(0xA11CE));
doc_a.transact(alice, |tx| { tx.root().set("k", "v"); });
// Full sync using remote frontier
let ops = doc_a.ops_since_remote(&doc_b.remote_version()).into_owned();
doc_b.merge_ops(ops).unwrap();
assert!(doc_b.root().contains_key("k"));Sourcepub fn get_map(&self, path: &[&str]) -> Option<MapRef<'_>>
pub fn get_map(&self, path: &[&str]) -> Option<MapRef<'_>>
Get a read-only reference to a map at the given path.
Path elements are keys in nested maps starting from root. Returns None if path doesn’t exist or isn’t a Map.
Sourcepub fn get_text(&self, path: &[&str]) -> Option<TextRef<'_>>
pub fn get_text(&self, path: &[&str]) -> Option<TextRef<'_>>
Get a read-only reference to a text CRDT at the given path. Returns None if path doesn’t exist or isn’t a Text.
Sourcepub fn get_set(&self, path: &[&str]) -> Option<SetRef<'_>>
pub fn get_set(&self, path: &[&str]) -> Option<SetRef<'_>>
Get a read-only reference to a set CRDT at the given path. Returns None if path doesn’t exist or isn’t a Set.
Sourcepub fn get_text_by_id(&self, id: CrdtId) -> Option<TextRef<'_>>
pub fn get_text_by_id(&self, id: CrdtId) -> Option<TextRef<'_>>
Get a read-only reference to a text CRDT by its ID.
Sourcepub fn get_set_by_id(&self, id: CrdtId) -> Option<SetRef<'_>>
pub fn get_set_by_id(&self, id: CrdtId) -> Option<SetRef<'_>>
Get a read-only reference to a set CRDT by its ID.
Sourcepub fn get_register_by_id(&self, id: CrdtId) -> Option<RegisterRef<'_>>
pub fn get_register_by_id(&self, id: CrdtId) -> Option<RegisterRef<'_>>
Get a read-only reference to a register CRDT by its ID.
Sourcepub fn transact<F, R>(&mut self, agent: AgentId, f: F) -> Rwhere
F: FnOnce(&mut Transaction<'_>) -> R,
pub fn transact<F, R>(&mut self, agent: AgentId, f: F) -> Rwhere
F: FnOnce(&mut Transaction<'_>) -> R,
Execute mutations in a transaction.
The transaction captures the agent ID, so you don’t need to pass it to each mutation. This also solves Rust’s borrow checker issues when modifying nested CRDTs.
§Example
use diamond_types_extended::{Document, Uuid};
let mut doc = Document::new();
let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
doc.transact(alice, |tx| {
tx.root().set("count", 42);
tx.root().create_map("nested");
// Access nested map in same transaction
if let Some(mut nested) = tx.get_map_mut(&["nested"]) {
nested.set("inner", "value");
}
});Sourcepub fn ops_since(&self, version: &Frontier) -> SerializedOps<'_>
pub fn ops_since(&self, version: &Frontier) -> SerializedOps<'_>
Get operations since a version for serialization.
Pass &Frontier::root() to get all operations (full sync).
Pass peer.version() to get only new operations (delta sync).
Returns SerializedOps which borrows from this document. Use .into() to
convert to SerializedOpsOwned for sending across threads or network.
§Example
use diamond_types_extended::{Document, Frontier, Uuid};
let mut doc_a = Document::new();
let mut doc_b = Document::new();
let alice = doc_a.create_agent(Uuid::from_u128(0xA11CE));
// Alice makes changes
doc_a.transact(alice, |tx| {
tx.root().set("key", "value");
});
// Full sync: get all operations
let all_ops = doc_a.ops_since(&Frontier::root()).into();
doc_b.merge_ops(all_ops).unwrap();
// Now doc_b has Alice's changes
assert!(doc_b.root().contains_key("key"));Sourcepub fn merge_ops(&mut self, ops: SerializedOpsOwned) -> Result<(), ParseError>
pub fn merge_ops(&mut self, ops: SerializedOpsOwned) -> Result<(), ParseError>
Merge operations from another peer (owned version).
Use this when receiving operations over a network or from another thread.
The owned version (SerializedOpsOwned) can be sent across thread boundaries.
Operations are applied causally - if an operation depends on operations you don’t have yet, it will still be stored and applied correctly once dependencies arrive.
Sourcepub fn merge_ops_borrowed(
&mut self,
ops: SerializedOps<'_>,
) -> Result<(), ParseError>
pub fn merge_ops_borrowed( &mut self, ops: SerializedOps<'_>, ) -> Result<(), ParseError>
Merge operations from another peer (borrowed version).
Use this when you have a SerializedOps reference and don’t need to
send it across threads. Slightly more efficient than merge_ops as it
avoids cloning strings.
Sourcepub fn checkout(&self) -> BTreeMap<String, MaterializedValue>
pub fn checkout(&self) -> BTreeMap<String, MaterializedValue>
Get the full document state as a nested map structure.
Returns a BTreeMap<String, MaterializedValue> representing the fully
resolved state of all CRDTs rooted at the document. Useful for:
- Comparing document content across peers (content convergence)
- Serialization to JSON or other formats
- Debugging and inspection
Note: For comparing documents after sync, compare checkout() output
rather than version(). Local versions (LVs) differ across peers
after concurrent operations, but content will converge.
§Example
use diamond_types_extended::{Document, Uuid};
let mut doc = Document::new();
let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
doc.transact(alice, |tx| {
tx.root().set("key", "value");
tx.root().create_text("content");
});
doc.transact(alice, |tx| {
tx.get_text_mut(&["content"]).unwrap().insert(0, "Hello!");
});
let state = doc.checkout();
// state is a BTreeMap with the full document treeSourcepub fn get(&self, path: &[&str], key: &str) -> Option<Value>
pub fn get(&self, path: &[&str], key: &str) -> Option<Value>
Get a primitive value by key from a map at path.
Path points to the containing map, key is the field name. Empty path reads from the root map.
use diamond_types_extended::{Document, Uuid};
let mut doc = Document::new();
let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
doc.transact(alice, |tx| {
tx.root().set("name", "Alice");
tx.root().create_map("meta");
});
doc.transact(alice, |tx| {
tx.get_map_mut(&["meta"]).unwrap().set("role", "admin");
});
assert_eq!(doc.get(&[], "name").unwrap().as_str(), Some("Alice"));
assert_eq!(doc.get(&["meta"], "role").unwrap().as_str(), Some("admin"));Sourcepub fn get_str(&self, path: &[&str], key: &str) -> Option<String>
pub fn get_str(&self, path: &[&str], key: &str) -> Option<String>
Get a string value by key from a map at path.
Returns None if the path doesn’t exist, the key doesn’t exist,
or the value isn’t a string.
Sourcepub fn get_int(&self, path: &[&str], key: &str) -> Option<i64>
pub fn get_int(&self, path: &[&str], key: &str) -> Option<i64>
Get an integer value by key from a map at path.
Returns None if the path doesn’t exist, the key doesn’t exist,
or the value isn’t an integer.
Sourcepub fn text_content(&self, path: &[&str]) -> Option<String>
pub fn text_content(&self, path: &[&str]) -> Option<String>
Get text content at a path (path points to the Text CRDT).
use diamond_types_extended::{Document, Uuid};
let mut doc = Document::new();
let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
doc.transact(alice, |tx| {
let id = tx.root().create_text("content");
tx.text_by_id(id).unwrap().insert(0, "Hello!");
});
assert_eq!(doc.text_content(&["content"]).unwrap(), "Hello!");Sourcepub fn map_keys(&self, path: &[&str]) -> Option<Vec<String>>
pub fn map_keys(&self, path: &[&str]) -> Option<Vec<String>>
Get keys of a map at a path (owned, no borrow issues).
Empty path returns root map keys.
Sourcepub fn ops_since_owned(&self, version: &Frontier) -> SerializedOpsOwned
pub fn ops_since_owned(&self, version: &Frontier) -> SerializedOpsOwned
Get owned operations since a version.
Convenience wrapper around ops_since() that returns the owned type
directly, saving the .into() call at every use site.
use diamond_types_extended::{Document, Frontier, Uuid};
let mut doc_a = Document::new();
let mut doc_b = Document::new();
let alice = doc_a.create_agent(Uuid::from_u128(0xA11CE));
doc_a.transact(alice, |tx| { tx.root().set("key", "value"); });
let ops = doc_a.ops_since_owned(&Frontier::root());
doc_b.merge_ops(ops).unwrap();
assert!(doc_b.root().contains_key("key"));Sourcepub fn to_json(&self) -> String
pub fn to_json(&self) -> String
Serialize the full document state as JSON.
Primitives are serialized naturally (strings as "foo", ints as 42,
bools as true/false, nil as null) so the output is human-friendly
regardless of the tagged enum representation used by MaterializedValue.
Sourcepub fn to_json_pretty(&self) -> String
pub fn to_json_pretty(&self) -> String
Serialize the full document state as pretty-printed JSON.
See to_json for details on the output format.
Sourcepub fn writer(&mut self, agent: AgentId) -> DocumentWriter<'_>
pub fn writer(&mut self, agent: AgentId) -> DocumentWriter<'_>
Create a closure-free mutation handle for FFI/WASM consumers.
DocumentWriter binds an agent to a document so mutations don’t
need closures. Each method executes a single operation. For batching
multiple operations in Rust, prefer transact().
use diamond_types_extended::{Document, Uuid};
let mut doc = Document::new();
let alice = doc.create_agent(Uuid::from_u128(0xA11CE));
let mut w = doc.writer(alice);
w.root_set("name", "Alice");
w.root_create_text("content");
assert!(w.text_push(&["content"], "Hello!"));
assert!(!w.text_push(&["nonexistent"], "oops"));
drop(w); // release borrow before reading
assert_eq!(doc.root().get("name").unwrap().as_str(), Some("Alice"));