@peerbit/shared-log 13.2.24 → 13.2.26

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.
Files changed (81) hide show
  1. package/dist/src/checked-prune.d.ts +12 -0
  2. package/dist/src/checked-prune.d.ts.map +1 -1
  3. package/dist/src/checked-prune.js +33 -0
  4. package/dist/src/checked-prune.js.map +1 -1
  5. package/dist/src/coordinate-persistence.d.ts +211 -0
  6. package/dist/src/coordinate-persistence.d.ts.map +1 -0
  7. package/dist/src/coordinate-persistence.js +1133 -0
  8. package/dist/src/coordinate-persistence.js.map +1 -0
  9. package/dist/src/errors.d.ts +1 -0
  10. package/dist/src/errors.d.ts.map +1 -1
  11. package/dist/src/errors.js +19 -0
  12. package/dist/src/errors.js.map +1 -1
  13. package/dist/src/index.d.ts +127 -133
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +1400 -4269
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/instance-lifecycle.d.ts +117 -0
  18. package/dist/src/instance-lifecycle.d.ts.map +1 -0
  19. package/dist/src/instance-lifecycle.js +254 -0
  20. package/dist/src/instance-lifecycle.js.map +1 -0
  21. package/dist/src/join-warmup.d.ts +86 -0
  22. package/dist/src/join-warmup.d.ts.map +1 -0
  23. package/dist/src/join-warmup.js +351 -0
  24. package/dist/src/join-warmup.js.map +1 -0
  25. package/dist/src/native-write-through-block-store.d.ts +123 -0
  26. package/dist/src/native-write-through-block-store.d.ts.map +1 -0
  27. package/dist/src/native-write-through-block-store.js +989 -0
  28. package/dist/src/native-write-through-block-store.js.map +1 -0
  29. package/dist/src/peer-session.d.ts +117 -0
  30. package/dist/src/peer-session.d.ts.map +1 -0
  31. package/dist/src/peer-session.js +259 -0
  32. package/dist/src/peer-session.js.map +1 -0
  33. package/dist/src/replication-announcement.d.ts +116 -0
  34. package/dist/src/replication-announcement.d.ts.map +1 -0
  35. package/dist/src/replication-announcement.js +515 -0
  36. package/dist/src/replication-announcement.js.map +1 -0
  37. package/dist/src/replicator-liveness.d.ts +59 -0
  38. package/dist/src/replicator-liveness.d.ts.map +1 -0
  39. package/dist/src/replicator-liveness.js +304 -0
  40. package/dist/src/replicator-liveness.js.map +1 -0
  41. package/dist/src/sync/dispatch-lifecycle.d.ts +48 -0
  42. package/dist/src/sync/dispatch-lifecycle.d.ts.map +1 -0
  43. package/dist/src/sync/dispatch-lifecycle.js +141 -0
  44. package/dist/src/sync/dispatch-lifecycle.js.map +1 -0
  45. package/dist/src/sync/factory.d.ts +29 -0
  46. package/dist/src/sync/factory.d.ts.map +1 -0
  47. package/dist/src/sync/factory.js +87 -0
  48. package/dist/src/sync/factory.js.map +1 -0
  49. package/dist/src/sync/pending-sync-store.d.ts +105 -0
  50. package/dist/src/sync/pending-sync-store.d.ts.map +1 -0
  51. package/dist/src/sync/pending-sync-store.js +877 -0
  52. package/dist/src/sync/pending-sync-store.js.map +1 -0
  53. package/dist/src/sync/rateless-iblt.d.ts +4 -1
  54. package/dist/src/sync/rateless-iblt.d.ts.map +1 -1
  55. package/dist/src/sync/rateless-iblt.js +102 -106
  56. package/dist/src/sync/rateless-iblt.js.map +1 -1
  57. package/dist/src/sync/simple.d.ts +10 -41
  58. package/dist/src/sync/simple.d.ts.map +1 -1
  59. package/dist/src/sync/simple.js +259 -952
  60. package/dist/src/sync/simple.js.map +1 -1
  61. package/dist/src/sync/sync-peer-state.d.ts +16 -0
  62. package/dist/src/sync/sync-peer-state.d.ts.map +1 -0
  63. package/dist/src/sync/sync-peer-state.js +82 -0
  64. package/dist/src/sync/sync-peer-state.js.map +1 -0
  65. package/package.json +12 -12
  66. package/src/checked-prune.ts +35 -0
  67. package/src/coordinate-persistence.ts +1796 -0
  68. package/src/errors.ts +21 -0
  69. package/src/index.ts +4014 -7858
  70. package/src/instance-lifecycle.ts +351 -0
  71. package/src/join-warmup.ts +507 -0
  72. package/src/native-write-through-block-store.ts +1170 -0
  73. package/src/peer-session.ts +336 -0
  74. package/src/replication-announcement.ts +744 -0
  75. package/src/replicator-liveness.ts +439 -0
  76. package/src/sync/dispatch-lifecycle.ts +230 -0
  77. package/src/sync/factory.ts +154 -0
  78. package/src/sync/pending-sync-store.ts +1085 -0
  79. package/src/sync/rateless-iblt.ts +122 -132
  80. package/src/sync/simple.ts +334 -1144
  81. package/src/sync/sync-peer-state.ts +107 -0
