@optimystic/db-p2p 0.22.0 → 0.24.1
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/dist/src/cluster/client.d.ts +10 -0
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +30 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +39 -14
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +226 -119
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/service.d.ts +32 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +43 -2
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cohort-topic/host.js +34 -11
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +37 -7
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +77 -19
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +9 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +88 -2
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +134 -28
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +25 -1
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/logger.d.ts +17 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +19 -2
- package/dist/src/logger.js.map +1 -1
- package/dist/src/matchmaking/query-transport.js +3 -3
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/owned-block-seed.d.ts +6 -3
- package/dist/src/owned-block-seed.d.ts.map +1 -1
- package/dist/src/owned-block-seed.js +16 -3
- package/dist/src/owned-block-seed.js.map +1 -1
- package/dist/src/peer-address-book.d.ts +72 -0
- package/dist/src/peer-address-book.d.ts.map +1 -0
- package/dist/src/peer-address-book.js +123 -0
- package/dist/src/peer-address-book.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts +4 -4
- package/dist/src/reactivity/notify-transport.js +6 -6
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -2
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/reactivity/recover-transport.d.ts +6 -2
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -1
- package/dist/src/reactivity/recover-transport.js +7 -3
- package/dist/src/reactivity/recover-transport.js.map +1 -1
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +11 -2
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +30 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +95 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +62 -9
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +242 -73
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/rn.d.ts +3 -0
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +3 -0
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +83 -0
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/cached-raw-storage.js +152 -0
- package/dist/src/storage/cached-raw-storage.js.map +1 -0
- package/dist/src/storage/cached-store-driver.d.ts +186 -0
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -0
- package/dist/src/storage/cached-store-driver.js +775 -0
- package/dist/src/storage/cached-store-driver.js.map +1 -0
- package/dist/src/storage/i-raw-storage.d.ts +12 -5
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +234 -0
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -0
- package/dist/src/storage/shared-cache-pool.js +354 -0
- package/dist/src/storage/shared-cache-pool.js.map +1 -0
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +13 -6
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +15 -6
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +2 -1
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +35 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +668 -668
- package/src/cluster/block-transfer.ts +424 -424
- package/src/cluster/client.ts +119 -88
- package/src/cluster/cluster-error.ts +64 -64
- package/src/cluster/cluster-policy.ts +203 -203
- package/src/cluster/cluster-repo.ts +242 -122
- package/src/cluster/cluster-size-coupling.ts +45 -45
- package/src/cluster/commit-cert.ts +139 -139
- package/src/cluster/i-transaction-state-store.ts +43 -43
- package/src/cluster/memory-transaction-state-store.ts +56 -56
- package/src/cluster/peer-key-binding.ts +37 -37
- package/src/cluster/persistent-transaction-state-store.ts +92 -92
- package/src/cluster/quorum-restore.ts +223 -223
- package/src/cluster/reconcile-block.ts +203 -203
- package/src/cluster/service.ts +293 -241
- package/src/cluster/supermajority-coupling.ts +37 -37
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -122
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -132
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -159
- package/src/cohort-topic/change-bridge.ts +109 -109
- package/src/cohort-topic/cohort-gossip-driver.ts +231 -231
- package/src/cohort-topic/cohort-gossip-transport.ts +84 -84
- package/src/cohort-topic/fret-trust-anchor.ts +153 -153
- package/src/cohort-topic/host.ts +42 -11
- package/src/cohort-topic/index.ts +13 -13
- package/src/cohort-topic/membership-publish-sink.ts +20 -20
- package/src/cohort-topic/membership-source.ts +68 -68
- package/src/cohort-topic/peer-codec.ts +31 -31
- package/src/cohort-topic/peer-sig.ts +86 -86
- package/src/cohort-topic/protocols.ts +71 -71
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -77
- package/src/cohort-topic/size-estimator.ts +16 -16
- package/src/cohort-topic/stream-util.ts +79 -19
- package/src/cohort-topic/threshold-crypto.ts +239 -239
- package/src/cohort-topic/topic-router.ts +77 -77
- package/src/dispute/arbitrator-selection.ts +138 -138
- package/src/dispute/cascade.ts +524 -524
- package/src/dispute/dispute-service.ts +11 -5
- package/src/dispute/invalidation.ts +625 -625
- package/src/inbound-authorization.ts +190 -190
- package/src/index.ts +52 -49
- package/src/libp2p-key-network.ts +1120 -990
- package/src/libp2p-node-base.ts +1675 -1651
- package/src/libp2p-node-rn.ts +30 -30
- package/src/libp2p-node.ts +36 -36
- package/src/logger.ts +19 -2
- package/src/matchmaking/aggregate-counts.ts +104 -104
- package/src/matchmaking/index.ts +20 -20
- package/src/matchmaking/module.ts +363 -363
- package/src/matchmaking/protocols.ts +51 -51
- package/src/matchmaking/provider-manager.ts +95 -95
- package/src/matchmaking/query-handler.ts +88 -88
- package/src/matchmaking/query-transport.ts +3 -3
- package/src/matchmaking/seeker-manager.ts +64 -64
- package/src/matchmaking/seeker-walk-client.ts +293 -293
- package/src/matchmaking/traffic-validation.ts +195 -195
- package/src/optimystic-node.ts +36 -36
- package/src/owned-block-seed.ts +53 -40
- package/src/peer-address-book.ts +149 -0
- package/src/protocol-limits.ts +33 -33
- package/src/reactivity/forwarder-host.ts +438 -438
- package/src/reactivity/index.ts +19 -19
- package/src/reactivity/notify-transport.ts +144 -144
- package/src/reactivity/origination-manager.ts +192 -192
- package/src/reactivity/protocols.ts +61 -61
- package/src/reactivity/push-state-gossip.ts +291 -291
- package/src/reactivity/recover-transport.ts +7 -3
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -256
- package/src/reactivity/subscriber-registry.ts +96 -96
- package/src/reactivity/subscription-manager.ts +450 -450
- package/src/reactivity/topic-bytes.ts +37 -37
- package/src/repo/client.ts +12 -2
- package/src/repo/cluster-coordinator.ts +99 -3
- package/src/repo/coordinator-repo.ts +281 -74
- package/src/repo/types.ts +7 -7
- package/src/rn.ts +39 -36
- package/src/rpc-deadline.ts +45 -45
- package/src/storage/arachnode-partition.ts +74 -74
- package/src/storage/cached-raw-storage.ts +180 -0
- package/src/storage/cached-store-driver.ts +859 -0
- package/src/storage/i-kv-store.ts +8 -8
- package/src/storage/i-raw-storage.ts +12 -5
- package/src/storage/kv-raw-storage.ts +135 -135
- package/src/storage/memory-kv-store.ts +28 -28
- package/src/storage/memory-storage.ts +25 -25
- package/src/storage/memory-store-driver.ts +157 -157
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +80 -80
- package/src/storage/ring-selector.ts +317 -317
- package/src/storage/ring-shift-coordinator.ts +271 -271
- package/src/storage/shared-cache-pool.ts +452 -0
- package/src/storage/storage-repo.ts +1014 -1014
- package/src/testing/cohort-topic-mesh-harness.ts +673 -663
- package/src/testing/index.ts +8 -8
- package/src/testing/matchmaking-mesh-harness.ts +475 -475
- package/src/testing/raw-storage-conformance.ts +453 -417
- package/src/testing/reactivity-mesh-harness.ts +922 -922
- package/dist/src/storage/restoration-coordinator-v2.d.ts +0 -67
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +0 -1
- package/dist/src/storage/restoration-coordinator-v2.js +0 -172
- package/dist/src/storage/restoration-coordinator-v2.js.map +0 -1
|
@@ -1,417 +1,453 @@
|
|
|
1
|
-
import { expect } from 'chai';
|
|
2
|
-
import type { BlockId, ActionId, IBlock, BlockHeader, Transform, Transforms } from '@optimystic/db-core';
|
|
3
|
-
import type { IRawStorage } from '../storage/i-raw-storage.js';
|
|
4
|
-
import type { BlockMetadata } from '../storage/struct.js';
|
|
5
|
-
import { StorageRepo } from '../storage/storage-repo.js';
|
|
6
|
-
import { BlockStorage } from '../storage/block-storage.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* What `makeStorage` hands each conformance case: a fresh `IRawStorage` and a
|
|
10
|
-
* `cleanup` that releases it (close a handle, delete a temp dir, etc.). The
|
|
11
|
-
* memory driver's cleanup is a no-op; disk/db backends do real teardown.
|
|
12
|
-
*/
|
|
13
|
-
export interface ConformanceHarness {
|
|
14
|
-
storage: IRawStorage;
|
|
15
|
-
cleanup: () => Promise<void>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const makeHeader = (id: string): BlockHeader => ({
|
|
19
|
-
id: id as BlockId,
|
|
20
|
-
type: 'test',
|
|
21
|
-
collectionId: 'collection-1' as BlockId
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const makeBlock = (id: string, data?: Record<string, unknown>): IBlock => ({
|
|
25
|
-
header: makeHeader(id),
|
|
26
|
-
...data
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const makeInsertTransforms = (blockId: BlockId, block: IBlock): Transforms => ({
|
|
30
|
-
inserts: { [blockId]: block },
|
|
31
|
-
updates: {},
|
|
32
|
-
deletes: []
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
async function collect<T>(iter: AsyncIterable<T>): Promise<T[]> {
|
|
36
|
-
const out: T[] = [];
|
|
37
|
-
for await (const item of iter) out.push(item);
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Registers a `describe(name, ...)` block asserting that a raw-storage backend
|
|
43
|
-
* behaves identically to every other backend. This is the ONE maintained parity
|
|
44
|
-
* target that replaces the four copied `test/*-storage.spec.ts` specs — each
|
|
45
|
-
* driver package calls it from its own `test/` with a factory for its backend.
|
|
46
|
-
*
|
|
47
|
-
* It covers the union of what the existing per-backend specs assert PLUS the
|
|
48
|
-
* behaviors the shared kernel makes parity-critical: clone-on-store/read (now
|
|
49
|
-
* structural via the byte boundary), promote atomicity + the exact missing-pend
|
|
50
|
-
* error, drain-before-yield iteration, and a `BlockStorage`-level slice that
|
|
51
|
-
* guards the `meta.ranges` open-ended seeding invariant
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
let
|
|
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
|
-
await storage.saveRevision(blockId,
|
|
104
|
-
await storage.saveRevision(blockId,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{ rev:
|
|
110
|
-
{ rev:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await storage.saveRevision(blockId,
|
|
118
|
-
await storage.saveRevision(blockId,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{ rev:
|
|
124
|
-
{ rev:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
await storage.saveRevision(
|
|
139
|
-
await storage.saveRevision(b,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await storage.saveRevision(blockId,
|
|
152
|
-
await storage.saveRevision(blockId,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
await
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
await storage.savePendingTransaction(a, '
|
|
182
|
-
await storage.savePendingTransaction(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
await storage.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
expect(await
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
await storage.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await storage.deletePendingTransaction(blockId, 'a1' as ActionId);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
await storage.savePendingTransaction(blockId, '
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
await storage.saveMaterializedBlock(blockId, 'a1' as ActionId,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
expect(await storage.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
expect(error
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
meta.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
await storage.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
pend.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
expect(storedPend!.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
first
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
await storage.saveMetadata('
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
expect(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
expect(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import type { BlockId, ActionId, IBlock, BlockHeader, Transform, Transforms } from '@optimystic/db-core';
|
|
3
|
+
import type { IRawStorage } from '../storage/i-raw-storage.js';
|
|
4
|
+
import type { BlockMetadata } from '../storage/struct.js';
|
|
5
|
+
import { StorageRepo } from '../storage/storage-repo.js';
|
|
6
|
+
import { BlockStorage } from '../storage/block-storage.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What `makeStorage` hands each conformance case: a fresh `IRawStorage` and a
|
|
10
|
+
* `cleanup` that releases it (close a handle, delete a temp dir, etc.). The
|
|
11
|
+
* memory driver's cleanup is a no-op; disk/db backends do real teardown.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConformanceHarness {
|
|
14
|
+
storage: IRawStorage;
|
|
15
|
+
cleanup: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const makeHeader = (id: string): BlockHeader => ({
|
|
19
|
+
id: id as BlockId,
|
|
20
|
+
type: 'test',
|
|
21
|
+
collectionId: 'collection-1' as BlockId
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const makeBlock = (id: string, data?: Record<string, unknown>): IBlock => ({
|
|
25
|
+
header: makeHeader(id),
|
|
26
|
+
...data
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const makeInsertTransforms = (blockId: BlockId, block: IBlock): Transforms => ({
|
|
30
|
+
inserts: { [blockId]: block },
|
|
31
|
+
updates: {},
|
|
32
|
+
deletes: []
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
async function collect<T>(iter: AsyncIterable<T>): Promise<T[]> {
|
|
36
|
+
const out: T[] = [];
|
|
37
|
+
for await (const item of iter) out.push(item);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Registers a `describe(name, ...)` block asserting that a raw-storage backend
|
|
43
|
+
* behaves identically to every other backend. This is the ONE maintained parity
|
|
44
|
+
* target that replaces the four copied `test/*-storage.spec.ts` specs — each
|
|
45
|
+
* driver package calls it from its own `test/` with a factory for its backend.
|
|
46
|
+
*
|
|
47
|
+
* It covers the union of what the existing per-backend specs assert PLUS the
|
|
48
|
+
* behaviors the shared kernel makes parity-critical: clone-on-store/read (now
|
|
49
|
+
* structural via the byte boundary), promote atomicity + the exact missing-pend
|
|
50
|
+
* error, drain-before-yield iteration, and a `BlockStorage`-level slice that
|
|
51
|
+
* guards the `meta.ranges` open-ended seeding invariant, tombstone read-back,
|
|
52
|
+
* and the exact id population `listBlockIds` yields on the node path.
|
|
53
|
+
*/
|
|
54
|
+
export function runRawStorageConformance(
|
|
55
|
+
name: string,
|
|
56
|
+
makeStorage: () => Promise<ConformanceHarness>
|
|
57
|
+
): void {
|
|
58
|
+
describe(name, () => {
|
|
59
|
+
let harness: ConformanceHarness;
|
|
60
|
+
let storage: IRawStorage;
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
harness = await makeStorage();
|
|
64
|
+
storage = harness.storage;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(async () => {
|
|
68
|
+
await harness.cleanup();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// --- Metadata ---
|
|
72
|
+
|
|
73
|
+
it('getMetadata returns undefined on a miss', async () => {
|
|
74
|
+
expect(await storage.getMetadata('absent' as BlockId)).to.equal(undefined);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('metadata round-trips, including a one-element open-ended range', async () => {
|
|
78
|
+
const blockId = 'meta-block' as BlockId;
|
|
79
|
+
// [1, 3] is a bounded range; [5] is OPEN-ENDED (undefined upper bound). The byte
|
|
80
|
+
// codec must preserve the open-ended encoding exactly — no range normalization.
|
|
81
|
+
const meta: BlockMetadata = { ranges: [[1, 3], [5]], latest: { rev: 5, actionId: 'tx:a5' as ActionId } };
|
|
82
|
+
|
|
83
|
+
await storage.saveMetadata(blockId, meta);
|
|
84
|
+
const got = await storage.getMetadata(blockId);
|
|
85
|
+
|
|
86
|
+
expect(got).to.deep.equal({ ranges: [[1, 3], [5]], latest: { rev: 5, actionId: 'tx:a5' } });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// --- Revisions ---
|
|
90
|
+
|
|
91
|
+
it('getRevision returns undefined on a miss', async () => {
|
|
92
|
+
expect(await storage.getRevision('block' as BlockId, 1)).to.equal(undefined);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('revision round-trips', async () => {
|
|
96
|
+
const blockId = 'rev-block' as BlockId;
|
|
97
|
+
await storage.saveRevision(blockId, 7, 'tx:a7' as ActionId);
|
|
98
|
+
expect(await storage.getRevision(blockId, 7)).to.equal('tx:a7');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('listRevisions is ascending, inclusive on both bounds, and skips sparse gaps', async () => {
|
|
102
|
+
const blockId = 'list-asc' as BlockId;
|
|
103
|
+
await storage.saveRevision(blockId, 1, 'a1' as ActionId);
|
|
104
|
+
await storage.saveRevision(blockId, 2, 'a2' as ActionId);
|
|
105
|
+
await storage.saveRevision(blockId, 4, 'a4' as ActionId); // rev 3 absent
|
|
106
|
+
|
|
107
|
+
const revs = await collect(storage.listRevisions(blockId, 1, 4));
|
|
108
|
+
expect(revs).to.deep.equal([
|
|
109
|
+
{ rev: 1, actionId: 'a1' },
|
|
110
|
+
{ rev: 2, actionId: 'a2' },
|
|
111
|
+
{ rev: 4, actionId: 'a4' }
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('listRevisions descends when startRev > endRev (bounds still inclusive)', async () => {
|
|
116
|
+
const blockId = 'list-desc' as BlockId;
|
|
117
|
+
await storage.saveRevision(blockId, 1, 'a1' as ActionId);
|
|
118
|
+
await storage.saveRevision(blockId, 2, 'a2' as ActionId);
|
|
119
|
+
await storage.saveRevision(blockId, 4, 'a4' as ActionId);
|
|
120
|
+
|
|
121
|
+
const revs = await collect(storage.listRevisions(blockId, 4, 1));
|
|
122
|
+
expect(revs).to.deep.equal([
|
|
123
|
+
{ rev: 4, actionId: 'a4' },
|
|
124
|
+
{ rev: 2, actionId: 'a2' },
|
|
125
|
+
{ rev: 1, actionId: 'a1' }
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('listRevisions with a single inclusive bound yields that one rev', async () => {
|
|
130
|
+
const blockId = 'list-single' as BlockId;
|
|
131
|
+
await storage.saveRevision(blockId, 3, 'a3' as ActionId);
|
|
132
|
+
expect(await collect(storage.listRevisions(blockId, 3, 3))).to.deep.equal([{ rev: 3, actionId: 'a3' }]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('listRevisions is scoped to one blockId (a second block does not leak in)', async () => {
|
|
136
|
+
const a = 'scope-a' as BlockId;
|
|
137
|
+
const b = 'scope-b' as BlockId;
|
|
138
|
+
await storage.saveRevision(a, 1, 'a1' as ActionId);
|
|
139
|
+
await storage.saveRevision(b, 1, 'b1' as ActionId);
|
|
140
|
+
await storage.saveRevision(b, 2, 'b2' as ActionId);
|
|
141
|
+
|
|
142
|
+
expect(await collect(storage.listRevisions(a, 1, 5))).to.deep.equal([{ rev: 1, actionId: 'a1' }]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('listRevisions over an empty range yields nothing', async () => {
|
|
146
|
+
expect(await collect(storage.listRevisions('empty' as BlockId, 1, 10))).to.deep.equal([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('listRevisions tolerates awaits interleaved between yields (drain-before-yield)', async () => {
|
|
150
|
+
const blockId = 'drain-revs' as BlockId;
|
|
151
|
+
await storage.saveRevision(blockId, 1, 'a1' as ActionId);
|
|
152
|
+
await storage.saveRevision(blockId, 2, 'a2' as ActionId);
|
|
153
|
+
await storage.saveRevision(blockId, 3, 'a3' as ActionId);
|
|
154
|
+
|
|
155
|
+
const seen: Array<[number, ActionId]> = [];
|
|
156
|
+
for await (const { rev, actionId } of storage.listRevisions(blockId, 1, 3)) {
|
|
157
|
+
// An unrelated storage read between yields would auto-commit / invalidate a
|
|
158
|
+
// native cursor that straddled the await, if the driver failed to drain first.
|
|
159
|
+
await storage.getMetadata('other' as BlockId);
|
|
160
|
+
seen.push([rev, actionId]);
|
|
161
|
+
}
|
|
162
|
+
expect(seen).to.deep.equal([[1, 'a1'], [2, 'a2'], [3, 'a3']]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// --- Pending transactions ---
|
|
166
|
+
|
|
167
|
+
it('getPendingTransaction returns undefined on a miss', async () => {
|
|
168
|
+
expect(await storage.getPendingTransaction('block' as BlockId, 'a' as ActionId)).to.equal(undefined);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('pending transaction round-trips', async () => {
|
|
172
|
+
const blockId = 'pend-block' as BlockId;
|
|
173
|
+
const transform: Transform = { insert: makeBlock('pend-block', { items: ['x'] }) };
|
|
174
|
+
await storage.savePendingTransaction(blockId, 'a1' as ActionId, transform);
|
|
175
|
+
expect(await storage.getPendingTransaction(blockId, 'a1' as ActionId)).to.deep.equal(transform);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('listPendingTransactions is scoped to one block', async () => {
|
|
179
|
+
const a = 'pend-a' as BlockId;
|
|
180
|
+
const b = 'pend-b' as BlockId;
|
|
181
|
+
await storage.savePendingTransaction(a, 'a1' as ActionId, { delete: true });
|
|
182
|
+
await storage.savePendingTransaction(a, 'a2' as ActionId, { delete: true });
|
|
183
|
+
await storage.savePendingTransaction(b, 'b1' as ActionId, { delete: true });
|
|
184
|
+
|
|
185
|
+
const ids = new Set(await collect(storage.listPendingTransactions(a)));
|
|
186
|
+
expect(ids).to.deep.equal(new Set(['a1', 'a2']));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('deletePendingTransaction removes the pending entry', async () => {
|
|
190
|
+
const blockId = 'pend-del' as BlockId;
|
|
191
|
+
await storage.savePendingTransaction(blockId, 'a1' as ActionId, { delete: true });
|
|
192
|
+
await storage.deletePendingTransaction(blockId, 'a1' as ActionId);
|
|
193
|
+
|
|
194
|
+
expect(await storage.getPendingTransaction(blockId, 'a1' as ActionId)).to.equal(undefined);
|
|
195
|
+
expect(await collect(storage.listPendingTransactions(blockId))).to.deep.equal([]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('deletePendingTransaction on an absent entry is a tolerated no-op', async () => {
|
|
199
|
+
// Load-bearing across backends: the commit path deletes a pending record it has NOT
|
|
200
|
+
// checked for presence — `StorageRepo.dropUnpromotablePendings` over a whole batch, and
|
|
201
|
+
// `BlockStorage.saveForwardRevision` maintaining Invariant P — so a backend that threw
|
|
202
|
+
// (or rejected) on a miss would turn every ordinary commit into a logged failure.
|
|
203
|
+
const blockId = 'pend-del-absent' as BlockId;
|
|
204
|
+
await storage.deletePendingTransaction(blockId, 'never-pended' as ActionId);
|
|
205
|
+
|
|
206
|
+
// Also a no-op beside a LIVE pending for a different action: the delete must be scoped
|
|
207
|
+
// to its action id, never a per-block clear.
|
|
208
|
+
await storage.savePendingTransaction(blockId, 'a1' as ActionId, { delete: true });
|
|
209
|
+
await storage.deletePendingTransaction(blockId, 'other' as ActionId);
|
|
210
|
+
expect(await storage.getPendingTransaction(blockId, 'a1' as ActionId)).to.deep.equal({ delete: true });
|
|
211
|
+
|
|
212
|
+
// Idempotent: deleting the same entry twice is fine.
|
|
213
|
+
await storage.deletePendingTransaction(blockId, 'a1' as ActionId);
|
|
214
|
+
await storage.deletePendingTransaction(blockId, 'a1' as ActionId);
|
|
215
|
+
expect(await collect(storage.listPendingTransactions(blockId))).to.deep.equal([]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('listPendingTransactions tolerates awaits interleaved between yields (drain-before-yield)', async () => {
|
|
219
|
+
const blockId = 'drain-pend' as BlockId;
|
|
220
|
+
await storage.savePendingTransaction(blockId, 'a1' as ActionId, { delete: true });
|
|
221
|
+
await storage.savePendingTransaction(blockId, 'a2' as ActionId, { delete: true });
|
|
222
|
+
|
|
223
|
+
const seen: ActionId[] = [];
|
|
224
|
+
for await (const id of storage.listPendingTransactions(blockId)) {
|
|
225
|
+
await storage.getMetadata('other' as BlockId);
|
|
226
|
+
seen.push(id);
|
|
227
|
+
}
|
|
228
|
+
expect(new Set(seen)).to.deep.equal(new Set(['a1', 'a2']));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// --- Committed transactions ---
|
|
232
|
+
|
|
233
|
+
it('getTransaction returns undefined on a miss', async () => {
|
|
234
|
+
expect(await storage.getTransaction('block' as BlockId, 'a' as ActionId)).to.equal(undefined);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('committed transaction round-trips', async () => {
|
|
238
|
+
const blockId = 'tx-block' as BlockId;
|
|
239
|
+
const transform: Transform = { updates: [['items', 0, 0, ['y']]] };
|
|
240
|
+
await storage.saveTransaction(blockId, 'a1' as ActionId, transform);
|
|
241
|
+
expect(await storage.getTransaction(blockId, 'a1' as ActionId)).to.deep.equal(transform);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// --- Materialized blocks ---
|
|
245
|
+
|
|
246
|
+
it('getMaterializedBlock returns undefined on a miss', async () => {
|
|
247
|
+
expect(await storage.getMaterializedBlock('block' as BlockId, 'a' as ActionId)).to.equal(undefined);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('materialized block round-trips', async () => {
|
|
251
|
+
const blockId = 'mat-block' as BlockId;
|
|
252
|
+
const block = makeBlock('mat-block', { items: ['z'] });
|
|
253
|
+
await storage.saveMaterializedBlock(blockId, 'a1' as ActionId, block);
|
|
254
|
+
expect(await storage.getMaterializedBlock(blockId, 'a1' as ActionId)).to.deep.equal(block);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('saveMaterializedBlock(undefined) deletes; a subsequent get returns undefined', async () => {
|
|
258
|
+
const blockId = 'mat-del' as BlockId;
|
|
259
|
+
await storage.saveMaterializedBlock(blockId, 'a1' as ActionId, makeBlock('mat-del'));
|
|
260
|
+
await storage.saveMaterializedBlock(blockId, 'a1' as ActionId, undefined);
|
|
261
|
+
expect(await storage.getMaterializedBlock(blockId, 'a1' as ActionId)).to.equal(undefined);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// --- Promote (the only cross-key atomic op) ---
|
|
265
|
+
|
|
266
|
+
it('promotePendingTransaction moves pending → committed atomically', async () => {
|
|
267
|
+
const blockId = 'promote-block' as BlockId;
|
|
268
|
+
const transform: Transform = { insert: makeBlock('promote-block') };
|
|
269
|
+
await storage.savePendingTransaction(blockId, 'a1' as ActionId, transform);
|
|
270
|
+
|
|
271
|
+
await storage.promotePendingTransaction(blockId, 'a1' as ActionId);
|
|
272
|
+
|
|
273
|
+
expect(await storage.getTransaction(blockId, 'a1' as ActionId), 'committed after promote').to.deep.equal(transform);
|
|
274
|
+
expect(await storage.getPendingTransaction(blockId, 'a1' as ActionId), 'pending removed after promote').to.equal(undefined);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('promotePendingTransaction throws the exact message when no pending entry exists', async () => {
|
|
278
|
+
const blockId = 'promote-missing' as BlockId;
|
|
279
|
+
let error: Error | undefined;
|
|
280
|
+
try {
|
|
281
|
+
await storage.promotePendingTransaction(blockId, 'nope' as ActionId);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
error = err as Error;
|
|
284
|
+
}
|
|
285
|
+
expect(error, 'promote of a missing pending throws').to.not.equal(undefined);
|
|
286
|
+
expect(error!.message).to.equal(`Pending action nope not found for block ${blockId}`);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// --- Clone-on-store / clone-on-read (now structural via the byte boundary) ---
|
|
290
|
+
|
|
291
|
+
it('saveMetadata snapshots — a later caller mutation does not corrupt stored state', async () => {
|
|
292
|
+
const blockId = 'clone-meta' as BlockId;
|
|
293
|
+
const meta: BlockMetadata = { ranges: [[1, 2]], latest: { rev: 1, actionId: 'a1' as ActionId } };
|
|
294
|
+
await storage.saveMetadata(blockId, meta);
|
|
295
|
+
|
|
296
|
+
// Mutate the caller's object AFTER saving — must not reach stored state.
|
|
297
|
+
meta.ranges.push([9, 10]);
|
|
298
|
+
meta.latest!.rev = 99;
|
|
299
|
+
|
|
300
|
+
const stored = await storage.getMetadata(blockId);
|
|
301
|
+
expect(stored).to.deep.equal({ ranges: [[1, 2]], latest: { rev: 1, actionId: 'a1' } });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('savePendingTransaction / saveTransaction snapshot — a later caller mutation does not corrupt stored state', async () => {
|
|
305
|
+
const blockId = 'clone-tx' as BlockId;
|
|
306
|
+
const pend: Transform = { insert: makeBlock('clone-tx', { items: ['p-original'] }) };
|
|
307
|
+
const committed: Transform = { insert: makeBlock('clone-tx', { items: ['c-original'] }) };
|
|
308
|
+
await storage.savePendingTransaction(blockId, 'p1' as ActionId, pend);
|
|
309
|
+
await storage.saveTransaction(blockId, 'c1' as ActionId, committed);
|
|
310
|
+
|
|
311
|
+
// Mutate both caller references AFTER saving — neither must reach stored state.
|
|
312
|
+
(pend.insert as IBlock & { items: string[] }).items.push('mutated');
|
|
313
|
+
pend.delete = true;
|
|
314
|
+
(committed.insert as IBlock & { items: string[] }).items.push('mutated');
|
|
315
|
+
|
|
316
|
+
const storedPend = await storage.getPendingTransaction(blockId, 'p1' as ActionId);
|
|
317
|
+
expect((storedPend!.insert as IBlock & { items: string[] }).items).to.deep.equal(['p-original']);
|
|
318
|
+
expect(storedPend!.delete).to.equal(undefined);
|
|
319
|
+
const storedCommitted = await storage.getTransaction(blockId, 'c1' as ActionId);
|
|
320
|
+
expect((storedCommitted!.insert as IBlock & { items: string[] }).items).to.deep.equal(['c-original']);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('getMetadata returns an independent copy each read (mutating one does not affect another)', async () => {
|
|
324
|
+
const blockId = 'clone-meta-read' as BlockId;
|
|
325
|
+
await storage.saveMetadata(blockId, { ranges: [[1, 2]], latest: { rev: 1, actionId: 'a1' as ActionId } });
|
|
326
|
+
|
|
327
|
+
const first = await storage.getMetadata(blockId);
|
|
328
|
+
first!.ranges.push([9, 10]);
|
|
329
|
+
|
|
330
|
+
const second = await storage.getMetadata(blockId);
|
|
331
|
+
expect(second!.ranges).to.deep.equal([[1, 2]]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('saveMaterializedBlock snapshots and getMaterializedBlock returns a fresh object', async () => {
|
|
335
|
+
const blockId = 'clone-mat' as BlockId;
|
|
336
|
+
const block = makeBlock('clone-mat', { items: ['original'] });
|
|
337
|
+
await storage.saveMaterializedBlock(blockId, 'a1' as ActionId, block);
|
|
338
|
+
|
|
339
|
+
// Mutate the caller's block after saving.
|
|
340
|
+
(block as IBlock & { items: string[] }).items.push('mutated');
|
|
341
|
+
|
|
342
|
+
const stored = await storage.getMaterializedBlock(blockId, 'a1' as ActionId) as IBlock & { items: string[] };
|
|
343
|
+
expect(stored.items, 'stored copy unaffected by post-save mutation').to.deep.equal(['original']);
|
|
344
|
+
|
|
345
|
+
// Mutate a returned copy; a fresh get is unaffected.
|
|
346
|
+
stored.items.push('mutated-again');
|
|
347
|
+
const reread = await storage.getMaterializedBlock(blockId, 'a1' as ActionId) as IBlock & { items: string[] };
|
|
348
|
+
expect(reread.items, 'fresh read unaffected by mutation of a prior read').to.deep.equal(['original']);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// --- listBlockIds (optional; only when the driver implements it) ---
|
|
352
|
+
|
|
353
|
+
it('listBlockIds yields exactly the blocks with metadata', async function () {
|
|
354
|
+
const listBlockIds = storage.listBlockIds;
|
|
355
|
+
if (typeof listBlockIds !== 'function') {
|
|
356
|
+
this.skip();
|
|
357
|
+
} else {
|
|
358
|
+
await storage.saveMetadata('b1' as BlockId, { ranges: [], latest: undefined });
|
|
359
|
+
await storage.saveMetadata('b2' as BlockId, { ranges: [], latest: undefined });
|
|
360
|
+
// RAW-DRIVER LAYER: this pends straight through the driver, which writes no metadata, so
|
|
361
|
+
// the block is not enumerated. It is NOT the node's behavior — a pend through
|
|
362
|
+
// `BlockStorage` seeds metadata first, and IS enumerated. See the parity case below.
|
|
363
|
+
await storage.savePendingTransaction('pending-only' as BlockId, 'x' as ActionId, { delete: true });
|
|
364
|
+
|
|
365
|
+
const ids = new Set(await collect(listBlockIds.call(storage)));
|
|
366
|
+
expect(ids).to.deep.equal(new Set(['b1', 'b2']));
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('listBlockIds yields nothing for an empty store', async function () {
|
|
371
|
+
const listBlockIds = storage.listBlockIds;
|
|
372
|
+
if (typeof listBlockIds !== 'function') {
|
|
373
|
+
this.skip();
|
|
374
|
+
} else {
|
|
375
|
+
expect(await collect(listBlockIds.call(storage))).to.deep.equal([]);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// --- BlockStorage-level parity slice (over the same driver) ---
|
|
380
|
+
|
|
381
|
+
it('BlockStorage pend → commit seeds open-ended ranges [[E]] (never [[0]]) and getBlock serves it', async () => {
|
|
382
|
+
const blockId = 'parity-commit' as BlockId;
|
|
383
|
+
const repo = new StorageRepo((id) => new BlockStorage(id, storage));
|
|
384
|
+
|
|
385
|
+
await repo.pend({
|
|
386
|
+
actionId: 'a1' as ActionId,
|
|
387
|
+
transforms: makeInsertTransforms(blockId, makeBlock('parity-commit', { items: [] })),
|
|
388
|
+
policy: 'c'
|
|
389
|
+
});
|
|
390
|
+
const commit = await repo.commit({ actionId: 'a1' as ActionId, blockIds: [blockId], tailId: blockId, rev: 1 });
|
|
391
|
+
expect(commit.success, 'commit succeeded').to.equal(true);
|
|
392
|
+
|
|
393
|
+
const meta = await storage.getMetadata(blockId);
|
|
394
|
+
expect(meta!.latest?.rev).to.equal(1);
|
|
395
|
+
// Open-ended from the earliest committed rev E=1 — NOT the over-claim [[0]].
|
|
396
|
+
expect(meta!.ranges, 'coverage open-ended from E=1').to.deep.equal([[1]]);
|
|
397
|
+
|
|
398
|
+
const got = await new BlockStorage(blockId, storage).getBlock();
|
|
399
|
+
expect(got?.block.header.id, 'committed block served').to.equal('parity-commit');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// The population `listBlockIds` really yields on the node path, pinned per backend. The startup
|
|
403
|
+
// owned-block seed (`seedOwnedBlocksFromStorage`) adds every enumerated id to the resilience
|
|
404
|
+
// monitors' tracked set, and `BlockStorage.savePendingTransaction` seeds metadata for a block
|
|
405
|
+
// that has none — so a pend-only block IS enumerated and IS seeded. The interface doc forbids
|
|
406
|
+
// filtering that out by reading each block's metadata (a per-block read at startup); this case
|
|
407
|
+
// is what stops a backend from "helpfully" adding such a filter.
|
|
408
|
+
it('listBlockIds enumerates a BlockStorage pend-only block (metadata seeded before the transform)', async function () {
|
|
409
|
+
const listBlockIds = storage.listBlockIds;
|
|
410
|
+
if (typeof listBlockIds !== 'function') {
|
|
411
|
+
this.skip();
|
|
412
|
+
} else {
|
|
413
|
+
const committed = 'parity-list-committed' as BlockId;
|
|
414
|
+
const pendOnly = 'parity-list-pend-only' as BlockId;
|
|
415
|
+
const repo = new StorageRepo((id) => new BlockStorage(id, storage));
|
|
416
|
+
|
|
417
|
+
await repo.pend({
|
|
418
|
+
actionId: 'a1' as ActionId,
|
|
419
|
+
transforms: makeInsertTransforms(committed, makeBlock('parity-list-committed', { items: [] })),
|
|
420
|
+
policy: 'c'
|
|
421
|
+
});
|
|
422
|
+
await repo.commit({ actionId: 'a1' as ActionId, blockIds: [committed], tailId: committed, rev: 1 });
|
|
423
|
+
|
|
424
|
+
await repo.pend({
|
|
425
|
+
actionId: 'a2' as ActionId,
|
|
426
|
+
transforms: makeInsertTransforms(pendOnly, makeBlock('parity-list-pend-only', { items: [] })),
|
|
427
|
+
policy: 'c'
|
|
428
|
+
});
|
|
429
|
+
expect(await storage.getMetadata(pendOnly), 'pend seeded metadata for a block that had none').to.exist;
|
|
430
|
+
|
|
431
|
+
expect(new Set(await collect(listBlockIds.call(storage)))).to.deep.equal(new Set([committed, pendOnly]));
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('BlockStorage saveReplica → saveDeletion reads the tombstoned rev back as undefined', async () => {
|
|
436
|
+
const blockId = 'parity-tombstone' as BlockId;
|
|
437
|
+
const bs = new BlockStorage(blockId, storage);
|
|
438
|
+
|
|
439
|
+
await bs.saveReplica(makeBlock('parity-tombstone', { items: ['live'] }), { rev: 1, actionId: 'r1' as ActionId });
|
|
440
|
+
const meta = await storage.getMetadata(blockId);
|
|
441
|
+
// saveReplica seeds open-ended coverage anchored at the replica rev, never [[0]].
|
|
442
|
+
expect(meta!.ranges, 'replica seeds open-ended [[1]]').to.deep.equal([[1]]);
|
|
443
|
+
|
|
444
|
+
const latest = await bs.saveDeletion({ rev: 2, actionId: 'd2' as ActionId });
|
|
445
|
+
expect(latest.rev).to.equal(2);
|
|
446
|
+
|
|
447
|
+
// getBlock at the tombstone rev reverse-applies { delete: true } → absent block.
|
|
448
|
+
expect(await bs.getBlock(), 'tombstoned rev reads back undefined').to.equal(undefined);
|
|
449
|
+
// The prior revision still materializes.
|
|
450
|
+
expect((await bs.getBlock(1))?.block.header.id, 'rev 1 still serves the live block').to.equal('parity-tombstone');
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|