@sovereignbase/sequencer 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTERNALS.md +1530 -0
- package/LICENSE +201 -201
- package/README.md +132 -99
- package/dist/index.cjs +157 -105
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -10
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +114 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -105
- package/dist/index.js.map +1 -1
- package/package.json +23 -23
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@sovereignbase/sequencer)
|
|
2
|
+
[](https://jsr.io/@sovereignbase/sequencer)
|
|
3
|
+
[](https://github.com/sovereignbase/sequencer/actions/workflows/ci.yaml)
|
|
4
|
+
[](https://codecov.io/gh/sovereignbase/sequencer)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
1
7
|
# Sequencer
|
|
2
8
|
|
|
3
|
-
Sequencer provides a deterministic total ordering for distributed data. It allows independently operating
|
|
9
|
+
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.
|
|
4
10
|
|
|
5
11
|
## Usage
|
|
6
12
|
|
|
@@ -10,133 +16,160 @@ Sequencer provides a deterministic total ordering for distributed data. It allow
|
|
|
10
16
|
import { Projection } from '@sovereignbase/sequencer'
|
|
11
17
|
|
|
12
18
|
const projection = new Projection<string>(1)
|
|
13
|
-
|
|
19
|
+
|
|
20
|
+
const gossip0 = projection.insert(['Hello World'], 0)
|
|
14
21
|
console.log(projection.value(0)) // "Hello World"
|
|
22
|
+
const gossip1 = projection.replace(['Hello Dude'], 0, 0)
|
|
23
|
+
console.log(projection.value(0)) // "Hello Dude"
|
|
24
|
+
const gossip2 = projection.remove(0, 0)
|
|
25
|
+
console.log(projection.value(0)) // undefined
|
|
15
26
|
|
|
16
27
|
projection.destroy()
|
|
17
28
|
```
|
|
18
29
|
|
|
30
|
+
### See documentation
|
|
31
|
+
|
|
32
|
+
- [How sequencer works?](/INTERNALS.md)
|
|
33
|
+
- [Typescript](https://sovereignbase.dev/sequencer/)
|
|
34
|
+
- [C++](https://sovereignbase.dev/sequencer/c++)
|
|
35
|
+
|
|
19
36
|
## Benchmarks
|
|
20
37
|
|
|
21
|
-
###
|
|
38
|
+
### Moderate bundle size
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
| Format | Raw (KiB) | Minified (KiB) | Minified + gzip (KiB) |
|
|
41
|
+
| -------- | --------: | -------------: | --------------------: |
|
|
42
|
+
| ESM | 123.83 | 100.04 | 36.07 |
|
|
43
|
+
| CommonJS | 123.94 | 100.86 | 36.28 |
|
|
27
44
|
|
|
28
|
-
|
|
29
|
-
public API call, weighted by call count across three runs.
|
|
45
|
+
Both bundles include WASM. Terser uses three compression passes; gzip uses level 9.
|
|
30
46
|
|
|
31
|
-
|
|
32
|
-
| ----------------------------------- | ---------------------: | -----------------------: | ------------------------: |
|
|
33
|
-
| `new Projection(actorID, sequence)` | 2.013 | 1.598 | 1.503 |
|
|
34
|
-
| `insert`, random position | 1.108 | 1.045 | 1.076 |
|
|
35
|
-
| `remove`, middle | 1.534 | 1.605 | 1.493 |
|
|
36
|
-
| `replace`, middle | 1.507 | 1.364 | 1.625 |
|
|
37
|
-
| `sequence()` | 0.288 | 0.320 | 0.331 |
|
|
38
|
-
| `value`, random position | 0.090 | 0.082 | 0.155 |
|
|
39
|
-
| `values()` | 4.787 | 174.213 | 1252.987 |
|
|
47
|
+
### Average throughput during editing to checkpoint
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
2,048 calls, then measures a burst of approximately 100 ms or at most 8,192 calls.
|
|
49
|
+
Arithmetic means of 3 runs, in ops/s, from each phase's start to checkpoint.
|
|
50
|
+
Strips contain 1–100 Frames (visible entries). Headers show Strip counts and mean Frame counts.
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
**↑ Scale up (0 → 25,000 Strips)**
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
| Method / case | 1,000 Strips<br>50,871 Frames | 5,000 Strips<br>252,214 Frames | 10,000 Strips<br>504,871 Frames | 15,000 Strips<br>758,280 Frames | 20,000 Strips<br>1,010,749 Frames | 25,000 Strips<br>1,264,003 Frames |
|
|
55
|
+
| -------------- | ----------------------------: | -----------------------------: | ------------------------------: | ------------------------------: | --------------------------------: | --------------------------------: |
|
|
56
|
+
| apply.random | 146,512 | 50,162 | 35,425 | 25,372 | 17,992 | 13,401 |
|
|
57
|
+
| insert.head | 1,103,240 | 627,952 | 723,841 | 741,193 | 692,500 | 629,581 |
|
|
58
|
+
| insert.random | 423,106 | 138,442 | 91,906 | 64,213 | 44,063 | 33,092 |
|
|
59
|
+
| insert.tail | 1,247,342 | 779,594 | 895,027 | 892,070 | 830,660 | 765,706 |
|
|
60
|
+
| remove.head | — | — | — | — | — | — |
|
|
61
|
+
| remove.random | 258,681 | 95,210 | 65,861 | 46,668 | 31,655 | 23,556 |
|
|
62
|
+
| remove.tail | — | — | — | — | — | — |
|
|
63
|
+
| replace.head | 380,869 | 309,212 | 303,788 | 291,544 | 270,032 | 245,792 |
|
|
64
|
+
| replace.random | 253,774 | 94,640 | 63,352 | 45,272 | 31,633 | 23,736 |
|
|
65
|
+
| replace.tail | 392,296 | 315,718 | 313,645 | 310,052 | 268,791 | 247,351 |
|
|
66
|
+
| value.head | 2,283,059 | 2,095,440 | 1,999,974 | 2,016,602 | 1,907,994 | 1,721,581 |
|
|
67
|
+
| value.random | 569,971 | 133,693 | 78,867 | 51,725 | 34,540 | 25,316 |
|
|
68
|
+
| value.tail | 4,749,438 | 3,636,984 | 3,461,656 | 3,227,876 | 2,914,996 | 2,408,625 |
|
|
49
69
|
|
|
50
|
-
|
|
51
|
-
| --------------------------- | -------: |
|
|
52
|
-
| Additional JS heap after GC | 0.501 MB |
|
|
53
|
-
| Shared WASM memory capacity | 1.049 MB |
|
|
70
|
+
**↓ Scale down (25,000 → 0 Strips)**
|
|
54
71
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
| Method / case | 20,000 Strips<br>1,012,742 Frames | 15,000 Strips<br>755,860 Frames | 10,000 Strips<br>504,103 Frames | 5,000 Strips<br>251,203 Frames | 1,000 Strips<br>51,057 Frames | 0 Strips<br>0 Frames |
|
|
73
|
+
| -------------- | --------------------------------: | ------------------------------: | ------------------------------: | -----------------------------: | ----------------------------: | -------------------: |
|
|
74
|
+
| apply.random | 7,268 | 7,592 | 8,263 | 8,783 | 9,083 | 9,219 |
|
|
75
|
+
| insert.head | — | — | — | — | — | — |
|
|
76
|
+
| insert.random | 17,296 | 18,458 | 20,290 | 21,279 | 22,052 | 22,459 |
|
|
77
|
+
| insert.tail | — | — | — | — | — | — |
|
|
78
|
+
| remove.head | 157,981 | 165,917 | 180,514 | 178,894 | 181,826 | 183,709 |
|
|
79
|
+
| remove.random | 13,036 | 13,992 | 15,527 | 16,535 | 17,163 | 17,459 |
|
|
80
|
+
| remove.tail | 192,199 | 192,220 | 203,528 | 209,615 | 209,694 | 210,394 |
|
|
81
|
+
| replace.head | 176,953 | 178,240 | 186,921 | 185,389 | 181,259 | 161,071 |
|
|
82
|
+
| replace.random | 13,038 | 14,084 | 15,647 | 16,708 | 17,397 | 17,697 |
|
|
83
|
+
| replace.tail | 190,938 | 184,284 | 193,773 | 187,715 | 182,304 | 182,496 |
|
|
84
|
+
| value.head | 794,128 | 838,606 | 885,880 | 922,502 | 940,112 | 948,944 |
|
|
85
|
+
| value.random | 13,677 | 14,768 | 16,600 | 18,037 | 18,894 | 19,271 |
|
|
86
|
+
| value.tail | 1,070,897 | 1,038,948 | 1,093,604 | 1,140,368 | 1,178,006 | 1,192,164 |
|
|
58
87
|
|
|
59
|
-
###
|
|
88
|
+
### API throughput at checkpoint
|
|
60
89
|
|
|
61
|
-
|
|
62
|
-
| -------- | -------: | -------: | --------------: |
|
|
63
|
-
| ESM | 120.9 kB | 101.6 kB | 36.8 kB |
|
|
64
|
-
| CommonJS | 121.0 kB | 102.4 kB | 37.0 kB |
|
|
65
|
-
|
|
66
|
-
Both bundles include the WASM module. Minified sizes use Terser with three
|
|
67
|
-
compression passes; source maps and type declarations are excluded. kB and MB
|
|
68
|
-
use decimal units.
|
|
69
|
-
|
|
70
|
-
### Compact data model
|
|
71
|
-
|
|
72
|
-
An insertion contains six uint32 structural words: 24 bytes before transport
|
|
73
|
-
framing or content. Numeric metadata accepts `Uint32Array` and JSON numeric
|
|
74
|
-
arrays. Generic content, called Footage, stays in JavaScript; only numeric
|
|
75
|
-
metadata enters WASM.
|
|
76
|
-
|
|
77
|
-
`sequence()` returns `[words, footage]`: one numeric array and one content
|
|
78
|
-
array, with no nested insertion or acknowledgement arrays. Small snapshots use
|
|
79
|
-
six-word records after length-prefixed acknowledgements. From 64 retained
|
|
80
|
-
insertions onward, predictable fields are omitted as WASM writes the snapshot;
|
|
81
|
-
`[0, frontiersEnd, insertionCount]` identifies this compact numeric layout.
|
|
82
|
-
Coordinates, acknowledgements and content order are preserved. The runtime
|
|
83
|
-
reads both layouts; compact snapshots require an updated reader.
|
|
84
|
-
Positive insertion lengths identify successive spans of `footage`.
|
|
85
|
-
An empty Sequence is `[[], []]`. Construction and `merge()` accept numeric
|
|
86
|
-
arrays or `Uint32Array` metadata; exported numeric arrays can be sent as JSON.
|
|
90
|
+
Checkpoint calls, in ops/s, averaged across runs.
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
**↑ Scale up (0 → 25,000 Strips)**
|
|
93
|
+
|
|
94
|
+
| Method | 1,000 Strips<br>50,871 Frames | 5,000 Strips<br>252,214 Frames | 10,000 Strips<br>504,871 Frames | 15,000 Strips<br>758,280 Frames | 20,000 Strips<br>1,010,749 Frames | 25,000 Strips<br>1,264,003 Frames |
|
|
95
|
+
| --------------------------------- | ----------------------------: | -----------------------------: | ------------------------------: | ------------------------------: | --------------------------------: | --------------------------------: |
|
|
96
|
+
| new Projection(actorID, sequence) | 2,513 | 795 | 408 | 215 | 154 | 107 |
|
|
97
|
+
| sequence() | 375 | 66 | 33 | 18 | 11 | 6 |
|
|
98
|
+
| values() | 480 | 163 | 108 | 70 | 49 | 32 |
|
|
99
|
+
|
|
100
|
+
**↓ Scale down (25,000 → 0 Strips)**
|
|
101
|
+
|
|
102
|
+
| Method | 20,000 Strips<br>1,012,742 Frames | 15,000 Strips<br>755,860 Frames | 10,000 Strips<br>504,103 Frames | 5,000 Strips<br>251,203 Frames | 1,000 Strips<br>51,057 Frames | 0 Strips<br>0 Frames |
|
|
103
|
+
| --------------------------------- | --------------------------------: | ------------------------------: | ------------------------------: | -----------------------------: | ----------------------------: | -------------------: |
|
|
104
|
+
| new Projection(actorID, sequence) | 116 | 212 | 273 | 586 | 1,881 | 16,098 |
|
|
105
|
+
| sequence() | 7 | 6 | 5 | 4 | 4 | 38,383 |
|
|
106
|
+
| values() | 44 | 51 | 58 | 62 | 199 | 68,912 |
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
without reserving another zero in its logical clock. Gossip contains only the
|
|
95
|
-
new suffix, still using six structural words. A removal or fragmentation ends
|
|
96
|
-
the local extension chain. Retained snapshots remain unchanged by later edits.
|
|
108
|
+
### Sequence disk size at checkpoint
|
|
97
109
|
|
|
98
|
-
|
|
99
|
-
their removal Gossip, and a snapshot captured before removal:
|
|
110
|
+
Complete exported Sequences, including Footage and acknowledgements. Sizes are in KiB.
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
| ---------------------------------------------- | ---------------------------------------: | ------: | ----------: | -------------: |
|
|
103
|
-
| 1,000 one-frame insertions | 44.8 B | 45.8 kB | 5.1 kB | 88.9% |
|
|
104
|
-
| 1,000 one-frame removals | 65.8 B | 66.8 kB | 7.5 kB | 88.8% |
|
|
105
|
-
| Snapshot containing 1,000 one-frame insertions | 4.0 B | 4.0 kB | 0.1 kB | 98.3% |
|
|
112
|
+
**↑ Scale up (0 → 25,000 Strips)**
|
|
106
113
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
| Encoding | 1,000 Strips<br>50,871 Frames | 5,000 Strips<br>252,214 Frames | 10,000 Strips<br>504,871 Frames | 15,000 Strips<br>758,280 Frames | 20,000 Strips<br>1,010,749 Frames | 25,000 Strips<br>1,264,003 Frames |
|
|
115
|
+
| ------------------ | ----------------------------: | -----------------------------: | ------------------------------: | ------------------------------: | --------------------------------: | --------------------------------: |
|
|
116
|
+
| JSON | 277.17 | 1,621.65 | 3,261.72 | 4,901.02 | 7,041.25 | 9,162.75 |
|
|
117
|
+
| JSON + gzip | 14.59 | 73.84 | 150.38 | 226.94 | 311.22 | 391.69 |
|
|
118
|
+
| MessagePack | 164.81 | 818.88 | 1,639.69 | 3,529.41 | 5,081.04 | 6,483.22 |
|
|
119
|
+
| MessagePack + gzip | 12.38 | 61.62 | 124.91 | 198.59 | 263.54 | 332.77 |
|
|
120
|
+
|
|
121
|
+
**↓ Scale down (25,000 → 0 Strips)**
|
|
122
|
+
|
|
123
|
+
| Encoding | 20,000 Strips<br>1,012,742 Frames | 15,000 Strips<br>755,860 Frames | 10,000 Strips<br>504,103 Frames | 5,000 Strips<br>251,203 Frames | 1,000 Strips<br>51,057 Frames | 0 Strips<br>0 Frames |
|
|
124
|
+
| ------------------ | --------------------------------: | ------------------------------: | ------------------------------: | -----------------------------: | ----------------------------: | -------------------: |
|
|
125
|
+
| JSON | 7,446.44 | 5,617.36 | 3,769.76 | 1,885.51 | 383.23 | 0.02 |
|
|
126
|
+
| JSON + gzip | 323.23 | 244.45 | 163.91 | 80.61 | 16.33 | 0.03 |
|
|
127
|
+
| MessagePack | 5,237.17 | 3,930.33 | 2,628.38 | 1,312.43 | 266.67 | 0.01 |
|
|
128
|
+
| MessagePack + gzip | 274.96 | 207.78 | 137.38 | 67.41 | 13.67 | 0.03 |
|
|
129
|
+
|
|
130
|
+
### Memory usage at checkpoint
|
|
131
|
+
|
|
132
|
+
One instance: WASM memory plus retained JS array slots, in KiB (1,024 bytes).
|
|
133
|
+
|
|
134
|
+
**↑ Scale up (0 → 25,000 Strips)**
|
|
135
|
+
|
|
136
|
+
| Measurement | 1,000 Strips<br>50,871 Frames | 5,000 Strips<br>252,214 Frames | 10,000 Strips<br>504,871 Frames | 15,000 Strips<br>758,280 Frames | 20,000 Strips<br>1,010,749 Frames | 25,000 Strips<br>1,264,003 Frames |
|
|
137
|
+
| ---------------------- | ----------------------------: | -----------------------------: | ------------------------------: | ------------------------------: | --------------------------------: | --------------------------------: |
|
|
138
|
+
| Estimated memory (KiB) | 5,343.67 | 25,100.58 | 50,470.89 | 82,701.27 | 98,036.35 | 153,462.93 |
|
|
139
|
+
|
|
140
|
+
**↓ Scale down (25,000 → 0 Strips)**
|
|
141
|
+
|
|
142
|
+
| Measurement | 20,000 Strips<br>1,012,742 Frames | 15,000 Strips<br>755,860 Frames | 10,000 Strips<br>504,103 Frames | 5,000 Strips<br>251,203 Frames | 1,000 Strips<br>51,057 Frames | 0 Strips<br>0 Frames |
|
|
143
|
+
| ---------------------- | --------------------------------: | ------------------------------: | ------------------------------: | -----------------------------: | ----------------------------: | -------------------: |
|
|
144
|
+
| Estimated memory (KiB) | 163,746.93 | 174,034.00 | 202,660.72 | 293,941.85 | 302,158.29 | 304,203.30 |
|
|
145
|
+
|
|
146
|
+
Measured on Node 24.16.0, V8 13.6.233.17-node.49,
|
|
147
|
+
win32 x64, Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz.
|
|
148
|
+
Peer synchronization, encoding and JS process garbage collection are outside API timings.
|
|
149
|
+
[Full lifecycle results](benchmark/.results/lifecycle.md) · [Throughput results](benchmark/.results/throughput.md).
|
|
111
150
|
|
|
112
151
|
## Tests
|
|
113
152
|
|
|
114
153
|
### Behaviour guaranteed by excessive tests
|
|
115
154
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
The latest verification passed 170 Vitest tests and 88 compatibility checks.
|
|
121
|
-
The configured 100% coverage threshold remains unmet (95.66% statements,
|
|
122
|
-
94.94% branches, 100% functions, 97.23% lines).
|
|
123
|
-
Convergence tests include 1,024 generated editing histories for each compaction
|
|
124
|
-
mode. See the [test suites](test/README.md) and
|
|
155
|
+
The latest verification passed 178 Vitest tests and 88 compatibility checks (1 failed).
|
|
156
|
+
The configured 100% coverage threshold is met (100% statements, 100% branches, 100% functions, 100% lines).
|
|
157
|
+
See the [test suites](test/README.md) and
|
|
125
158
|
[automated test report](https://sovereignbase.dev/sequencer/tests/).
|
|
126
159
|
|
|
127
160
|
### Works everywhere where JavaScript and Wasm works
|
|
128
161
|
|
|
129
162
|
Runtimes covered by the latest compatibility verification:
|
|
130
163
|
|
|
131
|
-
- Node.js, ESM
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
Firefox
|
|
139
|
-
|
|
164
|
+
- Node.js, ESM: passed
|
|
165
|
+
- Node.js, CommonJS: passed
|
|
166
|
+
- Deno: passed
|
|
167
|
+
- Bun: passed
|
|
168
|
+
- Edge Runtime: passed
|
|
169
|
+
- Cloudflare Workers through workerd: passed
|
|
170
|
+
- Chromium browser windows: passed
|
|
171
|
+
- Firefox browser windows: failed (matching Playwright browser binary unavailable)
|
|
172
|
+
- WebKit browser windows: passed
|
|
140
173
|
|
|
141
174
|
### Tested realtime and offline text editing behaviour examples
|
|
142
175
|
|
|
@@ -148,13 +181,13 @@ binary was unavailable in the latest run.
|
|
|
148
181
|
|
|
149
182
|
## Credits
|
|
150
183
|
|
|
151
|
-
|
|
184
|
+
This could not have been made without years of work from many great engineers of many great projects, to mention a few:
|
|
152
185
|
|
|
153
|
-
- AUTOMERGE
|
|
186
|
+
- [AUTOMERGE](https://github.com/automerge)
|
|
154
187
|
|
|
155
|
-
- YJS
|
|
188
|
+
- [YJS](https://github.com/yjs)
|
|
156
189
|
|
|
157
|
-
- DIAMOND TYPES
|
|
190
|
+
- [DIAMOND TYPES](https://github.com/josephg/diamond-types)
|
|
158
191
|
|
|
159
192
|
## License
|
|
160
193
|
|