@sovereignbase/sequencer
v1.0.0
Published
A causality-encoding engine written in TypeScript and C++ to build high-performance, conflict-free replicated data types for use on the web.
Maintainers
Readme
Sequencer
Sequencer provides a deterministic total ordering for distributed data. It allows independently operating actors to make concurrent changes and eventually converge on the same state without relying on network arrival order or perfectly synchronized clocks.
Usage
Simple typed API
import { Projection } from '@sovereignbase/sequencer'
const projection = new Projection<string>(1)
const gossip0 = projection.insert(['Hello World'], 0)
console.log(projection.value(0)) // "Hello World"
const gossip1 = projection.replace(['Hello Dude'], 0, 0)
console.log(projection.value(0)) // "Hello Dude"
const gossip2 = projection.remove(0, 0)
console.log(projection.value(0)) // undefined
projection.destroy()See documentation
Benchmarks
Moderate bundle size
| Format | Raw (KiB) | Minified (KiB) | Minified + gzip (KiB) | | -------- | --------: | -------------: | --------------------: | | ESM | 123.83 | 100.04 | 36.07 | | CommonJS | 123.94 | 100.86 | 36.28 |
Both bundles include WASM. Terser uses three compression passes; gzip uses level 9.
Average throughput during editing to checkpoint
Arithmetic means of 3 runs, in ops/s, from each phase's start to checkpoint. Strips contain 1–100 Frames (visible entries). Headers show Strip counts and mean Frame counts.
↑ Scale up (0 → 25,000 Strips)
| Method / case | 1,000 Strips50,871 Frames | 5,000 Strips252,214 Frames | 10,000 Strips504,871 Frames | 15,000 Strips758,280 Frames | 20,000 Strips1,010,749 Frames | 25,000 Strips1,264,003 Frames | | -------------- | ----------------------------: | -----------------------------: | ------------------------------: | ------------------------------: | --------------------------------: | --------------------------------: | | apply.random | 146,512 | 50,162 | 35,425 | 25,372 | 17,992 | 13,401 | | insert.head | 1,103,240 | 627,952 | 723,841 | 741,193 | 692,500 | 629,581 | | insert.random | 423,106 | 138,442 | 91,906 | 64,213 | 44,063 | 33,092 | | insert.tail | 1,247,342 | 779,594 | 895,027 | 892,070 | 830,660 | 765,706 | | remove.head | — | — | — | — | — | — | | remove.random | 258,681 | 95,210 | 65,861 | 46,668 | 31,655 | 23,556 | | remove.tail | — | — | — | — | — | — | | replace.head | 380,869 | 309,212 | 303,788 | 291,544 | 270,032 | 245,792 | | replace.random | 253,774 | 94,640 | 63,352 | 45,272 | 31,633 | 23,736 | | replace.tail | 392,296 | 315,718 | 313,645 | 310,052 | 268,791 | 247,351 | | value.head | 2,283,059 | 2,095,440 | 1,999,974 | 2,016,602 | 1,907,994 | 1,721,581 | | value.random | 569,971 | 133,693 | 78,867 | 51,725 | 34,540 | 25,316 | | value.tail | 4,749,438 | 3,636,984 | 3,461,656 | 3,227,876 | 2,914,996 | 2,408,625 |
↓ Scale down (25,000 → 0 Strips)
| Method / case | 20,000 Strips1,012,742 Frames | 15,000 Strips755,860 Frames | 10,000 Strips504,103 Frames | 5,000 Strips251,203 Frames | 1,000 Strips51,057 Frames | 0 Strips0 Frames | | -------------- | --------------------------------: | ------------------------------: | ------------------------------: | -----------------------------: | ----------------------------: | -------------------: | | apply.random | 7,268 | 7,592 | 8,263 | 8,783 | 9,083 | 9,219 | | insert.head | — | — | — | — | — | — | | insert.random | 17,296 | 18,458 | 20,290 | 21,279 | 22,052 | 22,459 | | insert.tail | — | — | — | — | — | — | | remove.head | 157,981 | 165,917 | 180,514 | 178,894 | 181,826 | 183,709 | | remove.random | 13,036 | 13,992 | 15,527 | 16,535 | 17,163 | 17,459 | | remove.tail | 192,199 | 192,220 | 203,528 | 209,615 | 209,694 | 210,394 | | replace.head | 176,953 | 178,240 | 186,921 | 185,389 | 181,259 | 161,071 | | replace.random | 13,038 | 14,084 | 15,647 | 16,708 | 17,397 | 17,697 | | replace.tail | 190,938 | 184,284 | 193,773 | 187,715 | 182,304 | 182,496 | | value.head | 794,128 | 838,606 | 885,880 | 922,502 | 940,112 | 948,944 | | value.random | 13,677 | 14,768 | 16,600 | 18,037 | 18,894 | 19,271 | | value.tail | 1,070,897 | 1,038,948 | 1,093,604 | 1,140,368 | 1,178,006 | 1,192,164 |
API throughput at checkpoint
Checkpoint calls, in ops/s, averaged across runs.
↑ Scale up (0 → 25,000 Strips)
| Method | 1,000 Strips50,871 Frames | 5,000 Strips252,214 Frames | 10,000 Strips504,871 Frames | 15,000 Strips758,280 Frames | 20,000 Strips1,010,749 Frames | 25,000 Strips1,264,003 Frames | | --------------------------------- | ----------------------------: | -----------------------------: | ------------------------------: | ------------------------------: | --------------------------------: | --------------------------------: | | new Projection(actorID, sequence) | 2,513 | 795 | 408 | 215 | 154 | 107 | | sequence() | 375 | 66 | 33 | 18 | 11 | 6 | | values() | 480 | 163 | 108 | 70 | 49 | 32 |
↓ Scale down (25,000 → 0 Strips)
| Method | 20,000 Strips1,012,742 Frames | 15,000 Strips755,860 Frames | 10,000 Strips504,103 Frames | 5,000 Strips251,203 Frames | 1,000 Strips51,057 Frames | 0 Strips0 Frames | | --------------------------------- | --------------------------------: | ------------------------------: | ------------------------------: | -----------------------------: | ----------------------------: | -------------------: | | new Projection(actorID, sequence) | 116 | 212 | 273 | 586 | 1,881 | 16,098 | | sequence() | 7 | 6 | 5 | 4 | 4 | 38,383 | | values() | 44 | 51 | 58 | 62 | 199 | 68,912 |
Sequence disk size at checkpoint
Complete exported Sequences, including Footage and acknowledgements. Sizes are in KiB.
↑ Scale up (0 → 25,000 Strips)
| Encoding | 1,000 Strips50,871 Frames | 5,000 Strips252,214 Frames | 10,000 Strips504,871 Frames | 15,000 Strips758,280 Frames | 20,000 Strips1,010,749 Frames | 25,000 Strips1,264,003 Frames | | ------------------ | ----------------------------: | -----------------------------: | ------------------------------: | ------------------------------: | --------------------------------: | --------------------------------: | | JSON | 277.17 | 1,621.65 | 3,261.72 | 4,901.02 | 7,041.25 | 9,162.75 | | JSON + gzip | 14.59 | 73.84 | 150.38 | 226.94 | 311.22 | 391.69 | | MessagePack | 164.81 | 818.88 | 1,639.69 | 3,529.41 | 5,081.04 | 6,483.22 | | MessagePack + gzip | 12.38 | 61.62 | 124.91 | 198.59 | 263.54 | 332.77 |
↓ Scale down (25,000 → 0 Strips)
| Encoding | 20,000 Strips1,012,742 Frames | 15,000 Strips755,860 Frames | 10,000 Strips504,103 Frames | 5,000 Strips251,203 Frames | 1,000 Strips51,057 Frames | 0 Strips0 Frames | | ------------------ | --------------------------------: | ------------------------------: | ------------------------------: | -----------------------------: | ----------------------------: | -------------------: | | JSON | 7,446.44 | 5,617.36 | 3,769.76 | 1,885.51 | 383.23 | 0.02 | | JSON + gzip | 323.23 | 244.45 | 163.91 | 80.61 | 16.33 | 0.03 | | MessagePack | 5,237.17 | 3,930.33 | 2,628.38 | 1,312.43 | 266.67 | 0.01 | | MessagePack + gzip | 274.96 | 207.78 | 137.38 | 67.41 | 13.67 | 0.03 |
Memory usage at checkpoint
One instance: WASM memory plus retained JS array slots, in KiB (1,024 bytes).
↑ Scale up (0 → 25,000 Strips)
| Measurement | 1,000 Strips50,871 Frames | 5,000 Strips252,214 Frames | 10,000 Strips504,871 Frames | 15,000 Strips758,280 Frames | 20,000 Strips1,010,749 Frames | 25,000 Strips1,264,003 Frames | | ---------------------- | ----------------------------: | -----------------------------: | ------------------------------: | ------------------------------: | --------------------------------: | --------------------------------: | | Estimated memory (KiB) | 5,343.67 | 25,100.58 | 50,470.89 | 82,701.27 | 98,036.35 | 153,462.93 |
↓ Scale down (25,000 → 0 Strips)
| Measurement | 20,000 Strips1,012,742 Frames | 15,000 Strips755,860 Frames | 10,000 Strips504,103 Frames | 5,000 Strips251,203 Frames | 1,000 Strips51,057 Frames | 0 Strips0 Frames | | ---------------------- | --------------------------------: | ------------------------------: | ------------------------------: | -----------------------------: | ----------------------------: | -------------------: | | Estimated memory (KiB) | 163,746.93 | 174,034.00 | 202,660.72 | 293,941.85 | 302,158.29 | 304,203.30 |
Measured on Node 24.16.0, V8 13.6.233.17-node.49, win32 x64, Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz. Peer synchronization, encoding and JS process garbage collection are outside API timings. Full lifecycle results · Throughput results.
Tests
Behaviour guaranteed by excessive tests
The latest verification passed 178 Vitest tests and 88 compatibility checks (1 failed). The configured 100% coverage threshold is met (100% statements, 100% branches, 100% functions, 100% lines). See the test suites and automated test report.
Works everywhere where JavaScript and Wasm works
Runtimes covered by the latest compatibility verification:
- Node.js, ESM: passed
- Node.js, CommonJS: passed
- Deno: passed
- Bun: passed
- Edge Runtime: passed
- Cloudflare Workers through workerd: passed
- Chromium browser windows: passed
- Firefox browser windows: failed (matching Playwright browser binary unavailable)
- WebKit browser windows: passed
Tested realtime and offline text editing behaviour examples
- Online and offline editors: six editors converge through delayed, reversed and shuffled delivery, including offline changes arriving during online editing.
- Concurrent replacements: independent replacement histories retain the same surviving content across delivery orders.
- Restart and redelivery: stored Sequences preserve visible content through hydration, duplicate delivery and further edits.
- Peer-to-peer fanout: independently editing browser peers synchronize through BroadcastChannel.
- WebSocket fanout: three browser peers converge through a stateless JSON Gossip relay.
Credits
This could not have been made without years of work from many great engineers of many great projects, to mention a few:
License
Apache-2.0
