@peerbit/native-backbone 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -191,9 +191,9 @@ const nativeBackboneCoordinatePersistenceFiles = {
191
191
  };
192
192
  export const nativeBackboneCoordinateDropTombstoneFile = "native-backbone-drop.tombstone";
193
193
  export const defaultNativeBackboneCoordinateFlushMaxPendingBytes = 1024 * 1024;
194
- /** @deprecated Built-in coordinate persistence compaction is currently disabled. */
194
+ /** Recommended explicit byte threshold for crash-safe Node checkpointing. */
195
195
  export const defaultNativeBackboneCoordinateCompactMaxJournalBytes = 64 * 1024 * 1024;
196
- const nativeBackboneCoordinateCompactionDisabledMessage = "Native backbone coordinate persistence compaction is disabled until snapshots use a crash-safe generation protocol";
196
+ const nativeBackboneCoordinateCompactionDisabledMessage = "Native backbone coordinate persistence compaction is disabled for stores without crash-safe atomic replacement";
197
197
  const nativeBackboneCoordinateJournalMagic = Uint8Array.from([
198
198
  0x50, 0x42, 0x52, 0x49, 0x44, 0x58, 0x57, 0x31,
199
199
  ]);
@@ -209,6 +209,171 @@ const coordinateJournalChecksum = (bytes) => {
209
209
  }
210
210
  return checksum;
211
211
  };
212
+ const nativeBackboneCoordinateCheckpointMagic = Uint8Array.from([
213
+ 0x50, 0x42, 0x52, 0x49, 0x44, 0x58, 0x43, 0x31,
214
+ ]);
215
+ const nativeBackboneCoordinateCheckpointStateMagic = Uint8Array.from([
216
+ 0x50, 0x42, 0x52, 0x49, 0x44, 0x58, 0x53, 0x31,
217
+ ]);
218
+ const nativeBackboneCoordinateLegacyMigrationSentinel = (() => {
219
+ const payload = Uint8Array.of(0xff);
220
+ const bytes = new Uint8Array(nativeBackboneCoordinateJournalMagic.byteLength + 8 + payload.byteLength);
221
+ bytes.set(nativeBackboneCoordinateJournalMagic, 0);
222
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
223
+ view.setUint32(nativeBackboneCoordinateJournalMagic.byteLength, 1, true);
224
+ view.setUint32(nativeBackboneCoordinateJournalMagic.byteLength + 4, coordinateJournalChecksum(payload), true);
225
+ bytes.set(payload, nativeBackboneCoordinateJournalMagic.byteLength + 8);
226
+ return bytes;
227
+ })();
228
+ const nativeBackboneCoordinateCheckpointVersion = 1;
229
+ const nativeBackboneCoordinateCheckpointHeaderBytes = 56;
230
+ const nativeBackboneCoordinateCheckpointStateBytes = 60;
231
+ const nativeBackboneCoordinateMaxU64 = (1n << 64n) - 1n;
232
+ const hasBytesPrefix = (bytes, prefix) => bytes.byteLength >= prefix.byteLength &&
233
+ prefix.every((byte, index) => bytes[index] === byte);
234
+ const writeCoordinateCheckpointU64 = (view, offset, value) => {
235
+ if (value < 0n || value > nativeBackboneCoordinateMaxU64) {
236
+ throw new RangeError("Native backbone checkpoint generation exceeds u64");
237
+ }
238
+ view.setUint32(offset, Number(value & 0xffffffffn), true);
239
+ view.setUint32(offset + 4, Number(value >> 32n), true);
240
+ };
241
+ const readCoordinateCheckpointU64 = (view, offset) => BigInt(view.getUint32(offset, true)) |
242
+ (BigInt(view.getUint32(offset + 4, true)) << 32n);
243
+ const encodeNativeBackboneCoordinateCheckpoint = (checkpoint) => {
244
+ const coordinateLength = checkpoint.coordinateSnapshot.byteLength;
245
+ const documentLength = checkpoint.documentSnapshot.byteLength;
246
+ const signerLength = checkpoint.documentSignerSnapshot.byteLength;
247
+ const byteLength = nativeBackboneCoordinateCheckpointHeaderBytes +
248
+ coordinateLength +
249
+ documentLength +
250
+ signerLength;
251
+ if (!Number.isSafeInteger(byteLength) || byteLength > 0xffff_ffff) {
252
+ throw new RangeError("Native backbone coordinate checkpoint is too large");
253
+ }
254
+ const bytes = new Uint8Array(byteLength);
255
+ bytes.set(nativeBackboneCoordinateCheckpointMagic, 0);
256
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
257
+ view.setUint32(8, nativeBackboneCoordinateCheckpointVersion, true);
258
+ view.setUint32(12, nativeBackboneCoordinateCheckpointHeaderBytes, true);
259
+ writeCoordinateCheckpointU64(view, 16, checkpoint.generation);
260
+ view.setUint32(24, coordinateLength, true);
261
+ view.setUint32(28, documentLength, true);
262
+ view.setUint32(32, signerLength, true);
263
+ view.setUint32(36, coordinateJournalChecksum(checkpoint.coordinateSnapshot), true);
264
+ view.setUint32(40, coordinateJournalChecksum(checkpoint.documentSnapshot), true);
265
+ view.setUint32(44, coordinateJournalChecksum(checkpoint.documentSignerSnapshot), true);
266
+ view.setUint32(48, checkpoint.configurationChecksum, true);
267
+ view.setUint32(52, coordinateJournalChecksum(bytes.subarray(8, 52)), true);
268
+ let offset = nativeBackboneCoordinateCheckpointHeaderBytes;
269
+ bytes.set(checkpoint.coordinateSnapshot, offset);
270
+ offset += coordinateLength;
271
+ bytes.set(checkpoint.documentSnapshot, offset);
272
+ offset += documentLength;
273
+ bytes.set(checkpoint.documentSignerSnapshot, offset);
274
+ return bytes;
275
+ };
276
+ const parseNativeBackboneCoordinateCheckpoint = (bytes, name) => {
277
+ if (bytes.byteLength < nativeBackboneCoordinateCheckpointHeaderBytes ||
278
+ !hasBytesPrefix(bytes, nativeBackboneCoordinateCheckpointMagic)) {
279
+ throw new Error(`Native backbone ${name} has an invalid checkpoint header`);
280
+ }
281
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
282
+ if (view.getUint32(8, true) !== nativeBackboneCoordinateCheckpointVersion ||
283
+ view.getUint32(12, true) !==
284
+ nativeBackboneCoordinateCheckpointHeaderBytes ||
285
+ view.getUint32(52, true) !==
286
+ coordinateJournalChecksum(bytes.subarray(8, 52))) {
287
+ throw new Error(`Native backbone ${name} has invalid checkpoint metadata`);
288
+ }
289
+ const coordinateLength = view.getUint32(24, true);
290
+ const documentLength = view.getUint32(28, true);
291
+ const signerLength = view.getUint32(32, true);
292
+ const expectedLength = nativeBackboneCoordinateCheckpointHeaderBytes +
293
+ coordinateLength +
294
+ documentLength +
295
+ signerLength;
296
+ if (!Number.isSafeInteger(expectedLength) ||
297
+ expectedLength !== bytes.byteLength) {
298
+ throw new Error(`Native backbone ${name} has an invalid checkpoint length`);
299
+ }
300
+ let offset = nativeBackboneCoordinateCheckpointHeaderBytes;
301
+ const coordinateSnapshot = bytes.subarray(offset, offset + coordinateLength);
302
+ offset += coordinateLength;
303
+ const documentSnapshot = bytes.subarray(offset, offset + documentLength);
304
+ offset += documentLength;
305
+ const documentSignerSnapshot = bytes.subarray(offset, offset + signerLength);
306
+ if (coordinateJournalChecksum(coordinateSnapshot) !==
307
+ view.getUint32(36, true) ||
308
+ coordinateJournalChecksum(documentSnapshot) !== view.getUint32(40, true) ||
309
+ coordinateJournalChecksum(documentSignerSnapshot) !==
310
+ view.getUint32(44, true)) {
311
+ throw new Error(`Native backbone ${name} has a checkpoint checksum mismatch`);
312
+ }
313
+ return {
314
+ configurationChecksum: view.getUint32(48, true),
315
+ generation: readCoordinateCheckpointU64(view, 16),
316
+ coordinateSnapshot,
317
+ documentSnapshot,
318
+ documentSignerSnapshot,
319
+ };
320
+ };
321
+ const encodeNativeBackboneCoordinateCheckpointState = (state) => {
322
+ const bytes = new Uint8Array(nativeBackboneCoordinateCheckpointStateBytes);
323
+ bytes.set(nativeBackboneCoordinateCheckpointStateMagic, 0);
324
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
325
+ view.setUint32(8, nativeBackboneCoordinateCheckpointVersion, true);
326
+ view.setUint32(12, state.configurationChecksum, true);
327
+ writeCoordinateCheckpointU64(view, 16, state.highwater);
328
+ writeCoordinateCheckpointU64(view, 24, state.pending?.generation ?? 0n);
329
+ view.setUint32(32, state.pending?.byteLength ?? 0, true);
330
+ view.setUint32(36, state.pending?.checksum ?? 0, true);
331
+ writeCoordinateCheckpointU64(view, 40, state.completed?.generation ?? 0n);
332
+ view.setUint32(48, state.completed?.byteLength ?? 0, true);
333
+ view.setUint32(52, state.completed?.checksum ?? 0, true);
334
+ view.setUint32(56, coordinateJournalChecksum(bytes.subarray(8, 56)), true);
335
+ return bytes;
336
+ };
337
+ const parseNativeBackboneCoordinateCheckpointState = (bytes, name) => {
338
+ if (bytes.byteLength !== nativeBackboneCoordinateCheckpointStateBytes ||
339
+ !hasBytesPrefix(bytes, nativeBackboneCoordinateCheckpointStateMagic)) {
340
+ throw new Error(`Native backbone ${name} has an invalid checkpoint authority`);
341
+ }
342
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
343
+ if (view.getUint32(8, true) !== nativeBackboneCoordinateCheckpointVersion ||
344
+ view.getUint32(56, true) !==
345
+ coordinateJournalChecksum(bytes.subarray(8, 56))) {
346
+ throw new Error(`Native backbone ${name} has corrupt checkpoint authority`);
347
+ }
348
+ const configurationChecksum = view.getUint32(12, true);
349
+ const highwater = readCoordinateCheckpointU64(view, 16);
350
+ const pendingGeneration = readCoordinateCheckpointU64(view, 24);
351
+ const completedGeneration = readCoordinateCheckpointU64(view, 40);
352
+ const authority = (generation, lengthOffset, checksumOffset) => {
353
+ const byteLength = view.getUint32(lengthOffset, true);
354
+ const checksum = view.getUint32(checksumOffset, true);
355
+ if (generation === 0n) {
356
+ if (byteLength !== 0 || checksum !== 0) {
357
+ throw new Error(`Native backbone ${name} has empty authority metadata`);
358
+ }
359
+ return undefined;
360
+ }
361
+ if (byteLength < nativeBackboneCoordinateCheckpointHeaderBytes) {
362
+ throw new Error(`Native backbone ${name} has invalid authority length`);
363
+ }
364
+ return { generation, byteLength, checksum };
365
+ };
366
+ const pending = authority(pendingGeneration, 32, 36);
367
+ const completed = authority(completedGeneration, 48, 52);
368
+ if ((completed && completed.generation > highwater) ||
369
+ (pending && pending.generation !== highwater) ||
370
+ (pending && completed && pending.generation <= completed.generation) ||
371
+ (!pending && completed && completed.generation !== highwater) ||
372
+ (!pending && !completed && highwater !== 0n)) {
373
+ throw new Error(`Native backbone ${name} has invalid generation authority`);
374
+ }
375
+ return { configurationChecksum, highwater, pending, completed };
376
+ };
212
377
  const hasCoordinateJournalMagic = (bytes) => bytes.byteLength >= nativeBackboneCoordinateJournalMagic.byteLength &&
