R

@robonen/crdt

Framework-agnostic CRDT primitives — an RGA sequence, last-writer-wins registers, fractional indexing, and version vectors that converge no matter the order, duplicates, or delays in which operations arrive.

Collaborative state is hard because two replicas can edit the same document at once, offline, with messages that arrive out of order or twice. A CRDT solves this by construction: every primitive here is commutative, idempotent, and convergent, so applying the same set of operations in any order yields the same state — a property verified by property tests. It's the convergence engine behind @robonen/editor, but stays fully domain-agnostic, ships zero runtime dependencies, and runs in both Node and the browser.

Convergent by construction

One deterministic tie-break — compareOpId (higher Lamport clock wins; site id breaks ties) — is shared by every primitive, so LWW and RGA agree on the same final state.

Causal buffering built in

Replica.receive dedups, holds ops whose dependencies haven't arrived yet (an insert before its origin), and retries them automatically as they land.

Delta sync, not full state

Version vectors let each side request exactly the ops it's missing via delta(version), with a transport-agnostic wire format.

Zero dependencies, pure TS

No runtime deps, no framework lock-in. Compose the primitives yourself, or lean on Replica to tie a clock, op log, and buffer together.

Install

Add the package with your preferred package manager.

sh
pnpm add @robonen/crdt

Quick start

Two replicas edit a string independently, then exchange only the operations each is missing and converge to the same result.

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

// Each editing site owns an RGA (the sequence state) wrapped by a Replica
// (clock + op log + causal buffering + delta sync).
type Op = {
  id: ReturnType<typeof opId>;
  value: string;
  originLeft: ReturnType<typeof opId> | null;
};

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

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

// A types "hi" locally.
let left: Op['originLeft'] = null;
for (const ch of 'hi') {
  const op: Op = { id: a.replica.nextId(), value: ch, originLeft: left };
  a.replica.commitLocal(op);
  left = op.id;
}

// Sync: send B only the ops it is missing, then send A only what it lacks.
b.replica.receive(a.replica.delta(b.replica.version));
a.replica.receive(b.replica.delta(a.replica.version));

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

Where to next

New to CRDTs? Work through the guide and finish in the live playground.

API Reference

Clock · 9

Doc · 1

Marks · 3

Oplog · 2

Ordering · 2

Registers · 2

Sequence · 2

Sync · 6