CRDT Operation Message
chronicle intermediate 5 min read
ELI5
A CRDT operation is a single sticky note: “at position X, insert ‘A’”, “at position X, delete”. The note carries an HLC stamp and a UUID so the relay can file it and replay history later. The server adds the author’s name to the note before posting it on every collaborator’s wall.
Technical Deep Dive
Variants
OperationType (schema.rs:209): insert | delete | format | retain — serialised snake_case.
OperationMsg
classDiagram class OperationMsg { +Uuid document_id +Uuid operation_id +OperationType operation_type +String position_id +Option~String~ content +Option~String~ end_position_id +Option~Value~ attributes +i64 hlc_wall_time +i32 hlc_counter +Vec~Uuid~ dependencies +Option~String~ author_id "server-added" } class OperationAckMsg { +Uuid operation_id +Uuid document_id +i64 server_timestamp +Option~String~ audit_ref } class OperationRejectedMsg { +Uuid operation_id +Uuid document_id +String error_code +String reason } class BatchedOperation { +Uuid operation_id +OperationType operation_type +String position_id +Option~String~ content +Option~String~ end_position_id +Option~Value~ attributes +i64 hlc_wall_time +i32 hlc_counter +String author_id "required" } class OperationBatchMsg { +Uuid document_id +Uuid batch_id +Vec~BatchedOperation~ operations } OperationBatchMsg o-- BatchedOperationField Semantics by Type
| Type | position_id | end_position_id | content | attributes |
|---|---|---|---|---|
insert | required (where) | unused | required (what to insert) | optional formatting |
delete | required (start) | optional (range end) | unused | unused |
format | required (start) | required (end) | unused | required attribute map |
retain | required | optional | unused | unused (used for cursor/intent) |
The persistence layer in services/operation.rs only handles insert | delete | format (OperationPayload enum); retain is a wire-only signal.
Round-Trip
sequenceDiagram autonumber participant Client participant WS as websocket.rs participant Repo as OperationRepository participant DB as SQLite participant Bus as document_channel Client->>WS: OperationMsg (no author_id) WS->>WS: validate (non-nil ids) WS->>WS: stamp author_id from VerifiedIdentity WS->>Repo: store(op, server_seq) Repo->>DB: INSERT INTO operations (...) alt UNIQUE constraint failed DB-->>Repo: error Repo-->>WS: OperationError::Duplicate WS-->>Client: OperationRejectedMsg else ok DB-->>Repo: ok Repo->>DB: update document_versions WS-->>Client: OperationAckMsg WS->>Bus: broadcast OperationMsg with author_id endDependencies Field
dependencies: Vec<Uuid> is a list of operation IDs this op causally depends on. Currently stored as a JSON array string in the dependencies column; readers can use it to reorder before applying. The wire Operation defaults to an empty Vec via #[serde(default)].
Key Terms
- server_seq → strictly increasing per-document sequence number assigned by the relay; combined with
document_idit has a UNIQUE constraint, providing the duplicate-detection guarantee. - vest_proof_ref → optional audit reference column on the operations row; an integration hook for the unbuilt
vest-nodeproof system referenced inCargo.toml. - OperationBatchMsg → bulk envelope for sync responses; uses
BatchedOperation, which makesauthor_idmandatory and dropsdependencies/document_idredundancy.
Q&A
Q: Can a client set author_id on an inbound Operation?
A: It’s allowed by the type, but the relay overwrites it with the verified user_id before persisting/broadcasting. Trusting the client field would defeat the auth gate.
Q: What does the relay return on a duplicate operation_id?
A: OperationRepository::store maps the SQLite UNIQUE constraint failed error to OperationError::Duplicate; the WS handler converts that to an OperationRejectedMsg.
Q: Why is retain defined if persistence ignores it?
A: It is a wire-level intent (e.g. cursor retention through OT-style transforms); the CRDT itself does not need to store retains.
Examples
Pasting “Hi” produces two insert operations with consecutive position IDs (generatePositionsBetween in lseq.ts:278), both stamped with the same ClientHlc.tick() wall_time but counter incremented. The relay assigns separate server_seq values to each.
neighbors on the map
- LSEQ Position IDs investigating runaway position-id growth
- Operations & Versions Schema writing a new sync query
- FNP CRDT Conflict-Free Merge Semantics understanding CRDT properties and guarantees
- FNP Insert Operation Complete Flow understanding the end-to-end insert operation