213
378
  nativeBackboneCoordinateJournalMagic.every((byte, index) => bytes[index] === byte);
214
379
  /**
@@ -246,6 +411,12 @@ const resolveCoordinateFlushMaxPendingBytes = (options) => options.flushMaxPendi
246
411
  : options.flushOnAppend === false
247
412
  ? defaultNativeBackboneCoordinateFlushMaxPendingBytes
248
413
  : undefined;
414
+ const validateCoordinateCompactionThreshold = (value, name) => {
415
+ if (!Number.isSafeInteger(value) || value <= 0) {
416
+ throw new RangeError(`Native backbone ${name} must be a positive safe integer`);
417
+ }
418
+ return value;
419
+ };
249
420
  const isNotFoundError = (error) => {
250
421
  const maybeError = error;
251
422
  return maybeError?.code === "ENOENT" || maybeError?.name === "NotFoundError";
@@ -656,7 +827,7 @@ const requestPruneEntryFromRow = (row) => {
656
827
  return entry;
657
828
  };
658
829
  const storageAppendResultFromRow = (resolution, row) => {
659
- const [entryRow, leaderRows, isLeader, assignedToRangeBoundary, coordinateRow, trimRows, trimHashRows, documentTrimmedHeadsProcessed, documentPreviousContextRow,] = row;
830
+ const [entryRow, leaderRows, isLeader, assignedToRangeBoundary, coordinateRow, trimRows, trimHashRows, documentTrimmedHeadsProcessed, documentPreviousContextRow, _trimGidExtension, trimGidRows,] = row;
660
831
  return {
661
832
  entry: storageFactsEntryFromRow(entryRow),
662
833
  leaders: rowsToSamples(leaderRows),
@@ -664,12 +835,13 @@ const storageAppendResultFromRow = (resolution, row) => {
664
835
  assignedToRangeBoundary,
665
836
  coordinate: appendCoordinatePlanFromRow(resolution, coordinateRow),
666
837
  ...trimmedRowsAndHashesResult(trimRows, trimHashRows),
838
+ trimmedGids: trimGidRows ?? trimRows.map((trim) => trim[1]),
667
839
  documentTrimmedHeadsProcessed,
668
840
  documentPreviousContext: documentContextFactsFromRow(documentPreviousContextRow),
669
841
  };
670
842
  };
671
843
  const committedStorageAppendResultFromRow = (resolution, row) => {
672
- const [entryRow, leaderRows, isLeader, assignedToRangeBoundary, coordinateRow, trimRows, trimHashRows, documentTrimmedHeadsProcessed, documentPreviousContextRow,] = row;
844
+ const [entryRow, leaderRows, isLeader, assignedToRangeBoundary, coordinateRow, trimRows, trimHashRows, documentTrimmedHeadsProcessed, documentPreviousContextRow, _trimGidExtension, trimGidRows,] = row;
673
845
  return {
674
846
  entry: committedStorageFactsEntryFromRow(entryRow),
675
847
  leaders: rowsToSamples(leaderRows),
@@ -677,15 +849,22 @@ const committedStorageAppendResultFromRow = (resolution, row) => {
677
849
  assignedToRangeBoundary,
678
850
  coordinate: appendCoordinatePlanFromRow(resolution, coordinateRow),
679
851
  ...trimmedRowsAndHashesResult(trimRows, trimHashRows),
852
+ trimmedGids: trimGidRows ?? trimRows.map((trim) => trim[1]),
680
853
  documentTrimmedHeadsProcessed,
681
854
  documentPreviousContext: documentContextFactsFromRow(documentPreviousContextRow),
682
855
  };
683
856
  };
684
857
  const compactCommittedNoNextStorageAppendResultFromRow = (resolution, row) => {
685
- const [hash, byteLength, metaBytes, fourth] = row;
858
+ const appendedTrimGids = row.length >= 12 &&
859
+ row[row.length - 2] === undefined &&
860
+ Array.isArray(row[row.length - 1])
861
+ ? row[row.length - 1]
862
+ : undefined;
863
+ const baseRow = appendedTrimGids ? row.slice(0, -2) : row;
864
+ const [hash, byteLength, metaBytes, fourth] = baseRow;
686
865
  const hasDigestRow = fourth instanceof Uint8Array;
687
866
  const hashDigestBytes = hasDigestRow ? fourth : undefined;
688
- const rest = row.slice(hasDigestRow ? 4 : 3);
867
+ const rest = baseRow.slice(hasDigestRow ? 4 : 3);
689
868
  const usesNestedCoordinateRow = Array.isArray(rest[0]);
690
869
  let coordinate;
691
870
  let leaderRows;
@@ -733,11 +912,18 @@ const compactCommittedNoNextStorageAppendResultFromRow = (resolution, row) => {
733
912
  coordinate,
734
913
  trimmed: [],
735
914
  trimmedHashes: trimHashRows ?? [],
915
+ trimmedGids: appendedTrimGids,
736
916
  documentTrimmedHeadsProcessed,
737
917
  };
738
918
  };
739
919
  const compactCommittedLatestStorageAppendResultFromRow = (resolution, row) => {
740
- const [hash, byteLength, metaBytes, fourth, fifth, sixth, seventh, eighth, ninth, tenth, eleventh, twelfth,] = row;
920
+ const appendedTrimGids = row.length >= 13 &&
921
+ row[row.length - 2] === undefined &&
922
+ Array.isArray(row[row.length - 1])
923
+ ? row[row.length - 1]
924
+ : undefined;
925
+ const baseRow = appendedTrimGids ? row.slice(0, -2) : row;
926
+ const [hash, byteLength, metaBytes, fourth, fifth, sixth, seventh, eighth, ninth, tenth, eleventh, twelfth,] = baseRow;
741
927
  const hasDigestRow = fourth instanceof Uint8Array;
742
928
  const hashDigestBytes = hasDigestRow ? fourth : undefined;
743
929
  const next = (hasDigestRow ? fifth : fourth);
@@ -765,6 +951,7 @@ const compactCommittedLatestStorageAppendResultFromRow = (resolution, row) => {
765
951
  coordinate,
766
952
  trimmed: [],
767
953
  trimmedHashes: trimHashRows ?? [],
954
+ trimmedGids: appendedTrimGids,
768
955
  documentTrimmedHeadsProcessed,
769
956
  documentPreviousContext: documentContextFactsFromRow(documentPreviousContextRow),
770
957
  };
@@ -784,6 +971,14 @@ const preparedCommitFactsFromRow = (row) => {
784
971
  }
785
972
  return prepared;
786
973
  };
974
+ const preparedCommitFactsWithTrimRefsFromRow = (row) => {
975
+ const prepared = committedStorageFactsEntryFromRow(row[0]);
976
+ return {
977
+ ...prepared,
978
+ trimmedEntryHashes: row[1],
979
+ trimmedEntryGids: row[2],
980
+ };
981
+ };
787
982
  const preparedCommitFactsWithLatestDocumentContextFromRow = (row) => {
788
983
  const [entryRow, trimHashRows, documentTrimmedHeadsProcessed, contextRow] = row;
789
984
  return {
@@ -793,24 +988,53 @@ const preparedCommitFactsWithLatestDocumentContextFromRow = (row) => {
793
988
  documentPreviousContext: documentContextFactsFromRow(contextRow),
794
989
  };
795
990
  };
796
- const compactPreparedCommitFactsWithTrimHashesFromRow = (row) => {
991
+ const preparedCommitFactsWithLatestDocumentContextAndTrimRefsFromRow = (row) => {
992
+ const [entryRow, trimHashRows, trimGidRows, documentTrimmedHeadsProcessed, contextRow,] = row;
993
+ return {
994
+ ...committedStorageFactsEntryFromRow(entryRow),
995
+ trimmedEntryHashes: trimHashRows,
996
+ trimmedEntryGids: trimGidRows,
997
+ documentTrimmedHeadsProcessed,
998
+ documentPreviousContext: documentContextFactsFromRow(contextRow),
999
+ };
1000
+ };
1001
+ const compactPreparedCommitFactsBaseFromRow = (row) => {
797
1002
  const [hash, byteLength, metaBytes, fourth] = row;
798
1003
  const hasDigestRow = fourth instanceof Uint8Array;
799
- const hashDigestBytes = hasDigestRow ? fourth : undefined;
800
- const trimHashOffset = hasDigestRow ? 4 : 3;
801
- const trimHashRows = row[trimHashOffset];
802
- const documentTrimmedHeadsProcessed = row[trimHashOffset + 1];
803
1004
  return {
804
- cid: hash,
805
- hash,
806
- next: [],
807
- metaBytes,
808
- byteLength,
809
- hashDigestBytes,
1005
+ entry: {
1006
+ cid: hash,
1007
+ hash,
1008
+ next: [],
1009
+ metaBytes,
1010
+ byteLength,
1011
+ hashDigestBytes: hasDigestRow ? fourth : undefined,
1012
+ },
1013
+ trimRowOffset: hasDigestRow ? 4 : 3,
1014
+ };
1015
+ };
1016
+ const compactPreparedCommitFactsWithTrimHashesFromRow = (row) => {
1017
+ const { entry, trimRowOffset } = compactPreparedCommitFactsBaseFromRow(row);
1018
+ const trimHashRows = row[trimRowOffset];
1019
+ const documentTrimmedHeadsProcessed = row[trimRowOffset + 1];
1020
+ return {
1021
+ ...entry,
810
1022
  trimmedEntryHashes: trimHashRows ?? [],
811
1023
  documentTrimmedHeadsProcessed,
812
1024
  };
813
1025
  };
1026
+ const compactPreparedCommitFactsWithTrimRefsFromRow = (row) => {
1027
+ const { entry, trimRowOffset } = compactPreparedCommitFactsBaseFromRow(row);
1028
+ const trimHashRows = row[trimRowOffset];
1029
+ const trimGidRows = row[trimRowOffset + 1];
1030
+ const documentTrimmedHeadsProcessed = row[trimRowOffset + 2];
1031
+ return {
1032
+ ...entry,
1033
+ trimmedEntryHashes: trimHashRows,
1034
+ trimmedEntryGids: trimGidRows,
1035
+ documentTrimmedHeadsProcessed,
1036
+ };
1037
+ };
814
1038
  const nativeLogCommitEntryColumns = (entries) => {
815
1039
  const hashes = new Array(entries.length);
816
1040
  const blockBytes = new Array(entries.length);
@@ -1266,13 +1490,60 @@ class NativeBackboneLogGraph {
1266
1490
  const documentIndex = input.documentIndex;
1267
1491
  const documentIndexArgs = nativeDocumentIndexArgs(documentIndex);
1268
1492
  const projection = documentIndex?.projection;
1493
+ if (!documentIndexArgs &&
1494
+ input.resolveTrimmedEntries === false &&
1495
+ input.trimLengthTo != null &&
1496
+ this.native.prepare_plain_entry_commit_facts_trim_refs) {
1497
+ return preparedCommitFactsWithTrimRefsFromRow(this.native.prepare_plain_entry_commit_facts_trim_refs(wallTime, logical, input.gid, input.next ?? [], entryType, input.metaData, input.payloadData, input.trimLengthTo));
1498
+ }
1269
1499
  if (documentIndex?.useLatestContext &&
1270
1500
  documentIndexArgs &&
1271
1501
  input.resolveTrimmedEntries === false) {
1272
1502
  if (projection && this.options?.documentProjectionPlanId) {
1273
- return preparedCommitFactsWithLatestDocumentContextFromRow(this.native.prepare_plain_entry_commit_latest_facts_document_index_cached_plan_trim_hashes(wallTime, logical, input.gid, entryType, input.metaData, input.payloadData, input.trimLengthTo, documentIndex.key, documentIndex.byteElementIndexLimit ?? 0, documentIndex.deleteTrimmedHeads === true, this.options.documentProjectionPlanId(projection.plan), projection.encodedDocument, projection.signer));
1503
+ const args = [
1504
+ wallTime,
1505
+ logical,
1506
+ input.gid,
1507
+ entryType,
1508
+ input.metaData,
1509
+ input.payloadData,
1510
+ input.trimLengthTo,
1511
+ documentIndex.key,
1512
+ documentIndex.byteElementIndexLimit ?? 0,
1513
+ documentIndex.deleteTrimmedHeads === true,
1514
+ this.options.documentProjectionPlanId(projection.plan),
1515
+ projection.encodedDocument,
1516
+ projection.signer,
1517
+ ];
1518
+ const prepareTrimRefs = this.native
1519
+ .prepare_plain_entry_commit_latest_facts_document_index_cached_plan_trim_refs;
1520
+ if (prepareTrimRefs) {
1521
+ return preparedCommitFactsWithLatestDocumentContextAndTrimRefsFromRow(prepareTrimRefs.call(this.native, ...args));
1522
+ }
1523
+ return preparedCommitFactsWithLatestDocumentContextFromRow(this.native.prepare_plain_entry_commit_latest_facts_document_index_cached_plan_trim_hashes(...args));
1524
+ }
1525
+ const args = [
1526
+ wallTime,
1527
+ logical,
1528
+ input.gid,
1529
+ entryType,
1530
+ input.metaData,
1531
+ input.payloadData,
1532
+ input.trimLengthTo,
1533
+ documentIndex.key,
1534
+ documentIndex.valuePrefixBytes ?? EMPTY_UINT8_ARRAY,
1535
+ documentIndex.byteElementIndexLimit ?? 0,
1536
+ documentIndex.deleteTrimmedHeads === true,
1537
+ projection?.plan,
1538
+ projection?.encodedDocument,
1539
+ projection?.signer,
1540
+ ];
1541
+ const prepareTrimRefs = this.native
1542
+ .prepare_plain_entry_commit_latest_facts_document_index_trim_refs;
1543
+ if (prepareTrimRefs) {
1544
+ return preparedCommitFactsWithLatestDocumentContextAndTrimRefsFromRow(prepareTrimRefs.call(this.native, ...args));
1274
1545
  }
1275
- return preparedCommitFactsWithLatestDocumentContextFromRow(this.native.prepare_plain_entry_commit_latest_facts_document_index_trim_hashes(wallTime, logical, input.gid, entryType, input.metaData, input.payloadData, input.trimLengthTo, documentIndex.key, documentIndex.valuePrefixBytes ?? EMPTY_UINT8_ARRAY, documentIndex.byteElementIndexLimit ?? 0, documentIndex.deleteTrimmedHeads === true, projection?.plan, projection?.encodedDocument, projection?.signer));
1546
+ return preparedCommitFactsWithLatestDocumentContextFromRow(this.native.prepare_plain_entry_commit_latest_facts_document_index_trim_hashes(...args));
1276
1547
  }
1277
1548
  if (documentIndexArgs &&
1278
1549
  projection &&
@@ -1281,16 +1552,57 @@ class NativeBackboneLogGraph {
1281
1552
  input.trimLengthTo != null &&
1282
1553
  hasNoNext) {
1283
1554
  const projectionPlanId = this.options.documentProjectionPlanId(projection.plan);
1555
+ const plainPutPayloadArgs = [
1556
+ wallTime,
1557
+ logical,
1558
+ input.gid,
1559
+ entryType,
1560
+ input.metaData,
1561
+ input.payloadData,
1562
+ input.trimLengthTo,
1563
+ documentIndex.key,
1564
+ documentIndex.existingCreated == null
1565
+ ? ""
1566
+ : integerString(documentIndex.existingCreated),
1567
+ documentIndex.byteElementIndexLimit ?? 0,
1568
+ documentIndex.deleteTrimmedHeads === true,
1569
+ projectionPlanId,
1570
+ projection.signer,
1571
+ ];
1572
+ const plainPutPayloadCommitWithTrimRefs = this.native
1573
+ .prepare_plain_entry_commit_no_next_facts_document_index_cached_plan_compact_trim_refs_plain_put_payload;
1574
+ if (plainPutPayloadCommitWithTrimRefs) {
1575
+ return compactPreparedCommitFactsWithTrimRefsFromRow(plainPutPayloadCommitWithTrimRefs.call(this.native, ...plainPutPayloadArgs));
1576
+ }
1284
1577
  const plainPutPayloadCommit = this.native
1285
1578
  .prepare_plain_entry_commit_no_next_facts_document_index_cached_plan_compact_trim_hashes_plain_put_payload;
1286
1579
  if (plainPutPayloadCommit) {
1287
- return compactPreparedCommitFactsWithTrimHashesFromRow(plainPutPayloadCommit.call(this.native, wallTime, logical, input.gid, entryType, input.metaData, input.payloadData, input.trimLengthTo, documentIndex.key, documentIndex.existingCreated == null
1580
+ return compactPreparedCommitFactsWithTrimHashesFromRow(plainPutPayloadCommit.call(this.native, ...plainPutPayloadArgs));
1581
+ }
1582
+ const args = [
1583
+ wallTime,
1584
+ logical,
1585
+ input.gid,
1586
+ entryType,
1587
+ input.metaData,
1588
+ input.payloadData,
1589
+ input.trimLengthTo,
1590
+ documentIndex.key,
1591
+ documentIndex.existingCreated == null
1288
1592
  ? ""
1289
- : integerString(documentIndex.existingCreated), documentIndex.byteElementIndexLimit ?? 0, documentIndex.deleteTrimmedHeads === true, projectionPlanId, projection.signer));
1593
+ : integerString(documentIndex.existingCreated),
1594
+ documentIndex.byteElementIndexLimit ?? 0,
1595
+ documentIndex.deleteTrimmedHeads === true,
1596
+ projectionPlanId,
1597
+ projection.encodedDocument,
1598
+ projection.signer,
1599
+ ];
1600
+ const prepareTrimRefs = this.native
1601
+ .prepare_plain_entry_commit_no_next_facts_document_index_cached_plan_compact_trim_refs;
1602
+ if (prepareTrimRefs) {
1603
+ return compactPreparedCommitFactsWithTrimRefsFromRow(prepareTrimRefs.call(this.native, ...args));
1290
1604
  }
1291
- return compactPreparedCommitFactsWithTrimHashesFromRow(this.native.prepare_plain_entry_commit_no_next_facts_document_index_cached_plan_compact_trim_hashes(wallTime, logical, input.gid, entryType, input.metaData, input.payloadData, input.trimLengthTo, documentIndex.key, documentIndex.existingCreated == null
1292
- ? ""
1293
- : integerString(documentIndex.existingCreated), documentIndex.byteElementIndexLimit ?? 0, documentIndex.deleteTrimmedHeads === true, projectionPlanId, projection.encodedDocument, projection.signer));
1605
+ return compactPreparedCommitFactsWithTrimHashesFromRow(this.native.prepare_plain_entry_commit_no_next_facts_document_index_cached_plan_compact_trim_hashes(...args));
1294
1606
  }
1295
1607
  if (documentIndexArgs &&
1296
1608
  projection &&
@@ -1333,6 +1645,11 @@ class NativeBackboneLogGraph {
1333
1645
  input.resolveTrimmedEntries === false &&
1334
1646
  input.trimLengthTo != null &&
1335
1647
  hasNoNext) {
1648
+ const prepareTrimRefs = this.native
1649
+ .prepare_plain_entry_commit_no_next_facts_document_index_compact_trim_refs;
1650
+ if (prepareTrimRefs) {
1651
+ return compactPreparedCommitFactsWithTrimRefsFromRow(prepareTrimRefs.call(this.native, wallTime, logical, input.gid, entryType, input.metaData, input.payloadData, input.trimLengthTo, ...documentIndexArgs));
1652
+ }
1336
1653
  return compactPreparedCommitFactsWithTrimHashesFromRow(this.native.prepare_plain_entry_commit_no_next_facts_document_index_compact_trim_hashes(wallTime, logical, input.gid, entryType, input.metaData, input.payloadData, input.trimLengthTo, ...documentIndexArgs));
1337
1654
  }
1338
1655
  const baseArgs = [
@@ -2263,6 +2580,12 @@ export class NativePeerbitBackbone {
2263
2580
  fullReplicaCandidatesFor(minReplicas, selfHash) {
2264
2581
  return this.native.full_replica_candidates_for(minReplicas, selfHash);
2265
2582
  }
2583
+ getRoutingFullReplicaLeaders(replicas, options) {
2584
+ if (!this.native.get_routing_full_replica_leaders) {
2585
+ return undefined;
2586
+ }
2587
+ return rowsToSamples(this.native.get_routing_full_replica_leaders(replicas, ...findLeaderArguments(options)));
2588
+ }
2266
2589
  clearEntryCoordinates() {
2267
2590
  this.native.clear_entry_coordinates();
2268
2591
  }
@@ -3016,6 +3339,7 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
3016
3339
  appendFailure;
3017
3340
  readLimited;
3018
3341
  durableBarrier;
3342
+ atomicReplace;
3019
3343
  constructor(directory, fs) {
3020
3344
  this.directory = directory;
3021
3345
  this.fs = fs;
@@ -3028,6 +3352,10 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
3028
3352
  if (!fs || typeof fs.openBoundedRead === "function") {
3029
3353
  this.readLimited = (name, maxBytes) => this.readWithinLimit(name, maxBytes);
3030
3354
  }
3355
+ if (!fs ||
3356
+ (typeof fs.open === "function" && typeof fs.rename === "function")) {
3357
+ this.atomicReplace = (name, bytes) => this.replaceAtomically(name, bytes);
3358
+ }
3031
3359
  }
3032
3360
  async nodeFs() {
3033
3361
  return this.fs ?? (await importNodeFs());
@@ -3159,6 +3487,43 @@ export class NativeBackboneNodeCoordinatePersistenceStore {
3159
3487
  await this.closeAppendHandle(path);
3160
3488
  await fs.writeFile(path, bytes);
3161
3489
  }
3490
+ async replaceAtomically(name, bytes) {
3491
+ const fs = await this.ensureDirectory();
3492
+ if (!fs.open || !fs.rename) {
3493
+ throw new Error("Node coordinate persistence does not expose atomic file replacement");
3494
+ }
3495
+ const validName = validateCoordinatePersistenceName(name);
3496
+ const temporaryName = validateCoordinatePersistenceName(`${validName}.tmp`);
3497
+ const path = await this.filePath(validName);
3498
+ const temporaryPath = await this.filePath(temporaryName);
3499
+ await this.closeAppendHandle(path);
3500
+ await this.closeAppendHandle(temporaryPath);
3501
+ await fs.rm(temporaryPath, { force: true });
3502
+ await fs.writeFile(temporaryPath, bytes);
3503
+ let temporaryHandle;
3504
+ try {
3505
+ temporaryHandle = await fs.open(temporaryPath, "r");
3506
+ if (typeof temporaryHandle.sync !== "function") {
3507
+ throw new Error("Node coordinate persistence atomic replacement does not expose FileHandle.sync");
3508
+ }
3509
+ await temporaryHandle.sync();
3510
+ }
3511
+ finally {
3512
+ await temporaryHandle?.close();
3513
+ }
3514
+ await fs.rename(temporaryPath, path);
3515
+ let directoryHandle;
3516
+ try {
3517
+ directoryHandle = await fs.open(this.directory, "r");
3518
+ if (typeof directoryHandle.sync !== "function") {
3519
+ throw new Error("Node coordinate persistence atomic replacement does not expose directory sync");
3520
+ }
3521
+ await directoryHandle.sync();
3522
+ }
3523
+ finally {
3524
+ await directoryHandle?.close();
3525
+ }
3526
+ }
3162
3527
  async append(name, bytes) {
3163
3528
  if (this.appendFailure !== undefined) {
3164
3529
  throw this.appendFailure;
@@ -3458,6 +3823,7 @@ export class NativeBackboneBufferedCoordinatePersistenceStore {
3458
3823
  readLimited;
3459
3824
  supportsRemoval;
3460
3825
  durableBarrier;
3826
+ atomicReplace;
3461
3827
  constructor(inner, options = {}) {
3462
3828
  this.inner = inner;
3463
3829
  this.options = options;
@@ -3473,6 +3839,12 @@ export class NativeBackboneBufferedCoordinatePersistenceStore {
3473
3839
  await inner.durableBarrier(name);
3474
3840
  };
3475
3841
  }
3842
+ if (typeof inner.atomicReplace === "function") {
3843
+ this.atomicReplace = async (name, bytes) => {
3844
+ await this.flush(name);
3845
+ await inner.atomicReplace(name, bytes);
3846
+ };
3847
+ }
3476
3848
  }
3477
3849
  buffer(name) {
3478
3850
  const validName = validateCoordinatePersistenceName(name);
@@ -3589,7 +3961,7 @@ export class NativeBackboneCoordinatePersistence {
3589
3961
  flushIntervalMs;
3590
3962
  compactMaxJournalBytes;
3591
3963
  compactMaxJournalRecords;
3592
- crashSafeCompaction = false;
3964
+ crashSafeCompaction;
3593
3965
  durableBarrier;
3594
3966
  supportsDrop;
3595
3967
  dropIsTerminal = true;
@@ -3599,9 +3971,22 @@ export class NativeBackboneCoordinatePersistence {
3599
3971
  documentJournalFile;
3600
3972
  documentSignerSnapshotFile;
3601
3973
  documentSignerJournalFile;
3974
+ checkpointConfigurationChecksum;
3975
+ checkpointStateFile;
3976
+ checkpointFiles;
3977
+ checkpointJournalFiles;
3602
3978
  journalInitialized;
3979
+ journalByteLength = 0;
3980
+ journalRecordCount = 0;
3603
3981
  documentJournalInitialized;
3982
+ documentJournalByteLength = 0;
3983
+ documentJournalRecordCount = 0;
3604
3984
  documentSignerJournalInitialized;
3985
+ documentSignerJournalByteLength = 0;
3986
+ documentSignerJournalRecordCount = 0;
3987
+ checkpointHighwater = 0n;
3988
+ checkpointCompleted;
3989
+ checkpointPending;
3605
3990
  lastFlushMs = Date.now();
3606
3991
  persistenceQueue;
3607
3992
  persistenceLifecycle = "active";
@@ -3627,24 +4012,61 @@ export class NativeBackboneCoordinatePersistence {
3627
4012
  nativeBackboneCoordinatePersistenceFiles.documentSignerSnapshot);
3628
4013
  this.documentSignerJournalFile = validateCoordinatePersistenceName(options.documentSignerJournal ??
3629
4014
  nativeBackboneCoordinatePersistenceFiles.documentSignerJournal);
3630
- if (this.configuredFiles().includes(nativeBackboneCoordinateDropTombstoneFile)) {
3631
- throw new Error("Native backbone coordinate persistence file conflicts with its drop tombstone");
3632
- }
4015
+ this.checkpointConfigurationChecksum = coordinateJournalChecksum(new TextEncoder().encode(JSON.stringify([
4016
+ this.snapshotFile,
4017
+ this.journalFile,
4018
+ this.documentSnapshotFile,
4019
+ this.documentJournalFile,
4020
+ this.documentSignerSnapshotFile,
4021
+ this.documentSignerJournalFile,
4022
+ ])));
4023
+ this.checkpointStateFile = validateCoordinatePersistenceName(`${this.snapshotFile}.checkpoint-state`);
4024
+ this.checkpointFiles = {
4025
+ a: validateCoordinatePersistenceName(`${this.snapshotFile}.checkpoint-a`),
4026
+ b: validateCoordinatePersistenceName(`${this.snapshotFile}.checkpoint-b`),
4027
+ };
4028
+ this.checkpointJournalFiles = {
4029
+ a: {
4030
+ coordinate: validateCoordinatePersistenceName(`${this.journalFile}.checkpoint-a`),
4031
+ document: validateCoordinatePersistenceName(`${this.documentJournalFile}.checkpoint-a`),
4032
+ signer: validateCoordinatePersistenceName(`${this.documentSignerJournalFile}.checkpoint-a`),
4033
+ },
4034
+ b: {
4035
+ coordinate: validateCoordinatePersistenceName(`${this.journalFile}.checkpoint-b`),
4036
+ document: validateCoordinatePersistenceName(`${this.documentJournalFile}.checkpoint-b`),
4037
+ signer: validateCoordinatePersistenceName(`${this.documentSignerJournalFile}.checkpoint-b`),
4038
+ },
4039
+ };
4040
+ if (this.configuredFiles().includes(nativeBackboneCoordinateDropTombstoneFile) ||
4041
+ new Set(this.configuredFiles()).size !== this.configuredFiles().length) {
4042
+ throw new Error("Native backbone coordinate persistence files conflict with one another or with its drop tombstone");
4043
+ }
4044
+ this.crashSafeCompaction =
4045
+ typeof store.atomicReplace === "function" &&
4046
+ this.supportsDrop &&
4047
+ this.durableBarrier;
3633
4048
  this.flushOnAppend = options.flushOnAppend ?? true;
3634
4049
  this.flushMaxPendingBytes = resolveCoordinateFlushMaxPendingBytes(options);
3635
4050
  if (options.flushIntervalMs != null) {
3636
4051
  this.flushIntervalMs = Math.max(0, options.flushIntervalMs);
3637
4052
  }
3638
- if (options.compactMaxJournalBytes != null ||
3639
- options.compactMaxJournalRecords != null) {
4053
+ if ((options.compactMaxJournalBytes != null ||
4054
+ options.compactMaxJournalRecords != null) &&
4055
+ !this.crashSafeCompaction) {
3640
4056
  throw new Error(nativeBackboneCoordinateCompactionDisabledMessage);
3641
4057
  }
4058
+ if (options.compactMaxJournalBytes != null) {
4059
+ this.compactMaxJournalBytes = validateCoordinateCompactionThreshold(options.compactMaxJournalBytes, "compactMaxJournalBytes");
4060
+ }
4061
+ if (options.compactMaxJournalRecords != null) {
4062
+ this.compactMaxJournalRecords = validateCoordinateCompactionThreshold(options.compactMaxJournalRecords, "compactMaxJournalRecords");
4063
+ }
3642
4064
  }
3643
4065
  /** Durable operation-intent capability for strict shared-log transactions. */
