Skip to main content

Document

Struct Document 

Source
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. Use get_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

Source

pub fn new() -> Self

Create a new empty document.

Source

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.

Source

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
Source

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);
Source

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"));
Source

pub fn is_empty(&self) -> bool

Check if the document is empty (no operations).

Source

pub fn root(&self) -> MapRef<'_>

Get a read-only reference to the root map.

Source

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.

Source

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.

Source

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.

Source

pub fn get_text_by_id(&self, id: CrdtId) -> Option<TextRef<'_>>

Get a read-only reference to a text CRDT by its ID.

Source

pub fn get_set_by_id(&self, id: CrdtId) -> Option<SetRef<'_>>

Get a read-only reference to a set CRDT by its ID.

Source

pub fn get_register_by_id(&self, id: CrdtId) -> Option<RegisterRef<'_>>

Get a read-only reference to a register CRDT by its ID.

Source

pub fn transact<F, R>(&mut self, agent: AgentId, f: F) -> R
where 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");
    }
});
Source

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"));
Source

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.

Source

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.

Source

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 tree
Source

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"));
Source

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.

Source

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.

Source

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!");
Source

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.

Source

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"));
Source

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.

Source

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.

Source

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"));

Trait Implementations§

Source§

impl Default for Document

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V