@sovereignbase/convergent-replicated-struct 0.0.0 → 1.1.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/LICENSE +201 -201
- package/README.md +402 -271
- package/dist/index.cjs +432 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +264 -96
- package/dist/index.d.ts +264 -96
- package/dist/index.js +430 -273
- package/dist/index.js.map +1 -1
- package/package.json +87 -89
package/README.md
CHANGED
|
@@ -1,271 +1,402 @@
|
|
|
1
|
-
[](https://github.com/sovereignbase/convergent-replicated-struct/actions/workflows/ci.yaml)
|
|
3
|
-
[](https://codecov.io/gh/sovereignbase/convergent-replicated-struct)
|
|
4
|
-
[](LICENSE)
|
|
5
|
-
|
|
6
|
-
# convergent-replicated-struct
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## Compatibility
|
|
11
|
-
|
|
12
|
-
- Runtimes: Node >= 20
|
|
13
|
-
- Module format: ESM + CommonJS.
|
|
14
|
-
- Required globals / APIs: `EventTarget`, `CustomEvent`, `structuredClone`.
|
|
15
|
-
- TypeScript: bundled types.
|
|
16
|
-
|
|
17
|
-
## Goals
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# or
|
|
31
|
-
|
|
32
|
-
# or
|
|
33
|
-
|
|
34
|
-
# or
|
|
35
|
-
|
|
36
|
-
# or
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
source
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
snapshot
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
replica.addEventListener('
|
|
137
|
-
console.log('
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
replica.addEventListener('
|
|
141
|
-
console.log('
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1
|
+
[](https://www.npmjs.com/package/@sovereignbase/convergent-replicated-struct)
|
|
2
|
+
[](https://github.com/sovereignbase/convergent-replicated-struct/actions/workflows/ci.yaml)
|
|
3
|
+
[](https://codecov.io/gh/sovereignbase/convergent-replicated-struct)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
# convergent-replicated-struct
|
|
7
|
+
|
|
8
|
+
Convergent Replicated Struct (CR-Struct), a delta CRDT for an fixed-key object structs.
|
|
9
|
+
|
|
10
|
+
## Compatibility
|
|
11
|
+
|
|
12
|
+
- Runtimes: Node >= 20, modern browsers, Bun, Deno, Cloudflare Workers, Edge Runtime.
|
|
13
|
+
- Module format: ESM + CommonJS.
|
|
14
|
+
- Required globals / APIs: `EventTarget`, `CustomEvent`, `structuredClone`.
|
|
15
|
+
- TypeScript: bundled types.
|
|
16
|
+
|
|
17
|
+
## Goals
|
|
18
|
+
|
|
19
|
+
- Deterministic convergence of the live struct projection under asynchronous gossip delivery.
|
|
20
|
+
- Consistent behavior across Node, browsers, worker, and edge runtimes.
|
|
21
|
+
- Garbage collection possibility without breaking live-view convergence.
|
|
22
|
+
- Event-driven API
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install @sovereignbase/convergent-replicated-struct
|
|
28
|
+
# or
|
|
29
|
+
pnpm add @sovereignbase/convergent-replicated-struct
|
|
30
|
+
# or
|
|
31
|
+
yarn add @sovereignbase/convergent-replicated-struct
|
|
32
|
+
# or
|
|
33
|
+
bun add @sovereignbase/convergent-replicated-struct
|
|
34
|
+
# or
|
|
35
|
+
deno add jsr:@sovereignbase/convergent-replicated-struct
|
|
36
|
+
# or
|
|
37
|
+
vlt install jsr:@sovereignbase/convergent-replicated-struct
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Copy-paste example
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import {
|
|
46
|
+
CRStruct,
|
|
47
|
+
type CRStructSnapshot,
|
|
48
|
+
} from '@sovereignbase/convergent-replicated-struct'
|
|
49
|
+
|
|
50
|
+
type MetaStruct = {
|
|
51
|
+
done: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type TodoStruct = {
|
|
55
|
+
title: string
|
|
56
|
+
count: number
|
|
57
|
+
meta: CRStructSnapshot<MetaStruct>
|
|
58
|
+
tags: string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const aliceMeta = new CRStruct<MetaStruct>({done: false})
|
|
62
|
+
|
|
63
|
+
const alice = new CRStruct<TodoStruct>({
|
|
64
|
+
title: '',
|
|
65
|
+
count: 0,
|
|
66
|
+
meta: aliceMeta.toJSON()
|
|
67
|
+
tags: [],
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const bobMeta = new CRStruct<MetaStruct>({done: false})
|
|
71
|
+
|
|
72
|
+
const bob = new CRStruct<TodoStruct>({
|
|
73
|
+
title: '',
|
|
74
|
+
count: 0,
|
|
75
|
+
meta: bobMeta.toJSON()
|
|
76
|
+
tags: [],
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
alice.addEventListener('delta', (event) => {
|
|
80
|
+
bob.merge(event.detail)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
aliceMeta.done = true
|
|
84
|
+
|
|
85
|
+
alice.title = 'hello world'
|
|
86
|
+
alice.meta = aliceMeta.toJSON()
|
|
87
|
+
|
|
88
|
+
console.log(bob.title) // 'hello world'
|
|
89
|
+
console.log(bobMeta.done) // true
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Hydrating from a snapshot
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import {
|
|
96
|
+
CRStruct,
|
|
97
|
+
type CRStructSnapshot,
|
|
98
|
+
} from '@sovereignbase/convergent-replicated-struct'
|
|
99
|
+
|
|
100
|
+
type DraftStruct = {
|
|
101
|
+
title: string
|
|
102
|
+
count: number
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const source = new CRStruct<DraftStruct>({
|
|
106
|
+
title: '',
|
|
107
|
+
count: 0,
|
|
108
|
+
})
|
|
109
|
+
let snapshot!: CRStructSnapshot<DraftStruct>
|
|
110
|
+
|
|
111
|
+
source.addEventListener('snapshot', (event) => {
|
|
112
|
+
localStorage.setItem('snapshot', JSON.stringify(event.detail))
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
source.title = 'draft'
|
|
116
|
+
source.snapshot()
|
|
117
|
+
|
|
118
|
+
const restored = new CRStruct<DraftStruct>(
|
|
119
|
+
{ title: '', count: 0 },
|
|
120
|
+
JSON.parse(localStorage.getItem('snapshot'))
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
console.log(restored.entries()) // [['title', 'draft'], ['count', 0]]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Event channels
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { CRStruct } from '@sovereignbase/convergent-replicated-struct'
|
|
130
|
+
|
|
131
|
+
const replica = new CRStruct({
|
|
132
|
+
name: '',
|
|
133
|
+
count: 0,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
replica.addEventListener('delta', (event) => {
|
|
137
|
+
console.log('delta', event.detail)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
replica.addEventListener('change', (event) => {
|
|
141
|
+
console.log('change', event.detail)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
replica.addEventListener('ack', (event) => {
|
|
145
|
+
console.log('ack', event.detail)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
replica.addEventListener('snapshot', (event) => {
|
|
149
|
+
console.log('snapshot', event.detail)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
replica.name = 'alice'
|
|
153
|
+
delete replica.name
|
|
154
|
+
replica.snapshot()
|
|
155
|
+
replica.acknowledge()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Iteration and JSON serialization
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { CRStruct } from '@sovereignbase/convergent-replicated-struct'
|
|
162
|
+
|
|
163
|
+
const struct = new CRStruct({
|
|
164
|
+
givenName: '',
|
|
165
|
+
familyName: '',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
struct.givenName = 'Jori'
|
|
169
|
+
struct.familyName = 'Lehtinen'
|
|
170
|
+
|
|
171
|
+
for (const key in struct) console.log(key)
|
|
172
|
+
for (const [key, val] of struct) console.log(key, val)
|
|
173
|
+
console.log(struct.keys())
|
|
174
|
+
console.log(struct.values())
|
|
175
|
+
console.log(struct.entries())
|
|
176
|
+
console.log(struct.clone())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Direct property reads, `for...of`, `values()`, `entries()`, and `clone()`
|
|
180
|
+
return detached copies of visible values. Mutating those returned values does
|
|
181
|
+
not mutate the underlying replica state.
|
|
182
|
+
|
|
183
|
+
### Acknowledgements and garbage collection
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import {
|
|
187
|
+
CRStruct,
|
|
188
|
+
type CRStructAck,
|
|
189
|
+
} from '@sovereignbase/convergent-replicated-struct'
|
|
190
|
+
|
|
191
|
+
type CounterStruct = {
|
|
192
|
+
title: string
|
|
193
|
+
count: number
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const alice = new CRStruct<CounterStruct>({
|
|
197
|
+
title: '',
|
|
198
|
+
count: 0,
|
|
199
|
+
})
|
|
200
|
+
const bob = new CRStruct<CounterStruct>({
|
|
201
|
+
title: '',
|
|
202
|
+
count: 0,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const frontiers = new Map<string, CRStructAck<CounterStruct>>()
|
|
206
|
+
|
|
207
|
+
alice.addEventListener('delta', (event) => {
|
|
208
|
+
bob.merge(event.detail)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
bob.addEventListener('delta', (event) => {
|
|
212
|
+
alice.merge(event.detail)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
alice.addEventListener('ack', (event) => {
|
|
216
|
+
frontiers.set('alice', event.detail)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
bob.addEventListener('ack', (event) => {
|
|
220
|
+
frontiers.set('bob', event.detail)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
alice.title = 'x'
|
|
224
|
+
alice.title = 'y'
|
|
225
|
+
delete alice.title
|
|
226
|
+
|
|
227
|
+
alice.acknowledge()
|
|
228
|
+
bob.acknowledge()
|
|
229
|
+
|
|
230
|
+
alice.garbageCollect([...frontiers.values()])
|
|
231
|
+
bob.garbageCollect([...frontiers.values()])
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Advanced exports
|
|
235
|
+
|
|
236
|
+
If you need to build your own fixed-key CRDT binding instead of using the
|
|
237
|
+
high-level `CRStruct` class, the package also exports the core CRUD and MAGS
|
|
238
|
+
functions together with the replica and payload types.
|
|
239
|
+
|
|
240
|
+
Those low-level exports let you build custom struct abstractions, protocol
|
|
241
|
+
wrappers, or framework-specific bindings while preserving the same convergence
|
|
242
|
+
rules as the default `CRStruct` binding.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
import {
|
|
246
|
+
__create,
|
|
247
|
+
__update,
|
|
248
|
+
__merge,
|
|
249
|
+
__snapshot,
|
|
250
|
+
type CRStructDelta,
|
|
251
|
+
type CRStructSnapshot,
|
|
252
|
+
} from '@sovereignbase/convergent-replicated-struct'
|
|
253
|
+
|
|
254
|
+
type DraftStruct = {
|
|
255
|
+
title: string
|
|
256
|
+
count: number
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const defaults: DraftStruct = {
|
|
260
|
+
title: '',
|
|
261
|
+
count: 0,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const replica = __create(defaults)
|
|
265
|
+
const local = __update('title', 'draft', replica)
|
|
266
|
+
|
|
267
|
+
if (local) {
|
|
268
|
+
const outgoing: CRStructDelta<DraftStruct> = local.delta
|
|
269
|
+
const remoteChange = __merge(outgoing, replica)
|
|
270
|
+
|
|
271
|
+
console.log(remoteChange)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const snapshot: CRStructSnapshot<DraftStruct> = __snapshot(replica)
|
|
275
|
+
console.log(snapshot)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The intended split is:
|
|
279
|
+
|
|
280
|
+
- `__create`, `__read`, `__update`, `__delete` for local replica mutations.
|
|
281
|
+
- `__merge`, `__acknowledge`, `__garbageCollect`, `__snapshot` for gossip,
|
|
282
|
+
compaction, and serialization.
|
|
283
|
+
- `CRStruct` when you want the default event-driven class API.
|
|
284
|
+
|
|
285
|
+
## Runtime behavior
|
|
286
|
+
|
|
287
|
+
### Validation and errors
|
|
288
|
+
|
|
289
|
+
Low-level exports can throw `CRStructError` with stable error codes:
|
|
290
|
+
|
|
291
|
+
- `DEFAULTS_NOT_CLONEABLE`
|
|
292
|
+
- `VALUE_NOT_CLONEABLE`
|
|
293
|
+
- `VALUE_TYPE_MISMATCH`
|
|
294
|
+
|
|
295
|
+
Ingress stays tolerant:
|
|
296
|
+
|
|
297
|
+
- malformed top-level merge payloads are ignored
|
|
298
|
+
- malformed snapshot values are dropped during hydration
|
|
299
|
+
- unknown keys are ignored
|
|
300
|
+
- invalid UUIDs and malformed field entries are ignored
|
|
301
|
+
- mismatched runtime kinds do not break live-state convergence
|
|
302
|
+
|
|
303
|
+
### Safety and copying semantics
|
|
304
|
+
|
|
305
|
+
- Snapshots are serializable full-state payloads keyed by field name.
|
|
306
|
+
- Deltas are serializable gossip payloads keyed by field name.
|
|
307
|
+
- `change` is a minimal field-keyed visible patch.
|
|
308
|
+
- `toJSON()` returns a detached serializable snapshot.
|
|
309
|
+
- Direct property reads, `for...of`, `values()`, `entries()`, and `clone()`
|
|
310
|
+
expose detached copies of visible values rather than mutable references into
|
|
311
|
+
replica state.
|
|
312
|
+
- Property assignment, `delete`, `clear()`, `merge()`, `snapshot()`,
|
|
313
|
+
`acknowledge()`, and `garbageCollect()` all operate on the live struct
|
|
314
|
+
projection.
|
|
315
|
+
|
|
316
|
+
### Convergence and compaction
|
|
317
|
+
|
|
318
|
+
- The convergence target is the live struct projection, not identical internal
|
|
319
|
+
tombstone sets.
|
|
320
|
+
- Tombstones remain until acknowledgement frontiers make them safe to collect.
|
|
321
|
+
- Garbage collection compacts overwritten identifiers below the smallest valid
|
|
322
|
+
acknowledgement frontier for a field while preserving the active predecessor
|
|
323
|
+
link.
|
|
324
|
+
- Internal overwrite history may differ between replicas after
|
|
325
|
+
acknowledgement-based garbage collection while the resolved live struct still
|
|
326
|
+
converges.
|
|
327
|
+
|
|
328
|
+
## Tests
|
|
329
|
+
|
|
330
|
+
```sh
|
|
331
|
+
npm run test
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
What the current test suite covers:
|
|
335
|
+
|
|
336
|
+
- Coverage on built `dist/**/*.js`: `100%` statements, `100%` branches,
|
|
337
|
+
`100%` functions, and `100%` lines via `c8`.
|
|
338
|
+
- Public `CRStruct` surface: proxy property access, deletes, `clear()`,
|
|
339
|
+
iteration, events, and JSON / inspect behavior.
|
|
340
|
+
- Core edge paths and hostile ingress handling for `__create`, `__read`,
|
|
341
|
+
`__update`, `__delete`, `__merge`, `__snapshot`, `__acknowledge`, and
|
|
342
|
+
`__garbageCollect`.
|
|
343
|
+
- Snapshot hydration independent of field order, acknowledgement and garbage
|
|
344
|
+
collection recovery, and deterministic multi-replica gossip scenarios.
|
|
345
|
+
- End-to-end runtime matrix for:
|
|
346
|
+
- Node ESM
|
|
347
|
+
- Node CJS
|
|
348
|
+
- Bun ESM
|
|
349
|
+
- Bun CJS
|
|
350
|
+
- Deno ESM
|
|
351
|
+
- Cloudflare Workers ESM
|
|
352
|
+
- Edge Runtime ESM
|
|
353
|
+
- Browsers via Playwright: Chromium, Firefox, WebKit, mobile Chrome, mobile Safari
|
|
354
|
+
- Current status: `npm run test` passes on Node `v22.14.0` (`win32 x64`).
|
|
355
|
+
|
|
356
|
+
## Benchmarks
|
|
357
|
+
|
|
358
|
+
```sh
|
|
359
|
+
npm run bench
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The benchmark runner currently uses:
|
|
363
|
+
|
|
364
|
+
- `HISTORY_DEPTH = 5_000`
|
|
365
|
+
- `RUN_TIMES = 250`
|
|
366
|
+
- output columns: `group`, `scenario`, `n`, `ops`, `ms`, `ms/op`, `ops/sec`
|
|
367
|
+
|
|
368
|
+
Last measured on Node `v22.14.0` (`win32 x64`):
|
|
369
|
+
|
|
370
|
+
| group | scenario | n | ops | ms | ms/op | ops/sec |
|
|
371
|
+
| ------- | -------------------------------- | ----: | --: | -----: | ----: | ---------: |
|
|
372
|
+
| `crud` | `create / hydrate snapshot` | 5,000 | 250 | 714.80 | 2.86 | 349.75 |
|
|
373
|
+
| `crud` | `read / primitive field` | 5,000 | 250 | 0.55 | 0.00 | 450,531.63 |
|
|
374
|
+
| `crud` | `read / object field` | 5,000 | 250 | 0.83 | 0.00 | 301,568.15 |
|
|
375
|
+
| `crud` | `update / overwrite string` | 5,000 | 250 | 5.77 | 0.02 | 43,291.54 |
|
|
376
|
+
| `crud` | `update / overwrite object` | 5,000 | 250 | 4.79 | 0.02 | 52,198.61 |
|
|
377
|
+
| `crud` | `delete / reset single field` | 5,000 | 250 | 3.67 | 0.01 | 68,162.61 |
|
|
378
|
+
| `crud` | `delete / reset all fields` | 5,000 | 250 | 18.86 | 0.08 | 13,253.95 |
|
|
379
|
+
| `mags` | `snapshot` | 5,000 | 250 | 7.80 | 0.03 | 32,062.38 |
|
|
380
|
+
| `mags` | `acknowledge` | 5,000 | 250 | 39.72 | 0.16 | 6,294.04 |
|
|
381
|
+
| `mags` | `garbage collect` | 5,000 | 250 | 260.93 | 1.04 | 958.12 |
|
|
382
|
+
| `mags` | `merge ordered deltas` | 5,000 | 250 | 204.53 | 0.82 | 1,222.32 |
|
|
383
|
+
| `mags` | `merge direct successor` | 5,000 | 250 | 1.46 | 0.01 | 171,385.48 |
|
|
384
|
+
| `mags` | `merge shuffled gossip` | 5,000 | 250 | 263.91 | 1.06 | 947.29 |
|
|
385
|
+
| `mags` | `merge stale conflict` | 5,000 | 250 | 2.11 | 0.01 | 118,315.19 |
|
|
386
|
+
| `class` | `constructor / hydrate snapshot` | 5,000 | 250 | 781.32 | 3.13 | 319.97 |
|
|
387
|
+
| `class` | `property read / primitive` | 5,000 | 250 | 0.45 | 0.00 | 559,659.73 |
|
|
388
|
+
| `class` | `property read / object` | 5,000 | 250 | 0.95 | 0.00 | 262,687.82 |
|
|
389
|
+
| `class` | `property write / string` | 5,000 | 250 | 5.04 | 0.02 | 49,613.02 |
|
|
390
|
+
| `class` | `property write / object` | 5,000 | 250 | 8.57 | 0.03 | 29,157.24 |
|
|
391
|
+
| `class` | `delete property` | 5,000 | 250 | 4.80 | 0.02 | 52,128.95 |
|
|
392
|
+
| `class` | `clear()` | 5,000 | 250 | 15.35 | 0.06 | 16,283.14 |
|
|
393
|
+
| `class` | `snapshot` | 5,000 | 250 | 9.49 | 0.04 | 26,356.29 |
|
|
394
|
+
| `class` | `acknowledge` | 5,000 | 250 | 45.49 | 0.18 | 5,495.59 |
|
|
395
|
+
| `class` | `garbage collect` | 5,000 | 250 | 162.70 | 0.65 | 1,536.53 |
|
|
396
|
+
| `class` | `merge ordered deltas` | 5,000 | 250 | 193.20 | 0.77 | 1,293.98 |
|
|
397
|
+
| `class` | `merge direct successor` | 5,000 | 250 | 2.90 | 0.01 | 86,331.93 |
|
|
398
|
+
| `class` | `merge shuffled gossip` | 5,000 | 250 | 264.43 | 1.06 | 945.44 |
|
|
399
|
+
|
|
400
|
+
## License
|
|
401
|
+
|
|
402
|
+
Apache-2.0
|