@sovereignbase/convergent-replicated-text
v1.1.1
Published
Convergent Replicated Text (CR-Text), a delta CRDT for text value state.
Maintainers
Readme
convergent-replicated-text
Convergent Replicated Text (CR-Text), a delta CRDT for text value state.
Compatibility
- Runtimes: Node >= 22, modern browsers, Bun, Deno, Cloudflare Workers, Edge Runtime.
- Module format: ESM + CommonJS.
- Required globals / APIs:
EventTarget,CustomEvent. - 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'),
].filter((element) => element instanceof HTMLElement)
text.addEventListener('change', (event) => {
for (const element of elements) {
void ChangeStreamAdapter(event, element, text)
}
void text.snapshot()
void text.acknowledge()
})
for (const element of elements) {
element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement
? (element.value = text.valueOf())
: (element.textContent = text.valueOf())
void element.addEventListener(
'beforeinput',
(event) => void BeforeInputStreamAdapter(event, text)
)
}
text.addEventListener('delta', (ev) => {
void station.relay(ev.detail)
})
station.addEventListener('message', (ev) => {
void 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, restores the caret for focused editable elements during incremental patching, and uses the suppliedCRTextinstance as a full-projection fallback for multi-entry patches.
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,075.09 | 0.09 | 11,681.44 | | throughput | editing / random inserts and deletes | 15,153 | 2000 random insert/remove operations | 24,000 | 3,354.05 | 0.14 | 7,155.53 | | projection | snapshot / toJSON revised article | 15,153 | detached snapshot | 240 | 1,809.78 | 7.54 | 132.61 | | projection | valueOf / materialize current string | 15,153 | string projection | 360 | 1,099.18 | 3.05 | 327.52 |
License
Apache-2.0
