Files
suaveplan/openspec/archive/2026-04-24-enterprise-crdt/proposal.md
2026-05-27 23:29:53 +01:00

8.5 KiB

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

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

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

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

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

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

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

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)

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

class CrdtError extends SuaveplanError<CrdtErrorCode>

type CrdtErrorCode =
  | "CRDT_MERGE_CONFLICT"
  | "CRDT_INVALID_STATE"
  | "CRDT_DESERIALIZE_FAILED"
  | "CRDT_OPERATION_INVALID"

function registerCrdtErrorCodes(): void

Zod schemas

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.

class CrdtSyncManager {
  register<T extends CRDT<unknown>>(crdt: T): void
  delta(since?: string): SyncDelta
  apply(delta: SyncDelta): void
}

3. Vector clock utilities

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

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).