@@ -0,0 +1,989 @@
1
+ import { logger as loggerFn } from "@peerbit/logger";
2
+ import { NativeDurableCommitError } from "./errors.js";
3
+ const warn = loggerFn("peerbit:shared-log").newScope("warn");
4
+ const isPromiseLike = (value) => !!value && typeof value.then === "function";
5
+ /**
6
+ * Write-through block store bridging the native backbone's in-wasm-memory block
7
+ * store to a durable per-program {@link AnyBlockStore}.
8
+ *
9
+ * WHY: when the native backbone is active the log's entry blocks live only in
10
+ * the native wasm block map (`NativeBackboneBlockStore.persisted() === false`).
11
+ * On a restart that map is empty, so the native graph cannot reload heads the
12
+ * durable heads index still lists ("Failed to load entry from head"). This
13
+ * wrapper mirrors every write into the same durable `blocks` sublevel the
14
+ * non-native path uses. On a native miss, reads fall through to durable storage
15
+ * and lazily repopulate the wasm map so the native graph can walk the DAG again.
16
+ *
17
+ * The native store stays the authoritative hot store the native graph reads
18
+ * from: reads hit native first and only fall back to durable (repopulating
19
+ * native on a hit so subsequent native-graph reads succeed).
20
+ *
21
+ * METHOD SURFACE (see #1006): `RemoteBlocks` and the log feature-detect the
22
+ * optional batch methods (`putMany`/`putKnown`/`putKnownMany`/
23
+ * `putKnownManyColumns`/`rmMany`). To keep the receive-fusion / columnar fast
24
+ * paths engaged this wrapper preserves the native store's optional write
25
+ * methods — including `putKnownManyColumns`, which `AnyBlockStore` does not have
26
+ * — and delegates each. It adds only local durability/trim coordination hooks
27
+ * consumed by `RemoteBlocks` and the log; protocol/native-handle capabilities
28
+ * such as `getBlockResponsePayload`/`getNativeLogBlockStoreHandle` stay absent,
29
+ * so their optional-chained probes keep the existing fallback behavior.
30
+ */
31
+ export class NativeBackboneWriteThroughBlockStore {
32
+ native;
33
+ durable;
34
+ constructor(native, durable) {
35
+ this.native = native;
36
+ this.durable = durable;
37
+ }
38
+ // A durable mirror write that cannot be awaited at its call site (the
39
+ // columnar putKnownManyColumns fast path must return a synchronous string[]
40
+ // because RemoteBlocks.putKnownManyColumns treats the result as sync) is
41
+ // tracked here instead of being silently `void`ed. Its rejection is stored
42
+ // and re-thrown on the next awaited wrapper method (and on stop()), so a
43
+ // failed durable write (IO/disk-full) surfaces as an error rather than
44
+ // vanishing and leaving the block out of durable while native/log report
45
+ // success. `.catch` also prevents unhandled-rejection noise.
46
+ pendingDurableWrites = new Set();
47
+ nativeDurableCommitFailure;
48
+ stopCompleted = false;
49
+ nativeDeleteTombstones = new Map();
50
+ nativeDeleteEpoch = 0;
51
+ nativeBlockWriteGenerations = new Map();
52
+ pendingNativeDeleteCleanup = new Map();
53
+ stagedNativeDeleteCleanups = new Map();
54
+ nextNativeDeleteCleanupToken = 0;
55
+ nativeDeleteCleanupRunning;
56
+ nextNativeCommitOwnershipToken = 0;
57
+ nativeCommitOwnerships = new Map();
58
+ nativeCommitOwnershipsByCid = new Map();
59
+ getNativeDurableCommitFailure() {
60
+ return this.nativeDurableCommitFailure;
61
+ }
62
+ recordNativeDurableCommitFailure(cause, options) {
63
+ if (this.nativeDurableCommitFailure) {
64
+ this.nativeDurableCommitFailure.addCommitContext(options);
65
+ return this.nativeDurableCommitFailure;
66
+ }
67
+ this.nativeDurableCommitFailure =
68
+ cause instanceof NativeDurableCommitError
69
+ ? cause
70
+ : new NativeDurableCommitError(cause, options);
71
+ if (cause instanceof NativeDurableCommitError) {
72
+ cause.addCommitContext(options);
73
+ }
74
+ return this.nativeDurableCommitFailure;
75
+ }
76
+ throwIfNativeDurableCommitFailed() {
77
+ if (this.nativeDurableCommitFailure) {
78
+ throw this.nativeDurableCommitFailure;
79
+ }
80
+ }
81
+ throwIfDurableWritesFailed() {
82
+ this.throwIfNativeDurableCommitFailed();
83
+ }
84
+ async commitDurableMutation(operation, committedCids, failedCids) {
85
+ this.throwIfNativeDurableCommitFailed();
86
+ const committedCidList = [...committedCids];
87
+ const failedCidList = failedCids ? [...failedCids] : committedCidList;
88
+ let result;
89
+ const operationResult = Promise.resolve().then(operation);
90
+ this.trackAwaitedDurable(operationResult);
91
+ try {
92
+ result = await operationResult;
93
+ }
94
+ catch (error) {
95
+ throw this.recordNativeDurableCommitFailure(error, {
96
+ committedCids: committedCidList,
97
+ failedCids: failedCidList,
98
+ });
99
+ }
100
+ // A different concurrent native mutation may have poisoned the wrapper
101
+ // while this durable call was in flight. Include this operation among the
102
+ // native-applied facts, but not among durable calls that actually failed.
103
+ if (this.nativeDurableCommitFailure) {
104
+ this.nativeDurableCommitFailure.addCommitContext({
105
+ committedCids: committedCidList,
106
+ failedCids: [],
107
+ });
108
+ throw this.nativeDurableCommitFailure;
109
+ }
110
+ return result;
111
+ }
112
+ beginNativeDelete(cids) {
113
+ this.nativeDeleteEpoch++;
114
+ for (const cid of cids) {
115
+ this.nativeDeleteTombstones.set(cid, (this.nativeDeleteTombstones.get(cid) ?? 0) + 1);
116
+ }
117
+ }
118
+ endNativeDelete(cids) {
119
+ for (const cid of cids) {
120
+ const remaining = (this.nativeDeleteTombstones.get(cid) ?? 1) - 1;
121
+ if (remaining <= 0) {
122
+ this.nativeDeleteTombstones.delete(cid);
123
+ }
124
+ else {
125
+ this.nativeDeleteTombstones.set(cid, remaining);
126
+ }
127
+ }
128
+ }
129
+ isNativeDeletePending(cid) {
130
+ return this.nativeDeleteTombstones.has(cid);
131
+ }
132
+ // A CID can be legitimately re-added after a native trim (content addressing
133
+ // makes the bytes identical, but its liveness is new). Cancel any queued trim
134
+ // for that CID and advance its generation before the write is exposed. An
135
+ // already-running cleanup uses the generation/pending map to avoid deleting
136
+ // the new native value; synchronous columnar writes also chain their durable
137
+ // mirror behind that cleanup below.
138
+ noteNativeBlockWrite(cids) {
139
+ const generations = new Map();
140
+ for (const cid of new Set(cids)) {
141
+ const generation = (this.nativeBlockWriteGenerations.get(cid) ?? 0) + 1;
142
+ this.nativeBlockWriteGenerations.set(cid, generation);
143
+ generations.set(cid, generation);
144
+ if (this.pendingNativeDeleteCleanup.delete(cid)) {
145
+ this.endNativeDelete([cid]);
146
+ }
147
+ for (const staged of this.stagedNativeDeleteCleanups.values()) {
148
+ if (staged.delete(cid)) {
149
+ this.endNativeDelete([cid]);
150
+ }
151
+ }
152
+ }
153
+ return generations;
154
+ }
155
+ beginNativeCommitOwnership(generations) {
156
+ if (generations.size === 0) {
157
+ return undefined;
158
+ }
159
+ const token = {
160
+ id: ++this.nextNativeCommitOwnershipToken,
161
+ rows: new Map(),
162
+ };
163
+ for (const [cid, generation] of generations) {
164
+ const owners = this.nativeCommitOwnershipsByCid.get(cid) ?? new Set();
165
+ const shared = owners.size > 0;
166
+ for (const ownerId of owners) {
167
+ const owner = this.nativeCommitOwnerships.get(ownerId);
168
+ const row = owner?.rows.get(cid);
169
+ if (row)
170
+ row.shared = true;
171
+ }
172
+ owners.add(token.id);
173
+ this.nativeCommitOwnershipsByCid.set(cid, owners);
174
+ token.rows.set(cid, { generation, shared });
175
+ }
176
+ this.nativeCommitOwnerships.set(token.id, token);
177
+ return token;
178
+ }
179
+ releaseNativeCommitOwnership(token) {
180
+ if (!token ||
181
+ typeof token !== "object" ||
182
+ typeof token.id !== "number") {
183
+ return;
184
+ }
185
+ const owned = this.nativeCommitOwnerships.get(token.id);
186
+ if (owned !== token) {
187
+ return;
188
+ }
189
+ this.nativeCommitOwnerships.delete(owned.id);
190
+ for (const cid of owned.rows.keys()) {
191
+ const owners = this.nativeCommitOwnershipsByCid.get(cid);
192
+ owners?.delete(owned.id);
193
+ if (owners?.size === 0) {
194
+ this.nativeCommitOwnershipsByCid.delete(cid);
195
+ }
196
+ }
197
+ }
198
+ acknowledgeNativeCommitOwnership(token) {
199
+ this.releaseNativeCommitOwnership(token);
200
+ }
201
+ enqueueNativeDeleteCleanup(cids) {
202
+ for (const cid of new Set(cids)) {
203
+ if (!this.pendingNativeDeleteCleanup.has(cid)) {
204
+ this.pendingNativeDeleteCleanup.set(cid, this.nativeBlockWriteGenerations.get(cid) ?? 0);
205
+ this.beginNativeDelete([cid]);
206
+ }
207
+ }
208
+ }
209
+ releaseStagedNativeDeleteCleanup(token) {
210
+ const staged = this.stagedNativeDeleteCleanups.get(token);
211
+ if (!staged) {
212
+ return false;
213
+ }
214
+ this.stagedNativeDeleteCleanups.delete(token);
215
+ for (const [cid] of staged) {
216
+ this.endNativeDelete([cid]);
217
+ }
218
+ return true;
219
+ }
220
+ discardStagedNativeDeleteCleanups() {
221
+ for (const token of [...this.stagedNativeDeleteCleanups.keys()]) {
222
+ this.releaseStagedNativeDeleteCleanup(token);
223
+ }
224
+ }
225
+ // Native commit callbacks call this immediately after the native transaction
226
+ // returns, before awaiting the new block's durable mirror. This stages read
227
+ // tombstones only. Durable deletion is promoted later by the exact EntryIndex
228
+ // consume token, so an unacknowledged/failed append cannot delete the old
229
+ // durable head that the lower log still publishes.
230
+ beginNativeDeleteCleanup(cids) {
231
+ this.throwIfNativeDurableCommitFailed();
232
+ const uniqueCids = [...new Set(cids)];
233
+ if (uniqueCids.length === 0) {
234
+ return undefined;
235
+ }
236
+ const token = ++this.nextNativeDeleteCleanupToken;
237
+ const staged = new Map(uniqueCids.map((cid) => [
238
+ cid,
239
+ this.nativeBlockWriteGenerations.get(cid) ?? 0,
240
+ ]));
241
+ this.stagedNativeDeleteCleanups.set(token, staged);
242
+ this.beginNativeDelete(uniqueCids);
243
+ return token;
244
+ }
245
+ cancelNativeDeleteCleanup(cleanupToken) {
246
+ if (typeof cleanupToken === "number") {
247
+ this.releaseStagedNativeDeleteCleanup(cleanupToken);
248
+ }
249
+ }
250
+ async waitForNativeDeleteCleanup() {
251
+ while (this.nativeDeleteCleanupRunning) {
252
+ await this.nativeDeleteCleanupRunning;
253
+ }
254
+ }
255
+ async waitForTrackedDurableWrites() {
256
+ while (this.pendingDurableWrites.size > 0) {
257
+ await Promise.allSettled([...this.pendingDurableWrites]);
258
+ }
259
+ }
260
+ async retryNativeDeleteCleanup(options) {
261
+ await this.waitForNativeDeleteCleanup();
262
+ if (this.nativeDurableCommitFailure && !options?.allowPoisoned) {
263
+ throw this.nativeDurableCommitFailure;
264
+ }
265
+ // A synchronous columnar write may already have scheduled its durable
266
+ // mirror. Let it settle before deleting queued CIDs, but leave any recorded
267
+ // error for drainDurable() to surface to its owning operation/stop.
268
+ await this.waitForTrackedDurableWrites();
269
+ await this.waitForNativeDeleteCleanup();
270
+ // A tracked mirror or the cleanup we just waited for may have poisoned the
271
+ // wrapper. Do not begin another durable mutation on the ordinary path after
272
+ // that asynchronous boundary. stop() alone opts into one cleanup retry so a
273
+ // transient delete failure can still release its tombstones and resources.
274
+ if (this.nativeDurableCommitFailure && !options?.allowPoisoned) {
275
+ throw this.nativeDurableCommitFailure;
276
+ }
277
+ if (this.pendingNativeDeleteCleanup.size === 0) {
278
+ return;
279
+ }
280
+ const cleanupEntries = [...this.pendingNativeDeleteCleanup].filter(([cid, generation]) => (this.nativeBlockWriteGenerations.get(cid) ?? 0) === generation);
281
+ if (cleanupEntries.length === 0) {
282
+ return;
283
+ }
284
+ const cids = cleanupEntries.map(([cid]) => cid);
285
+ let cleanupFailure;
286
+ const running = (async () => {
287
+ let durableRemoved = false;
288
+ let nativeRemoved = false;
289
+ try {
290
+ await this.durable.rmMany(cids);
291
+ durableRemoved = true;
292
+ }
293
+ catch (error) {
294
+ cleanupFailure = error;
295
+ // The new entry blocks are already durable at this point and the native
296
+ // transaction has selected the new graph/index state. Old content-addressed
297
+ // blocks are therefore harmless unreachable orphans. Keep this cleanup as
298
+ // retryable debt instead of poisoning/rolling back a fully durable append;
299
+ // a partial rmMany is safe for the same reason.
300
+ warn(`Failed durable native-trim cleanup; retaining retry debt: ${String(error)}`);
301
+ }
302
+ finally {
303
+ // A read that began in the native-transaction -> cleanup-hook gap may
304
+ // have repopulated native. Always repeat the native removal, even when
305
+ // durable rm failed. Exclude CIDs re-added while durable IO was pending;
306
+ // their generation change cancelled the queued delete.
307
+ const stillDeleted = cids.filter((cid) => this.pendingNativeDeleteCleanup.has(cid));
308
+ try {
309
+ if (stillDeleted.length > 0) {
310
+ await this.native.rmMany(stillDeleted);
311
+ }
312
+ nativeRemoved = true;
313
+ }
314
+ catch (error) {
315
+ cleanupFailure ??= error;
316
+ warn(`Failed to repeat native-trim hot block removal: ${String(error)}`);
317
+ }
318
+ }
319
+ if (durableRemoved && nativeRemoved) {
320
+ for (const [cid, generation] of cleanupEntries) {
321
+ if (this.pendingNativeDeleteCleanup.get(cid) === generation) {
322
+ this.pendingNativeDeleteCleanup.delete(cid);
323
+ this.nativeBlockWriteGenerations.delete(cid);
324
+ this.endNativeDelete([cid]);
325
+ }
326
+ }
327
+ }
328
+ })();
329
+ this.nativeDeleteCleanupRunning = running;
330
+ try {
331
+ await running;
332
+ }
333
+ finally {
334
+ if (this.nativeDeleteCleanupRunning === running) {
335
+ this.nativeDeleteCleanupRunning = undefined;
336
+ }
337
+ }
338
+ if (options?.throwOnFailure && cleanupFailure !== undefined) {
339
+ throw cleanupFailure;
340
+ }
341
+ }
342
+ trackDurable(result, cids) {
343
+ // The durable store may answer synchronously (an in-memory or already
344
+ // resolved store); only a real pending promise needs tracking. A sync
345
+ // success is already durable; a sync throw would have propagated already.
346
+ if (!isPromiseLike(result)) {
347
+ return;
348
+ }
349
+ const tracked = Promise.resolve(result).then(() => {
350
+ this.pendingDurableWrites.delete(tracked);
351
+ }, (error) => {
352
+ this.pendingDurableWrites.delete(tracked);
353
+ this.recordNativeDurableCommitFailure(error, {
354
+ committedCids: cids,
355
+ failedCids: cids,
356
+ });
357
+ });
358
+ this.pendingDurableWrites.add(tracked);
359
+ }
360
+ // Awaited mirror writes surface their own rejection to the append that
361
+ // created them. Track a non-rejecting settlement companion as well so stop()
362
+ // and later wrapper operations cannot close/use durable storage while that
363
+ // append barrier is still in flight.
364
+ trackAwaitedDurable(result) {
365
+ const tracked = result.then(() => {
366
+ this.pendingDurableWrites.delete(tracked);
367
+ }, () => {
368
+ this.pendingDurableWrites.delete(tracked);
369
+ });
370
+ this.pendingDurableWrites.add(tracked);
371
+ }
372
+ // Wait for every tracked (sync-path) durable write to settle, then surface
373
+ // the first failure. Awaited methods call this so a prior columnar durable
374
+ // failure propagates as back-pressure to the next caller.
375
+ async drainDurable() {
376
+ await this.waitForTrackedDurableWrites();
377
+ this.throwIfNativeDurableCommitFailed();
378
+ }
379
+ // --- lifecycle -------------------------------------------------------
380
+ // The native store's lifecycle hooks are no-ops; only the durable store
381
+ // needs starting/stopping. The wasm map is NOT eagerly rehydrated from disk:
382
+ // entry blocks are pulled back lazily on demand through the read fallback in
383
+ // getMany()/get() (durable hit -> repopulate the wasm map), which is what the
384
+ // log's DAG walk (EntryIndex.resolveMany -> store.getMany) exercises. Keeping
385
+ // the wasm map cold on open is required by the strict-native resident
386
+ // coordinate-state optimization: a reopened non-replicating native node must
387
+ // report hasBlock(head) === false and answer a same-signer append from the
388
+ // persisted coordinate + signer facts without resolving the entry block.
389
+ async start() {
390
+ await this.durable.start();
391
+ this.stopCompleted = false;
392
+ }
393
+ async stop() {
394
+ if (this.stopCompleted) {
395
+ return;
396
+ }
397
+ // Surface any tracked (sync-path) durable write failure and ensure all
398
+ // mirror writes have settled before the durable store is torn down. Closing
399
+ // the durable store is unconditional: a prior columnar mirror failure must
400
+ // not leak the store lifecycle resource.
401
+ let firstError;
402
+ let shutdownError;
403
+ try {
404
+ await this.drainDurable();
405
+ }
406
+ catch (error) {
407
+ firstError = error;
408
+ }
409
+ // Tokens not consumed by EntryIndex belong to native prepares that never
410
+ // published their lower-log trim. Release their read tombstones, but never
411
+ // promote them to durable deletion during shutdown.
412
+ this.discardStagedNativeDeleteCleanups();
413
+ try {
414
+ await this.retryNativeDeleteCleanup({ allowPoisoned: true });
415
+ }
416
+ catch (error) {
417
+ firstError ??= error;
418
+ shutdownError ??= error;
419
+ }
420
+ try {
421
+ await this.durable.stop();
422
+ }
423
+ catch (error) {
424
+ firstError ??= error;
425
+ shutdownError ??= error;
426
+ }
427
+ if (shutdownError === undefined) {
428
+ // A durable poison belongs to the generation being closed. Report it once
429
+ // so the owning terminal call observes the failed append, but remember that
430
+ // every mandatory shutdown stage completed. Conservative content-addressed
431
+ // trim debt may remain in this retired wrapper after best-effort cleanup; a
432
+ // fresh generation gets a new wrapper and must not be wedged by that debt.
433
+ // The exact terminal retry may therefore finish parent bookkeeping without
434
+ // rethrowing the same latched poison forever.
435
+ this.stopCompleted = true;
436
+ }
437
+ if (firstError !== undefined) {
438
+ throw firstError;
439
+ }
440
+ }
441
+ status() {
442
+ return this.durable.status();
443
+ }
444
+ waitFor() {
445
+ return Promise.resolve([]);
446
+ }
447
+ // Native commit APIs must keep their block-store callback synchronous. The
448
+ // lower log calls this barrier after that callback reports committed blocks
449
+ // and before publishing index/head facts.
450
+ waitForDurableWrites() {
451
+ return this.drainDurable();
452
+ }
453
+ // Mirror a single already-committed block (present in the wasm map) to the
454
+ // durable store ONLY. Used by the native commit-only append fast path: the
455
+ // native prepare commits the entry block into the wasm map and returns no raw
456
+ // bytes, and the strict-native resident-coordinate path deliberately does NOT
457
+ // route the block through the log's finishBlocks/putKnown* (that would disturb
458
+ // the commit-only append path the RCS optimization depends on). Instead the
459
+ // caller reads the committed bytes back and calls this so the block lands in
460
+ // durable directly. The caller awaits this method before acknowledging its
461
+ // append, so a failed write rejects the append that produced the block.
462
+ async mirrorToDurable(cid, bytes, options) {
463
+ this.throwIfNativeDurableCommitFailed();
464
+ // The native commit happened before this call. Mark the CID live before
465
+ // awaiting anything so a concurrent retry of an older trim cannot remove
466
+ // the newly committed hot block.
467
+ let ownership;
468
+ if (options?.nativeTrimmed !== true) {
469
+ ownership = this.beginNativeCommitOwnership(this.noteNativeBlockWrite([cid]));
470
+ await this.waitForNativeDeleteCleanup();
471
+ }
472
+ try {
473
+ await this.drainDurable();
474
+ if (ownership) {
475
+ const existed = await this.durable.hasMany([...ownership.rows.keys()]);
476
+ let index = 0;
477
+ for (const row of ownership.rows.values()) {
478
+ row.durableExistedBefore = existed[index++] === true;
479
+ }
480
+ }
481
+ const result = this.commitDurableMutation(() => this.durable.putKnown(cid, bytes), [cid]);
482
+ await result;
483
+ await this.retryNativeDeleteCleanup();
484
+ this.throwIfNativeDurableCommitFailed();
485
+ return ownership;
486
+ }
487
+ catch (error) {
488
+ // The caller never receives an ownership token for an indeterminate write.
489
+ // Release the in-memory claim and preserve any bytes the backend may have
490
+ // applied before rejecting.
491
+ this.releaseNativeCommitOwnership(ownership);
492
+ throw error;
493
+ }
494
+ }
495
+ async mirrorManyToDurable(blocks, options) {
496
+ this.throwIfNativeDurableCommitFailed();
497
+ const cids = blocks.map(([cid]) => cid);
498
+ // Rows trimmed later in the same native batch deliberately remain owned by
499
+ // that batch's staged cleanup; only mark surviving rows live.
500
+ const liveCids = cids.filter((cid) => !options?.nativeTrimmedCids?.has(cid));
501
+ const ownership = this.beginNativeCommitOwnership(this.noteNativeBlockWrite(liveCids));
502
+ if (liveCids.length > 0) {
503
+ await this.waitForNativeDeleteCleanup();
504
+ }
505
+ try {
506
+ await this.drainDurable();
507
+ if (ownership) {
508
+ const existed = await this.durable.hasMany([...ownership.rows.keys()]);
509
+ let index = 0;
510
+ for (const row of ownership.rows.values()) {
511
+ row.durableExistedBefore = existed[index++] === true;
512
+ }
513
+ }
514
+ if (blocks.length > 0) {
515
+ await this.commitDurableMutation(() => this.durable.putKnownMany(blocks), cids);
516
+ }
517
+ await this.retryNativeDeleteCleanup();
518
+ this.throwIfNativeDurableCommitFailed();
519
+ return ownership;
520
+ }
521
+ catch (error) {
522
+ this.releaseNativeCommitOwnership(ownership);
523
+ throw error;
524
+ }
525
+ }
526
+ async rollbackFailedNativeCommits(cids, restoreNativeCids = [], ownershipToken) {
527
+ // This is the sole mutation allowed after poison: it removes a native
528
+ // transaction that the lower log never published. Do not route through the
529
+ // guarded rmMany path, and settle the failed mirror before compensating it.
530
+ await this.waitForTrackedDurableWrites();
531
+ // A failed replacement never published its trim. Reassert the restored CIDs
532
+ // as live before any compensation IO so staged/pending delete intents cannot
533
+ // remove the last acknowledged blocks during stop or reopen.
534
+ const ownership = ownershipToken && typeof ownershipToken === "object"
535
+ ? this.nativeCommitOwnerships.get(ownershipToken.id)
536
+ : undefined;
537
+ const verifiedOwnership = ownership && ownership === ownershipToken ? ownership : undefined;
538
+ const restoreSet = new Set(restoreNativeCids);
539
+ const safeDurableDeletes = verifiedOwnership
540
+ ? [...new Set(cids)].filter((cid) => {
541
+ const row = verifiedOwnership.rows.get(cid);
542
+ const owners = this.nativeCommitOwnershipsByCid.get(cid);
543
+ return (!restoreSet.has(cid) &&
544
+ row?.durableExistedBefore === false &&
545
+ row.shared === false &&
546
+ (this.nativeBlockWriteGenerations.get(cid) ?? 0) ===
547
+ row.generation &&
548
+ owners?.size === 1 &&
549
+ owners.has(verifiedOwnership.id));
550
+ })
551
+ : [];
552
+ this.noteNativeBlockWrite(restoreNativeCids);
553
+ let firstError;
554
+ try {
555
+ if (safeDurableDeletes.length > 0) {
556
+ await this.durable.rmMany(safeDurableDeletes);
557
+ }
558
+ }
559
+ catch (error) {
560
+ firstError = error;
561
+ }
562
+ // A native prepare runs before ownership can observe the hot map, so it cannot
563
+ // prove that a CID was absent there before this operation. Keep native bytes as
564
+ // unreachable orphans rather than deleting acknowledged/shared/restored data.
565
+ if (restoreNativeCids.length > 0) {
566
+ try {
567
+ const values = await this.durable.getMany(restoreNativeCids);
568
+ const restore = [];
569
+ for (let index = 0; index < restoreNativeCids.length; index++) {
570
+ const value = values[index];
571
+ if (value)
572
+ restore.push([restoreNativeCids[index], value]);
573
+ }
574
+ if (restore.length > 0) {
575
+ this.native.putKnownMany(restore);
576
+ }
577
+ }
578
+ catch (error) {
579
+ firstError ??= error;
580
+ }
581
+ }
582
+ this.releaseNativeCommitOwnership(ownershipToken);
583
+ if (firstError !== undefined)
584
+ throw firstError;
585
+ }
586
+ /**
587
+ * Compensate a native prepare that failed before its durable mirror began.
588
+ * Durable presence proves a same-CID acknowledged owner; an active ownership
589
+ * token proves a concurrent mirror. Only an unowned, non-durable hot block is
590
+ * exclusively attributable to the failed prepare and safe to remove.
591
+ */
592
+ async rollbackUnmirroredNativeCommits(cids, restoreNativeCids = []) {
593
+ const unique = [...new Set(cids)];
594
+ // Native prepares bypass this wrapper. Any observed wrapper generation is
595
+ // therefore evidence of a generic/same-CID writer, not of the failed prepare.
596
+ // Snapshot before the first await and require both absence and stability so a
597
+ // write starting before or during durable.hasMany cannot lose its hot value to
598
+ // a stale `false` result.
599
+ const genericWriteGenerations = new Map(unique.map((cid) => [cid, this.nativeBlockWriteGenerations.get(cid)]));
600
+ await this.rollbackFailedNativeCommits(cids, restoreNativeCids);
601
+ const durablePresence = await this.durable.hasMany(unique);
602
+ const restore = new Set(restoreNativeCids);
603
+ const safeNativeDeletes = unique.filter((cid, index) => !restore.has(cid) &&
604
+ durablePresence[index] !== true &&
605
+ genericWriteGenerations.get(cid) === undefined &&
606
+ this.nativeBlockWriteGenerations.get(cid) === undefined &&
607
+ (this.nativeCommitOwnershipsByCid.get(cid)?.size ?? 0) === 0);
608
+ if (safeNativeDeletes.length > 0) {
609
+ await this.native.rmMany(safeNativeDeletes);
610
+ }
611
+ }
612
+ // --- writes (apply to BOTH: native first for the hot path, then durable) ---
613
+ async put(data) {
614
+ await this.drainDurable();
615
+ const cid = await this.native.put(data);
616
+ // The native store computes a raw-codec CID for a `Uint8Array`, storing
617
+ // the bytes verbatim (raw codec is identity), and stores `block.bytes`
618
+ // for the pre-CIDed object form. Either way the input bytes match what
619
+ // native stored, so feed durable the known cid+bytes without recomputing.
620
+ const value = data instanceof Uint8Array
621
+ ? data
622
+ : data.block.bytes;
623
+ this.noteNativeBlockWrite([cid]);
624
+ await this.waitForNativeDeleteCleanup();
625
+ // put() may have yielded while calculating the CID. Restore the hot value
626
+ // after any older cleanup that was already in flight.
627
+ this.throwIfNativeDurableCommitFailed();
628
+ this.native.putKnown(cid, value);
629
+ await this.commitDurableMutation(() => this.durable.putKnown(cid, value), [cid]);
630
+ return cid;
631
+ }
632
+ async putMany(blocks) {
633
+ await this.drainDurable();
634
+ const cids = await this.native.putMany(blocks);
635
+ const durableBlocks = cids.map((cid, index) => {
636
+ const block = blocks[index];
637
+ const value = block instanceof Uint8Array
638
+ ? block
639
+ : block.block.bytes;
640
+ return [cid, value];
641
+ });
642
+ this.noteNativeBlockWrite(cids);
643
+ await this.waitForNativeDeleteCleanup();
644
+ this.throwIfNativeDurableCommitFailed();
645
+ this.native.putKnownMany(durableBlocks);
646
+ await this.commitDurableMutation(() => this.durable.putKnownMany(durableBlocks), cids);
647
+ return cids;
648
+ }
649
+ // Native put is synchronous (the authoritative hot store); the durable mirror
650
+ // is awaited so the returned promise resolves only after BOTH native and
651
+ // durable succeed and a durable IO/disk-full failure rejects here instead of
652
+ // being swallowed. RemoteBlocks.putKnown and the log's putKnownEntryBytesBatch
653
+ // both await this method, so returning a promise is compatible.
654
+ async putKnown(cid, bytes) {
655
+ await this.drainDurable();
656
+ await this.waitForNativeDeleteCleanup();
657
+ this.throwIfNativeDurableCommitFailed();
658
+ const stored = this.native.putKnown(cid, bytes);
659
+ this.noteNativeBlockWrite([cid]);
660
+ await this.commitDurableMutation(() => this.durable.putKnown(cid, bytes), [cid]);
661
+ return stored;
662
+ }
663
+ async putKnownMany(blocks) {
664
+ await this.drainDurable();
665
+ await this.waitForNativeDeleteCleanup();
666
+ this.throwIfNativeDurableCommitFailed();
667
+ const cids = this.native.putKnownMany(blocks);
668
+ this.noteNativeBlockWrite(cids);
669
+ await this.commitDurableMutation(() => this.durable.putKnownMany(blocks), cids);
670
+ return cids;
671
+ }
672
+ putKnownManyColumns(cids, bytes) {
673
+ this.throwIfNativeDurableCommitFailed();
674
+ if (cids.length !== bytes.length) {
675
+ throw new Error("Expected equal block column lengths");
676
+ }
677
+ const cleanupBarrier = this.nativeDeleteCleanupRunning;
678
+ const stored = this.native.putKnownManyColumns(cids, bytes);
679
+ this.noteNativeBlockWrite(cids);
680
+ // AnyBlockStore has no columnar method; mirror via putKnownMany, which
681
+ // takes [cid, bytes] tuples and hits the same batched store path.
682
+ // This method must return a synchronous string[] (RemoteBlocks.putKnownManyColumns
683
+ // consumes the result synchronously), so the durable write cannot be awaited
684
+ // inline. Track it instead of `void`ing it so a durable rejection surfaces
685
+ // on the next awaited wrapper method / stop() rather than being swallowed.
686
+ const durableBlocks = cids.map((cid, index) => [cid, bytes[index]]);
687
+ let durableResult;
688
+ try {
689
+ durableResult = cleanupBarrier
690
+ ? cleanupBarrier.then(() => this.durable.putKnownMany(durableBlocks))
691
+ : this.durable.putKnownMany(durableBlocks);
692
+ }
693
+ catch (error) {
694
+ throw this.recordNativeDurableCommitFailure(error, {
695
+ committedCids: cids,
696
+ failedCids: cids,
697
+ });
698
+ }
699
+ this.trackDurable(durableResult, cids);
700
+ return stored;
701
+ }
702
+ // --- reads (native/wasm first; on miss, durable fallback + repopulate) ---
703
+ // RemoteBlocks.get awaits this single-get, so on a native miss consult the
704
+ // durable store (like getMany does) and repopulate the native map, rather
705
+ // than returning undefined and only scheduling a background repopulate. That
706
+ // avoids a spurious miss (which would otherwise fall through to a remote
707
+ // read) for a block that is present on disk.
708
+ async get(cid, _options) {
709
+ if (this.nativeDurableCommitFailure) {
710
+ // After poison, durable storage is the last acknowledged authority. Do not
711
+ // repopulate or otherwise mutate the native store until reopen.
712
+ return this.isNativeDeletePending(cid)
713
+ ? undefined
714
+ : this.durable.get(cid);
715
+ }
716
+ const deleteEpoch = this.nativeDeleteEpoch;
717
+ if (this.isNativeDeletePending(cid)) {
718
+ return undefined;
719
+ }
720
+ const local = this.native.get(cid);
721
+ if (local != null) {
722
+ return deleteEpoch === this.nativeDeleteEpoch ? local : this.get(cid);
723
+ }
724
+ const durableValue = await this.durable.get(cid);
725
+ if (this.nativeDurableCommitFailure) {
726
+ return this.isNativeDeletePending(cid) ? undefined : durableValue;
727
+ }
728
+ if (deleteEpoch !== this.nativeDeleteEpoch) {
729
+ return this.get(cid);
730
+ }
731
+ if (durableValue != null) {
732
+ // Repopulate the native map so the native graph reads it next time.
733
+ this.native.putKnownManyColumns([cid], [durableValue]);
734
+ return durableValue;
735
+ }
736
+ return undefined;
737
+ }
738
+ async getMany(cids) {
739
+ if (this.nativeDurableCommitFailure) {
740
+ const values = await this.durable.getMany(cids);
741
+ for (let index = 0; index < cids.length; index++) {
742
+ if (this.isNativeDeletePending(cids[index])) {
743
+ values[index] = undefined;
744
+ }
745
+ }
746
+ return values;
747
+ }
748
+ const deleteEpoch = this.nativeDeleteEpoch;
749
+ const results = await this.native.getMany(cids);
750
+ if (deleteEpoch !== this.nativeDeleteEpoch) {
751
+ return this.getMany(cids);
752
+ }
753
+ const missing = [];
754
+ const missingIndexes = [];
755
+ for (let i = 0; i < results.length; i++) {
756
+ if (this.isNativeDeletePending(cids[i])) {
757
+ results[i] = undefined;
758
+ }
759
+ else if (results[i] == null) {
760
+ missing.push(cids[i]);
761
+ missingIndexes.push(i);
762
+ }
763
+ }
764
+ if (missing.length === 0) {
765
+ return results;
766
+ }
767
+ const durableValues = await this.durable.getMany(missing);
768
+ if (this.nativeDurableCommitFailure) {
769
+ const values = await this.durable.getMany(cids);
770
+ for (let index = 0; index < cids.length; index++) {
771
+ if (this.isNativeDeletePending(cids[index])) {
772
+ values[index] = undefined;
773
+ }
774
+ }
775
+ return values;
776
+ }
777
+ if (deleteEpoch !== this.nativeDeleteEpoch) {
778
+ return this.getMany(cids);
779
+ }
780
+ const repopulateCids = [];
781
+ const repopulateBytes = [];
782
+ for (let missingIndex = 0; missingIndex < missingIndexes.length; missingIndex++) {
783
+ const i = missingIndexes[missingIndex];
784
+ const value = durableValues[missingIndex];
785
+ if (value != null) {
786
+ results[i] = value;
787
+ repopulateCids.push(cids[i]);
788
+ repopulateBytes.push(value);
789
+ }
790
+ }
791
+ if (repopulateCids.length > 0) {
792
+ // Repopulate the native map so the native graph sees these blocks.
793
+ this.native.putKnownManyColumns(repopulateCids, repopulateBytes);
794
+ }
795
+ return results;
796
+ }
797
+ async has(cid) {
798
+ if (this.nativeDurableCommitFailure) {
799
+ return this.isNativeDeletePending(cid) ? false : this.durable.has(cid);
800
+ }
801
+ const deleteEpoch = this.nativeDeleteEpoch;
802
+ if (this.isNativeDeletePending(cid)) {
803
+ return false;
804
+ }
805
+ if (this.native.has(cid)) {
806
+ return deleteEpoch === this.nativeDeleteEpoch ? true : this.has(cid);
807
+ }
808
+ // Mirror getMany/hasMany: a block absent from the native wasm map may still
809
+ // be present in the durable store (e.g. persisted on disk but not yet
810
+ // repopulated into wasm). Consult durable on a native miss so presence
811
+ // checks agree with the resolves that getMany/hasMany already durable-fall
812
+ // back on. `Blocks.has` is declared `MaybePromise<boolean>`, so returning a
813
+ // promise here is contract-compatible.
814
+ const durableHas = await this.durable.has(cid);
815
+ return deleteEpoch === this.nativeDeleteEpoch ? durableHas : this.has(cid);
816
+ }
817
+ async hasMany(cids) {
818
+ if (this.nativeDurableCommitFailure) {
819
+ const values = await this.durable.hasMany(cids);
820
+ for (let index = 0; index < cids.length; index++) {
821
+ if (this.isNativeDeletePending(cids[index])) {
822
+ values[index] = false;
823
+ }
824
+ }
825
+ return values;
826
+ }
827
+ const deleteEpoch = this.nativeDeleteEpoch;
828
+ const nativeHas = await this.native.hasMany(cids);
829
+ if (deleteEpoch !== this.nativeDeleteEpoch) {
830
+ return this.hasMany(cids);
831
+ }
832
+ const missing = [];
833
+ const missingIndexes = [];
834
+ for (let i = 0; i < nativeHas.length; i++) {
835
+ if (this.isNativeDeletePending(cids[i])) {
836
+ nativeHas[i] = false;
837
+ }
838
+ else if (!nativeHas[i]) {
839
+ missing.push(cids[i]);
840
+ missingIndexes.push(i);
841
+ }
842
+ }
843
+ if (missing.length === 0) {
844
+ return nativeHas;
845
+ }
846
+ const durableHas = await this.durable.hasMany(missing);
847
+ if (deleteEpoch !== this.nativeDeleteEpoch) {
848
+ return this.hasMany(cids);
849
+ }
850
+ for (let missingIndex = 0; missingIndex < missingIndexes.length; missingIndex++) {
851
+ nativeHas[missingIndexes[missingIndex]] = durableHas[missingIndex];
852
+ }
853
+ return nativeHas;
854
+ }
855
+ // --- removes (apply to BOTH) ----------------------------------------
856
+ // Native rm is synchronous; the durable rm is awaited so the returned promise
857
+ // resolves only after both succeed and a durable failure rejects here rather
858
+ // than being swallowed. All rm callers (RemoteBlocks.rm, the log) await it.
859
+ async rm(cid) {
860
+ this.throwIfNativeDurableCommitFailed();
861
+ const writeGeneration = this.nativeBlockWriteGenerations.get(cid);
862
+ this.beginNativeDelete([cid]);
863
+ try {
864
+ await this.drainDurable();
865
+ this.native.rm(cid);
866
+ await this.commitDurableMutation(() => this.durable.rm(cid), [cid]);
867
+ // A durable read that began before the tombstone may have repopulated
868
+ // native while durable rm was pending. Remove it idempotently again.
869
+ this.native.rm(cid);
870
+ if (this.nativeBlockWriteGenerations.get(cid) === writeGeneration &&
871
+ !this.pendingNativeDeleteCleanup.has(cid)) {
872
+ this.nativeBlockWriteGenerations.delete(cid);
873
+ }
874
+ }
875
+ finally {
876
+ this.endNativeDelete([cid]);
877
+ }
878
+ }
879
+ del(cid) {
880
+ return this.rm(cid);
881
+ }
882
+ async rmMany(cids) {
883
+ this.throwIfNativeDurableCommitFailed();
884
+ const writeGenerations = new Map(cids.map((cid) => [cid, this.nativeBlockWriteGenerations.get(cid)]));
885
+ this.beginNativeDelete(cids);
886
+ try {
887
+ await this.drainDurable();
888
+ const removed = await this.native.rmMany(cids);
889
+ await this.commitDurableMutation(() => this.durable.rmMany(cids), cids);
890
+ await this.native.rmMany(cids);
891
+ for (const cid of cids) {
892
+ if (this.nativeBlockWriteGenerations.get(cid) ===
893
+ writeGenerations.get(cid) &&
894
+ !this.pendingNativeDeleteCleanup.has(cid)) {
895
+ this.nativeBlockWriteGenerations.delete(cid);
896
+ }
897
+ }
898
+ return removed;
899
+ }
900
+ finally {
901
+ this.endNativeDelete(cids);
902
+ }
903
+ }
904
+ // Native trim may already have removed the hot wasm blocks. Queue the durable
905
+ // copy for cleanup, retaining read tombstones until removal succeeds; a
906
+ // cleanup failure is retried and never fed into ordinary append rollback.
907
+ // EntryIndex feature-detects this hook.
908
+ async rmManyAfterNativeDelete(cids, cleanupToken) {
909
+ if (this.nativeDurableCommitFailure) {
910
+ this.cancelNativeDeleteCleanup(cleanupToken);
911
+ throw this.nativeDurableCommitFailure;
912
+ }
913
+ let preannounced = false;
914
+ if (typeof cleanupToken === "number") {
915
+ const staged = this.stagedNativeDeleteCleanups.get(cleanupToken);
916
+ if (staged) {
917
+ preannounced = true;
918
+ this.stagedNativeDeleteCleanups.delete(cleanupToken);
919
+ for (const [cid, generation] of staged) {
920
+ if ((this.nativeBlockWriteGenerations.get(cid) ?? 0) !== generation) {
921
+ this.endNativeDelete([cid]);
922
+ continue;
923
+ }
924
+ if (this.pendingNativeDeleteCleanup.has(cid)) {
925
+ // Another delete already owns a tombstone for this CID.
926
+ this.endNativeDelete([cid]);
927
+ }
928
+ else {
929
+ // Transfer the staged tombstone to the now-published cleanup.
930
+ this.pendingNativeDeleteCleanup.set(cid, generation);
931
+ }
932
+ }
933
+ }
934
+ }
935
+ if (!preannounced) {
936
+ this.enqueueNativeDeleteCleanup(cids);
937
+ }
938
+ await this.retryNativeDeleteCleanup();
939
+ }
940
+ /** Finish committed trim GC before its durable recovery intent is retired. */
941
+ async completeCommittedNativeDeleteCleanup(cids, options) {
942
+ this.throwIfNativeDurableCommitFailed();
943
+ const uniqueCids = [...new Set(cids.filter(Boolean))];
944
+ if (uniqueCids.length === 0) {
945
+ return;
946
+ }
947
+ // Only restart recovery reconstructs missing debt. On the live path, an
948
+ // absent row may mean the CID was legitimately re-added after its original
949
+ // generation-owned trim completed or was cancelled; re-enqueueing it here
950
+ // would capture the new generation and delete live content.
951
+ if (options?.reconstructMissing) {
952
+ this.enqueueNativeDeleteCleanup(uniqueCids);
953
+ }
954
+ if (!uniqueCids.some((cid) => this.pendingNativeDeleteCleanup.has(cid))) {
955
+ return;
956
+ }
957
+ await this.retryNativeDeleteCleanup({ throwOnFailure: true });
958
+ const remaining = uniqueCids.filter((cid) => this.pendingNativeDeleteCleanup.has(cid));
959
+ if (remaining.length > 0) {
960
+ throw new Error(`Committed native trim cleanup remains incomplete: ${remaining.join(", ")}`);
961
+ }
962
+ }
963
+ // --- misc ------------------------------------------------------------
964
+ async *iterator() {
965
+ if (this.nativeDurableCommitFailure) {
966
+ for await (const block of this.durable.iterator()) {
967
+ if (!this.isNativeDeletePending(block[0])) {
968
+ yield block;
969
+ }
970
+ }
971
+ return;
972
+ }
973
+ yield* this.native.iterator();
974
+ }
975
+ async size() {
976
+ // The hot wasm map is intentionally cold after reopen, so it cannot be the
977
+ // storage-budget authority. Settle synchronous columnar mirrors first, then
978
+ // report durable bytes; pending trim cleanup remains conservatively counted
979
+ // until its durable deletion succeeds.
980
+ await this.drainDurable();
981
+ return this.durable.size();
982
+ }
983
+ persisted() {
984
+ // The blocks are now mirrored to a durable store, so report persisted so
985
+ // callers that gate durable-only behavior on this flag behave correctly.
986
+ return true;
987
+ }
988
+ }
989
+ //# sourceMappingURL=native-write-through-block-store.js.map