A small runtime for storing garbage-collection acknowledgement frontiers for Sovereignbase CRDT replicas.
- Runtimes: Node >= 22, modern browsers, Bun, Deno, Cloudflare Workers, Edge Runtime.
- Module format: ESM + CommonJS.
- Required globals / APIs: none beyond the target JavaScript runtime.
- TypeScript: bundled types.
- Keep acknowledgement frontiers separated by CRDT kind, CRDT target, and acknowledging entity.
- Provide the exact frontier arrays expected by
garbageCollect(frontiers). - Support
CRSet,CRMap,CRList,CRText, andCRStructacknowledgement types. - Stay storage-adapter-neutral: persist the JSON snapshot wherever the application already stores metadata.
- Keep the runtime small and explicit; callers that need defensive copies can clone snapshots at the storage boundary.
npm install @sovereignbase/frontier-store
# or
pnpm add @sovereignbase/frontier-store
# or
yarn add @sovereignbase/frontier-store
# or
bun add @sovereignbase/frontier-store
# or
deno add jsr:@sovereignbase/frontier-store
# or
vlt install jsr:@sovereignbase/frontier-storeimport { CRSet } from '@sovereignbase/convergent-replicated-set'
import { FrontierStore } from '@sovereignbase/frontier-store'
const alice = new CRSet<string>()
const bob = new CRSet<string>()
const frontiers = new FrontierStore()
alice.addEventListener('delta', (event) => {
bob.merge(event.detail)
})
bob.addEventListener('delta', (event) => {
alice.merge(event.detail)
})
alice.addEventListener('ack', (event) => {
frontiers.setFrontier('set', 'shopping-list', 'alice', event.detail)
})
bob.addEventListener('ack', (event) => {
frontiers.setFrontier('set', 'shopping-list', 'bob', event.detail)
})
alice.add('milk')
alice.delete('milk')
bob.add('coffee')
alice.acknowledge()
bob.acknowledge()
const acknowledged = frontiers.getFrontiers('set', 'shopping-list')
alice.garbageCollect(acknowledged)
bob.garbageCollect(acknowledged)import { FrontierStore } from '@sovereignbase/frontier-store'
const store = new FrontierStore(
JSON.parse(localStorage.getItem('frontiers') ?? '{}')
)
store.setFrontier(
'text',
'article:homepage',
'device:laptop',
'0198f0a8-1357-7c00-8000-000000000001'
)
localStorage.setItem('frontiers', JSON.stringify(store))Snapshots are plain structured-clone-compatible objects:
type FrontierStoreSnapshot = {
set?: Record<string, Record<string, CRSetAck>>
map?: Record<string, Record<string, CRMapAck>>
list?: Record<string, Record<string, CRListAck>>
text?: Record<string, Record<string, CRTextAck>>
struct?: Record<string, Record<string, CRStructAck<Record<string, unknown>>>>
}import { FrontierStore } from '@sovereignbase/frontier-store'
const store = new FrontierStore()
store.setFrontier('map', 'profile:alice', 'phone', mapAck)
store.setFrontier('text', 'profile:alice/name', 'phone', textAck)
store.setFrontier('struct', 'profile:alice/settings', 'phone', structAck)
profileMap.garbageCollect(store.getFrontiers('map', 'profile:alice'))
nameText.garbageCollect(store.getFrontiers('text', 'profile:alice/name'))
settingsStruct.garbageCollect(
store.getFrontiers('struct', 'profile:alice/settings')
)The kind and targetId are application-level routing keys. Keep them stable
for the lifetime of the CRDT object whose acknowledgements they describe.
import { FrontierStore } from '@sovereignbase/frontier-store'
const store = new FrontierStore(snapshot)
store.deleteFrontier('set', 'shopping-list', 'old-phone')
store.deleteFrontier('text', 'article:homepage')Passing an entityId removes one acknowledging entity. Omitting it removes all
frontiers for the target. Empty target and kind buckets are cleaned up.
- The constructor uses the provided snapshot as the backing state.
setFrontier()stores the provided acknowledgement value.getFrontiers()returns the currently stored acknowledgement values.toJSON(),JSON.stringify(), inspection symbols, and iteration expose the current snapshot model.deleteFrontier()is idempotent for missing kinds, targets, and entities.- The store does not validate CRDT frontier internals; the corresponding CRDT package remains the authority for accepting or ignoring a frontier.
- Clone snapshots before passing them to or reading them from the store when your application needs mutation isolation.
FrontierStoredoes not decide when a CRDT may compact history.- It records the latest acknowledgement frontier per
(kind, targetId, entityId). - Callers should pass the complete frontier set for the target to that target's
garbageCollect(). - A partial frontier set is caller misuse for Sovereignbase CRDTs that require every active replica's acknowledgement before compacting safely.
- Store snapshots can be replicated or persisted independently of the CRDT snapshots they describe.
npm run testThe test suite covers:
- Core
FrontierStorestorage, replacement, deletion, cleanup, iteration, inspection, and serialization behavior. - Snapshot/frontier exposure semantics for constructor input,
getFrontiers(),toJSON(), and iteration. - ESM and CommonJS package interop through JSON snapshots.
- End-to-end runtime matrix for Node ESM/CJS, Bun ESM/CJS, Deno ESM, Cloudflare Workers ESM, Edge Runtime ESM, and browsers via Playwright.
- Integration with
CRSet,CRMap,CRList,CRText, andCRStruct: collectackevents, store them by target/entity, retrieve complete frontier arrays, rungarbageCollect(), settle replicas, and hydrate snapshots. - Coverage on built
dist/**/*.jswith100%statements, branches, functions, and lines viac8.
Current status on Node v22.14.0 (win32 x64): npm run test passes.
npm run benchThe benchmark suite measures FrontierStore orchestration paths:
- constructor hydration
setFrontier()for string and struct acknowledgementsgetFrontiers()- entity and target deletion
toJSON()- iteration
Last measured on Node v22.14.0 (win32 x64):
| group | scenario | n | ops | ms | ms/op | ops/sec |
|---|---|---|---|---|---|---|
class |
constructor / hydrate snapshot |
200 | 100 | 0.06 | 0.00 | 1,675,041.88 |
class |
setFrontier / string ack |
200 | 100 | 0.40 | 0.00 | 247,647.35 |
class |
setFrontier / struct ack |
200 | 100 | 0.29 | 0.00 | 350,877.19 |
class |
getFrontiers |
200 | 100 | 0.24 | 0.00 | 422,832.98 |
class |
deleteFrontier / entity |
200 | 100 | 0.63 | 0.01 | 158,027.81 |
class |
deleteFrontier / target |
200 | 100 | 1.22 | 0.01 | 81,913.50 |
class |
toJSON |
200 | 100 | 0.05 | 0.00 | 1,862,197.39 |
class |
iterator |
200 | 100,000 | 5.42 | 0.00 | 18,448,482.61 |
Apache-2.0