CRUMB a card from devarno-cloud

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-- BatchedOperation

Field Semantics by Type

Typeposition_idend_position_idcontentattributes
insertrequired (where)unusedrequired (what to insert)optional formatting
deleterequired (start)optional (range end)unusedunused
formatrequired (start)required (end)unusedrequired attribute map
retainrequiredoptionalunusedunused (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
end

Dependencies 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_id it 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-node proof system referenced in Cargo.toml.
  • OperationBatchMsg → bulk envelope for sync responses; uses BatchedOperation, which makes author_id mandatory and drops dependencies/document_id redundancy.

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