@minnowdb/core 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/engine/auto-store.d.ts +52 -0
  2. package/dist/engine/auto-store.js +157 -0
  3. package/dist/engine/buffered-writer.d.ts +2 -0
  4. package/dist/engine/buffered-writer.js +15 -2
  5. package/dist/engine/client-audit-harness.js +123 -0
  6. package/dist/engine/client.d.ts +55 -6
  7. package/dist/engine/client.js +176 -46
  8. package/dist/engine/database.d.ts +15 -1
  9. package/dist/engine/database.js +1276 -251
  10. package/dist/engine/errors.d.ts +61 -2
  11. package/dist/engine/errors.js +116 -3
  12. package/dist/engine/index.d.ts +1 -0
  13. package/dist/engine/index.js +2 -0
  14. package/dist/engine/live.d.ts +24 -1
  15. package/dist/engine/live.js +33 -9
  16. package/dist/engine/scope-write-set.js +36 -0
  17. package/dist/engine/worker-auto.d.ts +1 -0
  18. package/dist/engine/worker-auto.js +3 -0
  19. package/dist/engine/worker-host.d.ts +2 -1
  20. package/dist/engine/worker-host.js +19 -1
  21. package/dist/engine/worker-server.d.ts +53 -1
  22. package/dist/engine/worker-server.js +122 -19
  23. package/dist/engine/worker-store-auto.js +36 -0
  24. package/dist/engine/worker-store-opfs.js +3 -2
  25. package/dist/engine/write-coordinator.js +44 -2
  26. package/dist/storage/indexeddb-audit-helpers.js +269 -0
  27. package/dist/storage/indexeddb.js +599 -374
  28. package/dist/storage/opfs/coordination-helpers.js +54 -0
  29. package/dist/storage/opfs/index.d.ts +1 -1
  30. package/dist/storage/opfs/index.js +3 -2
  31. package/dist/storage/opfs/leader.js +243 -17
  32. package/dist/storage/opfs/power-loss-model.js +62 -0
  33. package/dist/storage/opfs/rpc.js +24 -43
  34. package/dist/storage/opfs/store.d.ts +32 -0
  35. package/dist/storage/opfs/store.js +531 -65
  36. package/dist/storage/toolkit/record-core.js +67 -38
  37. package/dist/storage/toolkit/wal.js +16 -0
  38. package/dist/storage/toolkit/wire.d.ts +1 -1
  39. package/dist/storage/toolkit/wire.js +4 -4
  40. package/dist/storage/types.d.ts +31 -10
  41. package/dist/storage/types.js +27 -16
  42. package/dist/testing/opfs-shim.js +14 -6
  43. package/dist/transactions/index.d.ts +19 -0
  44. package/dist/transactions/index.js +99 -25
  45. package/dist/worker-protocol/index.d.ts +50 -2
  46. package/dist/worker-protocol/index.js +106 -4
  47. package/package.json +7 -2
@@ -1,4 +1,4 @@
1
- import { CompactionBacklogError, CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, collectFtsPostingsBounded, validateFtsCandidateLimit, validateFtsReadVersion, activePostingStorageColumnIds, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TEMP_OWNERS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_ACTIVE_TRANSACTIONS, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_RETIRED_HISTORY_BYTES, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_TEMP_OWNER_TTL_MS, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_ID_CHARACTERS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, SnapshotManifestMissingError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, assertTempRunPageBatchLimits, assertStorageBulkReadItems, boundedMaintenanceBatchItems, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, updateGarbageCollectionPlanningRecord, validateTableColumns, validateTableForeignKey, validateSecondaryIndexes, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, validateFtsPostingQueries, validateCanonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, snapshotAcceleratorItemRetainedUsage, assertSnapshotImportAcceleratorUsage, SchemaConflictError, WriteConflictError } from "../types.js";
1
+ import { CompactionBacklogError, CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, collectFtsPostingsBounded, validateFtsCandidateLimit, validateFtsReadVersion, activePostingStorageColumnIds, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TEMP_OWNERS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_ACTIVE_TRANSACTIONS, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_RETIRED_HISTORY_BYTES, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_TEMP_OWNER_TTL_MS, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_ID_CHARACTERS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, SnapshotManifestMissingError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, assertTransactionArtifactBatchLimits, assertTempRunPageBatchLimits, assertStorageBulkReadItems, boundedMaintenanceBatchItems, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, appendTransactionJournal, updateTransactionRecord, updateGarbageCollectionPlanningRecord, validateTableColumns, validateTableForeignKey, validateSecondaryIndexes, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, validateFtsPostingQueries, validateCanonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, snapshotAcceleratorItemRetainedUsage, assertSnapshotImportAcceleratorUsage, SchemaConflictError, WriteConflictError } from "../types.js";
2
2
  import { dateIsoString } from "../../date-value.js";
3
3
  import { assertWellFormedString, crc32, MAX_STORED_BLOCK_BYTE_LENGTH } from "../../block-format/index.js";
