Convergent Replicated Set (CR-Set), a delta CRDT for unordered, duplicate-free collections.
EventTarget, CustomEvent, structuredClone.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_ENCODABLEVALUE_NOT_CLONEABLEIngress stays tolerant through the underlying CR-Map replication layer:
deltavalues and tombstones.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.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.npm run test
What the current test suite covers:
dist/**/*.js: 100% statements, 100% branches, 100% functions, and 100% lines via c8.CRSet surface: constructor, add(), has(), delete(), clear(), values(), iteration, forEach(), events, and JSON / inspect behavior.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 snapshothas / primitive valuehas / object valuehas / falsy valuehas / missing valuevalues()iteratorforEach()add / stringadd / objectadd / duplicate objectdelete(value)clear()snapshotacknowledgegarbage collectmerge ordered deltasmerge direct successormerge shuffled gossipmerge stale conflictApache-2.0