Register (LWW, MV)
Zwei Register-CRDTs halten einen Einzelwert und lösen nebenläufige Writes unterschiedlich auf:
| Typ | Auflösung nebenläufiger Writes | Wann |
|---|---|---|
LWWRegister<T> | Last-Writer-Wins (per Timestamp) | Ein Konfigurationswert; der letzte Writer ist die Wahrheit. |
MVRegister<T> | Mehrere Werte werden behalten; Caller wählt | Wenn du nebenläufige Writes erkennen und offenlegen willst. |
Beide speichern konzeptionell einen Einzelwert — sie unterscheiden sich in der Konfliktauflösungs-Policy.
LWWRegister
Abschnitt betitelt „LWWRegister“import { LWWRegister } from 'actor-ts';
let r = LWWRegister.empty<string>();r = r.assign('node-a', 'hello', 1000); // Wert, Timestampr = r.assign('node-b', 'world', 2000);r.value(); // → 'world' (späterer Timestamp)Der State ist { value, timestamp, replicaId }. Merge wählt
den Eintrag mit dem größeren Timestamp; Ties werden für
Determinismus über die Replika-ID gelöst.
Der Vertrag:
- Timestamps müssen steigend sein, damit die LWW-Semantik
sinnvoll ist. Default ist
Date.now(), wenn nicht angegeben, aber du kannst jeden monotonen Wert übergeben. - Ties beim Timestamp werden über die Replika-ID als Tiebreaker entschieden. Gleicher Timestamp + gleiche Replika-ID = gleicher Write; gleicher Timestamp + verschiedene Replikas = lexikographisch späterer gewinnt.
- Der letzte Write gewinnt, still. Nebenläufige Writes werden dem Caller nicht offengelegt — einer davon ist einfach weg.
Nimm es für Konfig mit Einzelwert, wo der letzte Writer maßgeblich ist:
// Feature Flag — Admin schaltet um, alle Nodes sehen die Änderungdd.update<LWWRegister<boolean>>( 'feature-x-enabled', () => LWWRegister.empty<boolean>(), (r) => r.assign(dd.selfReplicaId(), true),);MVRegister
Abschnitt betitelt „MVRegister“import { MVRegister } from 'actor-ts';
let r = MVRegister.empty<string>();r = r.assign('node-a', 'hello');r = r.assign('node-b', 'world');r.values(); // → ['hello', 'world'] (beide behalten — Caller löst auf)Der State ist ein Set aus { value, dot }-Paaren, wobei dot
ein Logical Clock ist, der Kausalität tracked. Nebenläufige
Writes werden nebeneinander behalten; sequenzielle Writes
überschreiben.
let r = MVRegister.empty<string>();
// Sequenzielle Writes — neuer überschreibtr = r.assign('a', 'first');r = r.assign('a', 'second');r.values(); // → ['second']
// Nebenläufige Writes — beide behaltenlet ra = MVRegister.empty<string>().assign('a', 'fruit');let rb = MVRegister.empty<string>().assign('b', 'vegetable');ra.merge(rb).values(); // → ['fruit', 'vegetable']Der Caller wählt eine Auflösungs-Strategie:
const r = dd.get<MVRegister<UserPrefs>>('user-prefs');const values = r?.values() ?? [];
if (values.length === 0) { // Noch keine Writes} else if (values.length === 1) { // Ein Wert — eindeutig applyPrefs(values[0]);} else { // Mehrere nebenläufige Writes — Strategie wählen: // - Dem User ein Konflikt-UI zeigen // - Feld für Feld mergen // - Längsten / kürzesten / fachlich-jüngsten wählen}Auswahl zwischen beiden
Abschnitt betitelt „Auswahl zwischen beiden“Zwei Fragen:
-
Ist stilles Überschreiben akzeptabel?
- Ja →
LWWRegister. - Nein →
MVRegister.
- Ja →
-
Hast du eine zuverlässige Uhr?
LWWRegisterhängt davon ab, dassDate.now()über die Nodes hinweg konsistent ist. Weite Uhr-Drifts (mehr als ein paar Sekunden) produzieren falsche Antworten.MVRegistervertraut der Uhr nicht; es tracked Kausalität direkt.
Für Feature Flags, zuletzt bekannte Konfig, aktueller
aktiver Wert: LWWRegister ist die einfachere Wahl.
Für user-editierbare Werte, geteilte Dokumente, alles, wo
“der User hat X geschrieben, das System glaubt aber Y” zählt:
MVRegister bewahrt beide Seiten, damit deine App den Konflikt
offenlegen kann.
Größere Frage: muss der Wert überhaupt ein Register sein?
Abschnitt betitelt „Größere Frage: muss der Wert überhaupt ein Register sein?“Wenn der Wert eine Append-only-Historie ist (jeder Write
soll erhalten bleiben), nimm ein GSet oder ORSet. Register
sind für “den aktuellen Wert von X”, nicht “jeden Wert, den X
je hatte”.
Wenn der Wert aus Beiträgen abgeleitet ist (Counts pro Replika), nimm einen Counter oder eine Counter-Map.
Wenn der Wert eine strukturierte Map ist, nimm LWWMap oder
ORMap.
Register passen, wenn die Antwort wirklich ein skalarer Wert ist.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Counter — für additiven State pro Replika.
- Sets — GSet / ORSet für Collection-Style-State.
- Maps — LWWMap ist eine Register-wertige Map.
- Daten gestalten — den richtigen Typ pro Problem wählen.
Die API-Referenzen LWWRegister
und MVRegister decken die
vollständige Oberfläche ab.