@peerbit/native-backbone 0.1.4 → 0.2.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.
Files changed (44) hide show
  1. package/README.md +31 -0
  2. package/dist/src/durability/codec.d.ts +86 -0
  3. package/dist/src/durability/codec.d.ts.map +1 -0
  4. package/dist/src/durability/codec.js +365 -0
  5. package/dist/src/durability/codec.js.map +1 -0
  6. package/dist/src/durability/lease.d.ts +59 -0
  7. package/dist/src/durability/lease.d.ts.map +1 -0
  8. package/dist/src/durability/lease.js +48 -0
  9. package/dist/src/durability/lease.js.map +1 -0
  10. package/dist/src/durability/memory-storage.d.ts +37 -0
  11. package/dist/src/durability/memory-storage.d.ts.map +1 -0
  12. package/dist/src/durability/memory-storage.js +436 -0
  13. package/dist/src/durability/memory-storage.js.map +1 -0
  14. package/dist/src/durability/node-lease.d.ts +14 -0
  15. package/dist/src/durability/node-lease.d.ts.map +1 -0
  16. package/dist/src/durability/node-lease.js +214 -0
  17. package/dist/src/durability/node-lease.js.map +1 -0
  18. package/dist/src/durability/node-storage.d.ts +76 -0
  19. package/dist/src/durability/node-storage.d.ts.map +1 -0
  20. package/dist/src/durability/node-storage.js +1813 -0
  21. package/dist/src/durability/node-storage.js.map +1 -0
  22. package/dist/src/durability/storage.d.ts +224 -0
  23. package/dist/src/durability/storage.d.ts.map +1 -0
  24. package/dist/src/durability/storage.js +343 -0
  25. package/dist/src/durability/storage.js.map +1 -0
  26. package/dist/src/index.d.ts +93 -15
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +717 -199
  29. package/dist/src/index.js.map +1 -1
  30. package/dist/wasm/README.md +31 -0
  31. package/dist/wasm/native_backbone.d.ts +131 -115
  32. package/dist/wasm/native_backbone.js +96 -0
  33. package/dist/wasm/native_backbone_bg.wasm +0 -0
  34. package/dist/wasm/native_backbone_bg.wasm.d.ts +119 -115
  35. package/package.json +4 -3
  36. package/src/durability/codec.ts +683 -0
  37. package/src/durability/lease.ts +87 -0
  38. package/src/durability/memory-storage.ts +593 -0
  39. package/src/durability/node-lease.ts +293 -0
  40. package/src/durability/node-storage.ts +2798 -0
  41. package/src/durability/storage.ts +682 -0
  42. package/src/durability.rs +1872 -0
  43. package/src/index.ts +1392 -735
  44. package/src/lib.rs +1 -0
package/dist/src/index.js CHANGED
@@ -7,6 +7,12 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  return path;
8
8
  };
9
9
  import { calculateRawCid, cidifyString } from "@peerbit/blocks-interface";
10
+ export * from "./durability/codec.js";
11
+ export * from "./durability/lease.js";
12
+ export * from "./durability/memory-storage.js";
13
+ export * from "./durability/node-lease.js";
14
+ export * from "./durability/node-storage.js";
15
+ export * from "./durability/storage.js";
10
16
  const nativeBackboneHeadFlagsToBytes = (headFlags) => headFlags instanceof Uint8Array
11
17
  ? headFlags
12
18
  : new Uint8Array(headFlags.map((head) => (head ? 1 : 0)));
@@ -170,8 +176,58 @@ const nativeBackboneCoordinatePersistenceFiles = {
170
176
  documentSignerSnapshot: "document-signers.bin",
171
177
  documentSignerJournal: "document-signers.wal",
172
178
  };
179
+ export const nativeBackboneCoordinateDropTombstoneFile = "native-backbone-drop.tombstone";
173
180
  export const defaultNativeBackboneCoordinateFlushMaxPendingBytes = 1024 * 1024;
181
+ /** @deprecated Built-in coordinate persistence compaction is currently disabled. */
174
182
  export const defaultNativeBackboneCoordinateCompactMaxJournalBytes = 64 * 1024 * 1024;
183
+ const nativeBackboneCoordinateCompactionDisabledMessage = "Native backbone coordinate persistence compaction is disabled until snapshots use a crash-safe generation protocol";
184
+ const nativeBackboneCoordinateJournalMagic = Uint8Array.from([
185
+ 0x50, 0x42, 0x52, 0x49, 0x44, 0x58, 0x57, 0x31,
186
+ ]);
187
+ const readCoordinateJournalU32 = (bytes, offset) => (bytes[offset] |
188
+ (bytes[offset + 1] << 8) |
189
+ (bytes[offset + 2] << 16) |
190
+ (bytes[offset + 3] << 24)) >>>
191
+ 0;
192
+ const coordinateJournalChecksum = (bytes) => {
193
+ let checksum = 0x811c9dc5;
194
+ for (const byte of bytes) {
195
+ checksum = Math.imul(checksum ^ byte, 0x01000193) >>> 0;
196
+ }
197
+ return checksum;
198
+ };
199
+ const hasCoordinateJournalMagic = (bytes) => bytes.byteLength >= nativeBackboneCoordinateJournalMagic.byteLength &&
200
+ nativeBackboneCoordinateJournalMagic.every((byte, index) => bytes[index] === byte);
201
+ /**
202
+ * The Rust decoder intentionally stops at a torn/corrupt tail. Persistence
203
+ * must reject that tail before hydrate, otherwise a later append can land
204
+ * behind it and remain permanently invisible to replay.
205
+ */
206
+ const validateCoordinateJournal = (bytes, name) => {
207
+ if (!bytes || bytes.byteLength === 0) {
208
+ return;
209
+ }
210
+ let offset = hasCoordinateJournalMagic(bytes)
211
+ ? nativeBackboneCoordinateJournalMagic.byteLength
212
+ : 0;
213
+ while (offset < bytes.byteLength) {
214
+ if (bytes.byteLength - offset < 8) {
215
+ throw new Error(`Native backbone ${name} has a torn record header`);
216
+ }
217
+ const length = readCoordinateJournalU32(bytes, offset);
218
+ const expectedChecksum = readCoordinateJournalU32(bytes, offset + 4);
219
+ const payloadOffset = offset + 8;
220
+ const end = payloadOffset + length;
221
+ if (!Number.isSafeInteger(end) || end > bytes.byteLength) {
222
+ throw new Error(`Native backbone ${name} has a torn record payload`);
223
+ }
224
+ const payload = bytes.subarray(payloadOffset, end);
225
+ if (coordinateJournalChecksum(payload) !== expectedChecksum) {
226
+ throw new Error(`Native backbone ${name} has a checksum mismatch`);
227
+ }
228
+ offset = end;
229
+ }
230
+ };
175
231
  const resolveCoordinateFlushMaxPendingBytes = (options) => options.flushMaxPendingBytes != null
176
232
  ? Math.max(0, options.flushMaxPendingBytes)
177
233
  : options.flushOnAppend === false
@@ -181,6 +237,19 @@ const isNotFoundError = (error) => {
181
237
  const maybeError = error;
182
238
  return maybeError?.code === "ENOENT" || maybeError?.name === "NotFoundError";
183
239
  };
240
+ const isUnsupportedDirectorySyncError = (error) => {
241
+ const code = error?.code;
242
+ return (code === "EISDIR" ||
243
+ code === "EINVAL" ||
244
+ code === "EPERM" ||
245
+ code === "ENOTSUP");
246
+ };
247
+ const validateCoordinatePersistenceWriteProgress = (written, remaining, target) => {
248
+ if (!Number.isSafeInteger(written) || written <= 0 || written > remaining) {
249
+ throw new Error(`Invalid ${target} write progress: ${String(written)} for ${remaining} remaining bytes`);
250
+ }
251
+ return written;
252
+ };
184
253
  // Non-literal specifiers keep browser bundlers (esbuild statically resolves
