@sovereignbase/convergent-replicated-text
v1.0.0
Published
Convergent Replicated Text (CR-Text), a delta CRDT for text value state.
Downloads
203
Maintainers
Readme
convergent-replicated-text
Convergent Replicated Text (CR-Text), a delta CRDT for text value state.
Try the demo:
- https://sovereignbase.dev/convergent-replicated-text
Compatibility
- Runtimes: Node >= 20, modern browsers, Bun, Deno, Cloudflare Workers, Edge Runtime.
- Module format: ESM + CommonJS.
- Required globals / APIs:
EventTarget,CustomEvent,structuredClone. - TypeScript: bundled types.
Goals
- Deterministic convergence of the live text projection under asynchronous gossip delivery.
- Consistent behavior across Node, browsers, worker, and edge runtimes.
- Garbage collection possibility without breaking live-text convergence.
- Event-driven API.
Installation
npm install @sovereignbase/convergent-replicated-text
# or
pnpm add @sovereignbase/convergent-replicated-text
# or
yarn add @sovereignbase/convergent-replicated-text
# or
bun add @sovereignbase/convergent-replicated-text
# or
deno add jsr:@sovereignbase/convergent-replicated-text
# or
vlt install jsr:@sovereignbase/convergent-replicated-textUsage
Copy-paste example
import {
CRText,
BeforeInputStreamAdapter,
ChangeStreamAdapter,
} from '@sovereignbase/convergent-replicated-text'
import { StationClient } from '@sovereignbase/station-client'
const station = new StationClient()
const snapshot = JSON.parse(localStorage.getItem('state')) ?? undefined
const frontiers = JSON.parse(localStorage.getItem('frontiers')) ?? undefined
const text = new CRText(snapshot)
if (frontiers) {
void text.garbageCollect(frontiers)
}
text.addEventListener('snapshot', (ev) => {
void localStorage.setItem('state', JSON.stringify(ev.detail))
})
text.addEventListener('ack', (ev) => {
void localStorage.setItem('frontiers', JSON.stringify([ev.detail]))
})
const elements = [
document.getElementById('textarea-element'),
document.getElementById('input-element'),
document.getElementById('html-element'),
]
text.addEventListener('change', (event) => {
for (const element of elements) {
void ChangeStreamAdapter(event, element)
}
void text.snapshot()
void text.acknowledge()
})
for (const element of elements) {
element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
? (element.value = text)
: (element.textContent = text)
void element.addEventListener(
'beforeinput',
(event) => void BeforeInputStreamAdapter(event, text)
)
}
text.addEventListener('delta', (ev) => {
station.relay(ev.detail)
})
station.addEventListener('message', (ev) => {
text.merge(ev.detail)
})Runtime behavior
Validation and errors
insertAfter()andremoveAfter()validate parameter types and throwCRTextErrorwith the stable codeBAD_PARAMSfor invalid calls.insertAfter(-1, text)is the supported way to insert at the beginning of the document.- Local
insertAfter()andremoveAfter()emit bothdeltaandchange; remotemerge()emitschangeonly when the visible text projection changes. snapshot()emits a detached snapshot event andacknowledge()emits an acknowledgement frontier event.
Safety and copying semantics
CRTextstores text as grapheme-cluster entries backed by@sovereignbase/convergent-replicated-list.toJSON()returns a detached structured-clone-compatible snapshot andtoString()serializes that snapshot as JSON.valueOf(),Symbol.toPrimitive, iteration, and runtime inspect hooks expose the current visible string projection.BeforeInputStreamAdapter()prevents the browser's default DOM mutation and translatesbeforeinputevents intoinsertAfter()/removeAfter()calls.ChangeStreamAdapter()applieschangepatches to<input>,<textarea>, andcontenteditablehosts and restores the caret for focused editable elements.
Convergence and compaction
- The convergence target is the visible text returned by
valueOf(). merge()applies remote CR-List deltas to the underlying replica state while preserving the event-drivenCRTextsurface.garbageCollect(frontiers)compacts tombstoned history after acknowledgement frontiers make it safe to do so.
Tests
npm run testWhat the current test suite covers:
- Coverage on built
dist/**/*.js:100%statements,100%branches,100%functions, and100%lines. - Public API surface:
CRText,CRTextError,BeforeInputStreamAdapter,translateDOMBeforeInputEvent, andChangeStreamAdapter. CRTextinvariants: snapshot hydration, string coercion, grapheme-aware insert/remove semantics, event channels, duplicate delta idempotency, and garbage-collection-preserved text convergence.- DOM adapter behavior for input, textarea, and contenteditable hosts, including selection translation and caret restoration edge paths.
- Deterministic convergence stress for shuffled gossip delivery, replica restarts, and
valueOf()equality across replicas after randomized local edits. - End-to-end runtime matrix:
- 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
Benchmarks
npm run benchLast measured on Node v22.14.0 (win32 x64).
| group | scenario | chars | workload | ops | ms | ms/op | ops/sec |
| ------------ | -------------------------------------- | -----: | -------------------------------------- | -----: | -------: | ----: | --------: |
| throughput | typing / append random article | 12,500 | 1010 append operations | 24,240 | 2,301.96 | 0.09 | 10,530.14 |
| throughput | editing / random inserts and deletes | 15,153 | 2000 random insert/remove operations | 24,000 | 6,638.76 | 0.28 | 3,615.13 |
| projection | snapshot / toJSON revised article | 15,153 | detached snapshot | 240 | 6,020.95 | 25.09 | 39.86 |
| projection | valueOf / materialize current string | 15,153 | string projection | 360 | 8,024.94 | 22.29 | 44.86 |
License
Apache-2.0
