Registers (LWW, MV)
Two register CRDTs hold a single value, resolving concurrent writes differently:
| Type | Concurrent writes resolve as | When |
|---|---|---|
LWWRegister<T> | Last-Writer-Wins (by timestamp) | Single config value; latest writer is the truth. |
MVRegister<T> | Multiple values kept; caller picks | Want to detect and surface concurrent writes. |
Both store a single conceptual value — they differ in the conflict-resolution policy.
LWWRegister
Section titled “LWWRegister”import { LWWRegister } from 'actor-ts';
let r = LWWRegister.empty<string>();r = r.set('node-a', 'hello', 1000); // value, timestampr = r.set('node-b', 'world', 2000);r.value(); // → 'world' (later timestamp)State is { value, timestamp, replicaId }. Merge picks the entry
with the larger timestamp; ties broken by replica ID for
determinism.
The contract:
- Timestamps must be increasing for sensible LWW semantics.
Defaults to
Date.now()if omitted, but you can pass any monotonic value. - Ties on timestamp use replica ID as tiebreaker. Same timestamp + same replica ID = same write; same timestamp + different replicas = lexicographically-later wins.
- Last write wins, silently. Concurrent writes don’t surface to the caller — one of them is just gone.
Use for single-value config where the latest writer is authoritative:
// Feature flag — admin toggles it, all nodes see the changedd.update<LWWRegister<boolean>>( 'feature-x-enabled', () => LWWRegister.empty<boolean>(), (r) => r.set(dd.selfReplicaId(), true),);MVRegister
Section titled “MVRegister”import { MVRegister } from 'actor-ts';
let r = MVRegister.empty<string>();r = r.set('node-a', 'hello');r = r.set('node-b', 'world');r.values(); // → ['hello', 'world'] (both kept — caller resolves)State is a set of { value, dot } pairs, where dot is a logical
clock that tracks causality. Concurrent writes are kept
side-by-side; sequential writes overwrite.
let r = MVRegister.empty<string>();
// Sequential writes — newer overwritesr = r.set('a', 'first');r = r.set('a', 'second');r.values(); // → ['second']
// Concurrent writes — both keptlet ra = MVRegister.empty<string>().set('a', 'fruit');let rb = MVRegister.empty<string>().set('b', 'vegetable');ra.merge(rb).values(); // → ['fruit', 'vegetable']The caller chooses a resolution strategy:
const r = dd.get<MVRegister<UserPrefs>>('user-prefs');const values = r?.values() ?? [];
if (values.length === 0) { // No writes yet} else if (values.length === 1) { // Single value — unambiguous applyPrefs(values[0]);} else { // Multiple concurrent writes — pick one strategy: // - Show the user a conflict-resolution UI // - Merge field-by-field // - Pick the longest / shortest / most-recent-by-business-logic}Picking between them
Section titled “Picking between them”Two questions:
-
Is silent overwriting acceptable?
- Yes →
LWWRegister. - No →
MVRegister.
- Yes →
-
Do you have a reliable clock?
LWWRegisterdepends onDate.now()being consistent across nodes. Wide clock skew (more than a few seconds) produces wrong answers.MVRegisterdoesn’t trust the clock; it tracks causality directly.
For feature flags, last-known config, current-active value:
LWWRegister is the simpler choice.
For user-editable values, shared documents, anything where
“the user wrote X but the system thinks Y” matters: MVRegister
preserves both sides so your app can surface the conflict.
Wider question: does the value need to be a register at all?
Section titled “Wider question: does the value need to be a register at all?”If the value is append-only history (every write should
persist), use a GSet or ORSet. Registers are for “the
current value of X,” not “every value X has ever had.”
If the value is derived from contributions (per-replica counts), use a counter or counter-map.
If the value is a structured map, use LWWMap or ORMap.
Registers fit when the answer is genuinely one scalar value.
Where to next
Section titled “Where to next”- Counters — for additive per-replica state.
- Sets — GSet / ORSet for collection-style state.
- Maps — LWWMap is a register-valued map.
- Designing data — picking the right type per problem.
The LWWRegister and
MVRegister API references
cover the full surface.