3644
4066
  get intentStore() {
3645
4067
  return this.store;
3646
4068
  }
3647
- configuredFiles() {
4069
+ legacyConfiguredFiles() {
3648
4070
  return [
3649
4071
  this.snapshotFile,
3650
4072
  this.journalFile,
@@ -3654,26 +4076,79 @@ export class NativeBackboneCoordinatePersistence {
3654
4076
  this.documentSignerJournalFile,
3655
4077
  ];
3656
4078
  }
4079
+ configuredFiles() {
4080
+ return [
4081
+ ...this.legacyConfiguredFiles(),
4082
+ this.checkpointStateFile,
4083
+ this.checkpointFiles.a,
4084
+ this.checkpointFiles.b,
4085
+ this.checkpointJournalFiles.a.coordinate,
4086
+ this.checkpointJournalFiles.a.document,
4087
+ this.checkpointJournalFiles.a.signer,
4088
+ this.checkpointJournalFiles.b.coordinate,
4089
+ this.checkpointJournalFiles.b.document,
4090
+ this.checkpointJournalFiles.b.signer,
4091
+ `${this.checkpointStateFile}.tmp`,
4092
+ `${this.checkpointFiles.a}.tmp`,
4093
+ `${this.checkpointFiles.b}.tmp`,
4094
+ `${this.journalFile}.tmp`,
4095
+ `${nativeBackboneCoordinateDropTombstoneFile}.tmp`,
4096
+ ];
4097
+ }
4098
+ checkpointSlot(generation) {
4099
+ return generation % 2n === 1n ? "a" : "b";
4100
+ }
4101
+ activeJournalFiles() {
4102
+ const completed = this.checkpointCompleted;
4103
+ return completed
4104
+ ? this.checkpointJournalFiles[this.checkpointSlot(completed.generation)]
4105
+ : {
4106
+ coordinate: this.journalFile,
4107
+ document: this.documentJournalFile,
4108
+ signer: this.documentSignerJournalFile,
4109
+ };
4110
+ }
3657
4111
  assertLifecycleActive(operation) {
3658
4112
  if (this.persistenceLifecycle !== "active") {
3659
4113
  throw new Error(`Native backbone coordinate persistence can not ${operation} while ${this.persistenceLifecycle}`);
3660
4114
  }
3661
4115
  }
3662
4116
  assertActive(operation) {
3663
- if (this.persistenceFailure !== undefined) {
3664
- throw this.persistenceFailure;
3665
- }
4117
+ this.assertPersistenceHealthy();
3666
4118
  this.assertLifecycleActive(operation);
3667
4119
  if (this.dropInitiatedOnGeneration) {
3668
4120
  throw new Error(`Native backbone coordinate persistence can not ${operation} after drop was initiated; retry drop or resume drop first`);
3669
4121
  }
3670
4122
  }
4123
+ assertPersistenceHealthy() {
4124
+ if (this.persistenceFailure !== undefined) {
4125
+ throw this.persistenceFailure;
4126
+ }
4127
+ }
3671
4128
  resetJournalTracking() {
3672
4129
  this.journalInitialized = undefined;
4130
+ this.journalByteLength = 0;
4131
+ this.journalRecordCount = 0;
3673
4132
  this.documentJournalInitialized = undefined;
4133
+ this.documentJournalByteLength = 0;
4134
+ this.documentJournalRecordCount = 0;
3674
4135
  this.documentSignerJournalInitialized = undefined;
4136
+ this.documentSignerJournalByteLength = 0;
4137
+ this.documentSignerJournalRecordCount = 0;
4138
+ this.checkpointHighwater = 0n;
4139
+ this.checkpointCompleted = undefined;
4140
+ this.checkpointPending = undefined;
3675
4141
  this.lastFlushMs = Date.now();
3676
4142
  }
4143
+ async persistDropTombstone(files, preferAtomicReplace = false) {
4144
+ const bytes = nativeBackboneCoordinateDropTombstoneBytes(files);
4145
+ if (preferAtomicReplace && this.store.atomicReplace) {
4146
+ await this.store.atomicReplace(nativeBackboneCoordinateDropTombstoneFile, bytes);
4147
+ return;
4148
+ }
4149
+ await this.store.write(nativeBackboneCoordinateDropTombstoneFile, bytes);
4150
+ await this.barrierFile(nativeBackboneCoordinateDropTombstoneFile);
4151
+ }
3677
4152
  async eraseDropFiles(files) {
3678
4153
  if (!this.supportsDrop || !this.store.remove) {
3679
4154
  throw new Error("Native backbone coordinate persistence store does not support removal");
@@ -3709,13 +4184,7 @@ export class NativeBackboneCoordinatePersistence {
3709
4184
  this.dropInitiatedOnGeneration = true;
3710
4185
  this.persistenceLifecycle = "dropping";
3711
4186
  await this.enqueuePersistence(async () => {
3712
- await this.store.write(nativeBackboneCoordinateDropTombstoneFile, nativeBackboneCoordinateDropTombstoneBytes(files));
3713
- if (this.store.durableBarrier) {
3714
- await this.store.durableBarrier(nativeBackboneCoordinateDropTombstoneFile);
3715
- }
3716
- else {
3717
- await this.store.flush?.(nativeBackboneCoordinateDropTombstoneFile);
3718
- }
4187
+ await this.persistDropTombstone(files);
3719
4188
  await this.eraseDropFiles(files);
3720
4189
  this.resetJournalTracking();
3721
4190
  this.persistenceLifecycle = "dropped";
@@ -3748,6 +4217,43 @@ export class NativeBackboneCoordinatePersistence {
3748
4217
  return this.resumeDropInternal(completesInitiatedDrop ? "dropped" : "active");
3749
4218
  });
3750
4219
  }
4220
+ async barrierFile(name) {
4221
+ if (this.store.durableBarrier) {
4222
+ await this.store.durableBarrier(name);
4223
+ }
4224
+ else {
4225
+ await this.store.flush?.(name);
4226
+ }
4227
+ }
4228
+ async hasDurableLegacyMigrationSentinel() {
4229
+ const existing = await this.store.read(this.journalFile);
4230
+ if (existing &&
4231
+ existing.byteLength ===
4232
+ nativeBackboneCoordinateLegacyMigrationSentinel.byteLength &&
4233
+ nativeBackboneCoordinateLegacyMigrationSentinel.every((byte, index) => existing[index] === byte)) {
4234
+ // A previous process may have observed the bytes from page cache before
4235
+ // its durability barrier rejected. Cross a fresh barrier on every reopen
4236
+ // before any post-checkpoint WAL append can be admitted.
4237
+ await this.barrierFile(this.journalFile);
4238
+ return true;
4239
+ }
4240
+ return false;
4241
+ }
4242
+ async installLegacyMigrationSentinel() {
4243
+ if (await this.hasDurableLegacyMigrationSentinel()) {
4244
+ return;
4245
+ }
4246
+ if (!this.store.atomicReplace) {
4247
+ throw new Error("Native backbone completed checkpoint can not install its downgrade sentinel atomically");
4248
+ }
4249
+ await this.store.atomicReplace(this.journalFile, nativeBackboneCoordinateLegacyMigrationSentinel);
4250
+ }
4251
+ async requireLegacyMigrationSentinel() {
4252
+ if (await this.hasDurableLegacyMigrationSentinel()) {
4253
+ return;
4254
+ }
4255
+ throw new Error("Native backbone completed checkpoint downgrade sentinel is missing or has been replaced; refusing to overwrite possible legacy writes");
4256
+ }
3751
4257
  async hydrate(backbone) {
3752
4258
  this.assertActive("hydrate");
3753
4259
  // Claim the lifecycle synchronously. close() queues after this complete
@@ -3758,24 +4264,147 @@ export class NativeBackboneCoordinatePersistence {
3758
4264
  this.assertHydrating();
3759
4265
  await this.resumeDropInternal("hydrating");
3760
4266
  this.assertHydrating();
3761
- const [snapshot, journal, documentSnapshot, documentJournal, documentSignerSnapshot, documentSignerJournal,] = await Promise.all([
3762
- this.store.read(this.snapshotFile),
3763
- this.store.read(this.journalFile),
3764
- this.store.read(this.documentSnapshotFile),
3765
- this.store.read(this.documentJournalFile),
3766
- this.store.read(this.documentSignerSnapshotFile),
3767
- this.store.read(this.documentSignerJournalFile),
3768
- ]);
4267
+ const checkpointStateBytes = await this.store.read(this.checkpointStateFile);
3769
4268
  this.assertHydrating();
4269
+ let checkpointState;
4270
+ if (checkpointStateBytes) {
4271
+ try {
4272
+ checkpointState = parseNativeBackboneCoordinateCheckpointState(checkpointStateBytes, this.checkpointStateFile);
4273
+ if (checkpointState.configurationChecksum !==
4274
+ this.checkpointConfigurationChecksum) {
4275
+ throw new Error("Native backbone checkpoint authority does not match the configured persistence files");
4276
+ }
4277
+ }
4278
+ catch (error) {
4279
+ this.persistenceFailure ??= error;
4280
+ throw this.persistenceFailure;
4281
+ }
4282
+ }
4283
+ this.checkpointHighwater = checkpointState?.highwater ?? 0n;
4284
+ this.checkpointPending = checkpointState?.pending;
4285
+ this.checkpointCompleted = checkpointState?.completed;
4286
+ let checkpointAuthority = this.checkpointCompleted;
4287
+ let promotePendingCheckpoint = false;
4288
+ if (!checkpointAuthority && this.checkpointPending) {
4289
+ try {
4290
+ // On the first generation only, the sentinel is the durable stage
4291
+ // boundary: publication writes it after the pending checkpoint and its
4292
+ // WAL headers, but before completed authority. A pre-checkpoint reader
4293
+ // can no longer consume legacy state, so recovery may validate and
4294
+ // promote the staged generation. Once a completed generation exists the
4295
+ // sentinel predates later pending work and is not a promotion signal.
4296
+ if (await this.hasDurableLegacyMigrationSentinel()) {
4297
+ checkpointAuthority = this.checkpointPending;
4298
+ promotePendingCheckpoint = true;
4299
+ }
4300
+ }
4301
+ catch (error) {
4302
+ this.persistenceFailure ??= error;
4303
+ throw this.persistenceFailure;
4304
+ }
4305
+ }
4306
+ let snapshot;
4307
+ let journal;
4308
+ let documentSnapshot;
4309
+ let documentJournal;
4310
+ let documentSignerSnapshot;
4311
+ let documentSignerJournal;
4312
+ let journalNames = {
4313
+ coordinate: this.journalFile,
4314
+ document: this.documentJournalFile,
4315
+ signer: this.documentSignerJournalFile,
4316
+ };
4317
+ if (checkpointAuthority) {
4318
+ const authority = checkpointAuthority;
4319
+ const authorityKind = promotePendingCheckpoint
4320
+ ? "promotable pending"
4321
+ : "completed";
4322
+ const slot = this.checkpointSlot(authority.generation);
4323
+ const checkpointFile = this.checkpointFiles[slot];
4324
+ journalNames = this.checkpointJournalFiles[slot];
4325
+ const [checkpointBytes, coordinateJournal, valueJournal, signerJournal,] = await Promise.all([
4326
+ this.store.read(checkpointFile),
4327
+ this.store.read(journalNames.coordinate),
4328
+ this.store.read(journalNames.document),
4329
+ this.store.read(journalNames.signer),
4330
+ ]);
4331
+ this.assertHydrating();
4332
+ try {
4333
+ if (!checkpointBytes ||
4334
+ checkpointBytes.byteLength !== authority.byteLength ||
4335
+ coordinateJournalChecksum(checkpointBytes) !== authority.checksum) {
4336
+ throw new Error(`Native backbone ${authorityKind} checkpoint authority is missing or corrupt`);
4337
+ }
4338
+ if (!coordinateJournal || !valueJournal || !signerJournal) {
4339
+ throw new Error(`Native backbone ${authorityKind} checkpoint WAL generation is incomplete`);
4340
+ }
4341
+ const checkpoint = parseNativeBackboneCoordinateCheckpoint(checkpointBytes, checkpointFile);
4342
+ if (checkpoint.generation !== authority.generation ||
4343
+ checkpoint.configurationChecksum !==
4344
+ this.checkpointConfigurationChecksum) {
4345
+ throw new Error(`Native backbone checkpoint generation or configuration does not match its ${authorityKind} authority`);
4346
+ }
4347
+ snapshot = checkpoint.coordinateSnapshot;
4348
+ documentSnapshot = checkpoint.documentSnapshot;
4349
+ documentSignerSnapshot = checkpoint.documentSignerSnapshot;
4350
+ journal = coordinateJournal;
4351
+ documentJournal = valueJournal;
4352
+ documentSignerJournal = signerJournal;
4353
+ }
4354
+ catch (error) {
4355
+ this.persistenceFailure ??= error;
4356
+ throw this.persistenceFailure;
4357
+ }
4358
+ }
4359
+ else {
4360
+ [
4361
+ snapshot,
4362
+ journal,
4363
+ documentSnapshot,
4364
+ documentJournal,
4365
+ documentSignerSnapshot,
4366
+ documentSignerJournal,
4367
+ ] = await Promise.all([
4368
+ this.store.read(this.snapshotFile),
4369
+ this.store.read(this.journalFile),
4370
+ this.store.read(this.documentSnapshotFile),
4371
+ this.store.read(this.documentJournalFile),
4372
+ this.store.read(this.documentSignerSnapshotFile),
4373
+ this.store.read(this.documentSignerJournalFile),
4374
+ ]);
4375
+ this.assertHydrating();
4376
+ }
3770
4377
  try {
3771
- validateCoordinateJournal(journal, this.journalFile);
3772
- validateCoordinateJournal(documentJournal, this.documentJournalFile);
3773
- validateCoordinateJournal(documentSignerJournal, this.documentSignerJournalFile);
4378
+ if (checkpointAuthority &&
4379
+ (!journal ||
4380
+ !hasCoordinateJournalMagic(journal) ||
4381
+ !documentJournal ||
4382
+ !hasCoordinateJournalMagic(documentJournal) ||
4383
+ !documentSignerJournal ||
4384
+ !hasCoordinateJournalMagic(documentSignerJournal))) {
4385
+ throw new Error(`Native backbone ${promotePendingCheckpoint ? "promotable pending" : "completed"} checkpoint WAL generation has a missing or invalid header`);
4386
+ }
4387
+ validateCoordinateJournal(journal, journalNames.coordinate);
4388
+ validateCoordinateJournal(documentJournal, journalNames.document);
4389
+ validateCoordinateJournal(documentSignerJournal, journalNames.signer);
3774
4390
  }
3775
4391
  catch (error) {
3776
4392
  this.persistenceFailure ??= error;
3777
4393
  throw this.persistenceFailure;
3778
4394
  }
4395
+ if (checkpointAuthority && !promotePendingCheckpoint) {
4396
+ try {
4397
+ // Completed authority is never permission to overwrite an ordinary
4398
+ // legacy WAL: it may contain writes made by a downgraded process in the
4399
+ // publication cut. Require the exact durable sentinel before loading.
4400
+ await this.requireLegacyMigrationSentinel();
4401
+ this.assertHydrating();
4402
+ }
4403
+ catch (error) {
4404
+ this.persistenceFailure ??= error;
4405
+ throw this.persistenceFailure;
4406
+ }
4407
+ }
3779
4408
  let operations;
3780
4409
  let documentOperations;
3781
4410
  let documentSignerOperations;
@@ -3797,12 +4426,49 @@ export class NativeBackboneCoordinatePersistence {
3797
4426
  }
3798
4427
  throw error;
3799
4428
  }
4429
+ if (promotePendingCheckpoint) {
4430
+ try {
4431
+ const authority = checkpointAuthority;
4432
+ if (!this.store.atomicReplace) {
4433
+ throw new Error("Native backbone pending checkpoint can not publish completed authority atomically");
4434
+ }
4435
+ await this.store.atomicReplace(this.checkpointStateFile, encodeNativeBackboneCoordinateCheckpointState({
4436
+ configurationChecksum: this.checkpointConfigurationChecksum,
4437
+ highwater: authority.generation,
4438
+ completed: authority,
4439
+ }));
4440
+ this.checkpointCompleted = authority;
4441
+ this.checkpointPending = undefined;
4442
+ this.assertHydrating();
4443
+ }
4444
+ catch (error) {
4445
+ this.persistenceFailure ??= error;
4446
+ throw this.persistenceFailure;
4447
+ }
4448
+ }
4449
+ if (this.checkpointCompleted || this.checkpointPending) {
4450
+ try {
4451
+ await this.cleanupRetiredCheckpointFiles();
4452
+ this.assertHydrating();
4453
+ }
4454
+ catch (error) {
4455
+ this.persistenceFailure ??= error;
4456
+ throw this.persistenceFailure;
4457
+ }
4458
+ }
3800
4459
  this.assertHydrating();
3801
4460
  this.journalInitialized = !!journal && journal.byteLength > 0;
4461
+ this.journalByteLength = journal?.byteLength ?? 0;
4462
+ this.journalRecordCount = operations;
3802
4463
  this.documentJournalInitialized =
3803
4464
  !!documentJournal && documentJournal.byteLength > 0;
4465
+ this.documentJournalByteLength = documentJournal?.byteLength ?? 0;
4466
+ this.documentJournalRecordCount = documentOperations;
3804
4467
  this.documentSignerJournalInitialized =
3805
4468
  !!documentSignerJournal && documentSignerJournal.byteLength > 0;
4469
+ this.documentSignerJournalByteLength =
4470
+ documentSignerJournal?.byteLength ?? 0;
4471
+ this.documentSignerJournalRecordCount = documentSignerOperations;
3806
4472
  backbone.setCoordinateJournalEnabled(true);
3807
4473
  backbone.setDocumentJournalEnabled(true);
3808
4474
  backbone.setDocumentSignerJournalEnabled(true);
@@ -3838,13 +4504,17 @@ export class NativeBackboneCoordinatePersistence {
3838
4504
  return false;
3839
4505
  }
3840
4506
  let tombstone;
4507
+ let expandedFiles;
3841
4508
  try {
3842
4509
  tombstone = parseNativeBackboneCoordinateDropTombstone(bytes);
3843
- for (const file of this.configuredFiles()) {
4510
+ for (const file of this.legacyConfiguredFiles()) {
3844
4511
  if (!tombstone.files.includes(file)) {
3845
4512
  throw new Error("Native backbone drop tombstone does not cover the configured namespace");
3846
4513
  }
3847
4514
  }
4515
+ expandedFiles = [
4516
+ ...new Set([...tombstone.files, ...this.configuredFiles()]),
4517
+ ];
3848
4518
  }
3849
4519
  catch (error) {
3850
4520
  // Corruption must never hydrate stale files, but an explicit drop must
@@ -3853,6 +4523,17 @@ export class NativeBackboneCoordinatePersistence {
3853
4523
  this.persistenceLifecycle = this.closePromise ? "closing" : "active";
3854
4524
  throw this.persistenceFailure;
3855
4525
  }
4526
+ if (expandedFiles.length !== tombstone.files.length) {
4527
+ // Older valid markers only knew the six snapshot/WAL files. Widen the
4528
+ // destructive intent durably before erasing any checkpoint generation,
4529
+ // while preserving additional namespace files recorded by that version.
4530
+ await this.persistDropTombstone(expandedFiles, true);
4531
+ tombstone = { ...tombstone, files: expandedFiles };
4532
+ }
4533
+ // A prior replacement attempt may have made the expanded bytes visible before
4534
+ // its durability barrier rejected. Cross a fresh barrier on every recovery
4535
+ // before allowing any target removal.
4536
+ await this.barrierFile(nativeBackboneCoordinateDropTombstoneFile);
3856
4537
  await this.eraseDropFiles(tombstone.files);
3857
4538
  this.resetJournalTracking();
3858
4539
  this.persistenceLifecycle =
@@ -3896,7 +4577,13 @@ export class NativeBackboneCoordinatePersistence {
3896
4577
  this.assertActive("flush");
3897
4578
  // Serialized with compact() so a flush never clears records appended to
3898
4579
  // the wasm journal while a previous flush was awaiting its disk write.
3899
- return this.enqueuePersistence(() => this.flushJournalInternal(backbone));
4580
+ return this.enqueuePersistence(() => {
4581
+ // A call can be admitted while an earlier queued write is still in flight.
4582
+ // Re-check the sticky failure at execution time so no suffix can ACK after
4583
+ // that earlier write or authority switch failed.
4584
+ this.assertPersistenceHealthy();
4585
+ return this.flushJournalInternal(backbone);
4586
+ });
3900
4587
  }
3901
4588
  enqueuePersistence(fn) {
3902
4589
  // Runs `fn` immediately when no other persistence operation is in
@@ -3912,7 +4599,151 @@ export class NativeBackboneCoordinatePersistence {
3912
4599
  });
3913
4600
  return next;
3914
4601
  }
3915
- async flushJournalInternal(backbone) {
4602
+ shouldCompactJournal(additionalBytes, additionalRecords) {
4603
+ return ((this.compactMaxJournalBytes != null &&
4604
+ this.journalByteLength +
4605
+ this.documentJournalByteLength +
4606
+ this.documentSignerJournalByteLength +
4607
+ additionalBytes >=
4608
+ this.compactMaxJournalBytes) ||
4609
+ (this.compactMaxJournalRecords != null &&
4610
+ this.journalRecordCount +
4611
+ this.documentJournalRecordCount +
4612
+ this.documentSignerJournalRecordCount +
4613
+ additionalRecords >=
4614
+ this.compactMaxJournalRecords));
4615
+ }
4616
+ nextCheckpointGeneration() {
4617
+ let generation = this.checkpointHighwater + 1n;
4618
+ const active = this.checkpointCompleted;
4619
+ if (active &&
4620
+ this.checkpointSlot(generation) === this.checkpointSlot(active.generation)) {
4621
+ generation += 1n;
4622
+ }
4623
+ if (generation > nativeBackboneCoordinateMaxU64) {
4624
+ throw new RangeError("Native backbone checkpoint generation highwater is exhausted");
4625
+ }
4626
+ return generation;
4627
+ }
4628
+ async publishCheckpoint(checkpoint) {
4629
+ if (!this.crashSafeCompaction ||
4630
+ !this.store.atomicReplace ||
4631
+ !this.store.remove) {
4632
+ throw new Error(nativeBackboneCoordinateCompactionDisabledMessage);
4633
+ }
4634
+ const generation = this.nextCheckpointGeneration();
4635
+ const slot = this.checkpointSlot(generation);
4636
+ const checkpointBytes = encodeNativeBackboneCoordinateCheckpoint({
4637
+ configurationChecksum: this.checkpointConfigurationChecksum,
4638
+ generation,
4639
+ ...checkpoint,
4640
+ });
4641
+ const authority = {
4642
+ generation,
4643
+ byteLength: checkpointBytes.byteLength,
4644
+ checksum: coordinateJournalChecksum(checkpointBytes),
4645
+ };
4646
+ const pendingState = {
4647
+ configurationChecksum: this.checkpointConfigurationChecksum,
4648
+ highwater: generation,
4649
+ pending: authority,
4650
+ completed: this.checkpointCompleted,
4651
+ };
4652
+ await this.store.atomicReplace(this.checkpointStateFile, encodeNativeBackboneCoordinateCheckpointState(pendingState));
4653
+ this.checkpointHighwater = generation;
4654
+ this.checkpointPending = authority;
4655
+ const targetJournals = this.checkpointJournalFiles[slot];
4656
+ for (const file of [
4657
+ targetJournals.coordinate,
4658
+ targetJournals.document,
4659
+ targetJournals.signer,
4660
+ ]) {
4661
+ await this.store.remove(file);
4662
+ }
4663
+ const journalHeaders = [
4664
+ [targetJournals.coordinate, nativeBackboneCoordinateJournalMagic],
4665
+ [targetJournals.document, nativeBackboneCoordinateJournalMagic],
4666
+ [targetJournals.signer, nativeBackboneCoordinateJournalMagic],
4667
+ ];
4668
+ for (const [file, header] of journalHeaders) {
4669
+ await this.store.write(file, header);
4670
+ await this.barrierFile(file);
4671
+ }
4672
+ await this.store.atomicReplace(this.checkpointFiles[slot], checkpointBytes);
4673
+ // A prior release does not understand checkpoint authority. The sentinel is
4674
+ // the first-generation publication boundary: install and durably revalidate
4675
+ // it before completed authority becomes visible. On later generations it
4676
+ // must already be exact; never overwrite possible writes from a downgrade.
4677
+ if (this.checkpointCompleted) {
4678
+ await this.requireLegacyMigrationSentinel();
4679
+ }
4680
+ else {
4681
+ await this.installLegacyMigrationSentinel();
4682
+ }
4683
+ const completedState = {
4684
+ configurationChecksum: this.checkpointConfigurationChecksum,
4685
+ highwater: generation,
4686
+ completed: authority,
4687
+ };
4688
+ await this.store.atomicReplace(this.checkpointStateFile, encodeNativeBackboneCoordinateCheckpointState(completedState));
4689
+ // The completed authority is now the only writable generation. Update all
4690
+ // in-memory routing synchronously before another queued flush can start.
4691
+ this.checkpointCompleted = authority;
4692
+ this.checkpointPending = undefined;
4693
+ this.journalInitialized = true;
4694
+ this.documentJournalInitialized = true;
4695
+ this.documentSignerJournalInitialized = true;
4696
+ this.journalByteLength = nativeBackboneCoordinateJournalMagic.byteLength;
4697
+ this.documentJournalByteLength =
4698
+ nativeBackboneCoordinateJournalMagic.byteLength;
4699
+ this.documentSignerJournalByteLength =
4700
+ nativeBackboneCoordinateJournalMagic.byteLength;
4701
+ this.journalRecordCount = 0;
4702
+ this.documentJournalRecordCount = 0;
4703
+ this.documentSignerJournalRecordCount = 0;
4704
+ }
4705
+ async cleanupRetiredCheckpointFiles() {
4706
+ if (!this.store.remove ||
4707
+ (!this.checkpointCompleted && !this.checkpointPending)) {
4708
+ return;
4709
+ }
4710
+ const retiredSlot = this
4711
+ .checkpointCompleted
4712
+ ? this.checkpointSlot(this.checkpointCompleted.generation) === "a"
4713
+ ? "b"
4714
+ : "a"
4715
+ : this.checkpointSlot(this.checkpointPending.generation);
4716
+ const retiredJournals = this.checkpointJournalFiles[retiredSlot];
4717
+ const files = [
4718
+ ...(this.checkpointCompleted
4719
+ ? [
4720
+ this.snapshotFile,
4721
+ this.documentSnapshotFile,
4722
+ this.documentJournalFile,
4723
+ this.documentSignerSnapshotFile,
4724
+ this.documentSignerJournalFile,
4725
+ ]
4726
+ : []),
4727
+ this.checkpointFiles[retiredSlot],
4728
+ `${this.checkpointFiles[retiredSlot]}.tmp`,
4729
+ retiredJournals.coordinate,
4730
+ retiredJournals.document,
4731
+ retiredJournals.signer,
4732
+ ];
4733
+ const removals = await Promise.allSettled(files.map((file) => this.store.remove(file)));
4734
+ if (removals.every((result) => result.status === "fulfilled")) {
4735
+ try {
4736
+ await this.store.durableBarrier?.();
4737
+ }
4738
+ catch {
4739
+ // Cleanup is retryable debt. Publication and the caller's ACK already
4740
+ // belong to the new generation, so never turn this into an ambiguous
4741
+ // append failure after the authority switch.
4742
+ }
4743
+ }
4744
+ }
4745
+ async flushJournalInternal(backbone, forceCheckpoint = false) {
4746
+ this.assertPersistenceHealthy();
3916
4747
  let written = 0;
3917
4748
  let persistenceMutationStarted = false;
3918
4749
  let coordinateBytes;
@@ -3924,10 +4755,39 @@ export class NativeBackboneCoordinatePersistence {
3924
4755
  const documentRecordCount = backbone.documentPendingJournalLength;
3925
4756
  const signerRecords = backbone.documentSignerJournal();
3926
4757
  const signerRecordCount = backbone.documentSignerPendingJournalLength;
4758
+ const additionalBytes = coordinateRecords.byteLength +
4759
+ documentRecords.byteLength +
4760
+ signerRecords.byteLength +
4761
+ (coordinateRecords.byteLength > 0 && this.journalInitialized !== true
4762
+ ? backbone.coordinateJournalHeader().byteLength
4763
+ : 0) +
4764
+ (documentRecords.byteLength > 0 &&
4765
+ this.documentJournalInitialized !== true
4766
+ ? backbone.documentJournalHeader().byteLength
4767
+ : 0) +
4768
+ (signerRecords.byteLength > 0 &&
4769
+ this.documentSignerJournalInitialized !== true
4770
+ ? backbone.documentSignerJournalHeader().byteLength
4771
+ : 0);
4772
+ const additionalRecords = coordinateRecordCount + documentRecordCount + signerRecordCount;
4773
+ const checkpointRequested = forceCheckpoint ||
4774
+ (this.crashSafeCompaction &&
4775
+ this.shouldCompactJournal(additionalBytes, additionalRecords));
4776
+ // These three synchronous copies and the journal prefixes above are one
4777
+ // logical cut. No mutation can slip into the checkpoint without also being
4778
+ // represented by one of the captured prefixes.
4779
+ const checkpoint = checkpointRequested
4780
+ ? {
4781
+ coordinateSnapshot: backbone.coordinateSnapshot(),
4782
+ documentSnapshot: backbone.documentSnapshot(),
4783
+ documentSignerSnapshot: backbone.documentSignerSnapshot(),
4784
+ }
4785
+ : undefined;
3927
4786
  try {
4787
+ const activeJournals = this.activeJournalFiles();
3928
4788
  if (coordinateRecords.byteLength > 0) {
3929
4789
  if (this.journalInitialized === undefined) {
3930
- const existing = await this.store.read(this.journalFile);
4790
+ const existing = await this.store.read(activeJournals.coordinate);
3931
4791
  this.journalInitialized = !!existing && existing.byteLength > 0;
3932
4792
  }
3933
4793
  coordinateBytes = this.journalInitialized
@@ -3936,26 +4796,26 @@ export class NativeBackboneCoordinatePersistence {
3936
4796
  backbone.coordinateJournalHeader(),
3937
4797
  coordinateRecords,
3938
4798
  ]);
3939
- await this.store.append(this.journalFile, coordinateBytes);
4799
+ await this.store.append(activeJournals.coordinate, coordinateBytes);
3940
4800
  persistenceMutationStarted = true;
3941
4801
  written += coordinateRecords.byteLength;
3942
4802
  }
3943
4803
  if (documentRecords.byteLength > 0) {
3944
4804
  if (this.documentJournalInitialized === undefined) {
3945
- const existing = await this.store.read(this.documentJournalFile);
4805
+ const existing = await this.store.read(activeJournals.document);
3946
4806
  this.documentJournalInitialized =
3947
4807
  !!existing && existing.byteLength > 0;
3948
4808
  }
3949
4809
  documentBytes = this.documentJournalInitialized
3950
4810
  ? documentRecords
3951
4811
  : concatBytes([backbone.documentJournalHeader(), documentRecords]);
3952
- await this.store.append(this.documentJournalFile, documentBytes);
4812
+ await this.store.append(activeJournals.document, documentBytes);
3953
4813
  persistenceMutationStarted = true;
3954
4814
  written += documentRecords.byteLength;
3955
4815
  }
3956
4816
  if (signerRecords.byteLength > 0) {
3957
4817
  if (this.documentSignerJournalInitialized === undefined) {
3958
- const existing = await this.store.read(this.documentSignerJournalFile);
4818
+ const existing = await this.store.read(activeJournals.signer);
3959
4819
  this.documentSignerJournalInitialized =
3960
4820
  !!existing && existing.byteLength > 0;
3961
4821
  }
@@ -3965,42 +4825,50 @@ export class NativeBackboneCoordinatePersistence {
3965
4825
  backbone.documentSignerJournalHeader(),
3966
4826
  signerRecords,
3967
4827
  ]);
3968
- await this.store.append(this.documentSignerJournalFile, signerBytes);
4828
+ await this.store.append(activeJournals.signer, signerBytes);
3969
4829
  persistenceMutationStarted = true;
3970
4830
  written += signerRecords.byteLength;
3971
4831
  }
3972
- if (written === 0) {
4832
+ if (written === 0 && !checkpoint) {
3973
4833
  this.lastFlushMs = Date.now();
3974
4834
  return 0;
3975
4835
  }
3976
4836
  // `append` may only enqueue bytes in a buffered store. Drain and fsync
3977
4837
  // every affected WAL before clearing its wasm prefix or returning an ACK.
3978
4838
  for (const file of [
3979
- coordinateBytes ? this.journalFile : undefined,
3980
- documentBytes ? this.documentJournalFile : undefined,
3981
- signerBytes ? this.documentSignerJournalFile : undefined,
4839
+ coordinateBytes ? activeJournals.coordinate : undefined,
4840
+ documentBytes ? activeJournals.document : undefined,
4841
+ signerBytes ? activeJournals.signer : undefined,
3982
4842
  ]) {
3983
4843
  if (file) {
3984
- if (this.store.durableBarrier) {
3985
- await this.store.durableBarrier(file);
3986
- }
3987
- else {
3988
- await this.store.flush?.(file);
3989
- }
4844
+ await this.barrierFile(file);
3990
4845
  }
3991
4846
  }
3992
4847
  if (coordinateBytes) {
3993
4848
  this.journalInitialized = true;
4849
+ this.journalByteLength += coordinateBytes.byteLength;
4850
+ this.journalRecordCount += coordinateRecordCount;
3994
4851
  }
3995
4852
  if (documentBytes) {
3996
4853
  this.documentJournalInitialized = true;
4854
+ this.documentJournalByteLength += documentBytes.byteLength;
4855
+ this.documentJournalRecordCount += documentRecordCount;
3997
4856
  }
3998
4857
  if (signerBytes) {
3999
4858
  this.documentSignerJournalInitialized = true;
4859
+ this.documentSignerJournalByteLength += signerBytes.byteLength;
4860
+ this.documentSignerJournalRecordCount += signerRecordCount;
4861
+ }
4862
+ if (checkpoint) {
4863
+ persistenceMutationStarted = true;
4864
+ await this.publishCheckpoint(checkpoint);
4000
4865
  }
4001
4866
  backbone.clearCoordinateJournalPrefix(coordinateRecords.byteLength, coordinateRecordCount);
4002
4867
  backbone.clearDocumentJournalPrefix(documentRecords.byteLength, documentRecordCount);
4003
4868
  backbone.clearDocumentSignerJournalPrefix(signerRecords.byteLength, signerRecordCount);
4869
+ if (checkpoint) {
4870
+ await this.cleanupRetiredCheckpointFiles();
4871
+ }
4004
4872
  this.lastFlushMs = Date.now();
4005
4873
  return written;
4006
4874
  }
@@ -4012,9 +4880,15 @@ export class NativeBackboneCoordinatePersistence {
4012
4880
  throw error;
4013
4881
  }
4014
4882
  }
4015
- async compact(_backbone) {
4883
+ async compact(backbone) {
4016
4884
  this.assertActive("compact");
4017
- throw new Error(nativeBackboneCoordinateCompactionDisabledMessage);
4885
+ if (!this.crashSafeCompaction) {
4886
+ throw new Error(nativeBackboneCoordinateCompactionDisabledMessage);
4887
+ }
4888
+ await this.enqueuePersistence(async () => {
4889
+ this.assertPersistenceHealthy();
4890
+ await this.flushJournalInternal(backbone, true);
4891
+ });
4018
4892
  }
4019
4893
  close() {
4020
4894
  if (this.closePromise) {