@sovereignbase/convergent-replicated-struct 1.0.1 → 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/README.md +247 -116
- 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 +10 -12
package/README.md
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
[](https://www.npmjs.com/package/@sovereignbase/convergent-replicated-struct)
|
|
2
2
|
[](https://github.com/sovereignbase/convergent-replicated-struct/actions/workflows/ci.yaml)
|
|
3
3
|
[](https://codecov.io/gh/sovereignbase/convergent-replicated-struct)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
6
|
# convergent-replicated-struct
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Convergent Replicated Struct (CR-Struct), a delta CRDT for an fixed-key object structs.
|
|
9
9
|
|
|
10
10
|
## Compatibility
|
|
11
11
|
|
|
12
|
-
- Runtimes: Node >= 20
|
|
12
|
+
- Runtimes: Node >= 20, modern browsers, Bun, Deno, Cloudflare Workers, Edge Runtime.
|
|
13
13
|
- Module format: ESM + CommonJS.
|
|
14
14
|
- Required globals / APIs: `EventTarget`, `CustomEvent`, `structuredClone`.
|
|
15
15
|
- TypeScript: bundled types.
|
|
16
16
|
|
|
17
17
|
## Goals
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- Explicit `acknowledge()` and `garbageCollect()` APIs for overwrite-history compaction.
|
|
24
|
-
- Consistent behavior across Node, browsers, and worker/edge runtimes.
|
|
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
|
|
25
23
|
|
|
26
24
|
## Installation
|
|
27
25
|
|
|
@@ -44,42 +42,59 @@ vlt install jsr:@sovereignbase/convergent-replicated-struct
|
|
|
44
42
|
### Copy-paste example
|
|
45
43
|
|
|
46
44
|
```ts
|
|
47
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
CRStruct,
|
|
47
|
+
type CRStructSnapshot,
|
|
48
|
+
} from '@sovereignbase/convergent-replicated-struct'
|
|
49
|
+
|
|
50
|
+
type MetaStruct = {
|
|
51
|
+
done: boolean
|
|
52
|
+
}
|
|
48
53
|
|
|
49
54
|
type TodoStruct = {
|
|
50
55
|
title: string
|
|
51
56
|
count: number
|
|
52
|
-
meta:
|
|
57
|
+
meta: CRStructSnapshot<MetaStruct>
|
|
53
58
|
tags: string[]
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
const
|
|
61
|
+
const aliceMeta = new CRStruct<MetaStruct>({done: false})
|
|
62
|
+
|
|
63
|
+
const alice = new CRStruct<TodoStruct>({
|
|
57
64
|
title: '',
|
|
58
65
|
count: 0,
|
|
59
|
-
meta:
|
|
66
|
+
meta: aliceMeta.toJSON()
|
|
60
67
|
tags: [],
|
|
61
68
|
})
|
|
62
|
-
|
|
69
|
+
|
|
70
|
+
const bobMeta = new CRStruct<MetaStruct>({done: false})
|
|
71
|
+
|
|
72
|
+
const bob = new CRStruct<TodoStruct>({
|
|
63
73
|
title: '',
|
|
64
74
|
count: 0,
|
|
65
|
-
meta:
|
|
75
|
+
meta: bobMeta.toJSON()
|
|
66
76
|
tags: [],
|
|
67
77
|
})
|
|
68
78
|
|
|
69
|
-
alice.addEventListener('delta', (event) =>
|
|
70
|
-
|
|
71
|
-
|
|
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()
|
|
72
87
|
|
|
73
|
-
console.log(bob.
|
|
74
|
-
console.log(
|
|
88
|
+
console.log(bob.title) // 'hello world'
|
|
89
|
+
console.log(bobMeta.done) // true
|
|
75
90
|
```
|
|
76
91
|
|
|
77
92
|
### Hydrating from a snapshot
|
|
78
93
|
|
|
79
94
|
```ts
|
|
80
95
|
import {
|
|
81
|
-
|
|
82
|
-
type
|
|
96
|
+
CRStruct,
|
|
97
|
+
type CRStructSnapshot,
|
|
83
98
|
} from '@sovereignbase/convergent-replicated-struct'
|
|
84
99
|
|
|
85
100
|
type DraftStruct = {
|
|
@@ -87,29 +102,22 @@ type DraftStruct = {
|
|
|
87
102
|
count: number
|
|
88
103
|
}
|
|
89
104
|
|
|
90
|
-
const source = new
|
|
105
|
+
const source = new CRStruct<DraftStruct>({
|
|
91
106
|
title: '',
|
|
92
107
|
count: 0,
|
|
93
108
|
})
|
|
94
|
-
let snapshot!:
|
|
95
|
-
|
|
96
|
-
source.addEventListener(
|
|
97
|
-
'snapshot',
|
|
98
|
-
|
|
99
|
-
snapshot = event.detail
|
|
100
|
-
},
|
|
101
|
-
{ once: true }
|
|
102
|
-
)
|
|
109
|
+
let snapshot!: CRStructSnapshot<DraftStruct>
|
|
110
|
+
|
|
111
|
+
source.addEventListener('snapshot', (event) => {
|
|
112
|
+
localStorage.setItem('snapshot', JSON.stringify(event.detail))
|
|
113
|
+
})
|
|
103
114
|
|
|
104
|
-
source.
|
|
115
|
+
source.title = 'draft'
|
|
105
116
|
source.snapshot()
|
|
106
117
|
|
|
107
|
-
const restored =
|
|
108
|
-
{
|
|
109
|
-
|
|
110
|
-
count: 0,
|
|
111
|
-
},
|
|
112
|
-
snapshot
|
|
118
|
+
const restored = new CRStruct<DraftStruct>(
|
|
119
|
+
{ title: '', count: 0 },
|
|
120
|
+
JSON.parse(localStorage.getItem('snapshot'))
|
|
113
121
|
)
|
|
114
122
|
|
|
115
123
|
console.log(restored.entries()) // [['title', 'draft'], ['count', 0]]
|
|
@@ -118,9 +126,9 @@ console.log(restored.entries()) // [['title', 'draft'], ['count', 0]]
|
|
|
118
126
|
### Event channels
|
|
119
127
|
|
|
120
128
|
```ts
|
|
121
|
-
import {
|
|
129
|
+
import { CRStruct } from '@sovereignbase/convergent-replicated-struct'
|
|
122
130
|
|
|
123
|
-
const replica = new
|
|
131
|
+
const replica = new CRStruct({
|
|
124
132
|
name: '',
|
|
125
133
|
count: 0,
|
|
126
134
|
})
|
|
@@ -140,14 +148,44 @@ replica.addEventListener('ack', (event) => {
|
|
|
140
148
|
replica.addEventListener('snapshot', (event) => {
|
|
141
149
|
console.log('snapshot', event.detail)
|
|
142
150
|
})
|
|
151
|
+
|
|
152
|
+
replica.name = 'alice'
|
|
153
|
+
delete replica.name
|
|
154
|
+
replica.snapshot()
|
|
155
|
+
replica.acknowledge()
|
|
143
156
|
```
|
|
144
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
|
+
|
|
145
183
|
### Acknowledgements and garbage collection
|
|
146
184
|
|
|
147
185
|
```ts
|
|
148
186
|
import {
|
|
149
|
-
|
|
150
|
-
type
|
|
187
|
+
CRStruct,
|
|
188
|
+
type CRStructAck,
|
|
151
189
|
} from '@sovereignbase/convergent-replicated-struct'
|
|
152
190
|
|
|
153
191
|
type CounterStruct = {
|
|
@@ -155,116 +193,209 @@ type CounterStruct = {
|
|
|
155
193
|
count: number
|
|
156
194
|
}
|
|
157
195
|
|
|
158
|
-
const
|
|
196
|
+
const alice = new CRStruct<CounterStruct>({
|
|
159
197
|
title: '',
|
|
160
198
|
count: 0,
|
|
161
199
|
})
|
|
162
|
-
const
|
|
200
|
+
const bob = new CRStruct<CounterStruct>({
|
|
163
201
|
title: '',
|
|
164
202
|
count: 0,
|
|
165
203
|
})
|
|
166
204
|
|
|
167
|
-
const frontiers
|
|
205
|
+
const frontiers = new Map<string, CRStructAck<CounterStruct>>()
|
|
168
206
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
frontiers.push(event.detail)
|
|
173
|
-
},
|
|
174
|
-
{ once: true }
|
|
175
|
-
)
|
|
207
|
+
alice.addEventListener('delta', (event) => {
|
|
208
|
+
bob.merge(event.detail)
|
|
209
|
+
})
|
|
176
210
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)
|
|
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
|
+
})
|
|
184
222
|
|
|
185
|
-
|
|
186
|
-
|
|
223
|
+
alice.title = 'x'
|
|
224
|
+
alice.title = 'y'
|
|
225
|
+
delete alice.title
|
|
187
226
|
|
|
188
|
-
|
|
189
|
-
|
|
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)
|
|
190
276
|
```
|
|
191
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
|
+
|
|
192
285
|
## Runtime behavior
|
|
193
286
|
|
|
194
287
|
### Validation and errors
|
|
195
288
|
|
|
196
|
-
|
|
289
|
+
Low-level exports can throw `CRStructError` with stable error codes:
|
|
197
290
|
|
|
198
291
|
- `DEFAULTS_NOT_CLONEABLE`
|
|
199
292
|
- `VALUE_NOT_CLONEABLE`
|
|
200
293
|
- `VALUE_TYPE_MISMATCH`
|
|
201
294
|
|
|
202
|
-
|
|
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
|
|
203
302
|
|
|
204
303
|
### Safety and copying semantics
|
|
205
304
|
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
- `
|
|
209
|
-
- `
|
|
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.
|
|
210
315
|
|
|
211
316
|
### Convergence and compaction
|
|
212
317
|
|
|
213
|
-
- The convergence
|
|
214
|
-
|
|
215
|
-
-
|
|
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.
|
|
216
327
|
|
|
217
328
|
## Tests
|
|
218
329
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
- E2E runtimes: Node ESM, Node CJS, Bun ESM, Bun CJS, Deno ESM, Cloudflare Workers ESM, Edge Runtime ESM.
|
|
223
|
-
- Browser E2E: Chromium, Firefox, WebKit, mobile Chrome, mobile Safari via Playwright.
|
|
224
|
-
- Current status: `npm run test` passes on Node 22.14.0 (`win32 x64`).
|
|
330
|
+
```sh
|
|
331
|
+
npm run test
|
|
332
|
+
```
|
|
225
333
|
|
|
226
|
-
|
|
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`).
|
|
227
355
|
|
|
228
|
-
|
|
356
|
+
## Benchmarks
|
|
229
357
|
|
|
230
358
|
```sh
|
|
231
|
-
|
|
359
|
+
npm run bench
|
|
232
360
|
```
|
|
233
361
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
|
|
|
245
|
-
| read
|
|
246
|
-
| read
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
|
|
|
260
|
-
|
|
|
261
|
-
|
|
|
262
|
-
|
|
|
263
|
-
|
|
|
264
|
-
|
|
|
265
|
-
|
|
|
266
|
-
|
|
267
|
-
|
|
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 |
|
|
268
399
|
|
|
269
400
|
## License
|
|
270
401
|
|