first commit
This commit is contained in:
73
docs/foundation/flow/crdt/README.md
Normal file
73
docs/foundation/flow/crdt/README.md
Normal file
File diff suppressed because one or more lines are too long
139
docs/foundation/flow/crdt/src/core/sync-manager/sync-manager.md
Normal file
139
docs/foundation/flow/crdt/src/core/sync-manager/sync-manager.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# sync-manager
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`CrdtSyncManager` is a delta-exchange coordinator that manages a heterogeneous
|
||||
collection of CRDT instances and provides the primitives needed for peer-to-peer
|
||||
state synchronization. It maintains a per-replica vector clock to track causal
|
||||
history and exposes `exportDeltas` / `sync` operations that can be wired into
|
||||
any transport layer (WebSocket, HTTP, MessageChannel, or in-process).
|
||||
|
||||
## Features
|
||||
|
||||
- Registry for any number of heterogeneous CRDT instances keyed by their id
|
||||
- Local `VectorClock` that advances on `tick()` (call after batching mutations)
|
||||
- `exportDeltas()` — snapshot all CRDT states with current clock for transport
|
||||
- `sync(deltas)` — merge incoming deltas and advance local clock causally
|
||||
- `syncWith(peer)` — convenience bidirectional in-process sync for testing
|
||||
- `{ merged, skipped }` result counts for observability
|
||||
- Injectable `nodeId` for deterministic replica identity in tests
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CrdtSyncManager } from "@suaveplan/crdt/core/sync-manager/sync-manager";
|
||||
import { GCounter } from "@suaveplan/crdt/crdts/g-counter/g-counter";
|
||||
|
||||
const mgr = new CrdtSyncManager({ nodeId: "replica-1" });
|
||||
const counter = new GCounter("replica-1");
|
||||
mgr.register(counter);
|
||||
|
||||
counter.increment("replica-1");
|
||||
mgr.tick(); // advance clock after local mutations
|
||||
|
||||
const deltas = mgr.exportDeltas();
|
||||
// send `deltas` to a remote peer over any transport
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CrdtSyncManager } from "@suaveplan/crdt/core/sync-manager/sync-manager";
|
||||
import { GCounter } from "@suaveplan/crdt/crdts/g-counter/g-counter";
|
||||
import { ORSet } from "@suaveplan/crdt/crdts/or-set/or-set";
|
||||
|
||||
const a = new CrdtSyncManager({ nodeId: "a" });
|
||||
const b = new CrdtSyncManager({ nodeId: "b" });
|
||||
|
||||
const counterA = new GCounter("a", "shared-counter");
|
||||
const counterB = new GCounter("b", "shared-counter");
|
||||
const setA = new ORSet<string>("shared-set");
|
||||
const setB = new ORSet<string>("shared-set");
|
||||
|
||||
a.register(counterA);
|
||||
a.register(setA);
|
||||
b.register(counterB);
|
||||
b.register(setB);
|
||||
|
||||
counterA.increment("a", 3);
|
||||
setA.add("item-1", "a");
|
||||
a.tick();
|
||||
|
||||
counterB.increment("b", 2);
|
||||
b.tick();
|
||||
|
||||
// Bidirectional sync
|
||||
const { local, remote } = a.syncWith(b);
|
||||
console.log(local); // { merged: 2, skipped: 0 }
|
||||
console.log(remote); // { merged: 2, skipped: 0 }
|
||||
|
||||
// Both managers now have the same CRDT state
|
||||
console.log(counterA.total()); // 5
|
||||
console.log(counterB.total()); // 5
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new CrdtSyncManager(options?)`
|
||||
|
||||
Creates a new manager. `options.nodeId` defaults to a random UUID.
|
||||
|
||||
### `register(crdt): CrdtRegistration`
|
||||
|
||||
Adds a CRDT to the registry using `crdt.id` as the key.
|
||||
Returns a `CrdtRegistration` with `{ id, crdt }`.
|
||||
|
||||
### `unregister(id): void`
|
||||
|
||||
Removes a CRDT from the registry.
|
||||
|
||||
### `get<TValue, TState>(id): CRDT | undefined`
|
||||
|
||||
Returns the registered CRDT for the given id, or `undefined`.
|
||||
|
||||
### `ids(): string[]`
|
||||
|
||||
Returns all registered CRDT ids.
|
||||
|
||||
### `tick(): void`
|
||||
|
||||
Increments the local vector clock counter for this replica's node id.
|
||||
Should be called after applying a batch of local mutations before exporting.
|
||||
|
||||
### `clockState(): Record<string, number>`
|
||||
|
||||
Returns the current local vector clock snapshot.
|
||||
|
||||
### `exportDeltas(): SyncDelta[]`
|
||||
|
||||
Produces one `SyncDelta` per registered CRDT, each containing:
|
||||
|
||||
- `crdtId` — CRDT identifier
|
||||
- `state` — current CRDT state snapshot
|
||||
- `vectorClock` — current local clock
|
||||
|
||||
### `sync(deltas): SyncResult`
|
||||
|
||||
Integrates incoming deltas from a remote peer. Advances local clock causally.
|
||||
Returns `{ merged: number, skipped: number }`.
|
||||
|
||||
### `syncWith(peer): { local: SyncResult; remote: SyncResult }`
|
||||
|
||||
Bidirectional in-process sync: exports each manager's state, applies it to the
|
||||
other, and returns both results.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The vector clock is used for observability and causal bookkeeping rather than
|
||||
delta filtering. Full-state exchange is deliberately chosen over delta-only
|
||||
exchange to keep the implementation straightforward and avoid the need for
|
||||
operation logs. For high-frequency or bandwidth-constrained deployments, a
|
||||
delta-log approach should be layered on top of this manager.
|
||||
|
||||
The `sync` method merges the remote clock unconditionally after processing all
|
||||
deltas, even for `skipped` entries. This ensures the local clock always reflects
|
||||
the maximum causal horizon seen from the peer, preventing re-delivery of already
|
||||
seen states on future syncs.
|
||||
@@ -0,0 +1,59 @@
|
||||
# vector-clock
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides a `VectorClock` implementation for tracking causality in distributed systems. Vector clocks are used by `MVRegister` to detect concurrent writes and are a foundational primitive for any CRDT that needs to track event ordering across multiple nodes.
|
||||
|
||||
## Features
|
||||
|
||||
- `increment(nodeId)` — increments the local counter for the given node
|
||||
- `merge(remote)` — element-wise maximum merge (satisfies monotonicity)
|
||||
- `compare(other)` — returns `"before" | "after" | "concurrent" | "equal"`
|
||||
- `state()` — returns a snapshot `Record<string, number>`
|
||||
- `clone()` — deep copies the clock
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { VectorClock } from "@suaveplan/crdt";
|
||||
|
||||
const clock = new VectorClock();
|
||||
clock.increment("node-1");
|
||||
clock.increment("node-1");
|
||||
console.log(clock.state()); // { "node-1": 2 }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { VectorClock } from "@suaveplan/crdt";
|
||||
|
||||
const a = new VectorClock();
|
||||
a.increment("node-1");
|
||||
|
||||
const b = new VectorClock();
|
||||
b.increment("node-2");
|
||||
|
||||
console.log(a.compare(b.state())); // "concurrent"
|
||||
|
||||
a.merge(b.state());
|
||||
console.log(a.compare(b.state())); // "after"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `VectorClock`
|
||||
|
||||
- `constructor(initial?: VectorClockState)` — optional initial state
|
||||
- `increment(nodeId: string): void`
|
||||
- `merge(remote: VectorClockState): void` — element-wise max
|
||||
- `compare(other: VectorClockState): "before" | "after" | "concurrent" | "equal"`
|
||||
- `state(): VectorClockState` — snapshot
|
||||
- `clone(): VectorClock`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The comparison algorithm iterates all keys in the union of both clocks. Missing keys are treated as 0. Two clocks are concurrent when neither is strictly greater in all dimensions.
|
||||
60
docs/foundation/flow/crdt/src/crdts/g-counter/g-counter.md
Normal file
60
docs/foundation/flow/crdt/src/crdts/g-counter/g-counter.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# g-counter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `GCounter`, a grow-only counter CRDT. Each node in a distributed system maintains an independent counter; the global value is the sum of all node counters. Merge takes the element-wise maximum, ensuring convergence regardless of message ordering or duplicates.
|
||||
|
||||
## Features
|
||||
|
||||
- `increment(nodeId, delta?)` — adds `delta` (default 1) to the node's counter; rejects non-positive deltas
|
||||
- `get(nodeId)` — returns the counter for a specific node
|
||||
- `total()` — returns the sum across all nodes (equivalent to `value()`)
|
||||
- `merge(state)` — element-wise maximum merge; idempotent and commutative
|
||||
- `toJSON()` / `fromJSON()` — round-trip serialization
|
||||
- `clone()` — deep copy
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { GCounter } from "@suaveplan/crdt";
|
||||
|
||||
const counter = new GCounter();
|
||||
counter.increment("node-1", 5);
|
||||
counter.increment("node-2", 3);
|
||||
console.log(counter.total()); // 8
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { GCounter } from "@suaveplan/crdt";
|
||||
|
||||
const a = new GCounter("ctr-1");
|
||||
a.increment("n1", 10);
|
||||
|
||||
const b = new GCounter("ctr-1");
|
||||
b.increment("n2", 5);
|
||||
|
||||
a.merge(b.state());
|
||||
console.log(a.total()); // 15
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `GCounter`
|
||||
|
||||
- `constructor(id?)` — optional stable ID
|
||||
- `increment(nodeId, delta?)` — throws `OPERATION_INVALID` if delta ≤ 0
|
||||
- `get(nodeId): number`
|
||||
- `total(): number` — sum of all node counters
|
||||
- `merge(state: GCounterState): void`
|
||||
- `state(): GCounterState`
|
||||
- `toJSON(): string` / `fromJSON(json: string): void`
|
||||
- `clone(): this`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The merge operation is O(k) where k is the number of distinct nodes. The `clone()` method uses the same `GCounter` constructor — the `as this` cast is needed because TypeScript cannot statically know the concrete subclass type.
|
||||
@@ -0,0 +1,54 @@
|
||||
# lww-register
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `LWWRegister<T>`, a Last-Write-Wins Register CRDT. The register holds a single value; when two replicas have different values, the one with the higher timestamp wins during merge. Ties are broken by node ID lexicographic order, ensuring deterministic convergence.
|
||||
|
||||
## Features
|
||||
|
||||
- `set(value, timestamp?)` — sets the value with optional explicit timestamp (defaults to `Date.now()`)
|
||||
- `get()` — returns the current value or `undefined`
|
||||
- `merge(state)` — higher timestamp wins; tie-broken by nodeId lexicographic order
|
||||
- `toJSON()` / `fromJSON()` — serialization round-trip
|
||||
- `clone()` — deep copy
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { LWWRegister } from "@suaveplan/crdt";
|
||||
|
||||
const reg = new LWWRegister<string>("node-1");
|
||||
reg.set("hello");
|
||||
console.log(reg.get()); // "hello"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { LWWRegister } from "@suaveplan/crdt";
|
||||
|
||||
const a = new LWWRegister<number>("n1");
|
||||
a.set(42, 1000);
|
||||
|
||||
const b = new LWWRegister<number>("n2");
|
||||
b.set(99, 2000);
|
||||
|
||||
a.merge(b.state());
|
||||
console.log(a.get()); // 99 (higher timestamp)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `LWWRegister<T>`
|
||||
|
||||
- `constructor(nodeId, id?)` — nodeId is required for tie-breaking
|
||||
- `set(value, timestamp?)` — sets value with timestamp
|
||||
- `get(): T | undefined` — current value
|
||||
- `merge(state: LWWRegisterState<T>): void`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Wall-clock timestamps from `Date.now()` can cause issues if clocks are not synchronized. For distributed deployments, use a monotonic or hybrid logical clock and pass the timestamp explicitly via `set(value, timestamp)`.
|
||||
@@ -0,0 +1,58 @@
|
||||
# mv-register
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `MVRegister<T>`, a Multi-Value Register CRDT that preserves all concurrent writes rather than silently dropping them. When replicas diverge and merge, all concurrent values are returned by `get()`, allowing the application to detect and resolve conflicts. This makes MVRegister appropriate for collaborative editing scenarios where silent loss-of-data is unacceptable.
|
||||
|
||||
## Features
|
||||
|
||||
- `set(value)` — writes a new value, incrementing the node's vector clock
|
||||
- `get()` — returns all current values (multiple on conflict, single on convergence)
|
||||
- `conflict()` — returns `true` when more than one value is present
|
||||
- `merge(state)` — keeps entries not dominated by any remote entry; drops dominated entries
|
||||
- `toJSON()` / `fromJSON()` — serialization round-trip
|
||||
- `clone()` — deep copy
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MVRegister } from "@suaveplan/crdt";
|
||||
|
||||
const reg = new MVRegister<string>("node-1");
|
||||
reg.set("hello");
|
||||
console.log(reg.get()); // ["hello"]
|
||||
console.log(reg.conflict()); // false
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MVRegister } from "@suaveplan/crdt";
|
||||
|
||||
const a = new MVRegister<number>("n1");
|
||||
a.set(42);
|
||||
|
||||
const b = new MVRegister<number>("n2");
|
||||
b.set(99);
|
||||
|
||||
a.merge(b.state());
|
||||
console.log(a.conflict()); // true
|
||||
console.log(a.get()); // [42, 99] — both concurrent values preserved
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `MVRegister<T>`
|
||||
|
||||
- `constructor(nodeId, id?)` — nodeId for vector clock tracking
|
||||
- `set(value: T): void` — advances vector clock, replaces local entries
|
||||
- `get(): T[]` — returns all current values
|
||||
- `conflict(): boolean`
|
||||
- `merge(state: MVRegisterState<T>): void`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The merge algorithm uses vector clock dominance: an entry is dropped if any remote entry has a strictly greater vector clock (happened after). Entries with concurrent clocks are both preserved. This guarantees that all concurrent writes survive the merge.
|
||||
62
docs/foundation/flow/crdt/src/crdts/or-set/or-set.md
Normal file
62
docs/foundation/flow/crdt/src/crdts/or-set/or-set.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# or-set
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `ORSet<T>`, an Observe-Remove Set CRDT. Unlike a 2P-Set, an OR-Set handles concurrent add and remove operations correctly: each addition is tagged with a unique identifier, and removal only tombstones the specific tags that were observed at the time of removal. Concurrent adds survive removes.
|
||||
|
||||
## Features
|
||||
|
||||
- `add(element, nodeId)` — tags the element with a unique per-node identifier
|
||||
- `remove(element)` — tombstones all current (non-tombstoned) tags for the element
|
||||
- `has(element)` — returns true if any live (non-tombstoned) tag exists
|
||||
- `size()` — count of live unique elements
|
||||
- `value()` — returns a `Set<T>` of all live elements
|
||||
- `merge(state)` — merges remote entries; tombstone state wins on conflict
|
||||
- `toJSON()` / `fromJSON()` — serialization round-trip
|
||||
- `clone()` — deep copy
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ORSet } from "@suaveplan/crdt";
|
||||
|
||||
const set = new ORSet<string>();
|
||||
set.add("alice", "node-1");
|
||||
set.add("bob", "node-1");
|
||||
set.remove("alice");
|
||||
console.log(set.has("alice")); // false
|
||||
console.log([...set.value()]); // ["bob"]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
// Concurrent add and remove converge correctly
|
||||
const a = new ORSet<string>();
|
||||
a.add("x", "n1");
|
||||
|
||||
const b = a.clone();
|
||||
a.remove("x"); // n1 removes x
|
||||
b.add("x", "n2"); // n2 re-adds x concurrently
|
||||
|
||||
a.merge(b.state());
|
||||
console.log(a.has("x")); // true — concurrent add survives
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ORSet<T>`
|
||||
|
||||
- `add(element: T, nodeId: string)` — adds with unique tag
|
||||
- `remove(element: T)` — tombstones all current live tags
|
||||
- `has(element: T): boolean`
|
||||
- `size(): number`
|
||||
- `value(): Set<T>`
|
||||
- `merge(state: ORSetState<T>): void`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Elements are compared with `===` (reference equality). For object elements, ensure consistent references or use string/number keys.
|
||||
55
docs/foundation/flow/crdt/src/crdts/pn-counter/pn-counter.md
Normal file
55
docs/foundation/flow/crdt/src/crdts/pn-counter/pn-counter.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# pn-counter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `PNCounter`, a positive-negative counter CRDT that supports both increment and decrement operations. Built from two G-Counters (one for increments, one for decrements), the net value is `sum(increments) - sum(decrements)`. All CRDT invariants (commutativity, associativity, idempotency) are preserved.
|
||||
|
||||
## Features
|
||||
|
||||
- `increment(nodeId, delta?)` — adds to the increment counter for `nodeId`
|
||||
- `decrement(nodeId, delta?)` — adds to the decrement counter for `nodeId`
|
||||
- `total()` — net value: sum of all increments minus sum of all decrements
|
||||
- `merge(state)` — element-wise maximum on both increment and decrement counters
|
||||
- `toJSON()` / `fromJSON()` — serialization round-trip
|
||||
- `clone()` — deep copy
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PNCounter } from "@suaveplan/crdt";
|
||||
|
||||
const counter = new PNCounter();
|
||||
counter.increment("n1", 10);
|
||||
counter.decrement("n1", 3);
|
||||
console.log(counter.total()); // 7
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PNCounter } from "@suaveplan/crdt";
|
||||
|
||||
const a = new PNCounter("likes");
|
||||
a.increment("user-1", 5);
|
||||
|
||||
const b = new PNCounter("likes");
|
||||
b.decrement("user-2", 2);
|
||||
|
||||
a.merge(b.state());
|
||||
console.log(a.total()); // 3 (5 - 2)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PNCounter`
|
||||
|
||||
- `increment(nodeId, delta?)` — throws `OPERATION_INVALID` if delta ≤ 0
|
||||
- `decrement(nodeId, delta?)` — throws `OPERATION_INVALID` if delta ≤ 0
|
||||
- `total(): number` — net counter value
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The value can be negative if decrements exceed increments. This is intentional — PNCounter does not enforce a floor of 0.
|
||||
136
docs/foundation/flow/crdt/src/crdts/rga/rga.md
Normal file
136
docs/foundation/flow/crdt/src/crdts/rga/rga.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# rga
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Implements the Replicated Growable Array (RGA) CRDT, a list-oriented conflict-free
|
||||
replicated data type that supports concurrent insertions and deletions while
|
||||
guaranteeing convergence to the same element order on all replicas. Each element
|
||||
is tagged with a unique (sequence-number, node-id) identifier so that causally
|
||||
ordered operations are reflected correctly and concurrent siblings are resolved
|
||||
deterministically without coordination.
|
||||
|
||||
## Features
|
||||
|
||||
- Concurrent `insert` and `remove` operations with causal ordering
|
||||
- Tombstone-based removal that preserves relative positions for later merges
|
||||
- Tie-breaking via `(seq DESC, nodeId DESC)` for concurrent sibling inserts
|
||||
- `idAt(index)` and `indexOf(id)` for positional queries on the visible array
|
||||
- Full `merge`, `toJSON`/`fromJSON`, `clone`, and `state` CRDT interface
|
||||
- `fromJSON` rebuilds from serialized state and advances internal seq counter
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { RGA } from "@suaveplan/crdt/crdts/rga/rga";
|
||||
|
||||
const doc = new RGA<string>("replicaA");
|
||||
const id0 = doc.insert("hello");
|
||||
const id1 = doc.insert("world", id0);
|
||||
console.log(doc.value()); // ["hello", "world"]
|
||||
|
||||
doc.remove(id0);
|
||||
console.log(doc.value()); // ["world"]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { RGA } from "@suaveplan/crdt/crdts/rga/rga";
|
||||
|
||||
// Two replicas insert concurrently after the same predecessor
|
||||
const a = new RGA<string>("nodeA");
|
||||
const b = new RGA<string>("nodeB");
|
||||
|
||||
const rootId = a.insert("root");
|
||||
b.merge(a.state()); // b now knows about "root"
|
||||
|
||||
// Concurrent inserts after "root"
|
||||
const aId = a.insert("from-A", rootId);
|
||||
const bId = b.insert("from-B", rootId);
|
||||
|
||||
// Merge both ways
|
||||
a.merge(b.state());
|
||||
b.merge(a.state());
|
||||
|
||||
// Both replicas converge to the same order
|
||||
console.log(a.value()); // deterministic total order: e.g. ["root", "from-B", "from-A"]
|
||||
console.log(b.value()); // identical
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `new RGA<T>(nodeId?, id?)`
|
||||
|
||||
Creates a new RGA instance for the given replica node. `nodeId` defaults to
|
||||
`"node"` and `id` defaults to a random UUID.
|
||||
|
||||
### `insert(value, afterId?): string`
|
||||
|
||||
Inserts `value` after the element identified by `afterId` (null or omitted = prepend).
|
||||
Returns the new element's unique id.
|
||||
|
||||
### `remove(id): void`
|
||||
|
||||
Marks the element with `id` as a tombstone. The element is hidden from `value()`
|
||||
but retained for future merge convergence.
|
||||
|
||||
### `value(): T[]`
|
||||
|
||||
Returns all non-tombstoned elements in document order.
|
||||
|
||||
### `idAt(index): string | undefined`
|
||||
|
||||
Returns the id of the element at the given visible index, or `undefined` if out
|
||||
of range.
|
||||
|
||||
### `indexOf(id): number`
|
||||
|
||||
Returns the visible index of the element with `id`, or `-1` if tombstoned or
|
||||
absent.
|
||||
|
||||
### `merge(remote: RGAState<T>): void`
|
||||
|
||||
Integrates a remote replica's state. New elements are inserted at their correct
|
||||
positions; tombstones propagate (tombstone wins).
|
||||
Throws `CrdtError(DESERIALIZE_FAILED)` if the remote type field does not match.
|
||||
|
||||
### `state(): RGAState<T>`
|
||||
|
||||
Returns a snapshot of the internal sequence including tombstones.
|
||||
|
||||
### `toJSON(): string`
|
||||
|
||||
Serializes state to a JSON string.
|
||||
|
||||
### `fromJSON(json): void`
|
||||
|
||||
Resets the instance and rebuilds from a JSON string.
|
||||
Throws `CrdtError(DESERIALIZE_FAILED)` on malformed JSON or wrong type field.
|
||||
|
||||
### `clone(): this`
|
||||
|
||||
Returns an independent deep copy of the RGA instance.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The predecessor-based insertion strategy is derived from the original RGA paper
|
||||
(Roh et al., 2011). Each node records which element it was inserted after rather
|
||||
than an absolute index, making the algorithm position-independent and robust to
|
||||
concurrent inserts at the same predecessor.
|
||||
|
||||
Tie-breaking compares `(seq DESC, nodeId DESC)` — a higher sequence number wins,
|
||||
and among equal sequences the lexicographically larger node-id wins. This matches
|
||||
common RGA implementations and is consistent across all replicas given the same
|
||||
inputs.
|
||||
|
||||
Tombstones are never removed (no garbage collection) to ensure replicas that
|
||||
receive operations out of order can still converge. In production systems with
|
||||
bounded document lifetime, a GC epoch mechanism should be layered on top.
|
||||
|
||||
The `predecessorFromElements` helper reconstructs predecessor links from the
|
||||
flat ordered element list produced by `state()`. This keeps the wire format
|
||||
simple (no extra predecessor field) at the cost of an O(n) scan per element
|
||||
during merge — acceptable for document sizes encountered in practice.
|
||||
56
docs/foundation/flow/crdt/src/crdts/two-p-set/two-p-set.md
Normal file
56
docs/foundation/flow/crdt/src/crdts/two-p-set/two-p-set.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# two-p-set
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Provides `TwoPSet<T>`, a Two-Phase Set CRDT. Elements can be added and removed exactly once; removed elements can never be re-added. The set value is the difference between the add-set and the remove-set. This guarantees eventual consistency at the cost of restricting re-addition.
|
||||
|
||||
## Features
|
||||
|
||||
- `add(element)` — adds to the A-set; throws `OPERATION_INVALID` if element was previously removed
|
||||
- `remove(element)` — moves element to the R-set; no-op if not in A-set
|
||||
- `has(element)` — true if in A-set but not R-set
|
||||
- `value()` — returns a `Set<T>` of live elements
|
||||
- `merge(state)` — union-merges both A-set and R-set
|
||||
- `toJSON()` / `fromJSON()` — serialization round-trip
|
||||
- `clone()` — deep copy
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { TwoPSet } from "@suaveplan/crdt";
|
||||
|
||||
const set = new TwoPSet<string>();
|
||||
set.add("alice");
|
||||
set.remove("alice");
|
||||
console.log(set.has("alice")); // false
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { TwoPSet } from "@suaveplan/crdt";
|
||||
|
||||
const a = new TwoPSet<number>();
|
||||
a.add(1);
|
||||
const b = new TwoPSet<number>();
|
||||
b.add(2);
|
||||
a.merge(b.state());
|
||||
console.log([...a.value()]); // [1, 2]
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `TwoPSet<T>`
|
||||
|
||||
- `add(element)` — throws `OPERATION_INVALID` if previously removed
|
||||
- `remove(element)` — no-op if element not in A-set
|
||||
- `has(element): boolean`
|
||||
- `value(): Set<T>`
|
||||
- `merge(state: TwoPSetState<T>): void`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Elements are serialised to JSON for key comparison, enabling support for object elements with structural equality. The restriction that removed elements cannot be re-added is a fundamental invariant of 2P-Set that enables simple merge logic.
|
||||
52
docs/foundation/flow/crdt/src/errors/crdt-error.md
Normal file
52
docs/foundation/flow/crdt/src/errors/crdt-error.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# crdt-error
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the structured error type and stable error-code registry for `@suaveplan/crdt`. CRDT operations should never throw in normal use — these errors guard against invalid states, deserialization failures, and programmer errors (invalid operations).
|
||||
|
||||
## Features
|
||||
|
||||
- `CRDT_CODES` registry via `@suaveplan/error`'s `defineCodes` — codes namespaced `CRDT.*`
|
||||
- `CrdtError` extends `SuaveplanError` for cause-chain traversal
|
||||
- Four codes: `MERGE_CONFLICT`, `INVALID_STATE`, `DESERIALIZE_FAILED`, `OPERATION_INVALID`
|
||||
- `CrdtCode` union type for exhaustive switch statements
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CrdtError, CRDT_CODES } from "@suaveplan/crdt";
|
||||
|
||||
throw new CrdtError(CRDT_CODES.DESERIALIZE_FAILED, "Unknown CRDT type in JSON");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CrdtError, CRDT_CODES } from "@suaveplan/crdt";
|
||||
|
||||
function validateMerge(state: unknown): void {
|
||||
if (typeof state !== "object" || state === null) {
|
||||
throw new CrdtError(CRDT_CODES.INVALID_STATE, "State must be an object");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CRDT_CODES`
|
||||
|
||||
- `CRDT_CODES.MERGE_CONFLICT` — incompatible state types during merge
|
||||
- `CRDT_CODES.INVALID_STATE` — deserialized or constructed state is structurally invalid
|
||||
- `CRDT_CODES.DESERIALIZE_FAILED` — JSON deserialization failed
|
||||
- `CRDT_CODES.OPERATION_INVALID` — attempt to remove from G-Set or similar invariant violation
|
||||
|
||||
### `CrdtError`
|
||||
|
||||
Extends `SuaveplanError<CrdtCode>`. Constructor: `(code, message, options?)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Codes are prefixed with `"CRDT."` by `defineCodes`. Most CRDT methods are synchronous and do not throw in normal use — errors indicate programmer mistakes or data corruption.
|
||||
48
docs/foundation/flow/crdt/src/types/crdt.types.md
Normal file
48
docs/foundation/flow/crdt/src/types/crdt.types.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# crdt.types
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared TypeScript type definitions for the `@suaveplan/crdt` package. Defines the `CRDT<TValue, TState>` base interface, all serializable state shapes, and construction options. All CRDT implementations depend on these types for consistent merge, serialization, and cloning contracts.
|
||||
|
||||
## Features
|
||||
|
||||
- `CRDT<TValue, TState>` — base interface with `value()`, `merge()`, `state()`, `toJSON()`, `fromJSON()`, `clone()`
|
||||
- State types: `GCounterState`, `PNCounterState`, `ORSetState<T>`, `LWWRegisterState<T>`, `MVRegisterState<T>`, `TwoPSetState<T>`, `RGAState<T>`
|
||||
- `VectorClockState` — alias for `Record<string, number>`
|
||||
- `CrdtOptions` — base constructor options with optional `id` and `nodeId`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { CRDT, GCounterState } from "@suaveplan/crdt";
|
||||
|
||||
function merge<T>(a: CRDT<T>, b: CRDT<T>): void {
|
||||
a.merge(b.state());
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { ORSetState } from "@suaveplan/crdt";
|
||||
|
||||
function serialize<T>(state: ORSetState<T>): string {
|
||||
return JSON.stringify(state);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CRDT<TValue, TState>`
|
||||
|
||||
- `id: string` — stable identifier
|
||||
- `value(): TValue` — current computed value
|
||||
- `merge(remote: TState): void` — merges remote state
|
||||
- `state(): TState` — serializable snapshot
|
||||
- `toJSON(): string` — JSON serialisation
|
||||
- `fromJSON(json: string): void` — deserialises into this instance
|
||||
- `clone(): this` — deep copy
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
All state types include a `type` discriminator string for runtime identification during deserialization. The `RGAElement<T>` uses `value: T | null` where `null` represents a tombstoned element.
|
||||
Reference in New Issue
Block a user