185
254
  // literal dynamic imports and hard-fails on node builtins) from following
186
255
  // these node-only imports; they are only reached on node runtimes.
@@ -211,6 +280,62 @@ const validateCoordinatePersistenceName = (name) => {
211
280
  }
212
281
  return name;
213
282
  };
283
+ const nativeBackboneCoordinateDropTombstoneBodyBytes = (body) => new TextEncoder().encode(JSON.stringify(body));
284
+ const nativeBackboneCoordinateDropChecksum = (bytes) => {
285
+ let checksum = 0xffffffff;
286
+ for (const byte of bytes) {
287
+ checksum ^= byte;
288
+ for (let bit = 0; bit < 8; bit++) {
289
+ checksum = (checksum >>> 1) ^ (checksum & 1 ? 0xedb88320 : 0);
290
+ }
291
+ }
292
+ return ((checksum ^ 0xffffffff) >>> 0).toString(16).padStart(8, "0");
293
+ };
294
+ const nativeBackboneCoordinateDropTombstoneBytes = (files) => {
295
+ const body = {
296
+ format: "peerbit-native-backbone-coordinate-drop",
297
+ version: 1,
298
+ files: [...files],
299
+ };
300
+ return new TextEncoder().encode(JSON.stringify({
301
+ ...body,
302
+ checksum: nativeBackboneCoordinateDropChecksum(nativeBackboneCoordinateDropTombstoneBodyBytes(body)),
303
+ }));
304
+ };
305
+ const parseNativeBackboneCoordinateDropTombstone = (bytes) => {
306
+ let parsed;
307
+ try {
308
+ parsed = JSON.parse(new TextDecoder().decode(bytes));
309
+ }
310
+ catch (error) {
311
+ throw new Error("Invalid native backbone drop tombstone JSON", {
312
+ cause: error,
313
+ });
314
+ }
315
+ const candidate = parsed;
316
+ if (!candidate ||
317
+ candidate.format !== "peerbit-native-backbone-coordinate-drop" ||
318
+ candidate.version !== 1 ||
319
+ !Array.isArray(candidate.files) ||
320
+ candidate.files.some((name) => typeof name !== "string") ||
321
+ typeof candidate.checksum !== "string") {
322
+ throw new Error("Invalid native backbone drop tombstone");
323
+ }
324
+ const files = candidate.files.map(validateCoordinatePersistenceName);
325
+ if (new Set(files).size !== files.length ||
326
+ files.includes(nativeBackboneCoordinateDropTombstoneFile)) {
327
+ throw new Error("Invalid native backbone drop tombstone file set");
328
+ }
329
+ const body = {
330
+ format: "peerbit-native-backbone-coordinate-drop",
331
+ version: 1,
332
+ files,
333
+ };
334
+ if (nativeBackboneCoordinateDropChecksum(nativeBackboneCoordinateDropTombstoneBodyBytes(body)) !== candidate.checksum) {
335
+ throw new Error("Native backbone drop tombstone checksum mismatch");
336
+ }
337
+ return body;
338
+ };
214
339
  const concatBytes = (chunks) => {
215
340
  const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
216
341
  const out = new Uint8Array(total);
@@ -706,7 +831,9 @@ const validateNativeBackboneCoordinateCommitColumns = (columns) => {
706
831
  columns.assignedToRangeBoundaries.length !== length) {
707
832
  throw new Error("Expected equal native coordinate commit column lengths");
708
833
  }
709
- if (columns.hashNumberValues || columns.coordinateCounts || columns.coordinateValues) {
834
+ if (columns.hashNumberValues ||
835
+ columns.coordinateCounts ||
836
+ columns.coordinateValues) {
710
837
  if (!columns.hashNumberValues ||
711
838
  !columns.coordinateCounts ||
712
839
  !columns.coordinateValues ||
@@ -726,7 +853,9 @@ const validateNativeBackboneCoordinateCommitColumns = (columns) => {
726
853
  columns.requestedReplicaValues.length !== length) {
727
854
  throw new Error("Expected equal native coordinate replica column lengths");
728
855
  }
729
- if (columns.hashNumbers || columns.coordinateBatches || columns.requestedReplicas) {
856
+ if (columns.hashNumbers ||
857
+ columns.coordinateBatches ||
858
+ columns.requestedReplicas) {
730
859
  if (!columns.hashNumbers ||
731
860
  !columns.coordinateBatches ||
732
861
  !columns.requestedReplicas ||
@@ -752,7 +881,9 @@ const hasNativeBackboneNumericCoordinateCommitColumns = (columns) => !!columns.h
752
881
  !!columns.coordinateValues &&
753
882
  !!columns.requestedReplicaValues;
754
883
  const nativeBackboneCoordinateCommitStringColumns = (columns) => {
755
- if (columns.hashNumbers && columns.coordinateBatches && columns.requestedReplicas) {
884
+ if (columns.hashNumbers &&
885
+ columns.coordinateBatches &&
886
+ columns.requestedReplicas) {
756
887
  return {
757
888
  hashNumbers: columns.hashNumbers,
758
889
  coordinateBatches: columns.coordinateBatches,
@@ -1171,9 +1302,7 @@ class NativeBackboneLogGraph {
1171
1302
  ? ""
1172
1303
  : integerString(documentIndex.existingCreated), documentIndex.byteElementIndexLimit ?? 0, documentIndex.deleteTrimmedHeads === true, this.options.documentProjectionPlanId(projection.plan), projection.encodedDocument, projection.signer));
1173
1304
  }
1174
- if (documentIndexArgs &&
1175
- input.trimLengthTo == null &&
1176
- hasNoNext) {
1305
+ if (documentIndexArgs && input.trimLengthTo == null && hasNoNext) {
1177
1306
  return preparedCommitFactsFromRow(this.native.prepare_plain_entry_commit_no_next_facts_document_index_compact(wallTime, logical, input.gid, entryType, input.metaData, input.payloadData, ...documentIndexArgs));
1178
1307
  }
1179
1308
  if (documentIndexArgs &&
@@ -2137,7 +2266,7 @@ export class NativePeerbitBackbone {
2137
2266
  });
2138
2267
  }
2139
2268
  commitEntryCoordinatesColumnsBatch(columns) {
2140
- const { hashes, gids, nextHashBatches, assignedToRangeBoundaries, } = columns;
2269
+ const { hashes, gids, nextHashBatches, assignedToRangeBoundaries } = columns;
2141
2270
  if (hashes.length === 0) {
2142
2271
  return;
2143
2272
  }
@@ -2628,9 +2757,7 @@ export class NativePeerbitBackbone {
2628
2757
  ];
2629
2758
  const documentKeys = input.entries.map((entry) => entry.documentIndex.key);
2630
2759
  const usePlainPutPayload = input.entries.every((entry) => entry.documentIndex.usePlainPutPayload === true);
2631
- if (!requiredPreviousSignerPublicKey &&
2632
- useCompact &&
2633
- usePlainPutPayload) {
2760
+ if (!requiredPreviousSignerPublicKey && useCompact && usePlainPutPayload) {
2634
2761
  const nativeCompactPlainPutPayloadBatch = this.native
2635
2762
  .prepare_plain_committed_storage_append_document_index_latest_compact_plain_put_payload_batch_transaction;
2636
2763
  if (nativeCompactPlainPutPayloadBatch) {
@@ -2774,11 +2901,8 @@ export class NativePeerbitBackbone {
2774
2901
  new Uint32Array(input.entries.map((entry) => this.documentProjectionPlanId(entry.documentIndex.projection.plan))),
2775
2902
  ];
2776
2903
  const rows = usePlainPutPayload
2777
- ? this.native
2778
- .prepare_plain_committed_no_next_storage_append_document_index_cached_plan_compact_plain_put_payload_batch_transaction?.call(this.native, ...baseArgs, input.entries.map((entry) => entry.documentIndex.projection.signer), input.trimLengthTo)
2779
- : this.native
2780
- .prepare_plain_committed_no_next_storage_append_document_index_cached_plan_compact_batch_transaction?.call(this.native, ...baseArgs, input.entries.map((entry) => entry.documentIndex.projection
2781
- .encodedDocument), input.entries.map((entry) => entry.documentIndex.projection.signer), input.trimLengthTo);
2904
+ ? this.native.prepare_plain_committed_no_next_storage_append_document_index_cached_plan_compact_plain_put_payload_batch_transaction?.call(this.native, ...baseArgs, input.entries.map((entry) => entry.documentIndex.projection.signer), input.trimLengthTo)
2905
+ : this.native.prepare_plain_committed_no_next_storage_append_document_index_cached_plan_compact_batch_transaction?.call(this.native, ...baseArgs, input.entries.map((entry) => entry.documentIndex.projection.encodedDocument), input.entries.map((entry) => entry.documentIndex.projection.signer), input.trimLengthTo);
2782
2906
  if (!rows) {
2783
2907
  return undefined;
2784
2908
  }
@@ -2827,6 +2951,7 @@ export class NativeBackboneMemoryCoordinatePersistenceStore {
2827
2951
  async remove(name) {
2828
2952
  this.files.delete(validateCoordinatePersistenceName(name));
2829
2953
  }
2954
+ async flush(_name) { }
2830
2955
  }
2831
2956
  export class NativeBackboneNodeCoordinatePersistenceStore {
2832
2957
  directory;
@@ -2834,9 +2959,17 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
2834
2959
  appendHandles = new Map();
2835
2960
  filePaths = new Map();
2836
2961
  directoryEnsured = false;
2962
+ appendFailure;
2963
+ durableBarrier;
2837
2964
  constructor(directory, fs) {
2838
2965
  this.directory = directory;
2839
2966
  this.fs = fs;
2967
+ // The default Node backend has FileHandle.sync. Injected test/custom
2968
+ // backends only advertise durability when they at least expose open(); the
2969
+ // barrier itself verifies sync on every opened handle before ACK.
2970
+ if (!fs || typeof fs.open === "function") {
2971
+ this.durableBarrier = (name) => this.syncDurably(name);
2972
+ }
2840
2973
  }
2841
2974
  async nodeFs() {
2842
2975
  return this.fs ?? (await importNodeFs());
@@ -2896,14 +3029,44 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
2896
3029
  await fs.writeFile(path, bytes);
2897
3030
  }
2898
3031
  async append(name, bytes) {
3032
+ if (this.appendFailure !== undefined) {
3033
+ throw this.appendFailure;
3034
+ }
2899
3035
  const fs = await this.ensureDirectory();
2900
3036
  const path = await this.filePath(name);
2901
3037
  const handle = await this.appendHandle(fs, path);
2902
3038
  if (handle) {
2903
- await handle.write(bytes);
3039
+ let offset = 0;
3040
+ while (offset < bytes.byteLength) {
3041
+ let result;
3042
+ try {
3043
+ result = await handle.write(bytes.subarray(offset));
3044
+ }
3045
+ catch (error) {
3046
+ // FileHandle.write does not expose progress on rejection, even for the
3047
+ // first call, so a partial tail can not be ruled out.
3048
+ this.appendFailure ??= error;
3049
+ throw this.appendFailure;
3050
+ }
3051
+ try {
3052
+ offset += validateCoordinatePersistenceWriteProgress(result.bytesWritten, bytes.byteLength - offset, "Node coordinate WAL");
3053
+ }
3054
+ catch (error) {
3055
+ this.appendFailure ??= error;
3056
+ throw this.appendFailure;
3057
+ }
3058
+ }
2904
3059
  return;
2905
3060
  }
2906
- await fs.appendFile(path, bytes);
3061
+ try {
3062
+ await fs.appendFile(path, bytes);
3063
+ }
3064
+ catch (error) {
3065
+ // appendFile does not report partial progress. Conservatively prevent a
3066
+ // later append from landing behind a possibly torn tail.
3067
+ this.appendFailure ??= error;
3068
+ throw this.appendFailure;
3069
+ }
2907
3070
  }
2908
3071
  async remove(name) {
2909
3072
  const fs = await this.nodeFs();
@@ -2918,6 +3081,67 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
2918
3081
  }
2919
3082
  }
2920
3083
  }
3084
+ async flush(name) {
3085
+ if (!this.durableBarrier) {
3086
+ return;
3087
+ }
3088
+ try {
3089
+ await this.durableBarrier(name);
3090
+ }
3091
+ catch (error) {
3092
+ // Ordinary buffering reads flush a named queue before discovering whether
3093
+ // the backing file exists. Missing files are expected there; strict ACK
3094
+ // paths call durableBarrier directly and therefore remain fail-closed.
3095
+ if (!name || !isNotFoundError(error)) {
3096
+ throw error;
3097
+ }
3098
+ }
3099
+ }
3100
+ async syncDurably(name) {
3101
+ const fs = await this.nodeFs();
3102
+ if (!fs.open) {
3103
+ throw new Error("Node coordinate persistence does not expose a durable FileHandle.sync barrier");
3104
+ }
3105
+ const syncHandle = async (handle, target) => {
3106
+ if (typeof handle.sync !== "function") {
3107
+ throw new Error(`Node coordinate persistence ${target} does not expose FileHandle.sync`);
3108
+ }
3109
+ await handle.sync();
3110
+ };
3111
+ if (name) {
3112
+ const path = await this.filePath(name);
3113
+ const appendHandle = this.appendHandles.get(path);
3114
+ if (appendHandle) {
3115
+ await syncHandle(await appendHandle, `file ${name}`);
3116
+ }
3117
+ else {
3118
+ let handle;
3119
+ try {
3120
+ handle = await fs.open(path, "r");
3121
+ await syncHandle(handle, `file ${name}`);
3122
+ }
3123
+ finally {
3124
+ await handle?.close();
3125
+ }
3126
+ }
3127
+ }
3128
+ else {
3129
+ await Promise.all([...this.appendHandles.entries()].map(async ([path, handle]) => syncHandle(await handle, `file ${path}`)));
3130
+ }
3131
+ let directoryHandle;
3132
+ try {
3133
+ directoryHandle = await fs.open(this.directory, "r");
3134
+ await syncHandle(directoryHandle, "directory");
3135
+ }
3136
+ catch (error) {
3137
+ if (!isNotFoundError(error) && !isUnsupportedDirectorySyncError(error)) {
3138
+ throw error;
3139
+ }
3140
+ }
3141
+ finally {
3142
+ await directoryHandle?.close();
3143
+ }
3144
+ }
2921
3145
  async close() {
2922
3146
  this.directoryEnsured = false;
2923
3147
  const handles = [...this.appendHandles.values()];
@@ -2927,6 +3151,7 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
2927
3151
  }
2928
3152
  export class NativeBackboneOPFSCoordinatePersistenceStore {
2929
3153
  directory;
3154
+ appendFailure;
2930
3155
  constructor(directory) {
2931
3156
  this.directory = directory;
2932
3157
  }
@@ -2979,6 +3204,9 @@ export class NativeBackboneOPFSCoordinatePersistenceStore {
2979
3204
  }
2980
3205
  }
2981
3206
  async append(name, bytes) {
3207
+ if (this.appendFailure !== undefined) {
3208
+ throw this.appendFailure;
3209
+ }
2982
3210
  const handle = await this.directory.getFileHandle(validateCoordinatePersistenceName(name), { create: true });
2983
3211
  if (handle.createSyncAccessHandle) {
2984
3212
  let access;
@@ -2989,11 +3217,28 @@ export class NativeBackboneOPFSCoordinatePersistenceStore {
2989
3217
  // Main-thread OPFS and some browser contexts do not expose sync handles.
2990
3218
  }
2991
3219
  if (access) {
3220
+ const originalSize = access.getSize();
3221
+ let offset = 0;
2992
3222
  try {
2993
- access.write(bytes, { at: access.getSize() });
3223
+ while (offset < bytes.byteLength) {
3224
+ offset += validateCoordinatePersistenceWriteProgress(access.write(bytes.subarray(offset), {
3225
+ at: originalSize + offset,
3226
+ }), bytes.byteLength - offset, "OPFS coordinate WAL");
3227
+ }
2994
3228
  access.flush?.();
2995
3229
  return;
2996
3230
  }
3231
+ catch (error) {
3232
+ try {
3233
+ access.truncate?.(originalSize);
3234
+ access.flush?.();
3235
+ }
3236
+ catch {
3237
+ // The sticky error below prevents appending behind a torn tail.
3238
+ }
3239
+ this.appendFailure ??= error;
3240
+ throw this.appendFailure;
3241
+ }
2997
3242
  finally {
2998
3243
  access.close();
2999
3244
  }
@@ -3004,10 +3249,19 @@ export class NativeBackboneOPFSCoordinatePersistenceStore {
3004
3249
  try {
3005
3250
  await writable.seek(file.size);
3006
3251
  await writable.write(bytes);
3007
- }
3008
- finally {
3009
3252
  await writable.close();
3010
3253
  }
3254
+ catch (error) {
3255
+ try {
3256
+ await writable.close();
3257
+ }
3258
+ catch {
3259
+ // Preserve the first error; the adapter is terminal either way.
3260
+ }
3261
+ // Async OPFS reports no write progress, so failure may leave a torn tail.
3262
+ this.appendFailure ??= error;
3263
+ throw this.appendFailure;
3264
+ }
3011
3265
  }
3012
3266
  async remove(name) {
3013
3267
  try {
@@ -3019,15 +3273,45 @@ export class NativeBackboneOPFSCoordinatePersistenceStore {
3019
3273
  }
3020
3274
  }
3021
3275
  }
3276
+ async flush(_name) { }
3277
+ async durableBarrier(name) {
3278
+ if (!name) {
3279
+ throw new Error("OPFS coordinate persistence requires a named durable barrier");
3280
+ }
3281
+ const handle = await this.directory.getFileHandle(validateCoordinatePersistenceName(name), { create: false });
3282
+ if (!handle.createSyncAccessHandle) {
3283
+ throw new Error("OPFS coordinate persistence durable barriers require createSyncAccessHandle");
3284
+ }
3285
+ const access = await handle.createSyncAccessHandle();
3286
+ try {
3287
+ if (typeof access.flush !== "function") {
3288
+ throw new Error("OPFS coordinate persistence sync handle does not expose flush");
3289
+ }
3290
+ access.flush();
3291
+ }
3292
+ finally {
3293
+ access.close();
3294
+ }
3295
+ }
3022
3296
  }
3023
3297
  export class NativeBackboneBufferedCoordinatePersistenceStore {
3024
3298
  inner;
3025
3299
  options;
3026
3300
  buffers = new Map();
3027
3301
  bufferedBytes = 0;
3302
+ supportsRemoval;
3303
+ durableBarrier;
3028
3304
  constructor(inner, options = {}) {
3029
3305
  this.inner = inner;
3030
3306
  this.options = options;
3307
+ this.supportsRemoval =
3308
+ inner.supportsRemoval ?? typeof inner.remove === "function";
3309
+ if (typeof inner.durableBarrier === "function") {
3310
+ this.durableBarrier = async (name) => {
3311
+ await this.flush(name);
3312
+ await inner.durableBarrier(name);
3313
+ };
3314
+ }
3031
3315
  }
3032
3316
  buffer(name) {
3033
3317
  const validName = validateCoordinatePersistenceName(name);
@@ -3067,7 +3351,10 @@ export class NativeBackboneBufferedCoordinatePersistenceStore {
3067
3351
  this.bufferedBytes -= chunk.byteLength;
3068
3352
  }
3069
3353
  }
3070
- await this.inner.remove?.(name);
3354
+ if (!this.inner.remove) {
3355
+ throw new Error("Native backbone coordinate persistence store does not support removal");
3356
+ }
3357
+ await this.inner.remove(name);
3071
3358
  }
3072
3359
  async flush(name) {
3073
3360
  const names = name
@@ -3083,11 +3370,17 @@ export class NativeBackboneBufferedCoordinatePersistenceStore {
3083
3370
  this.bufferedBytes -= bytes.byteLength;
3084
3371
  await this.inner.append(fileName, bytes);
3085
3372
  }
3086
- await this.inner.flush?.();
3373
+ await this.inner.flush?.(name);
3087
3374
  }
3088
- async close() {
3089
- await this.flush();
3090
- await this.inner.close?.();
3375
+ async close(options) {
3376
+ if (options?.flush === false) {
3377
+ this.buffers.clear();
3378
+ this.bufferedBytes = 0;
3379
+ }
3380
+ else {
3381
+ await this.flush();
3382
+ }
3383
+ await this.inner.close?.(options);
3091
3384
  }
3092
3385
  }
3093
3386
  export class NativeBackboneCoordinatePersistence {
@@ -3097,6 +3390,10 @@ export class NativeBackboneCoordinatePersistence {
3097
3390
  flushIntervalMs;
3098
3391
  compactMaxJournalBytes;
3099
3392
  compactMaxJournalRecords;
3393
+ crashSafeCompaction = false;
3394
+ durableBarrier;
3395
+ supportsDrop;
3396
+ dropIsTerminal = true;
3100
3397
  snapshotFile;
3101
3398
  journalFile;
3102
3399
  documentSnapshotFile;
@@ -3104,75 +3401,270 @@ export class NativeBackboneCoordinatePersistence {
3104
3401
  documentSignerSnapshotFile;
3105
3402
  documentSignerJournalFile;
3106
3403
  journalInitialized;
3107
- journalByteLength = 0;
3108
- journalRecordCount = 0;
3109
3404
  documentJournalInitialized;
3110
- documentJournalByteLength = 0;
3111
- documentJournalRecordCount = 0;
3112
3405
  documentSignerJournalInitialized;
3113
- documentSignerJournalByteLength = 0;
3114
- documentSignerJournalRecordCount = 0;
3115
3406
  lastFlushMs = Date.now();
3116
3407
  persistenceQueue;
3408
+ persistenceLifecycle = "active";
3409
+ dropInitiatedOnGeneration = false;
3410
+ persistenceFailure;
3411
+ closeMode;
3412
+ closeFlushCompleted = false;
3413
+ closeStoreCompleted = false;
3414
+ closePromise;
3117
3415
  constructor(store, options = {}) {
3118
3416
  this.store = store;
3119
- this.snapshotFile =
3120
- options.snapshot ?? nativeBackboneCoordinatePersistenceFiles.snapshot;
3121
- this.journalFile =
3122
- options.journal ?? nativeBackboneCoordinatePersistenceFiles.journal;
3123
- this.documentSnapshotFile =
3124
- options.documentSnapshot ??
3125
- nativeBackboneCoordinatePersistenceFiles.documentSnapshot;
3126
- this.documentJournalFile =
3127
- options.documentJournal ??
3128
- nativeBackboneCoordinatePersistenceFiles.documentJournal;
3129
- this.documentSignerSnapshotFile =
3130
- options.documentSignerSnapshot ??
3131
- nativeBackboneCoordinatePersistenceFiles.documentSignerSnapshot;
3132
- this.documentSignerJournalFile =
3133
- options.documentSignerJournal ??
3134
- nativeBackboneCoordinatePersistenceFiles.documentSignerJournal;
3417
+ this.durableBarrier = typeof store.durableBarrier === "function";
3418
+ this.supportsDrop =
3419
+ (store.supportsRemoval ?? typeof store.remove === "function") &&
3420
+ typeof store.remove === "function";
3421
+ this.snapshotFile = validateCoordinatePersistenceName(options.snapshot ?? nativeBackboneCoordinatePersistenceFiles.snapshot);
3422
+ this.journalFile = validateCoordinatePersistenceName(options.journal ?? nativeBackboneCoordinatePersistenceFiles.journal);
3423
+ this.documentSnapshotFile = validateCoordinatePersistenceName(options.documentSnapshot ??
3424
+ nativeBackboneCoordinatePersistenceFiles.documentSnapshot);
3425
+ this.documentJournalFile = validateCoordinatePersistenceName(options.documentJournal ??
3426
+ nativeBackboneCoordinatePersistenceFiles.documentJournal);
3427
+ this.documentSignerSnapshotFile = validateCoordinatePersistenceName(options.documentSignerSnapshot ??
3428
+ nativeBackboneCoordinatePersistenceFiles.documentSignerSnapshot);
3429
+ this.documentSignerJournalFile = validateCoordinatePersistenceName(options.documentSignerJournal ??
3430
+ nativeBackboneCoordinatePersistenceFiles.documentSignerJournal);
3431
+ if (this.configuredFiles().includes(nativeBackboneCoordinateDropTombstoneFile)) {
3432
+ throw new Error("Native backbone coordinate persistence file conflicts with its drop tombstone");
3433
+ }
3135
3434
  this.flushOnAppend = options.flushOnAppend ?? true;
3136
3435
  this.flushMaxPendingBytes = resolveCoordinateFlushMaxPendingBytes(options);
3137
3436
  if (options.flushIntervalMs != null) {
3138
3437
  this.flushIntervalMs = Math.max(0, options.flushIntervalMs);
3139
3438
  }
3140
- if (options.compactMaxJournalBytes != null) {
3141
- this.compactMaxJournalBytes = Math.max(0, options.compactMaxJournalBytes);
3439
+ if (options.compactMaxJournalBytes != null ||
3440
+ options.compactMaxJournalRecords != null) {
3441
+ throw new Error(nativeBackboneCoordinateCompactionDisabledMessage);
3142
3442
  }
3143
- if (options.compactMaxJournalRecords != null) {
3144
- this.compactMaxJournalRecords = Math.max(0, options.compactMaxJournalRecords);
3443
+ }
3444
+ /** Durable operation-intent capability for strict shared-log transactions. */
3445
+ get intentStore() {
3446
+ return this.store;
3447
+ }
3448
+ configuredFiles() {
3449
+ return [
3450
+ this.snapshotFile,
3451
+ this.journalFile,
3452
+ this.documentSnapshotFile,
3453
+ this.documentJournalFile,
3454
+ this.documentSignerSnapshotFile,
3455
+ this.documentSignerJournalFile,
3456
+ ];
3457
+ }
3458
+ assertLifecycleActive(operation) {
3459
+ if (this.persistenceLifecycle !== "active") {
3460
+ throw new Error(`Native backbone coordinate persistence can not ${operation} while ${this.persistenceLifecycle}`);
3145
3461
  }
3146
3462
  }
3147
- async hydrate(backbone) {
3148
- const [snapshot, journal, documentSnapshot, documentJournal, documentSignerSnapshot, documentSignerJournal,] = await Promise.all([
3149
- this.store.read(this.snapshotFile),
3150
- this.store.read(this.journalFile),
3151
- this.store.read(this.documentSnapshotFile),
3152
- this.store.read(this.documentJournalFile),
3153
- this.store.read(this.documentSignerSnapshotFile),
3154
- this.store.read(this.documentSignerJournalFile),
3155
- ]);
3156
- const operations = backbone.loadCoordinateSnapshotAndJournal(snapshot, journal);
3157
- const documentOperations = backbone.loadDocumentSnapshotAndJournal(documentSnapshot, documentJournal);
3158
- const documentSignerOperations = backbone.loadDocumentSignerSnapshotAndJournal(documentSignerSnapshot, documentSignerJournal);
3159
- this.journalInitialized = !!journal && journal.byteLength > 0;
3160
- this.journalByteLength = journal?.byteLength ?? 0;
3161
- this.journalRecordCount = operations;
3162
- this.documentJournalInitialized =
3163
- !!documentJournal && documentJournal.byteLength > 0;
3164
- this.documentJournalByteLength = documentJournal?.byteLength ?? 0;
3165
- this.documentJournalRecordCount = documentOperations;
3166
- this.documentSignerJournalInitialized =
3167
- !!documentSignerJournal && documentSignerJournal.byteLength > 0;
3168
- this.documentSignerJournalByteLength =
3169
- documentSignerJournal?.byteLength ?? 0;
3170
- this.documentSignerJournalRecordCount = documentSignerOperations;
3171
- backbone.setCoordinateJournalEnabled(true);
3172
- backbone.setDocumentJournalEnabled(true);
3173
- backbone.setDocumentSignerJournalEnabled(true);
3463
+ assertActive(operation) {
3464
+ if (this.persistenceFailure !== undefined) {
3465
+ throw this.persistenceFailure;
3466
+ }
3467
+ this.assertLifecycleActive(operation);
3468
+ if (this.dropInitiatedOnGeneration) {
3469
+ throw new Error(`Native backbone coordinate persistence can not ${operation} after drop was initiated; retry drop or resume drop first`);
3470
+ }
3471
+ }
3472
+ resetJournalTracking() {
3473
+ this.journalInitialized = undefined;
3474
+ this.documentJournalInitialized = undefined;
3475
+ this.documentSignerJournalInitialized = undefined;
3174
3476
  this.lastFlushMs = Date.now();
3175
- return operations + documentOperations + documentSignerOperations;
3477
+ }
3478
+ async eraseDropFiles(files) {
3479
+ if (!this.supportsDrop || !this.store.remove) {
3480
+ throw new Error("Native backbone coordinate persistence store does not support removal");
3481
+ }
3482
+ const removals = await Promise.allSettled(files.map((name) => this.store.remove(name)));
3483
+ const failures = removals.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
3484
+ if (failures.length > 0) {
3485
+ throw new AggregateError(failures, "Failed to erase native backbone coordinate persistence namespace");
3486
+ }
3487
+ await this.store.flush?.();
3488
+ await this.store.remove(nativeBackboneCoordinateDropTombstoneFile);
3489
+ await this.store.flush?.();
3490
+ }
3491
+ async drop(additionalFiles = []) {
3492
+ // Drop is the recovery escape hatch for a persistence generation that has
3493
+ // already failed closed, so only the lifecycle (not persistenceFailure)
3494
+ // gates this destructive operation.
3495
+ this.assertLifecycleActive("drop");
3496
+ const files = [
3497
+ ...new Set([
3498
+ ...this.configuredFiles(),
3499
+ ...additionalFiles.map(validateCoordinatePersistenceName),
3500
+ ]),
3501
+ ];
3502
+ if (files.includes(nativeBackboneCoordinateDropTombstoneFile)) {
3503
+ throw new Error("Native backbone drop targets can not include the drop tombstone");
3504
+ }
3505
+ if (!this.supportsDrop || !this.store.remove) {
3506
+ throw new Error("Native backbone coordinate persistence store does not support removal");
3507
+ }
3508
+ // Flip synchronously so no later append/compact can enqueue behind this
3509
+ // erase and recreate a file after its removal.
3510
+ this.dropInitiatedOnGeneration = true;
3511
+ this.persistenceLifecycle = "dropping";
3512
+ await this.enqueuePersistence(async () => {
3513
+ await this.store.write(nativeBackboneCoordinateDropTombstoneFile, nativeBackboneCoordinateDropTombstoneBytes(files));
3514
+ if (this.store.durableBarrier) {
3515
+ await this.store.durableBarrier(nativeBackboneCoordinateDropTombstoneFile);
3516
+ }
3517
+ else {
3518
+ await this.store.flush?.(nativeBackboneCoordinateDropTombstoneFile);
3519
+ }
3520
+ await this.eraseDropFiles(files);
3521
+ this.resetJournalTracking();
3522
+ this.persistenceLifecycle = "dropped";
3523
+ });
3524
+ }
3525
+ async resumeDrop() {
3526
+ if (this.persistenceLifecycle === "dropped") {
3527
+ return true;
3528
+ }
3529
+ if (this.closeMode !== undefined) {
3530
+ throw new Error("Native backbone coordinate persistence can not resume drop after close was initiated");
3531
+ }
3532
+ const resumesInProgress = this.persistenceLifecycle === "dropping";
3533
+ const completesInitiatedDrop = this.dropInitiatedOnGeneration;
3534
+ if (!resumesInProgress) {
3535
+ if (completesInitiatedDrop) {
3536
+ this.assertLifecycleActive("resume drop");
3537
+ }
3538
+ else {
3539
+ this.assertActive("resume drop");
3540
+ }
3541
+ }
3542
+ this.persistenceLifecycle = "dropping";
3543
+ return this.enqueuePersistence(async () => {
3544
+ // A concurrent drop may have completed before this queued recovery runs.
3545
+ // Preserve its terminal lifecycle instead of reactivating the generation.
3546
+ if (this.persistenceLifecycle === "dropped") {
3547
+ return true;
3548
+ }
3549
+ return this.resumeDropInternal(completesInitiatedDrop ? "dropped" : "active");
3550
+ });
3551
+ }
3552
+ async hydrate(backbone) {
3553
+ this.assertActive("hydrate");
3554
+ // Claim the lifecycle synchronously. close() queues after this complete
3555
+ // read/validate/load operation; drop() rejects before erasing anything.
3556
+ this.persistenceLifecycle = "hydrating";
3557
+ return this.enqueuePersistence(async () => {
3558
+ try {
3559
+ this.assertHydrating();
3560
+ await this.resumeDropInternal("hydrating");
3561
+ this.assertHydrating();
3562
+ const [snapshot, journal, documentSnapshot, documentJournal, documentSignerSnapshot, documentSignerJournal,] = await Promise.all([
3563
+ this.store.read(this.snapshotFile),
3564
+ this.store.read(this.journalFile),
3565
+ this.store.read(this.documentSnapshotFile),
3566
+ this.store.read(this.documentJournalFile),
3567
+ this.store.read(this.documentSignerSnapshotFile),
3568
+ this.store.read(this.documentSignerJournalFile),
3569
+ ]);
3570
+ this.assertHydrating();
3571
+ try {
3572
+ validateCoordinateJournal(journal, this.journalFile);
3573
+ validateCoordinateJournal(documentJournal, this.documentJournalFile);
3574
+ validateCoordinateJournal(documentSignerJournal, this.documentSignerJournalFile);
3575
+ }
3576
+ catch (error) {
3577
+ this.persistenceFailure ??= error;
3578
+ throw this.persistenceFailure;
3579
+ }
3580
+ let operations;
3581
+ let documentOperations;
3582
+ let documentSignerOperations;
3583
+ try {
3584
+ this.assertHydrating();
3585
+ operations = backbone.loadCoordinateSnapshotAndJournal(snapshot, journal);
3586
+ this.assertHydrating();
3587
+ documentOperations = backbone.loadDocumentSnapshotAndJournal(documentSnapshot, documentJournal);
3588
+ this.assertHydrating();
3589
+ documentSignerOperations =
3590
+ backbone.loadDocumentSignerSnapshotAndJournal(documentSignerSnapshot, documentSignerJournal);
3591
+ }
3592
+ catch (error) {
3593
+ if (journal?.byteLength ||
3594
+ documentJournal?.byteLength ||
3595
+ documentSignerJournal?.byteLength) {
3596
+ this.persistenceFailure ??= error;
3597
+ throw this.persistenceFailure;
3598
+ }
3599
+ throw error;
3600
+ }
3601
+ this.assertHydrating();
3602
+ this.journalInitialized = !!journal && journal.byteLength > 0;
3603
+ this.documentJournalInitialized =
3604
+ !!documentJournal && documentJournal.byteLength > 0;
3605
+ this.documentSignerJournalInitialized =
3606
+ !!documentSignerJournal && documentSignerJournal.byteLength > 0;
3607
+ backbone.setCoordinateJournalEnabled(true);
3608
+ backbone.setDocumentJournalEnabled(true);
3609
+ backbone.setDocumentSignerJournalEnabled(true);
3610
+ this.lastFlushMs = Date.now();
3611
+ this.persistenceLifecycle = this.closePromise ? "closing" : "active";
3612
+ return operations + documentOperations + documentSignerOperations;
3613
+ }
3614
+ catch (error) {
3615
+ if (this.persistenceLifecycle === "hydrating") {
3616
+ this.persistenceLifecycle = this.closePromise ? "closing" : "active";
3617
+ }
3618
+ throw error;
3619
+ }
3620
+ });
3621
+ }
3622
+ assertHydrating() {
3623
+ if (this.persistenceLifecycle !== "hydrating") {
3624
+ throw new Error(`Native backbone coordinate persistence hydrate was interrupted while ${this.persistenceLifecycle}`);
3625
+ }
3626
+ }
3627
+ async resumeDropInternal(finalLifecycle) {
3628
+ const bytes = await this.store.read(nativeBackboneCoordinateDropTombstoneFile);
3629
+ if (!bytes) {
3630
+ // A failed drop can stop before its tombstone is durable. Restore the
3631
+ // generation to active so the caller can start a complete drop; only a
3632
+ // tombstone-backed erase may finish it as dropped.
3633
+ this.persistenceLifecycle =
3634
+ finalLifecycle === "hydrating"
3635
+ ? "hydrating"
3636
+ : this.closePromise
3637
+ ? "closing"
3638
+ : "active";
3639
+ return false;
3640
+ }
3641
+ let tombstone;
3642
+ try {
3643
+ tombstone = parseNativeBackboneCoordinateDropTombstone(bytes);
3644
+ for (const file of this.configuredFiles()) {
3645
+ if (!tombstone.files.includes(file)) {
3646
+ throw new Error("Native backbone drop tombstone does not cover the configured namespace");
3647
+ }
3648
+ }
3649
+ }
3650
+ catch (error) {
3651
+ // Corruption must never hydrate stale files, but an explicit drop must
3652
+ // remain available to overwrite the bad marker and erase the namespace.
3653
+ this.persistenceFailure ??= error;
3654
+ this.persistenceLifecycle = this.closePromise ? "closing" : "active";
3655
+ throw this.persistenceFailure;
3656
+ }
3657
+ await this.eraseDropFiles(tombstone.files);
3658
+ this.resetJournalTracking();
3659
+ this.persistenceLifecycle =
3660
+ finalLifecycle === "dropped"
3661
+ ? "dropped"
3662
+ : finalLifecycle === "hydrating"
3663
+ ? "hydrating"
3664
+ : this.closePromise
3665
+ ? "closing"
3666
+ : "active";
3667
+ return true;
3176
3668
  }
3177
3669
  shouldFlushJournalOnAppend(backbone, now = Date.now()) {
3178
3670
  if (this.flushOnAppend !== false) {
@@ -3195,12 +3687,14 @@ export class NativeBackboneCoordinatePersistence {
3195
3687
  now - this.lastFlushMs >= this.flushIntervalMs);
3196
3688
  }
3197
3689
  flushJournalOnAppend(backbone) {
3690
+ this.assertActive("flush on append");
3198
3691
  if (!this.shouldFlushJournalOnAppend(backbone)) {
3199
3692
  return 0;
3200
3693
  }
3201
3694
  return this.flushJournal(backbone);
3202
3695
  }
3203
3696
  flushJournal(backbone) {
3697
+ this.assertActive("flush");
3204
3698
  // Serialized with compact() so a flush never clears records appended to
3205
3699
  // the wasm journal while a previous flush was awaiting its disk write.
3206
3700
  return this.enqueuePersistence(() => this.flushJournalInternal(backbone));
@@ -3209,9 +3703,7 @@ export class NativeBackboneCoordinatePersistence {
3209
3703
  // Runs `fn` immediately when no other persistence operation is in
3210
3704
  // flight so the common uncontended flush starts synchronously; queued
3211
3705
  // operations still run strictly one at a time, in order.
3212
- const next = this.persistenceQueue
3213
- ? this.persistenceQueue.then(fn)
3214
- : fn();
3706
+ const next = this.persistenceQueue ? this.persistenceQueue.then(fn) : fn();
3215
3707
  const tail = next.then(() => undefined, () => undefined);
3216
3708
  this.persistenceQueue = tail;
3217
3709
  void tail.then(() => {
@@ -3223,122 +3715,157 @@ export class NativeBackboneCoordinatePersistence {
3223
3715
  }
3224
3716
  async flushJournalInternal(backbone) {
3225
3717
  let written = 0;
3718
+ let persistenceMutationStarted = false;
3719
+ let coordinateBytes;
3720
+ let documentBytes;
3721
+ let signerBytes;
3226
3722
  const coordinateRecords = backbone.coordinateJournal();
3227
3723
  const coordinateRecordCount = backbone.coordinatePendingJournalLength;
3228
- if (coordinateRecords.byteLength > 0) {
3229
- if (this.journalInitialized === undefined) {
3230
- const existing = await this.store.read(this.journalFile);
3231
- this.journalInitialized = !!existing && existing.byteLength > 0;
3232
- }
3233
- const bytes = this.journalInitialized
3234
- ? coordinateRecords
3235
- : concatBytes([backbone.coordinateJournalHeader(), coordinateRecords]);
3236
- await this.store.append(this.journalFile, bytes);
3237
- this.journalInitialized = true;
3238
- this.journalByteLength += bytes.byteLength;
3239
- this.journalRecordCount += coordinateRecordCount;
3240
- backbone.clearCoordinateJournalPrefix(coordinateRecords.byteLength, coordinateRecordCount);
3241
- written += coordinateRecords.byteLength;
3242
- }
3243
3724
  const documentRecords = backbone.documentJournal();
3244
3725
  const documentRecordCount = backbone.documentPendingJournalLength;
3245
- if (documentRecords.byteLength > 0) {
3246
- if (this.documentJournalInitialized === undefined) {
3247
- const existing = await this.store.read(this.documentJournalFile);
3248
- this.documentJournalInitialized = !!existing && existing.byteLength > 0;
3249
- }
3250
- const bytes = this.documentJournalInitialized
3251
- ? documentRecords
3252
- : concatBytes([backbone.documentJournalHeader(), documentRecords]);
3253
- await this.store.append(this.documentJournalFile, bytes);
3254
- this.documentJournalInitialized = true;
3255
- this.documentJournalByteLength += bytes.byteLength;
3256
- this.documentJournalRecordCount += documentRecordCount;
3257
- backbone.clearDocumentJournalPrefix(documentRecords.byteLength, documentRecordCount);
3258
- written += documentRecords.byteLength;
3259
- }
3260
3726
  const signerRecords = backbone.documentSignerJournal();
3261
3727
  const signerRecordCount = backbone.documentSignerPendingJournalLength;
3262
- if (signerRecords.byteLength > 0) {
3263
- if (this.documentSignerJournalInitialized === undefined) {
3264
- const existing = await this.store.read(this.documentSignerJournalFile);
3265
- this.documentSignerJournalInitialized =
3266
- !!existing && existing.byteLength > 0;
3267
- }
3268
- const bytes = this.documentSignerJournalInitialized
3269
- ? signerRecords
3270
- : concatBytes([backbone.documentSignerJournalHeader(), signerRecords]);
3271
- await this.store.append(this.documentSignerJournalFile, bytes);
3272
- this.documentSignerJournalInitialized = true;
3273
- this.documentSignerJournalByteLength += bytes.byteLength;
3274
- this.documentSignerJournalRecordCount += signerRecordCount;
3728
+ try {
3729
+ if (coordinateRecords.byteLength > 0) {
3730
+ if (this.journalInitialized === undefined) {
3731
+ const existing = await this.store.read(this.journalFile);
3732
+ this.journalInitialized = !!existing && existing.byteLength > 0;
3733
+ }
3734
+ coordinateBytes = this.journalInitialized
3735
+ ? coordinateRecords
3736
+ : concatBytes([
3737
+ backbone.coordinateJournalHeader(),
3738
+ coordinateRecords,
3739
+ ]);
3740
+ await this.store.append(this.journalFile, coordinateBytes);
3741
+ persistenceMutationStarted = true;
3742
+ written += coordinateRecords.byteLength;
3743
+ }
3744
+ if (documentRecords.byteLength > 0) {
3745
+ if (this.documentJournalInitialized === undefined) {
3746
+ const existing = await this.store.read(this.documentJournalFile);
3747
+ this.documentJournalInitialized =
3748
+ !!existing && existing.byteLength > 0;
3749
+ }
3750
+ documentBytes = this.documentJournalInitialized
3751
+ ? documentRecords
3752
+ : concatBytes([backbone.documentJournalHeader(), documentRecords]);
3753
+ await this.store.append(this.documentJournalFile, documentBytes);
3754
+ persistenceMutationStarted = true;
3755
+ written += documentRecords.byteLength;
3756
+ }
3757
+ if (signerRecords.byteLength > 0) {
3758
+ if (this.documentSignerJournalInitialized === undefined) {
3759
+ const existing = await this.store.read(this.documentSignerJournalFile);
3760
+ this.documentSignerJournalInitialized =
3761
+ !!existing && existing.byteLength > 0;
3762
+ }
3763
+ signerBytes = this.documentSignerJournalInitialized
3764
+ ? signerRecords
3765
+ : concatBytes([
3766
+ backbone.documentSignerJournalHeader(),
3767
+ signerRecords,
3768
+ ]);
3769
+ await this.store.append(this.documentSignerJournalFile, signerBytes);
3770
+ persistenceMutationStarted = true;
3771
+ written += signerRecords.byteLength;
3772
+ }
3773
+ if (written === 0) {
3774
+ this.lastFlushMs = Date.now();
3775
+ return 0;
3776
+ }
3777
+ // `append` may only enqueue bytes in a buffered store. Drain and fsync
3778
+ // every affected WAL before clearing its wasm prefix or returning an ACK.
3779
+ for (const file of [
3780
+ coordinateBytes ? this.journalFile : undefined,
3781
+ documentBytes ? this.documentJournalFile : undefined,
3782
+ signerBytes ? this.documentSignerJournalFile : undefined,
3783
+ ]) {
3784
+ if (file) {
3785
+ if (this.store.durableBarrier) {
3786
+ await this.store.durableBarrier(file);
3787
+ }
3788
+ else {
3789
+ await this.store.flush?.(file);
3790
+ }
3791
+ }
3792
+ }
3793
+ if (coordinateBytes) {
3794
+ this.journalInitialized = true;
3795
+ }
3796
+ if (documentBytes) {
3797
+ this.documentJournalInitialized = true;
3798
+ }
3799
+ if (signerBytes) {
3800
+ this.documentSignerJournalInitialized = true;
3801
+ }
3802
+ backbone.clearCoordinateJournalPrefix(coordinateRecords.byteLength, coordinateRecordCount);
3803
+ backbone.clearDocumentJournalPrefix(documentRecords.byteLength, documentRecordCount);
3275
3804
  backbone.clearDocumentSignerJournalPrefix(signerRecords.byteLength, signerRecordCount);
3276
- written += signerRecords.byteLength;
3277
- }
3278
- if (written === 0) {
3279
3805
  this.lastFlushMs = Date.now();
3280
- return 0;
3806
+ return written;
3807
+ }
3808
+ catch (error) {
3809
+ if (persistenceMutationStarted) {
3810
+ this.persistenceFailure ??= error;
3811
+ throw this.persistenceFailure;
3812
+ }
3813
+ throw error;
3281
3814
  }
3282
- this.lastFlushMs = Date.now();
3283
- if (this.shouldCompactJournal()) {
3284
- await this.compactInternal(backbone);
3285
- }
3286
- return written;
3287
- }
3288
- shouldCompactJournal() {
3289
- return ((this.compactMaxJournalBytes != null &&
3290
- this.journalByteLength +
3291
- this.documentJournalByteLength +
3292
- this.documentSignerJournalByteLength >=
3293
- this.compactMaxJournalBytes) ||
3294
- (this.compactMaxJournalRecords != null &&
3295
- this.journalRecordCount +
3296
- this.documentJournalRecordCount +
3297
- this.documentSignerJournalRecordCount >=
3298
- this.compactMaxJournalRecords));
3299
- }
3300
- compact(backbone) {
3301
- return this.enqueuePersistence(() => this.compactInternal(backbone));
3302
- }
3303
- async compactInternal(backbone) {
3304
- // The snapshots below cover exactly the journal records pending right
3305
- // now; records appended during the awaited writes must survive the
3306
- // clears at the end, so only this prefix is dropped.
3307
- const coordinateJournalByteLength = backbone.coordinatePendingJournalByteLength;
3308
- const coordinateJournalRecordCount = backbone.coordinatePendingJournalLength;
3309
- const documentJournalByteLength = backbone.documentPendingJournalByteLength;
3310
- const documentJournalRecordCount = backbone.documentPendingJournalLength;
3311
- const documentSignerJournalByteLength = backbone.documentSignerPendingJournalByteLength;
3312
- const documentSignerJournalRecordCount = backbone.documentSignerPendingJournalLength;
3313
- await Promise.all([
3314
- this.store.write(this.snapshotFile, backbone.coordinateSnapshot()),
3315
- this.store.write(this.documentSnapshotFile, backbone.documentSnapshot()),
3316
- this.store.write(this.documentSignerSnapshotFile, backbone.documentSignerSnapshot()),
3317
- ]);
3318
- await Promise.all([
3319
- this.store.remove?.(this.journalFile),
3320
- this.store.remove?.(this.documentJournalFile),
3321
- this.store.remove?.(this.documentSignerJournalFile),
3322
- ]);
3323
- this.journalInitialized = false;
3324
- this.journalByteLength = 0;
3325
- this.journalRecordCount = 0;
3326
- this.documentJournalInitialized = false;
3327
- this.documentJournalByteLength = 0;
3328
- this.documentJournalRecordCount = 0;
3329
- this.documentSignerJournalInitialized = false;
3330
- this.documentSignerJournalByteLength = 0;
3331
- this.documentSignerJournalRecordCount = 0;
3332
- backbone.clearCoordinateJournalPrefix(coordinateJournalByteLength, coordinateJournalRecordCount);
3333
- backbone.clearDocumentJournalPrefix(documentJournalByteLength, documentJournalRecordCount);
3334
- backbone.clearDocumentSignerJournalPrefix(documentSignerJournalByteLength, documentSignerJournalRecordCount);
3335
- // Compact persists the full pending state, so it restarts the
3336
- // flushIntervalMs pacing window like a flush does.
3337
- this.lastFlushMs = Date.now();
3338
3815
  }
3339
- async close() {
3340
- await this.store.flush?.();
3341
- await this.store.close?.();
3816
+ async compact(_backbone) {
3817
+ this.assertActive("compact");
3818
+ throw new Error(nativeBackboneCoordinateCompactionDisabledMessage);
3819
+ }
3820
+ close() {
3821
+ if (this.closePromise) {
3822
+ return this.closePromise;
3823
+ }
3824
+ const closeMode = this.closeMode ??
3825
+ (this.persistenceLifecycle === "active" && !this.dropInitiatedOnGeneration
3826
+ ? "flush"
3827
+ : "withoutFlush");
3828
+ this.closeMode = closeMode;
3829
+ // Close wins once invoked from an active generation. The synchronous
3830
+ // transition rejects any later drop/hydrate/flush before the terminal
3831
+ // store close starts, including for stores that forbid all post-close I/O.
3832
+ if (this.persistenceLifecycle === "active") {
3833
+ this.persistenceLifecycle = "closing";
3834
+ }
3835
+ const closeAttempt = this.enqueuePersistence(async () => {
3836
+ if (closeMode === "flush") {
3837
+ this.persistenceLifecycle = "closing";
3838
+ if (!this.closeFlushCompleted) {
3839
+ await this.store.flush?.();
3840
+ this.closeFlushCompleted = true;
3841
+ }
3842
+ if (!this.closeStoreCompleted) {
3843
+ await this.store.close?.();
3844
+ this.closeStoreCompleted = true;
3845
+ }
3846
+ this.persistenceLifecycle = "closed";
3847
+ return;
3848
+ }
3849
+ // A drop already in flight wins. Drain no buffered bytes after its
3850
+ // tombstone erase and preserve the dropped terminal lifecycle.
3851
+ if (!this.closeStoreCompleted) {
3852
+ await this.store.close?.({ flush: false });
3853
+ this.closeStoreCompleted = true;
3854
+ }
3855
+ if (this.persistenceLifecycle !== "dropped") {
3856
+ this.persistenceLifecycle = "closed";
3857
+ }
3858
+ });
3859
+ this.closePromise = closeAttempt;
3860
+ void closeAttempt.catch(() => {
3861
+ // A rejected close is only the failed attempt, not terminal progress.
3862
+ // Retain completed stages above, but let the exact owner retry the first
3863
+ // incomplete store operation.
3864
+ if (this.closePromise === closeAttempt) {
3865
+ this.closePromise = undefined;
3866
+ }
3867
+ });
3868
+ return closeAttempt;
3342
3869
  }
3343
3870
  }
3344
3871
  const resolveNodeWriteBufferMaxBytes = (options) => options.writeBufferMaxBytes != null
@@ -3375,18 +3902,12 @@ export const createNativeBackboneCoordinatePersistence = (config) => {
3375
3902
  return config;
3376
3903
  }
3377
3904
  const { store, buffered, ...options } = config;
3378
- const isBuffered = buffered === true || !!buffered;
3379
3905
  const resolvedStore = buffered === true
3380
3906
  ? new NativeBackboneBufferedCoordinatePersistenceStore(store)
3381
3907
  : buffered
3382
3908
  ? new NativeBackboneBufferedCoordinatePersistenceStore(store, buffered)
3383
3909
  : store;
3384
- return new NativeBackboneCoordinatePersistence(resolvedStore, {
3385
- ...options,
3386
- compactMaxJournalBytes: isBuffered && options.compactMaxJournalBytes == null
3387
- ? defaultNativeBackboneCoordinateCompactMaxJournalBytes
3388
- : options.compactMaxJournalBytes,
3389
- });
3910
+ return new NativeBackboneCoordinatePersistence(resolvedStore, options);
3390
3911
  };
3391
3912
  export const createBufferedNativeBackboneCoordinatePersistence = (store, options = {}) => {
3392
3913
  const maxBufferedBytes = options.maxBufferedBytes ??
@@ -3405,8 +3926,7 @@ export const createBufferedNativeBackboneCoordinatePersistence = (store, options
3405
3926
  flushOnAppend: false,
3406
3927
  flushMaxPendingBytes,
3407
3928
  flushIntervalMs: options.flushIntervalMs,
3408
- compactMaxJournalBytes: options.compactMaxJournalBytes ??
3409
- defaultNativeBackboneCoordinateCompactMaxJournalBytes,
3929
+ compactMaxJournalBytes: options.compactMaxJournalBytes,
3410
3930
  compactMaxJournalRecords: options.compactMaxJournalRecords,
3411
3931
  });
3412
3932
  };
@@ -3418,8 +3938,6 @@ export const createBufferedNativeBackboneNodeCoordinatePersistence = (directory,
3418
3938
  flushOnAppend: false,
3419
3939
  flushMaxPendingBytes,
3420
3940
  writeBufferMaxBytes: options.writeBufferMaxBytes ?? flushMaxPendingBytes,
3421
- compactMaxJournalBytes: options.compactMaxJournalBytes ??
3422
- defaultNativeBackboneCoordinateCompactMaxJournalBytes,
3423
3941
  });
3424
3942
  };
3425
3943
  export const createNativePeerbitBackbone = NativePeerbitBackbone.create;