@sovereignbase/convergent-replicated-set
    Preparing search index...

    @sovereignbase/convergent-replicated-set

    npm version CI codecov license

    convergent-replicated-set

    Convergent Replicated Set (CR-Set), a delta CRDT for unordered, duplicate-free collections.

    • Runtimes: Node >= 20, modern browsers, Bun, Deno, Cloudflare Workers, Edge Runtime.
    • Module format: ESM + CommonJS.
    • Required globals / APIs: EventTarget, CustomEvent, structuredClone.
    • TypeScript: bundled types.
    • Deterministic convergence of the live set projection under asynchronous gossip delivery.
    • Content-addressed membership for unordered, duplicate-free values.
    • Consistent behavior across Node, browsers, worker, and edge runtimes.
    • Garbage collection possibility without breaking live-set convergence.
    • Event-driven API.
    npm install @sovereignbase/convergent-replicated-set
    # or
    pnpm add @sovereignbase/convergent-replicated-set
    # or
    yarn add @sovereignbase/convergent-replicated-set
    # or
    bun add @sovereignbase/convergent-replicated-set
    # or
    deno add jsr:@sovereignbase/convergent-replicated-set
    # or
    vlt install jsr:@sovereignbase/convergent-replicated-set
    import { CRSet } from '@sovereignbase/convergent-replicated-set'

    const alice = new CRSet<string>()
    const bob = new CRSet<string>()

    alice.addEventListener('delta', (event) => {
    bob.merge(event.detail)
    })

    alice.add('alpha')
    alice.add('beta')
    alice.add('alpha')

    console.log(alice.size) // 2
    console.log(bob.has('alpha')) // true
    console.log(bob.values()) // ['alpha', 'beta']
    import {
    CRSet,
    type CRSetSnapshot,
    } from '@sovereignbase/convergent-replicated-set'

    type Member = {
    id: string
    role: 'admin' | 'member'
    }

    const source = new CRSet<Member>()
    let snapshot!: CRSetSnapshot<Member>

    source.addEventListener(
    'snapshot',
    (event) => {
    snapshot = event.detail
    },
    { once: true }
    )

    source.add({ id: 'alice', role: 'admin' })
    source.add({ id: 'bob', role: 'member' })
    source.snapshot()

    const restored = new CRSet<Member>(snapshot)

    console.log(restored.size) // 2
    console.log(restored.has({ id: 'alice', role: 'admin' })) // true

    This example assumes your set values are JSON-compatible if you persist snapshots with JSON.stringify / JSON.parse. For general structuredClone-compatible values such as Date, Map, or BigInt, persist snapshots with a structured-clone-capable store or an application-level codec instead of plain JSON.

    import { CRSet } from '@sovereignbase/convergent-replicated-set'

    const set = new CRSet<string>()

    set.addEventListener('delta', (event) => {
    console.log('delta', event.detail)
    })

    set.addEventListener('change', (event) => {
    console.log('change', event.detail)
    })

    set.addEventListener('snapshot', (event) => {
    console.log('snapshot', event.detail)
    })

    set.addEventListener('ack', (event) => {
    console.log('ack', event.detail)
    })

    set.add('draft')
    set.delete('draft')
    set.snapshot()
    set.acknowledge()
    import { CRSet } from '@sovereignbase/convergent-replicated-set'

    const set = new CRSet<string>()

    set.add('red')
    set.add('green')
    set.add('blue')

    const serialized = JSON.stringify(set)
    const restored = new CRSet<string>(JSON.parse(serialized))

    for (const value of set) {
    console.log(value)
    }

    set.forEach((value, target) => {
    console.log(value, target.size)
    })

    console.log(set.values())
    console.log(restored.has('green')) // true

    values(), for...of, and forEach() return detached copies of visible values. Mutating those returned values does not mutate the underlying replica state.

    import { CRSet, type CRSetAck } from '@sovereignbase/convergent-replicated-set'

    const alice = new CRSet<string>()
    const bob = new CRSet<string>()
    const frontiers = new Map<string, CRSetAck>()

    alice.addEventListener('delta', (event) => {
    bob.merge(event.detail)
    })

    bob.addEventListener('delta', (event) => {
    alice.merge(event.detail)
    })

    alice.addEventListener('ack', (event) => {
    frontiers.set('alice', event.detail)
    })

    bob.addEventListener('ack', (event) => {
    frontiers.set('bob', event.detail)
    })

    alice.add('x')
    alice.delete('x')

    alice.acknowledge()
    bob.acknowledge()

    alice.garbageCollect([...frontiers.values()])
    bob.garbageCollect([...frontiers.values()])

    Public mutations can throw CRSetError with stable error codes:

    • VALUE_NOT_ENCODABLE
    • VALUE_NOT_CLONEABLE

    Ingress stays tolerant through the underlying CR-Map replication layer:

    • duplicate identical additions are no-ops
    • duplicate delete and merge payloads are idempotent
    • stale or dominated incoming state does not break live-set convergence
    • dominated incoming state may emit a reply delta
    • Values are identified by the SHA-256 Base64URL digest of their canonical MessagePack encoding.
    • Snapshots are serializable full-state payloads with values and tombstones.
    • Deltas are serializable partial snapshot payloads with values and tombstones.
    • change is a minimal value-keyed visible patch where deleted values map to undefined.
    • toJSON() returns a detached serializable snapshot.
    • JSON.stringify() and toString() are only reliable when set values are JSON-compatible.
    • values(), for...of, and forEach() expose detached copies of visible values rather than mutable references into replica state.
    • add(), has(), delete(), clear(), merge(), snapshot(), acknowledge(), and garbageCollect() all operate on the live set projection.
    • The convergence target is the visible set projection, not identical internal tombstone sets.
    • Membership is content-addressed: structurally identical canonical MessagePack values resolve to the same set member.
    • add() is idempotent when the value's content key is already visible.
    • delete() removes the visible value identified by the value's current content key.
    • Tombstones remain until acknowledgement frontiers make them safe to collect.
    • Garbage collection compacts tombstoned history while preserving the converged live projection for replicas that later catch up from delta or snapshot state.
    npm run test
    

    What the current test suite covers:

    • Coverage on built dist/**/*.js: 100% statements, 100% branches, 100% functions, and 100% lines via c8.
    • Public CRSet surface: constructor, add(), has(), delete(), clear(), values(), iteration, forEach(), events, and JSON / inspect behavior.
    • Value semantics for primitive, object, falsy, duplicate, cloned, and non-encodable values.
    • Snapshot hydration, delta merge idempotency, acknowledgement, and garbage collection recovery.
    • Integration convergence stress for shuffled asynchronous gossip delivery and replica restarts.
    • End-to-end runtime matrix for:
      • Node ESM
      • Node CJS
      • Bun ESM
      • Bun CJS
      • Deno ESM
      • Cloudflare Workers ESM
      • Edge Runtime ESM
      • Browsers via Playwright: Chromium, Firefox, WebKit, mobile Chrome, mobile Safari
    npm run bench
    

    The benchmark suite measures the default CRSet class surface across hydration, membership checks, projection reads, local mutations, compaction, and merge scenarios.

    Current scenarios:

    • constructor / hydrate snapshot
    • has / primitive value
    • has / object value
    • has / falsy value
    • has / missing value
    • values()
    • iterator
    • forEach()
    • add / string
    • add / object
    • add / duplicate object
    • delete(value)
    • clear()
    • snapshot
    • acknowledge
    • garbage collect
    • merge ordered deltas
    • merge direct successor
    • merge shuffled gossip
    • merge stale conflict

    Apache-2.0