first commit
This commit is contained in:
152
plugins/library-navigator/skills/sp-crdt/SKILL.md
Normal file
152
plugins/library-navigator/skills/sp-crdt/SKILL.md
Normal 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.
|
||||
Reference in New Issue
Block a user