first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
---
name: sp-crdt
description: Composable CRDT primitives (counter, set, register, array) with vector-clock causality and a sync manager for distributed state convergence.
---
# CRDTs for Suaveplan
Conflict-free Replicated Data Types for distributed systems. Provides eight composable CRDT primitives—grow-only and positive-negative counters, observe-remove and two-phase sets, last-write-wins and multi-value registers, and a replicated growable array—plus a vector-clock for causality tracking and a sync manager that coordinates delta exchange across heterogeneous replicas.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `GCounter` | Grow-only counter; merge takes the element-wise maximum of per-node counters |
| `PNCounter` | Bidirectional counter composed of two internal G-Counters (increments minus decrements) |
| `ORSet<T>` | Observe-Remove Set; each add generates a unique tag so concurrent adds survive removes |
| `TwoPSet<T>` | Two-Phase Set; elements can be added and removed, but removed elements can never be re-added |
| `LWWRegister<T>` | Last-Write-Wins Register; highest timestamp wins, ties broken by lexicographic nodeId |
| `MVRegister<T>` | Multi-Value Register; preserves all concurrent writes using vector-clock dominance |
| `RGA<T>` | Replicated Growable Array; ordered list with causal insert/delete and deterministic (seq, nodeId) tie-breaking |
| `VectorClock` | Per-node monotonic counters with `compare` returning before/after/equal/concurrent |
| `CrdtSyncManager` | Registry of CRDTs; `tick()` advances local clock, `exportDeltas()` / `sync()` exchange state |
| `CrdtError` | Typed error class using `CRDT_CODES` (MERGE_CONFLICT, INVALID_STATE, DESERIALIZE_FAILED, OPERATION_INVALID) |
## File Structure
```
foundation/flow/crdt/
src/crdts/g-counter/ # Grow-only counter
src/crdts/pn-counter/ # Positive-negative counter
src/crdts/or-set/ # Observe-remove set
src/crdts/two-p-set/ # Two-phase set
src/crdts/lww-register/ # Last-write-wins register
src/crdts/mv-register/ # Multi-value register
src/crdts/rga/ # Replicated growable array
src/core/vector-clock/ # Causality tracking
src/core/sync-manager/ # Delta-exchange coordinator
src/errors/ # CrdtError and CRDT_CODES
src/types/ # Shared CRDT interfaces and state shapes
src/constants/ # DEFAULT_NODE_ID, DEFAULT_CRDT_ID_PREFIX
```
Sibling packages:
- `@suaveplan/fsm` — typed finite state machines with guards, actions, and history tracking
- `@suaveplan/queue` — enterprise-grade queue management with workers, retry, and dead-letter
- `@suaveplan/workflow` — workflow/pipeline orchestration with saga compensation and retry
## Patterns
### Quick Start
```typescript
import { GCounter, CrdtSyncManager } from "@suaveplan/crdt";
const counter = new GCounter("my-counter");
counter.increment("node-1", 3);
counter.increment("node-2", 5);
console.log(counter.total()); // 8
const mgr = new CrdtSyncManager({ nodeId: "node-1" });
mgr.register(counter);
mgr.tick();
const deltas = mgr.exportDeltas(); // send over network
```
### Bidirectional Sync Between Replicas
```typescript
import { GCounter, CrdtSyncManager } from "@suaveplan/crdt";
const a = new CrdtSyncManager({ nodeId: "a" });
const b = new CrdtSyncManager({ nodeId: "b" });
const counterA = new GCounter("shared");
const counterB = new GCounter("shared");
a.register(counterA);
b.register(counterB);
counterA.increment("a", 3);
counterB.increment("b", 4);
a.tick();
b.tick();
const { local, remote } = a.syncWith(b);
console.log(counterA.total()); // 7
console.log(counterB.total()); // 7
```
### Concurrent Add/Remove with OR-Set
```typescript
import { ORSet } from "@suaveplan/crdt";
const a = new ORSet<string>();
a.add("x", "n1");
const b = a.clone();
a.remove("x");
b.add("x", "n2");
a.merge(b.state());
console.log(a.has("x")); // true — concurrent add survives remove
```
### Multi-Value Register Conflict Detection
```typescript
import { MVRegister } from "@suaveplan/crdt";
const a = new MVRegister<number>("n1");
a.set(1);
const b = new MVRegister<number>("n2");
b.set(2);
a.merge(b.state());
console.log(a.conflict()); // true
console.log(a.get()); // [1, 2]
a.set(3); // sequential overwrite resolves conflict
console.log(a.conflict()); // false
console.log(a.get()); // [3]
```
### RGA Ordered Insertions
```typescript
import { RGA } from "@suaveplan/crdt";
const r = new RGA<string>("node-a");
const idA = r.insert("a");
const idB = r.insert("b", idA);
console.log(r.value()); // ["a", "b"]
r.remove(idA);
console.log(r.value()); // ["b"]
console.log(r.indexOf(idB)); // 0
```
## Constraints
- All CRDT constructors generate a random UUID id via `crypto.randomUUID()` when no id is provided; LWWRegister and MVRegister additionally require a `nodeId` argument.
- `GCounter.increment` and `PNCounter.increment`/`decrement` throw `CrdtError(CRDT_CODES.OPERATION_INVALID)` for non-positive deltas.
- `TwoPSet.add` throws `CrdtError(CRDT_CODES.OPERATION_INVALID)` if the element was previously removed.
- `fromJSON` on every CRDT throws `CrdtError(CRDT_CODES.DESERIALIZE_FAILED)` for invalid JSON or a mismatched `type` field.
- `RGA.insert` returns a globally unique id of the form `<seq>@<nodeId>`; concurrent inserts at the same predecessor are ordered by higher `seq` first, then lexicographically larger `nodeId`.
- `CrdtSyncManager.sync` skips deltas for unknown CRDT ids and returns them in the `skipped` count.
- Every CRDT implements the `CRDT<TValue, TState>` interface: `id`, `value()`, `merge(remoteState)`, `state()`, `toJSON()`, `fromJSON(json)`, `clone()`.
## Checklist
1. Verify the required CRDT is available in the exports map (root, `crdts/<name>`, `core/vector-clock`, `core/sync-manager`, `errors/crdt-error`).
2. Ensure `nodeId` is passed to `LWWRegister`, `MVRegister`, and `RGA` constructors; omitting it causes runtime issues or incorrect tie-breaking.
3. After local mutations, call `CrdtSyncManager.tick()` before `exportDeltas()` so the vector clock advances.
4. When merging remote state, use the typed `state()` output of the source CRDT; do not hand-craft state objects unless you preserve the exact `type` discriminator.
5. Check for `CrdtError` with `CRDT_CODES` constants; never assert against raw string literals.