Files
tools/core/crdt
Renovate Bot 63b3c5f2fd
CI / Discover packages (pull_request) Failing after 53s
CI / ${{ matrix.package }} (pull_request) Has been skipped
CI / CI (pull_request) Failing after 5s
chore(deps): update pnpm to v11
2026-06-15 22:06:55 +00:00
..
2026-06-07 16:28:58 +07:00
2026-06-15 22:06:55 +00:00

@robonen/crdt

Framework-agnostic CRDT primitives — the convergence engine behind @robonen/writekit, usable on their own. Zero runtime dependencies; pure TypeScript; runs in Node and the browser.

Every primitive is built so that applying the same set of operations in any order, with duplicates, yields the same state (commutative, idempotent, convergent), verified by property tests.

Primitives

Module Exports Purpose
clock OpId, LamportClock, VersionVector, compareOpId, createSiteId Causality: per-site Lamport ids with a deterministic total order; version vectors for dedup + deltas.
registers LwwRegister, LwwMap Last-writer-wins values / maps (conflict resolved by op id).
ordering keyBetween, keysBetween Fractional indexing — place an item strictly between two neighbors (or move it) with one string key.
sequence Rga Replicated Growable Array — a sequence CRDT with tombstones, higher-op-id-first tie-break, causal-buffering API.
marks MarkStore, MarkSpan, MarkValue Lightweight Peritext: formatting spans anchored to character op ids, resolved per character by highest op id.
oplog OpLog Append-only op log with a version vector; computes deltas.
sync encodeStateVector, encodeDelta/encodeOps, decode* Transport-agnostic wire encoding (JSON-over-bytes in v1).
doc Replica Ties a clock + op log + causal buffer together; integrates local/remote ops and exposes deltas.

Example: a converging replicated string

import { Replica, Rga, opId } from '@robonen/crdt';

function makeReplica(site: string) {
  const rga = new Rga<string>();
  const replica = new Replica<{ id: ReturnType<typeof opId>; originLeft: ReturnType<typeof opId> | null; value: string }>(
    { integrate: op => rga.integrateInsert(op.id, op.value, op.originLeft) },
    site,
  );
  return { rga, replica };
}

const a = makeReplica('a');
const b = makeReplica('b');

// ... A and B make concurrent local edits via replica.commitLocal(...) ...

// Exchange only what each side is missing:
b.replica.receive(a.replica.delta(b.replica.version));
a.replica.receive(b.replica.delta(a.replica.version));

a.rga.toArray().join('') === b.rga.toArray().join(''); // true — converged

Replica.receive buffers ops whose causal dependencies haven't arrived yet (an insert before its origin, a delete before its target) and retries them automatically.

Notes

  • compareOpId is the single deterministic tie-break (higher clock wins; site id breaks ties) every primitive agrees on — that's what makes LWW and RGA converge.
  • VersionVector assumes dense per-site clocks (1, 2, 3, …).
  • The v1 wire format is JSON encoded to bytes — simple and debuggable; a compact varint format is a later optimization with no API change.
  • A writekit-specific composition of these primitives (blocks + text + marks ↔ writekit steps) lives in @robonen/writekit under crdt/native/, not here — this package stays domain-agnostic.

Development

pnpm --filter @robonen/crdt test    # property/convergence tests
pnpm --filter @robonen/crdt build   # tsdown (ESM + CJS + dts)