4
4
  function cloneRecord(value) {
@@ -418,6 +418,14 @@ class RecordRootIndex {
418
418
  else
419
419
  index.delete(record.version);
420
420
  }
421
+ appendTransactionArtifacts(record, appended) {
422
+ this.#change(this.#transactionBlocks, appended.blockIds, 1);
423
+ this.#change(this.#transactionSegments, appended.segmentIds, 1);
424
+ if (record.status !== "active")
425
+ return;
426
+ this.#change(this.#activeTransactionBlocks, appended.blockIds, 1);
427
+ this.#change(this.#activeTransactionSegments, appended.segmentIds, 1);
428
+ }
421
429
  changeTransaction(record, delta) {
422
430
  this.#change(this.#transactionBlocks, record.pendingBlockIds, delta);
423
431
  this.#change(this.#transactionSegments, record.pendingSegmentIds, delta);
@@ -616,6 +624,9 @@ class RecordCore {
616
624
  #activeStagedArtifactBytes = 0;
617
625
  #terminalTransactionCount = 0;
618
626
  #transactionBlockLengthOverrides;
627
+ #journalBlockSets = /* @__PURE__ */ new WeakMap();
628
+ #journalBytes = /* @__PURE__ */ new WeakMap();
629
+ #transactionAppendHint;
619
630
  #activeTransactionExpiries = new OrderedKeyIndex(compareExpiryKeys);
620
631
  #activeTransactionExpiryKeys = /* @__PURE__ */ new Map();
621
632
  #transactions = new OrderedRecordMap((previous, next) => {
@@ -691,6 +702,11 @@ class RecordCore {
691
702
  this.#activeStagedBlockCount += after.blocks - before.blocks;
692
703
  this.#activeStagedSegmentCount += after.segments - before.segments;
693
704
  this.#activeStagedArtifactBytes += after.bytes - before.bytes;
705
+ const hint = this.#transactionAppendHint;
706
+ if (hint !== void 0 && previous !== void 0 && next !== void 0 && hint.previous === previous && previous.status === "active" && next.status === "active" && next.snapshotVersion === previous.snapshotVersion && next.pendingBlockIds.length === previous.pendingBlockIds.length + hint.blockIds.length && next.pendingSegmentIds.length === previous.pendingSegmentIds.length + hint.segmentIds.length) {
707
+ this.#roots.appendTransactionArtifacts(next, hint);
708
+ return;
709
+ }
694
710
  if (previous !== void 0) {
695
711
  const linkedJobs = this.#roots.liveJobTransactionCount(previous.id);
696
712
  if (previous.status === "committed") {
@@ -829,15 +845,19 @@ class RecordCore {
829
845
  #transactionResources(record, overrides) {
830
846
  if (record?.status !== "active")
831
847
  return { transactions: 0, blocks: 0, segments: 0, bytes: 0 };
832
- let bytes = 0;
833
- for (const id of record.pendingBlockIds) {
834
- const length = overrides?.get(id) ?? this.#physical.blockByteLength(id);
835
- if (length === void 0)
836
- throw new Error(`Active transaction references missing block: ${id}`);
837
- bytes += length;
838
- if (!Number.isSafeInteger(bytes)) {
839
- throw new RangeError("Active transaction staged bytes exceed the safe integer range");
848
+ let bytes = this.#journalBytes.get(record.pendingBlockIds);
849
+ if (bytes === void 0) {
850
+ bytes = 0;
851
+ for (const id of record.pendingBlockIds) {
852
+ const length = overrides?.get(id) ?? this.#physical.blockByteLength(id);
853
+ if (length === void 0)
854
+ throw new Error(`Active transaction references missing block: ${id}`);
855
+ bytes += length;
856
+ if (!Number.isSafeInteger(bytes)) {
857
+ throw new RangeError("Active transaction staged bytes exceed the safe integer range");
858
+ }
840
859
  }
860
+ this.#journalBytes.set(record.pendingBlockIds, bytes);
841
861
  }
842
862
  return {
843
863
  transactions: 1,
@@ -1943,10 +1963,6 @@ class RecordCore {
1943
1963
  if (replacement.pendingSegmentIds.includes(desired.id)) {
1944
1964
  throw new Error(`Replacement transaction already journals segment: ${desired.id}`);
1945
1965
  }
1946
- assertTransactionArtifactJournalLimits(replacement.pendingBlockIds, [
1947
- ...replacement.pendingSegmentIds,
1948
- desired.id
1949
- ]);
1950
1966
  for (const id of segmentBlockIds(stored)) {
1951
1967
  const provenance = this.#manifestBlocks.get(id);
1952
1968
  if (provenance !== void 0 && this.#manifestBlockHasReadableVersion(provenance)) {
@@ -2612,9 +2628,29 @@ class RecordCore {
2612
2628
  ]));
2613
2629
  this.#assertTransactionResourceTransition(plan.updated, blockByteLengths);
2614
2630
  this.#replaceSegments(plan.segments);
2615
- this.#setTransaction(plan.updated, blockByteLengths);
2631
+ this.#transactionAppendHint = {
2632
+ previous: plan.current,
2633
+ blockIds: [...blockByteLengths.keys()],
2634
+ segmentIds: plan.segments.map((segment) => segment.id)
2635
+ };
2636
+ try {
2637
+ this.#setTransaction(plan.updated, blockByteLengths);
2638
+ } finally {
2639
+ this.#transactionAppendHint = void 0;
2640
+ }
2641
+ for (const id of blockByteLengths.keys())
2642
+ plan.journalBlocks.add(id);
2643
+ this.#journalBlockSets.set(plan.updated.pendingBlockIds, plan.journalBlocks);
2616
2644
  return cloneRecord(plan.updated);
2617
2645
  }
2646
+ #journalBlockSet(record) {
2647
+ let set = this.#journalBlockSets.get(record.pendingBlockIds);
2648
+ if (set === void 0) {
2649
+ set = new Set(record.pendingBlockIds);
2650
+ this.#journalBlockSets.set(record.pendingBlockIds, set);
2651
+ }
2652
+ return set;
2653
+ }
2618
2654
  preflightTransactionArtifactStage(input) {
2619
2655
  const plan = this.#planTransactionArtifactStage(input, {});
2620
2656
  this.#assertTransactionResourceTransition(plan.updated, new Map(input.blocks.map((block) => [block.id, block.bytes.byteLength])));
@@ -2627,25 +2663,18 @@ class RecordCore {
2627
2663
  assertGenericTransactionUpdateAllowed(current, { updatedAt: input.updatedAt });
2628
2664
  validateTimestampRuntime(input.updatedAt, "Transaction staging timestamp");
2629
2665
  assertTransactionArtifactBatchLimits(input.blocks, input.segments);
2630
- const pendingBlockIds = [...current.pendingBlockIds, ...input.blocks.map((block) => block.id)];
2631
- const pendingSegmentIds = [
2632
- ...current.pendingSegmentIds,
2633
- ...input.segments.map((segment) => segment.id)
2634
- ];
2635
- assertTransactionArtifactJournalLimits(pendingBlockIds, pendingSegmentIds);
2666
+ const journalBlocks = this.#journalBlockSet(current);
2636
2667
  const inputBlockIds = /* @__PURE__ */ new Set();
2668
+ let addedBytes = 0;
2637
2669
  for (const block of input.blocks) {
2638
2670
  validateId(block.id);
2639
2671
  validateBlockWriteBytes(block.bytes);
2640
- if (inputBlockIds.has(block.id) || options.blocksPrevalidated !== true && this.#physical.hasBlock(block.id)) {
2672
+ if (inputBlockIds.has(block.id) || journalBlocks.has(block.id) || options.blocksPrevalidated !== true && this.#physical.hasBlock(block.id)) {
2641
2673
  throw new Error(`Block already exists: ${block.id}`);
2642
2674
  }
2643
2675
  inputBlockIds.add(block.id);
2676
+ addedBytes += options.blockByteLengths?.get(block.id) ?? block.bytes.byteLength;
2644
2677
  }
2645
- const stagedBlockIds = /* @__PURE__ */ new Set([
2646
- ...current.pendingBlockIds,
2647
- ...input.blocks.map((block) => block.id)
2648
- ]);
2649
2678
  const segmentIds = /* @__PURE__ */ new Set();
2650
2679
  const normalizedSegments = [];
2651
2680
  for (const segment of input.segments) {
@@ -2661,19 +2690,16 @@ class RecordCore {
2661
2690
  if (normalized.commitOrdinal !== expectedCommitOrdinal) {
2662
2691
  throw new TypeError(`Segment ${segment.id} commit ordinal must be ${String(expectedCommitOrdinal)}`);
2663
2692
  }
2664
- const availableBlockIds = new Set(stagedBlockIds);
2693
+ const availableBlockIds = /* @__PURE__ */ new Set();
2665
2694
  for (const id of segmentBlockIds(normalized)) {
2666
- if (this.#isManifestBlockVisible(current.snapshotVersion, id))
2695
+ if (inputBlockIds.has(id) || journalBlocks.has(id) || this.#isManifestBlockVisible(current.snapshotVersion, id)) {
2667
2696
  availableBlockIds.add(id);
2697
+ }
2668
2698
  }
2669
2699
  this.#validateStagedSegment(normalized, input.transactionId, availableBlockIds);
2670
2700
  normalizedSegments.push(normalized);
2671
2701
  }
2672
- const update = {
2673
- pendingBlockIds,
2674
- pendingSegmentIds,
2675
- updatedAt: input.updatedAt
2676
- };
2702
+ let pendingTableNextRowId;
2677
2703
  if (current.pendingTable !== void 0) {
2678
2704
  let nextRowId = current.pendingTableNextRowId;
2679
2705
  if (nextRowId === void 0)
@@ -2687,13 +2713,17 @@ class RecordCore {
2687
2713
  }
2688
2714
  nextRowId = segment.rowIdEndExclusive;
2689
2715
  }
2690
- update.pendingTableNextRowId = nextRowId;
2716
+ pendingTableNextRowId = nextRowId;
2691
2717
  }
2692
- assertGenericTransactionUpdateAllowed(current, update);
2693
- const updated = updateTransactionRecord(current, update);
2694
- this.#assertPendingArtifactsAvailable(current, true, true);
2718
+ const updated = appendTransactionJournal(current, {
2719
+ blockIds: input.blocks.map((block) => block.id),
2720
+ segmentIds: normalizedSegments.map((segment) => segment.id),
2721
+ updatedAt: input.updatedAt,
2722
+ ...pendingTableNextRowId === void 0 ? {} : { pendingTableNextRowId }
2723
+ });
2724
+ this.#journalBytes.set(updated.pendingBlockIds, this.#transactionResources(current, options.blockByteLengths).bytes + addedBytes);
2695
2725
  this.#segmentUsageAfter(normalizedSegments);
2696
- return { updated, segments: normalizedSegments };
2726
+ return { updated, segments: normalizedSegments, journalBlocks, current };
2697
2727
  }
2698
2728
  rollbackTransactionArtifacts(input) {
2699
2729
  const current = this.#transactions.get(input.transactionId);
@@ -2878,7 +2908,6 @@ class RecordCore {
2878
2908
  }
2879
2909
  const pendingBlockIds = [...base.pendingBlockIds, ...blockIds];
2880
2910
  const pendingSegmentIds = [...base.pendingSegmentIds, ...segments.keys()];
2881
- assertTransactionArtifactJournalLimits(pendingBlockIds, pendingSegmentIds);
2882
2911
  let segmentIndex = 0;
2883
2912
  for (const segment of segments.values()) {
2884
2913
  const expectedCommitOrdinal = base.pendingSegmentIds.length + segmentIndex;
@@ -79,6 +79,8 @@ function* iterateWalFrames(handle) {
79
79
  while (offset + FRAME_HEADER_BYTES <= size) {
80
80
  readFully(handle, header, offset, "reading a WAL frame header for recovery");
81
81
  if (headerView.getUint32(0, true) !== FRAME_MAGIC) {
82
+ if (isZeroFilled(handle, offset, size))
83
+ break;
82
84
  throw new Error(`WAL frame marker mismatch at offset ${String(offset)}`);
83
85
  }
84
86
  const length = headerView.getUint32(4, true);
@@ -92,12 +94,26 @@ function* iterateWalFrames(handle) {
92
94
  const payloadBytes = new Uint8Array(length);
93
95
  readFully(handle, payloadBytes, offset + FRAME_HEADER_BYTES, "reading a WAL frame payload for recovery");
94
96
  if (crc32(payloadBytes) !== checksum) {
97
+ if (payloadBytes.every((byte) => byte === 0) && isZeroFilled(handle, end, size))
98
+ break;
95
99
  throw new Error(`WAL frame checksum mismatch at offset ${String(offset)}`);
96
100
  }
97
101
  yield { payload: decodeRecordJson(payloadBytes), frameEnd: end };
98
102
  offset = end;
99
103
  }
100
104
  }
105
+ function isZeroFilled(handle, offset, size) {
106
+ const chunk = new Uint8Array(Math.min(64 * 1024, Math.max(0, size - offset)));
107
+ for (let at = offset; at < size; at += chunk.byteLength) {
108
+ const length = Math.min(chunk.byteLength, size - at);
109
+ const window = length === chunk.byteLength ? chunk : chunk.subarray(0, length);
110
+ readFully(handle, window, at, "reading a WAL tail for recovery");
111
+ for (let index = 0; index < length; index += 1)
112
+ if (window[index] !== 0)
113
+ return false;
114
+ }
115
+ return true;
116
+ }
101
117
  export {
102
118
  MAX_WAL_FRAME_BYTES,
103
119
  WalWriter,
@@ -9,7 +9,7 @@
9
9
  * can produce that object naturally — keys are ids and column names, values are JSON scalars —
10
10
  * and the reviver only converts an object whose sole key is `$n`.
11
11
  */
12
- export declare const LOG_FORMAT_VERSION = 5;
12
+ export declare const LOG_FORMAT_VERSION = 6;
13
13
  export declare function encodeRecordJson(value: unknown): Uint8Array;
14
14
  export declare function decodeRecordJson(bytes: Uint8Array): unknown;
15
15
  /** Immutable artifact payloads (full-text base chunks) inside extents. */
@@ -1,6 +1,6 @@
1
1
  import { crc32 } from "../../block-format/index.js";
2
2
  import { MAX_ROW_ID_EXCLUSIVE_END, StorageFormatVersionError } from "../types.js";
3
- const LOG_FORMAT_VERSION = 5;
3
+ const LOG_FORMAT_VERSION = 6;
4
4
  const textEncoder = new TextEncoder();
5
5
  const textDecoder = new TextDecoder("utf-8", { fatal: true });
6
6
  function encodeRecordJson(value) {
@@ -58,15 +58,15 @@ function decodeEnvelope(magic, bytes) {
58
58
  }
59
59
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
60
60
  const version = view.getUint32(8, true);
61
- if (version !== LOG_FORMAT_VERSION) {
62
- throw new StorageFormatVersionError("opfs", `envelope/${magic}`, version, LOG_FORMAT_VERSION, version < LOG_FORMAT_VERSION ? "older" : "newer");
63
- }
64
61
  const payloadLength = view.getUint32(12, true);
65
62
  if (bytes.byteLength !== ENVELOPE_HEADER_BYTES + payloadLength)
66
63
  return void 0;
67
64
  const payload = bytes.subarray(ENVELOPE_HEADER_BYTES, ENVELOPE_HEADER_BYTES + payloadLength);
68
65
  if (crc32(payload) !== view.getUint32(16, true))
69
66
  return void 0;
67
+ if (version !== LOG_FORMAT_VERSION) {
68
+ throw new StorageFormatVersionError("opfs", `envelope/${magic}`, version, LOG_FORMAT_VERSION, version < LOG_FORMAT_VERSION ? "older" : "newer");
69
+ }
70
70
  return payload;
71
71
  }
72
72
  const CHUNK_MAGIC = "MNWCHNK1";
@@ -480,8 +480,8 @@ export declare const MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL: number;
480
480
  export declare const MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL = 16777216;
481
481
  export declare const MAX_ACTIVE_TRANSACTIONS = 4096;
482
482
  export declare const MAX_GLOBAL_STAGED_ARTIFACT_BYTES: number;
483
- export declare const MAX_GLOBAL_STAGED_BLOCKS = 65536;
484
- export declare const MAX_GLOBAL_STAGED_SEGMENTS = 65536;
483
+ export declare const MAX_GLOBAL_STAGED_BLOCKS = 1048576;
484
+ export declare const MAX_GLOBAL_STAGED_SEGMENTS = 1048576;
485
485
  /** Catalog enumeration remains bounded even for cold public inspection APIs. */
486
486
  export declare const MAX_CATALOG_RECORDS = 4096;
487
487
  /** Total canonical UTF-8 record-wire bytes retained by the durable catalog. */
@@ -502,11 +502,11 @@ export declare const MAX_PINNED_RETIRED_BYTES: number;
502
502
  export declare const MAX_RETIRED_HISTORY_BYTES: number;
503
503
  /** A durable resource family reached its fixed corruption/growth safety ceiling. */
504
504
  export declare class StorageResourceLimitError extends Error {
505
- readonly resource: "lease" | "temp owner" | "temp run" | "temp page" | "temporary run total" | "temporary page total" | "temp owner byte" | "temporary byte" | "compaction job" | "garbage collection job" | "unique-key build" | "full-text build" | "secondary-index build" | "accelerator build byte" | "accelerator build entry" | "transaction" | "staged artifact byte" | "staged block" | "staged segment" | "catalog record" | "catalog byte" | "manifest record" | "manifest byte" | "segment record" | "segment byte" | "terminal transaction" | "terminal compaction job" | "completed garbage collection job" | "pinned manifest version lag" | "pinned retired block" | "pinned retired byte" | "snapshot accelerator byte" | "snapshot accelerator entry" | "retired history byte";
505
+ readonly resource: "lease" | "temp owner" | "temp run" | "temp page" | "temporary run total" | "temporary page total" | "temp owner byte" | "temporary byte" | "compaction job" | "garbage collection job" | "unique-key build" | "full-text build" | "secondary-index build" | "accelerator build byte" | "accelerator build entry" | "transaction" | "staged artifact byte" | "staged block" | "staged segment" | "catalog record" | "catalog byte" | "manifest record" | "manifest byte" | "segment record" | "segment byte" | "terminal transaction" | "terminal compaction job" | "completed garbage collection job" | "pinned manifest version lag" | "pinned retired block" | "pinned retired byte" | "snapshot accelerator byte" | "snapshot accelerator entry" | "retired history byte" | "checkpoint byte";
506
506
  readonly count: number;
507
507
  readonly limit: number;
508
508
  readonly name = "StorageResourceLimitError";
509
- constructor(resource: "lease" | "temp owner" | "temp run" | "temp page" | "temporary run total" | "temporary page total" | "temp owner byte" | "temporary byte" | "compaction job" | "garbage collection job" | "unique-key build" | "full-text build" | "secondary-index build" | "accelerator build byte" | "accelerator build entry" | "transaction" | "staged artifact byte" | "staged block" | "staged segment" | "catalog record" | "catalog byte" | "manifest record" | "manifest byte" | "segment record" | "segment byte" | "terminal transaction" | "terminal compaction job" | "completed garbage collection job" | "pinned manifest version lag" | "pinned retired block" | "pinned retired byte" | "snapshot accelerator byte" | "snapshot accelerator entry" | "retired history byte", count: number, limit: number);
509
+ constructor(resource: "lease" | "temp owner" | "temp run" | "temp page" | "temporary run total" | "temporary page total" | "temp owner byte" | "temporary byte" | "compaction job" | "garbage collection job" | "unique-key build" | "full-text build" | "secondary-index build" | "accelerator build byte" | "accelerator build entry" | "transaction" | "staged artifact byte" | "staged block" | "staged segment" | "catalog record" | "catalog byte" | "manifest record" | "manifest byte" | "segment record" | "segment byte" | "terminal transaction" | "terminal compaction job" | "completed garbage collection job" | "pinned manifest version lag" | "pinned retired block" | "pinned retired byte" | "snapshot accelerator byte" | "snapshot accelerator entry" | "retired history byte" | "checkpoint byte", count: number, limit: number);
510
510
  }
511
511
  export interface LeaseRecord {
512
512
  id: string;
@@ -1043,13 +1043,8 @@ export declare const MAX_TRANSACTION_STAGE_SEGMENTS = 64;
1043
1043
  * maximum-size physical block; callers split collections of smaller blocks into bounded calls.
1044
1044
  */
1045
1045
  export declare const MAX_TRANSACTION_STAGE_BYTES: number;
1046
- /** Durable journal ceilings prevent a forgotten or hostile transaction growing without bound. */
1047
- export declare const MAX_TRANSACTION_PENDING_BLOCKS = 4096;
1048
- export declare const MAX_TRANSACTION_PENDING_SEGMENTS = 4096;
1049
1046
  /** Storage-boundary preflight shared by staging and single-shot write implementations. */
1050
1047
  export declare function assertTransactionArtifactBatchLimits(blocks: readonly BlockWrite[], segments: readonly SegmentRecord[]): void;
1051
- /** Preflight for the complete persisted journal after a bounded staging operation. */
1052
- export declare function assertTransactionArtifactJournalLimits(pendingBlockIds: readonly string[], pendingSegmentIds: readonly string[]): void;
1053
1048
  /**
1054
1049
  * Atomically rewinds one active transaction's artifact journal to an earlier set and removes
1055
1050
  * exactly the artifacts that are no longer reachable. Journals are canonically sorted, so an
@@ -1351,11 +1346,26 @@ export declare class StorageCorruptionError extends Error {
1351
1346
  readonly name = "StorageCorruptionError";
1352
1347
  constructor(backend: string, location: string, message: string);
1353
1348
  }
1349
+ /**
1350
+ * The base of every error that means "the operation may have happened": the reply was lost,
1351
+ * not the request. Nothing that extends it may be retried blindly — reconcile a stable id or
1352
+ * revision first. `instanceof` survives the worker and follower hops.
1353
+ */
1354
+ export declare class UnknownOutcomeError extends Error {
1355
+ readonly name: string;
1356
+ }
1357
+ /**
1358
+ * The base of every error that means "this connection is finished": every later call on it
1359
+ * fails the same way, and the remedy is a new connection (`client.reopen()`), not a retry.
1360
+ */
1361
+ export declare class ConnectionLostError extends Error {
1362
+ readonly name: string;
1363
+ }
1354
1364
  /**
1355
1365
  * A remote OPFS leader may have committed a mutation whose acknowledgement was lost.
1356
1366
  * Reconcile stable identities or revisions before retrying the named operation.
1357
1367
  */
1358
- export declare class OpfsUncertainOutcomeError extends Error {
1368
+ export declare class OpfsUncertainOutcomeError extends UnknownOutcomeError {
1359
1369
  readonly method: string;
1360
1370
  readonly name = "OpfsUncertainOutcomeError";
1361
1371
  constructor(method: string);
@@ -2448,6 +2458,17 @@ export declare function createManifest(input: CreateManifestInput): Manifest;
2448
2458
  /** Strictly clones the one canonical v1 segment shape. */
2449
2459
  export declare function normalizeSegmentRecord(record: SegmentRecord): SegmentRecord;
2450
2460
  export declare function updateTransactionRecord(record: TransactionRecord, update: TransactionRecordUpdate): TransactionRecord;
2461
+ /**
2462
+ * Appends artifacts to a transaction's journal without re-validating the journal it already
2463
+ * holds. The caller proves the additions are new; this checks only that they are well-formed and
2464
+ * distinct among themselves, so one staging call costs what it stages, not what came before.
2465
+ */
2466
+ export declare function appendTransactionJournal(record: TransactionRecord, additions: {
2467
+ blockIds: readonly string[];
2468
+ segmentIds: readonly string[];
2469
+ updatedAt: string;
2470
+ pendingTableNextRowId?: bigint;
2471
+ }): TransactionRecord;
2451
2472
  export declare function createGarbageCollectionJobRecord(input: CreateGarbageCollectionJobInput): GarbageCollectionJobRecord;
2452
2473
  export declare function updateGarbageCollectionPlanningRecord(record: GarbageCollectionJobRecord, input: UpdateGarbageCollectionPlanningInput): GarbageCollectionJobRecord;
2453
2474
  export declare function normalizeGarbageCollectionJobRecord(record: GarbageCollectionJobRecord): GarbageCollectionJobRecord;
@@ -553,8 +553,8 @@ const MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL = 1024 * 1024 * 1024;
553
553
  const MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL = 16777216;
554
554
  const MAX_ACTIVE_TRANSACTIONS = 4096;
555
555
  const MAX_GLOBAL_STAGED_ARTIFACT_BYTES = 512 * 1024 * 1024;
556
- const MAX_GLOBAL_STAGED_BLOCKS = 65536;
557
- const MAX_GLOBAL_STAGED_SEGMENTS = 65536;
556
+ const MAX_GLOBAL_STAGED_BLOCKS = 1048576;
557
+ const MAX_GLOBAL_STAGED_SEGMENTS = 1048576;
558
558
  const MAX_CATALOG_RECORDS = 4096;
559
559
  const MAX_CATALOG_RETAINED_BYTES = 64 * 1024 * 1024;
560
560
  const MAX_MANIFEST_RECORDS = 65536;
@@ -874,8 +874,6 @@ class LeaseOwnerConflictError extends Error {
874
874
  const MAX_TRANSACTION_STAGE_BLOCKS = 64;
875
875
  const MAX_TRANSACTION_STAGE_SEGMENTS = 64;
876
876
  const MAX_TRANSACTION_STAGE_BYTES = MAX_STORED_BLOCK_BYTE_LENGTH;
877
- const MAX_TRANSACTION_PENDING_BLOCKS = 4096;
878
- const MAX_TRANSACTION_PENDING_SEGMENTS = 4096;
879
877
  function assertTransactionArtifactBatchLimits(blocks, segments) {
880
878
  if (blocks.length > MAX_TRANSACTION_STAGE_BLOCKS) {
881
879
  throw new RangeError(`Transaction artifact batch exceeds ${String(MAX_TRANSACTION_STAGE_BLOCKS)} blocks`);
@@ -894,14 +892,6 @@ function assertTransactionArtifactBatchLimits(blocks, segments) {
894
892
  }
895
893
  }
896
894
  }
897
- function assertTransactionArtifactJournalLimits(pendingBlockIds, pendingSegmentIds) {
898
- if (pendingBlockIds.length > MAX_TRANSACTION_PENDING_BLOCKS) {
899
- throw new RangeError(`Transaction journal exceeds ${String(MAX_TRANSACTION_PENDING_BLOCKS)} pending blocks`);
900
- }
901
- if (pendingSegmentIds.length > MAX_TRANSACTION_PENDING_SEGMENTS) {
902
- throw new RangeError(`Transaction journal exceeds ${String(MAX_TRANSACTION_PENDING_SEGMENTS)} pending segments`);
903
- }
904
- }
905
895
  const MAX_SNAPSHOT_SESSION_TTL_MS = 60 * 60 * 1e3;
906
896
  const SNAPSHOT_FRAME_KINDS = [
907
897
  "catalog-page",
@@ -997,7 +987,13 @@ class StorageCorruptionError extends Error {
997
987
  this.location = location;
998
988
  }
999
989
  }
1000
- class OpfsUncertainOutcomeError extends Error {
990
+ class UnknownOutcomeError extends Error {
991
+ name = "UnknownOutcomeError";
992
+ }
993
+ class ConnectionLostError extends Error {
994
+ name = "ConnectionLostError";
995
+ }
996
+ class OpfsUncertainOutcomeError extends UnknownOutcomeError {
1001
997
  method;
1002
998
  name = "OpfsUncertainOutcomeError";
1003
999
  constructor(method) {
@@ -1641,6 +1637,21 @@ function updateTransactionRecord(record, update) {
1641
1637
  }
1642
1638
  return updated;
1643
1639
  }
1640
+ function appendTransactionJournal(record, additions) {
1641
+ if (record.status !== "active") {
1642
+ throw new TypeError(`Only active transactions can be updated; found ${record.status}`);
1643
+ }
1644
+ const blockIds = orderedUniqueIds(additions.blockIds, "Transaction pending block ID");
1645
+ const segmentIds = orderedUniqueIds(additions.segmentIds, "Transaction pending segment ID");
1646
+ return {
1647
+ ...record,
1648
+ pendingBlockIds: blockIds.length === 0 ? record.pendingBlockIds : [...record.pendingBlockIds, ...blockIds],
1649
+ pendingSegmentIds: segmentIds.length === 0 ? record.pendingSegmentIds : [...record.pendingSegmentIds, ...segmentIds],
1650
+ ...additions.pendingTableNextRowId === void 0 ? {} : { pendingTableNextRowId: additions.pendingTableNextRowId },
1651
+ updatedAt: additions.updatedAt,
1652
+ revision: safeSum([record.revision, 1], "Transaction revision")
1653
+ };
1654
+ }
1644
1655
  function createGarbageCollectionJobRecord(input) {
1645
1656
  const candidateManifestVersions = uniqueWholeNumbers(input.candidateManifestVersions, "Garbage collection candidate manifest version");
1646
1657
  const candidateSegmentIds = uniqueIds(input.candidateSegmentIds, "Garbage collection candidate segment ID", true);
@@ -2950,6 +2961,7 @@ export {
2950
2961
  BlockReadBatchTooLargeError,
2951
2962
  CompactionBacklogError,
2952
2963
  CompactionJobConflictError,
2964
+ ConnectionLostError,
2953
2965
  GarbageCollectionJobConflictError,
2954
2966
  IndexedDbSchemaUpgradeBlockedError,
2955
2967
  LeaseConflictError,
@@ -3033,8 +3045,6 @@ export {
3033
3045
  MAX_TERMINAL_TRANSACTION_RECORDS,
3034
3046
  MAX_TRANSACTION_COMMIT_DELTA_BYTES,
3035
3047
  MAX_TRANSACTION_COMMIT_DELTA_ENTRIES,
3036
- MAX_TRANSACTION_PENDING_BLOCKS,
3037
- MAX_TRANSACTION_PENDING_SEGMENTS,
3038
3048
  MAX_TRANSACTION_STAGE_BLOCKS,
3039
3049
  MAX_TRANSACTION_STAGE_BYTES,
3040
3050
  MAX_TRANSACTION_STAGE_SEGMENTS,
@@ -3062,15 +3072,16 @@ export {
3062
3072
  UniqueIndexCoverageError,
3063
3073
  UniqueKeyBuildConflictError,
3064
3074
  UniqueKeyConflictError,
3075
+ UnknownOutcomeError,
3065
3076
  WriteConflictError,
3066
3077
  activePostingStorageColumnIds,
3067
3078
  advanceGarbageCollectionJobRecord,
3079
+ appendTransactionJournal,
3068
3080
  assertCompactionOutputProvenance,
3069
3081
  assertSnapshotImportAcceleratorUsage,
3070
3082
  assertStorageBulkReadItems,
3071
3083
  assertTempRunPageBatchLimits,
3072
3084
  assertTransactionArtifactBatchLimits,
3073
- assertTransactionArtifactJournalLimits,
3074
3085
  boundedMaintenanceBatchItems,
3075
3086
  canonicalManifestChangedTableIds,
3076
3087
  catalogRecordRetainedBytes,
@@ -181,6 +181,18 @@ class ShimDirectoryHandle {
181
181
  return this.entries();
182
182
  }
183
183
  }
184
+ function grownFileBytes(bytes, length) {
185
+ const owned = bytes.byteOffset === 0 && bytes.buffer instanceof ArrayBuffer;
186
+ const capacity = owned ? bytes.buffer.byteLength : bytes.byteLength;
187
+ if (owned && length <= capacity) {
188
+ const grown = new Uint8Array(bytes.buffer, 0, length);
189
+ grown.fill(0, bytes.byteLength);
190
+ return grown;
191
+ }
192
+ const backing = new Uint8Array(Math.max(length, capacity * 2, 4096));
193
+ backing.set(bytes);
194
+ return new Uint8Array(backing.buffer, 0, length);
195
+ }
184
196
  function assertSubtreeUnlocked(node, name) {
185
197
  for (const child of node.children.values()) {
186
198
  if (child.kind === "file") {
@@ -262,9 +274,7 @@ class ShimSyncAccessHandle {
262
274
  const source = requested.subarray(0, limit);
263
275
  const end = at + source.byteLength;
264
276
  if (source.byteLength > 0 && end > this.#node.bytes.byteLength) {
265
- const grown = new Uint8Array(end);
266
- grown.set(this.#node.bytes);
267
- this.#node.bytes = grown;
277
+ this.#node.bytes = grownFileBytes(this.#node.bytes, end);
268
278
  }
269
279
  this.#node.bytes.set(source, at);
270
280
  return source.byteLength;
@@ -276,9 +286,7 @@ class ShimSyncAccessHandle {
276
286
  this.#node.bytes = this.#node.bytes.slice(0, size);
277
287
  return;
278
288
  }
279
- const grown = new Uint8Array(size);
280
- grown.set(this.#node.bytes);
281
- this.#node.bytes = grown;
289
+ this.#node.bytes = grownFileBytes(this.#node.bytes, size);
282
290
  }
283
291
  flush() {
284
292
  this.#assertOpen();
@@ -112,6 +112,18 @@ export declare class DatabaseTransaction {
112
112
  get snapshotVersion(): number | null;
113
113
  get pendingBlockIds(): string[];
114
114
  get pendingSegmentIds(): string[];
115
+ /** Whether the journal, persisted or process-local, holds the block. */
116
+ hasPendingBlock(id: string): boolean;
117
+ /**
118
+ * Every segment this transaction has staged, persisted or process-local, as the records it
119
+ * staged them with. One shared array, rebuilt only when the set changes: a scope reads it on
120
+ * every statement, and copying a long journal's records each time would cost more than the
121
+ * read. Callers must not mutate it.
122
+ */
123
+ get stagedSegments(): readonly SegmentRecord[];
124
+ /** Journal sizes without materializing the id lists. */
125
+ get pendingBlockCount(): number;
126
+ get pendingSegmentCount(): number;
115
127
  /** Process-local segments retained for a bounded single-shot commit. */
116
128
  get deferredSegments(): SegmentRecord[];
117
129
  get pendingTableNextRowId(): bigint | undefined;
@@ -133,6 +145,13 @@ export declare class DatabaseTransaction {
133
145
  get compactionSourceBlockIds(): string[];
134
146
  /** Extends durable ownership; scope owners call this while user code is legitimately waiting. */
135
147
  renew(): Promise<void>;
148
+ /**
149
+ * Renews the lease and record only when a third of their lifetime has gone: an O(1) deadline
150
+ * check otherwise. A scope that buffers statement after statement stages nothing, so nothing
151
+ * renews inline, and a loop that never yields to the event loop never lets the heartbeat
152
+ * timer fire either — the buffered path calls this per statement instead.
153
+ */
154
+ renewIfDue(): Promise<void>;
136
155
  /**
137
156
  * Counts every registration this transaction carries: staged blocks and segments, unique-key
138
157
  * entries, full-text entries, and supersessions. A caller that fails part-way through a