CRDTs

Conflict-free Replicated Data Types — data structures that several replicas can update independently, with no coordination, and still converge to the same value once they have all seen each other's state. These are the state-based CvRDTs: each type's merge is a join over a semilattice, so it is commutative, associative, and idempotent — replicas can merge in any order, any number of times, and land on the same result. That makes them the substrate for eventually- consistent state across an active/active cluster, offline-first clients that reconcile on reconnect, or any "last writer need not win, but everyone agrees" counter / set / register / map.

Every type exposes the same contract: local mutators (e.g. inc, add, set), merge(other) which returns a new converged instance without mutating either operand, value() for the materialized value, and state() / fromState() for a JSON- serializable form to snapshot (via b.archive / b.backup) or ship to a peer. Each replica carries a replicaId so per-replica contributions stay distinct.

This release covers the state-based family — grow-only and PN counters, grow-only / two-phase / observed-remove sets, a last-write-wins register, and an observed-remove map. Operation-based sequence CRDTs (RGA), delta- state mutators, and a live event-bus replicator are not included; the state-based types merge correctly without a causal channel, which is the whole point.

b.crdt.gCounter(opts?) #

stable0.13.4soc2
{
  replicaId: string,   // this replica's id (default: random)
}

A grow-only counter: each replica tracks its own increment-only tally, and the value is their sum. merge takes the per-replica maximum, so it converges no matter the order. Increments only — use pnCounter when you also need to decrement.

var a = b.crdt.gCounter({ replicaId: "a" }).inc(3);
var c = b.crdt.gCounter({ replicaId: "c" }).inc(5);
a.merge(c).value();   // → 8

b.crdt.pnCounter(opts?) #

stable0.13.4soc2
{
  replicaId: string,   // this replica's id (default: random)
}

A positive-negative counter: two grow-only counters (increments and decrements) whose difference is the value, so it supports both inc and dec and still converges.

var a = b.crdt.pnCounter({ replicaId: "a" }).inc(5).dec(2);
var c = b.crdt.pnCounter({ replicaId: "c" }).inc(1);
a.merge(c).value();   // → 4

b.crdt.gSet(opts?) #

stable0.13.4soc2
{
  replicaId: string,   // this replica's id (default: random)
}

A grow-only set: elements can be added but never removed; merge is set union. The simplest convergent set — reach for orSet when removal is needed. Elements may be strings or JSON-serializable values.

var a = b.crdt.gSet().add("x");
var c = b.crdt.gSet().add("y");
a.merge(c).value();   // → ["x", "y"]

b.crdt.twoPSet(opts?) #

stable0.13.4soc2
{
  replicaId: string,   // this replica's id (default: random)
}

A two-phase set: an add-set and a remove-set (tombstones). An element can be added and removed, but once removed it can never be re-added — remove wins permanently. When re-adding must work, use orSet.

var s = b.crdt.twoPSet().add("a").add("b").remove("a");
s.value();   // → ["b"]

b.crdt.orSet(opts?) #

stable0.13.4soc2
{
  replicaId:          string,   // this replica's id (default: random)
  tombstoneRetention: number,   // optional cap on retained tombstones (default: unbounded)
}

An observed-remove set: each add stamps a unique tag, and remove tombstones the tags it has observed for that element, so an element survives if any concurrent add was not seen by the remove — re-adding works, and a concurrent add-vs-remove resolves add-wins. tombstoneRetention optionally caps the tombstone set to bound memory against a remove flood; it drops the oldest tombstones, which can resurrect a concurrently-removed element, so leave it unset unless that trade-off is acceptable.

Each add stamps a unique tag; remove tombstones the tags currently observed for that element. An element is present if it has a live (un-tombstoned) tag.

var a = b.crdt.orSet().add("x");
var c = b.crdt.orSet.fromState(a.state()).add("x");  // re-add elsewhere
a.remove("x");
a.merge(c).value();   // → ["x"]  (concurrent re-add survives)

b.crdt.lwwRegister(opts?) #

stable0.13.4soc2
{
  replicaId: string,   // this replica's id (default: random)
}

A last-write-wins register: holds a single value with a timestamp; merge keeps the higher-timestamped write, breaking ties by the higher replicaId so the outcome is deterministic. Pass an explicit timestamp to set for a logical clock, or omit it to use wall-clock milliseconds.

var a = b.crdt.lwwRegister({ replicaId: "a" }).set("first", 1);
var c = b.crdt.lwwRegister({ replicaId: "c" }).set("second", 2);
a.merge(c).value();   // → "second"

b.crdt.orMap(opts?) #

stable0.13.4soc2
{
  replicaId: string,   // this replica's id (default: random)
}

An observed-remove map: key presence follows observed-remove-set semantics (a key can be set, removed, and set again), and each key's value is a last-write-wins register, so concurrent writes to a live key converge by timestamp (higher wins, ties by replicaId). Removing a key clears its value register locally, so a re-add on the same replica starts clean; across replicas the value is strictly last-write-wins by timestamp — supply monotonic timestamps (the default wall-clock does) for re-add to win. Keys are non-empty strings.

Keys follow OR-Set add/remove semantics; each key's value is an LWW register, so concurrent writes to the same key converge by last-write-wins.

var a = b.crdt.orMap({ replicaId: "a" }).set("k", "v1", 1);
var c = b.crdt.orMap({ replicaId: "c" }).set("k", "v2", 2);
a.merge(c).value();   // → { k: "v2" }

Last updated 2026-08-08T16:39:49.652Z by seeder.