348 lines
8.5 KiB
Markdown
348 lines
8.5 KiB
Markdown
# OpenSpec Change: enterprise-crdt
|
|
|
|
- **Change ID**: enterprise-crdt
|
|
- **Package**: `@suaveplan/crdt`
|
|
- **Category**: universal
|
|
- **Target path**: `universal/data/crdt`
|
|
- **Status**: PROPOSED
|
|
- **Date**: 2026-04-24
|
|
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/crdt.md`
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Deliver `@suaveplan/crdt` on the genesis branch from scratch at enterprise quality. The package provides conflict-free replicated data type primitives: G-Counter, PN-Counter, OR-Set, LWW-Register, MV-Register, 2P-Set, and RGA (Replicated Growable Array). All CRDTs implement a common `CRDT` interface with `merge`, `value`, and `toJSON`/`fromJSON` for serialization. Genesis adds structured errors via `@suaveplan/error`, event-bus integration via `@suaveplan/events` for state-change propagation, diff/patch integration via `@suaveplan/diff` for efficient delta exchange, injectable clock and ID generator, OTel instrumentation, and property-based correctness proofs.
|
|
|
|
---
|
|
|
|
## Current-state audit (genesis)
|
|
|
|
The package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data.
|
|
|
|
---
|
|
|
|
## Available workspace deps on genesis
|
|
|
|
| Package | Role |
|
|
|---|---|
|
|
| `@suaveplan/error` (`workspace:^`) | Base class for `CrdtError`; `defineCodes` for `CRDT.*` codes |
|
|
| `@suaveplan/types` (`workspace:^`) | `Clock`, `IdGenerator`, `EventBusLike` injection interfaces |
|
|
| `@suaveplan/events` (`workspace:^`) | Emit typed state-change events on merge |
|
|
| `@suaveplan/diff` (`workspace:^`) | Delta encoding and patch application for efficient state sync |
|
|
| `zod` (`catalog:forms`) | Zod schemas for all public option and serialization types |
|
|
| `@opentelemetry/api` (`catalog:observability`) | Spans and counters on merge, update, and sync operations |
|
|
| `tslib` (`catalog:build`) | Runtime helpers |
|
|
|
|
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`).
|
|
|
|
---
|
|
|
|
## Public API surface
|
|
|
|
### Common CRDT interface
|
|
|
|
```ts
|
|
interface CRDT<TValue, TState = unknown> {
|
|
readonly id: string
|
|
value(): TValue
|
|
merge(remote: TState): void
|
|
state(): TState
|
|
toJSON(): string
|
|
fromJSON(json: string): void
|
|
clone(): this
|
|
}
|
|
|
|
interface CrdtOptions {
|
|
id?: string
|
|
clock?: Clock
|
|
idGenerator?: IdGenerator
|
|
}
|
|
```
|
|
|
|
### G-Counter
|
|
|
|
```ts
|
|
class GCounter implements CRDT<number, GCounterState> {
|
|
increment(nodeId: string, delta?: number): void
|
|
get(nodeId: string): number
|
|
total(): number
|
|
}
|
|
|
|
interface GCounterState {
|
|
type: "g-counter"
|
|
counters: Record<string, number>
|
|
}
|
|
```
|
|
|
|
### PN-Counter
|
|
|
|
```ts
|
|
class PNCounter implements CRDT<number, PNCounterState> {
|
|
increment(nodeId: string, delta?: number): void
|
|
decrement(nodeId: string, delta?: number): void
|
|
total(): number
|
|
}
|
|
|
|
interface PNCounterState {
|
|
type: "pn-counter"
|
|
increments: Record<string, number>
|
|
decrements: Record<string, number>
|
|
}
|
|
```
|
|
|
|
### OR-Set
|
|
|
|
```ts
|
|
class ORSet<T> implements CRDT<Set<T>, ORSetState<T>> {
|
|
add(element: T, nodeId: string): void
|
|
remove(element: T): void
|
|
has(element: T): boolean
|
|
size(): number
|
|
}
|
|
|
|
interface ORSetState<T> {
|
|
type: "or-set"
|
|
entries: Array<{ element: T; tag: string; tombstone: boolean }>
|
|
}
|
|
```
|
|
|
|
### LWW-Register
|
|
|
|
```ts
|
|
class LWWRegister<T> implements CRDT<T | undefined, LWWRegisterState<T>> {
|
|
set(value: T, nodeId: string, timestamp?: number): void
|
|
get(): T | undefined
|
|
}
|
|
|
|
interface LWWRegisterState<T> {
|
|
type: "lww-register"
|
|
value: T | undefined
|
|
timestamp: number
|
|
nodeId: string
|
|
}
|
|
```
|
|
|
|
### MV-Register
|
|
|
|
```ts
|
|
class MVRegister<T> implements CRDT<T[], MVRegisterState<T>> {
|
|
set(value: T, nodeId: string): void
|
|
get(): T[]
|
|
conflict(): boolean
|
|
}
|
|
|
|
interface MVRegisterState<T> {
|
|
type: "mv-register"
|
|
entries: Array<{ value: T; vectorClock: Record<string, number> }>
|
|
}
|
|
```
|
|
|
|
### 2P-Set
|
|
|
|
```ts
|
|
class TwoPSet<T> implements CRDT<Set<T>, TwoPSetState<T>> {
|
|
add(element: T): void
|
|
remove(element: T): void
|
|
has(element: T): boolean
|
|
}
|
|
|
|
interface TwoPSetState<T> {
|
|
type: "2p-set"
|
|
added: T[]
|
|
removed: T[]
|
|
}
|
|
```
|
|
|
|
### RGA (Replicated Growable Array)
|
|
|
|
```ts
|
|
class RGA<T> implements CRDT<T[], RGAState<T>> {
|
|
insert(index: number, element: T, nodeId: string): void
|
|
delete(index: number): void
|
|
get(index: number): T
|
|
length(): number
|
|
}
|
|
|
|
interface RGAState<T> {
|
|
type: "rga"
|
|
elements: Array<{ id: string; value: T | null; tombstone: boolean }>
|
|
}
|
|
```
|
|
|
|
### Error handling
|
|
|
|
```ts
|
|
class CrdtError extends SuaveplanError<CrdtErrorCode>
|
|
|
|
type CrdtErrorCode =
|
|
| "CRDT_MERGE_CONFLICT"
|
|
| "CRDT_INVALID_STATE"
|
|
| "CRDT_DESERIALIZE_FAILED"
|
|
| "CRDT_OPERATION_INVALID"
|
|
|
|
function registerCrdtErrorCodes(): void
|
|
```
|
|
|
|
### Zod schemas
|
|
|
|
```ts
|
|
const GCounterStateSchema: ZodSchema
|
|
const PNCounterStateSchema: ZodSchema
|
|
const ORSetStateSchema: ZodSchema
|
|
const LWWRegisterStateSchema: ZodSchema
|
|
const MVRegisterStateSchema: ZodSchema
|
|
const TwoPSetStateSchema: ZodSchema
|
|
const RGAStateSchema: ZodSchema
|
|
```
|
|
|
|
---
|
|
|
|
## New additions for genesis
|
|
|
|
### 1. Event-bus integration
|
|
|
|
Every CRDT constructor accepts an optional `bus?: EventBusLike`. When provided, merge operations emit typed events:
|
|
- `crdt:merged` — `{ id, type, previousState, nextState }`
|
|
- `crdt:updated` — `{ id, type, operation, args }`
|
|
|
|
### 2. Delta-based sync via `@suaveplan/diff`
|
|
|
|
`CrdtSyncManager` wraps one or more CRDTs and uses `@suaveplan/diff` JSON Patch to compute minimal delta payloads between sync points. Callers exchange deltas rather than full state snapshots.
|
|
|
|
```ts
|
|
class CrdtSyncManager {
|
|
register<T extends CRDT<unknown>>(crdt: T): void
|
|
delta(since?: string): SyncDelta
|
|
apply(delta: SyncDelta): void
|
|
}
|
|
```
|
|
|
|
### 3. Vector clock utilities
|
|
|
|
```ts
|
|
class VectorClock {
|
|
increment(nodeId: string): void
|
|
merge(remote: VectorClockState): void
|
|
compare(other: VectorClockState): "before" | "after" | "concurrent" | "equal"
|
|
state(): VectorClockState
|
|
}
|
|
|
|
type VectorClockState = Record<string, number>
|
|
```
|
|
|
|
### 4. OTel instrumentation
|
|
|
|
- Per-operation spans: `crdt.<type>.merge`, `crdt.<type>.update`, `crdt.sync.delta`, `crdt.sync.apply`
|
|
- Counters: `crdt.merges_total`, `crdt.updates_total`, `crdt.conflicts_total`
|
|
- Attributes: `crdt.type`, `crdt.id`, `crdt.node_id`
|
|
|
|
### 5. Property-based tests (fast-check)
|
|
|
|
- **Commutativity**: `merge(a, b) ≡ merge(b, a)` for all CRDT types
|
|
- **Associativity**: `merge(merge(a, b), c) ≡ merge(a, merge(b, c))`
|
|
- **Idempotency**: `merge(a, a) ≡ a`
|
|
- **Monotonicity**: `value(merge(a, b)) ≥ value(a)` for counter types
|
|
- **RGA convergence**: concurrent inserts on two replicas converge after mutual merge
|
|
|
|
### 6. Benchmark suite (`src/__benchmarks__/`)
|
|
|
|
- GCounter with 100 nodes, 10k increments
|
|
- ORSet with 1k add/remove operations
|
|
- RGA with 1k concurrent inserts on two replicas
|
|
|
|
---
|
|
|
|
## File layout
|
|
|
|
```text
|
|
universal/data/crdt/
|
|
package.json
|
|
tsconfig.json
|
|
vitest.config.ts
|
|
src/
|
|
index.ts
|
|
error-codes.ts
|
|
constants/
|
|
index.ts
|
|
types/
|
|
common.ts
|
|
common.md
|
|
core/
|
|
crdt-interface/
|
|
crdt-interface.ts
|
|
crdt-interface.md
|
|
crdt-interface.types.ts
|
|
index.ts
|
|
vector-clock/
|
|
vector-clock.ts
|
|
vector-clock.md
|
|
vector-clock.types.ts
|
|
vector-clock.test.ts
|
|
index.ts
|
|
sync-manager/
|
|
sync-manager.ts
|
|
sync-manager.md
|
|
sync-manager.types.ts
|
|
sync-manager.test.ts
|
|
index.ts
|
|
crdts/
|
|
g-counter/
|
|
g-counter.ts
|
|
g-counter.md
|
|
g-counter.types.ts
|
|
g-counter.test.ts
|
|
index.ts
|
|
pn-counter/
|
|
pn-counter.ts
|
|
pn-counter.md
|
|
pn-counter.types.ts
|
|
pn-counter.test.ts
|
|
index.ts
|
|
or-set/
|
|
or-set.ts
|
|
or-set.md
|
|
or-set.types.ts
|
|
or-set.test.ts
|
|
index.ts
|
|
lww-register/
|
|
lww-register.ts
|
|
lww-register.md
|
|
lww-register.types.ts
|
|
lww-register.test.ts
|
|
index.ts
|
|
mv-register/
|
|
mv-register.ts
|
|
mv-register.md
|
|
mv-register.types.ts
|
|
mv-register.test.ts
|
|
index.ts
|
|
two-p-set/
|
|
two-p-set.ts
|
|
two-p-set.md
|
|
two-p-set.types.ts
|
|
two-p-set.test.ts
|
|
index.ts
|
|
rga/
|
|
rga.ts
|
|
rga.md
|
|
rga.types.ts
|
|
rga.test.ts
|
|
index.ts
|
|
__benchmarks__/
|
|
crdt.bench.ts
|
|
tests/
|
|
integration.test.ts
|
|
property.test.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Impact
|
|
|
|
**Downstream packages**: collaborative editing, offline-first sync, and multi-peer state reconciliation features. No existing genesis package depends on it yet.
|
|
|
|
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/diff` must be published to Verdaccio before this package can be built.
|
|
|
|
**Breaking changes**: none (new package).
|