@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,8 +1,8 @@
1
- import { CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, CompactionBacklogError, createManifest, createGarbageCollectionJobRecord, storeNames, advanceGarbageCollectionJobRecord as advanceGarbageCollectionJobRecordUnchecked, BlockReadBatchTooLargeError, activePostingStorageColumnIds, assertTempRunPageBatchLimits, assertStorageBulkReadItems, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, boundedMaintenanceBatchItems, canonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, collectFtsPostings, ftsPostingQueryMatches, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_FTS_CANDIDATE_ROW_IDS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_ORDERED_READ_BYTES, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_BLOCK_READ_BATCH_BYTES, MAX_STORAGE_ID_CHARACTERS, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TRANSACTIONS, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_RETIRED_HISTORY_BYTES, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_GARBAGE_COLLECTION_JOBS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_ACTIVE_FTS_BASE_BUILDS, MAX_ACTIVE_SECONDARY_INDEX_BUILDS, MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL, MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL, MAX_POSTING_BUILD_TTL_MS, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, MAX_SNAPSHOT_METADATA_FRAME_BYTES, SNAPSHOT_FRAME_KINDS, MAX_LEVEL_ZERO_SEGMENTS, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, StorageCorruptionError, StorageFormatVersionError, IndexedDbSchemaUpgradeBlockedError, StorageResourceLimitError, SnapshotManifestMissingError, SnapshotImportConflictError, PostingBuildConflictError, validateTableForeignKey, TableInUseError, TableRecordConflictError, TempOwnerConflictError, MAX_TEMP_OWNER_TTL_MS, MAX_ACTIVE_TEMP_OWNERS, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_RUNS_TOTAL, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, SchemaConflictError, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord as updateCompactionJobRecordUnchecked, updateGarbageCollectionPlanningRecord, updateTransactionRecord as updateTransactionRecordUnchecked, validateColumnDefault, validateCatalogName, validateCanonicalManifestChangedTableIds, validateEnumValues, validateFtsCandidateLimit, validateFtsOrderedReadLimits, validateFtsReadVersion, validateFtsPostingQueries, validateStorageId, validateStorageDatabaseName, validateTableColumns, validateSecondaryIndexes, validateTableRecordBounds, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, WriteConflictError } from "./types.js";
1
+ import { CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, CompactionBacklogError, createManifest, createGarbageCollectionJobRecord, storeNames, advanceGarbageCollectionJobRecord as advanceGarbageCollectionJobRecordUnchecked, BlockReadBatchTooLargeError, activePostingStorageColumnIds, assertTempRunPageBatchLimits, assertStorageBulkReadItems, assertTransactionArtifactBatchLimits, boundedMaintenanceBatchItems, canonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, collectFtsPostings, ftsPostingQueryMatches, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_FTS_CANDIDATE_ROW_IDS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_ORDERED_READ_BYTES, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_BLOCK_READ_BATCH_BYTES, MAX_STORAGE_ID_CHARACTERS, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TRANSACTIONS, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_RETIRED_HISTORY_BYTES, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_GARBAGE_COLLECTION_JOBS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_ACTIVE_FTS_BASE_BUILDS, MAX_ACTIVE_SECONDARY_INDEX_BUILDS, MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL, MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL, MAX_POSTING_BUILD_TTL_MS, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, MAX_SNAPSHOT_METADATA_FRAME_BYTES, SNAPSHOT_FRAME_KINDS, MAX_LEVEL_ZERO_SEGMENTS, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, StorageCorruptionError, StorageFormatVersionError, IndexedDbSchemaUpgradeBlockedError, StorageResourceLimitError, SnapshotManifestMissingError, SnapshotImportConflictError, PostingBuildConflictError, validateTableForeignKey, TableInUseError, TableRecordConflictError, TempOwnerConflictError, MAX_TEMP_OWNER_TTL_MS, MAX_ACTIVE_TEMP_OWNERS, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_RUNS_TOTAL, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, SchemaConflictError, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord as updateCompactionJobRecordUnchecked, updateGarbageCollectionPlanningRecord, updateTransactionRecord as updateTransactionRecordUnchecked, validateColumnDefault, validateCatalogName, validateCanonicalManifestChangedTableIds, validateEnumValues, validateFtsCandidateLimit, validateFtsOrderedReadLimits, validateFtsReadVersion, validateFtsPostingQueries, validateStorageId, validateStorageDatabaseName, validateTableColumns, validateSecondaryIndexes, validateTableRecordBounds, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, WriteConflictError } from "./types.js";
2
2
  import { crc32, verifyStoredBlock } from "../block-format/index.js";
3
3
  import { dateIsoString } from "../date-value.js";
4
4
  import { decodeSnapshotMetadataItems, encodeSnapshotMetadataPage, extendSnapshotFrameStreamChecksum, prepareSnapshotFrameStreamHeader, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity } from "./snapshot-stream.js";
5
- const SCHEMA_VERSION = 1;
5
+ const SCHEMA_VERSION = 2;
6
6
  const FIRST_STABLE_SCHEMA_VERSION = 1;
7
7
  const CURRENT_MANIFEST_KEY = "manifest/current";
8
8
  const MANIFEST_PRUNE_CLEANUP_KEY = "manifest/prune-cleanup";
@@ -16,7 +16,13 @@ const SNAPSHOT_UNIQUE_OWNER_PREFIX = "snapshot/frame-import/unique-owner/";
16
16
  const SNAPSHOT_POSTING_STAGING_PREFIX = "snapshot/frame-import/posting/";
17
17
  const SNAPSHOT_POSTING_OWNER_PREFIX = "snapshot/frame-import/posting-owner/";
18
18
  const SNAPSHOT_HEADER_STORE = "snapshotHeaders";
19
- const indexedDbStoreNames = [...storeNames, SNAPSHOT_HEADER_STORE];
19
+ const TRANSACTION_JOURNAL_STORE = "transactionJournal";
20
+ const indexedDbStoreNames = [
21
+ ...storeNames,
22
+ SNAPSHOT_HEADER_STORE,
23
+ TRANSACTION_JOURNAL_STORE
24
+ ];
25
+ const TRANSACTION_JOURNAL_CHUNK_ENTRIES = 1024;
20
26
  const SEGMENT_TABLE_INDEX = "byTable";
21
27
  const LEASE_EXPIRY_INDEX = "byExpiry";
22
28
  const TRANSACTION_STATUS_INDEX = "byStatus";
@@ -64,7 +70,50 @@ const ACTIVE_GARBAGE_COLLECTION_KEY = "maintenance/active-garbage-collection";
64
70
  const MAINTENANCE_QUOTA_KEY = "maintenance/quota";
65
71
  const storageTextEncoder = new TextEncoder();
66
72
  const storageTextBuffer = new Uint8Array(1024);
67
- const indexedDbSchemaMigrations = [];
73
+ const indexedDbSchemaMigrations = [
74
+ {
75
+ targetVersion: 2,
76
+ migrate: migrateTransactionJournalsToChunks
77
+ }
78
+ ];
79
+ function migrateTransactionJournalsToChunks(database, upgrade, fail) {
80
+ if (!database.objectStoreNames.contains("transactions") || database.objectStoreNames.contains(TRANSACTION_JOURNAL_STORE)) {
81
+ throw corruption("schema", "schema 1 layout cannot be migrated to schema 2");
82
+ }
83
+ database.createObjectStore(TRANSACTION_JOURNAL_STORE);
84
+ const transactionStore = upgrade.objectStore("transactions");
85
+ const journalStore = upgrade.objectStore(TRANSACTION_JOURNAL_STORE);
86
+ const request = transactionStore.openCursor();
87
+ request.onerror = () => fail(request.error ?? new Error("IndexedDB migration cursor failed"));
88
+ request.onsuccess = () => {
89
+ const cursor = request.result;
90
+ if (cursor === null)
91
+ return;
92
+ try {
93
+ const key = cursor.key;
94
+ if (typeof key !== "string")
95
+ throw corruption("transactions", "record key is invalid");
96
+ const value = cursor.value;
97
+ if (!isRecord(value))
98
+ throw corruption(`transactions/${key}`, "record is not an object");
99
+ const { pendingBlockIds, pendingSegmentIds, ...header } = value;
100
+ const blockIds = requiredUniqueStringArray(pendingBlockIds, `transactions/${key}/pendingBlockIds`);
101
+ const segmentIds = requiredUniqueStringArray(pendingSegmentIds, `transactions/${key}/pendingSegmentIds`);
102
+ packTransactionJournal(blockIds, segmentIds).forEach((chunk, index) => {
103
+ journalStore.add(chunk, transactionJournalChunkKey(key, index));
104
+ });
105
+ cursor.update({
106
+ ...header,
107
+ journalChunkCount: transactionJournalChunkCount(blockIds.length, segmentIds.length),
108
+ pendingBlockCount: blockIds.length,
109
+ pendingSegmentCount: segmentIds.length
110
+ });
111
+ cursor.continue();
112
+ } catch (error) {
113
+ fail(error instanceof Error ? error : new Error(String(error)));
114
+ }
115
+ };
116
+ }
68
117
  assertIndexedDbSchemaMigrationRegistry();
69
118
  const DEFAULT_UNIQUE_KEY_CACHE_BYTES = 8 * 1024 * 1024;
70
119
  const TABLE_NAME_CACHE_LIMIT = 256;
@@ -76,6 +125,7 @@ class IndexedDbBlockStore {
76
125
  #tableIdsByName = /* @__PURE__ */ new Map();
77
126
  #uniqueKeyCache;
78
127
  #manifestCache;
128
+ #journalCache;
79
129
  #snapshotPeakRetainedItems = 0;
80
130
  #snapshotPeakRetainedBytes = 0;
81
131
  #closed = false;
@@ -113,7 +163,10 @@ class IndexedDbBlockStore {
113
163
  createCurrentIndexedDbSchema(request.result, upgrade);
114
164
  return;
115
165
  }
116
- applyIndexedDbSchemaMigrations(request.result, upgrade, event.oldVersion);
166
+ applyIndexedDbSchemaMigrations(request.result, upgrade, event.oldVersion, (error) => {
167
+ upgradeError ??= error;
168
+ abortIfActive(upgrade);
169
+ });
117
170
  } catch (error) {
118
171
  upgradeError = error instanceof Error ? error : new Error(String(error));
119
172
  request.transaction?.abort();
@@ -200,8 +253,7 @@ class IndexedDbBlockStore {
200
253
  return result;
201
254
  } catch (error) {
202
255
  abortIfActive(transaction);
203
- await ignoreAbort(transaction);
204
- throw error;
256
+ throw await refusalBehindAbort(transaction, error);
205
257
  }
206
258
  }
207
259
  async readManifestBlock(version, id) {
@@ -235,8 +287,7 @@ class IndexedDbBlockStore {
235
287
  return bytes;
236
288
  } catch (error) {
237
289
  abortIfActive(transaction);
238
- await ignoreAbort(transaction);
239
- throw error;
290
+ throw await refusalBehindAbort(transaction, error);
240
291
  }
241
292
  }
242
293
  async hasManifestBlocks(version, ids) {
@@ -261,8 +312,7 @@ class IndexedDbBlockStore {
261
312
  return result;
262
313
  } catch (error) {
263
314
  abortIfActive(transaction);
264
- await ignoreAbort(transaction);
265
- throw error;
315
+ throw await refusalBehindAbort(transaction, error);
266
316
  }
267
317
  }
268
318
  async listManifestBlockPage(input) {
@@ -293,8 +343,7 @@ class IndexedDbBlockStore {
293
343
  };
294
344
  } catch (error) {
295
345
  abortIfActive(transaction);
296
- await ignoreAbort(transaction);
297
- throw error;
346
+ throw await refusalBehindAbort(transaction, error);
298
347
  }
299
348
  }
300
349
  async listRetiredManifestBlockPage(input) {
@@ -319,8 +368,7 @@ class IndexedDbBlockStore {
319
368
  };
320
369
  } catch (error) {
321
370
  abortIfActive(transaction);
322
- await ignoreAbort(transaction);
323
- throw error;
371
+ throw await refusalBehindAbort(transaction, error);
324
372
  }
325
373
  }
326
374
  async putTempRunPage(page) {
@@ -339,8 +387,7 @@ class IndexedDbBlockStore {
339
387
  await transactionDone(transaction);
340
388
  } catch (error) {
341
389
  abortIfActive(transaction);
342
- await ignoreAbort(transaction);
343
- throw error;
390
+ throw await refusalBehindAbort(transaction, error);
344
391
  }
345
392
  }
346
393
  async getTempRunPage(ownerId, runId, pageIndex) {
@@ -359,8 +406,7 @@ class IndexedDbBlockStore {
359
406
  await transactionDone(transaction);
360
407
  } catch (error) {
361
408
  abortIfActive(transaction);
362
- await ignoreAbort(transaction);
363
- throw error;
409
+ throw await refusalBehindAbort(transaction, error);
364
410
  }
365
411
  }
366
412
  async removeTempOwner(ownerId) {
@@ -372,8 +418,7 @@ class IndexedDbBlockStore {
372
418
  await transactionDone(transaction);
373
419
  } catch (error) {
374
420
  abortIfActive(transaction);
375
- await ignoreAbort(transaction);
376
- throw error;
421
+ throw await refusalBehindAbort(transaction, error);
377
422
  }
378
423
  }
379
424
  async createTempOwner(record) {
@@ -395,8 +440,7 @@ class IndexedDbBlockStore {
395
440
  await transactionDone(transaction);
396
441
  } catch (error) {
397
442
  abortIfActive(transaction);
398
- await ignoreAbort(transaction);
399
- throw error;
443
+ throw await refusalBehindAbort(transaction, error);
400
444
  }
401
445
  }
402
446
  async getTempOwner(ownerId) {
@@ -536,8 +580,7 @@ class IndexedDbBlockStore {
536
580
  await transactionDone(transaction);
537
581
  } catch (error) {
538
582
  abortIfActive(transaction);
539
- await ignoreAbort(transaction);
540
- throw error;
583
+ throw await refusalBehindAbort(transaction, error);
541
584
  }
542
585
  }
543
586
  async getTable(id) {
@@ -727,8 +770,7 @@ class IndexedDbBlockStore {
727
770
  return updated;
728
771
  } catch (error) {
729
772
  abortIfActive(transaction);
730
- await ignoreAbort(transaction);
731
- throw error;
773
+ throw await refusalBehindAbort(transaction, error);
732
774
  }
733
775
  }
734
776
  async beginUniqueKeyBuild(input) {
@@ -783,8 +825,7 @@ class IndexedDbBlockStore {
783
825
  return structuredClone(record);
784
826
  } catch (error) {
785
827
  abortIfActive(transaction);
786
- await ignoreAbort(transaction);
787
- throw error;
828
+ throw await refusalBehindAbort(transaction, error);
788
829
  }
789
830
  }
790
831
  async getUniqueKeyBuild(buildId) {
@@ -819,8 +860,7 @@ class IndexedDbBlockStore {
819
860
  return structuredClone(record);
820
861
  } catch (error) {
821
862
  abortIfActive(transaction);
822
- await ignoreAbort(transaction);
823
- throw error;
863
+ throw await refusalBehindAbort(transaction, error);
824
864
  }
825
865
  }
826
866
  async appendUniqueKeyBuildChunk(input) {
@@ -893,8 +933,7 @@ class IndexedDbBlockStore {
893
933
  return structuredClone(record);
894
934
  } catch (error) {
895
935
  abortIfActive(transaction);
896
- await ignoreAbort(transaction);
897
- throw error;
936
+ throw await refusalBehindAbort(transaction, error);
898
937
  }
899
938
  }
900
939
  async finishUniqueKeyBuild(input) {
@@ -986,8 +1025,7 @@ class IndexedDbBlockStore {
986
1025
  return structuredClone(updated);
987
1026
  } catch (error) {
988
1027
  abortIfActive(transaction);
989
- await ignoreAbort(transaction);
990
- throw error;
1028
+ throw await refusalBehindAbort(transaction, error);
991
1029
  }
992
1030
  }
993
1031
  async abortUniqueKeyBuild(input) {
@@ -1014,8 +1052,7 @@ class IndexedDbBlockStore {
1014
1052
  await transactionDone(transaction);
1015
1053
  } catch (error) {
1016
1054
  abortIfActive(transaction);
1017
- await ignoreAbort(transaction);
1018
- throw error;
1055
+ throw await refusalBehindAbort(transaction, error);
1019
1056
  }
1020
1057
  await this.#cleanupUniqueKeyBuildArtifacts(input.buildId);
1021
1058
  return true;
@@ -1040,8 +1077,7 @@ class IndexedDbBlockStore {
1040
1077
  }
1041
1078
  } catch (error) {
1042
1079
  abortIfActive(transaction);
1043
- await ignoreAbort(transaction);
1044
- throw error;
1080
+ throw await refusalBehindAbort(transaction, error);
1045
1081
  }
1046
1082
  }
1047
1083
  async dropTable(input) {
@@ -1125,6 +1161,7 @@ class IndexedDbBlockStore {
1125
1161
  await bumpSchemaEpoch(catalog);
1126
1162
  await transactionDone(transaction);
1127
1163
  this.#manifestCache = void 0;
1164
+ this.#journalCache = void 0;
1128
1165
  if (this.#uniqueKeyCache?.tableId === input.tableId || this.#uniqueKeyCache?.tableId.startsWith(`${input.tableId}\0secondary-index\0`) === true) {
1129
1166
  this.#uniqueKeyCache = void 0;
1130
1167
  } else if (this.#uniqueKeyCache !== void 0) {
@@ -1133,8 +1170,7 @@ class IndexedDbBlockStore {
1133
1170
  return manifest;
1134
1171
  } catch (error) {
1135
1172
  abortIfActive(transaction);
1136
- await ignoreAbort(transaction);
1137
- throw error;
1173
+ throw await refusalBehindAbort(transaction, error);
1138
1174
  }
1139
1175
  }
1140
1176
  async dropTableColumn(input) {
@@ -1265,13 +1301,13 @@ class IndexedDbBlockStore {
1265
1301
  await bumpSchemaEpoch(catalog);
1266
1302
  await transactionDone(transaction);
1267
1303
  this.#manifestCache = void 0;
1304
+ this.#journalCache = void 0;
1268
1305
  if (this.#uniqueKeyCache !== void 0)
1269
1306
  this.#uniqueKeyCache.version = manifest.version;
1270
1307
  return manifest;
1271
1308
  } catch (error) {
1272
1309
  abortIfActive(transaction);
1273
- await ignoreAbort(transaction);
1274
- throw error;
1310
+ throw await refusalBehindAbort(transaction, error);
1275
1311
  }
1276
1312
  }
1277
1313
  async writeFtsBase(tableId, columnId, input) {
@@ -1407,8 +1443,7 @@ class IndexedDbBlockStore {
1407
1443
  await transactionDone(transaction);
1408
1444
  } catch (error) {
1409
1445
  abortIfActive(transaction);
1410
- await ignoreAbort(transaction);
1411
- throw error;
1446
+ throw await refusalBehindAbort(transaction, error);
1412
1447
  }
1413
1448
  }
1414
1449
  async writeFtsBaseBuildChunk(input) {
@@ -1826,8 +1861,7 @@ class IndexedDbBlockStore {
1826
1861
  };
1827
1862
  } catch (error) {
1828
1863
  abortIfActive(transaction);
1829
- await ignoreAbort(transaction);
1830
- throw error;
1864
+ throw await refusalBehindAbort(transaction, error);
1831
1865
  }
1832
1866
  }
1833
1867
  async removeAbortedSegment(segmentId, expectedTransactionId) {
@@ -1845,12 +1879,10 @@ class IndexedDbBlockStore {
1845
1879
  if (segment.id !== segmentId || segment.transactionId !== expectedTransactionId) {
1846
1880
  throw new Error(`Segment ${segmentId} is not owned by transaction ${expectedTransactionId}`);
1847
1881
  }
1848
- const transactionStore = transaction.objectStore("transactions");
1849
- const ownerValue = await requestResult(transactionStore.get(expectedTransactionId));
1850
- if (ownerValue === void 0) {
1882
+ const owner = await readTransactionRecord(transaction, expectedTransactionId);
1883
+ if (owner === void 0) {
1851
1884
  throw corruption(`segments/${segmentId}`, `owning transaction ${expectedTransactionId} is missing`);
1852
1885
  }
1853
- const owner = asTransactionRecord(ownerValue, expectedTransactionId);
1854
1886
  if (owner.status !== "aborted") {
1855
1887
  throw new Error(`Segment ${segmentId} owner must be aborted; found ${owner.status}`);
1856
1888
  }
@@ -1861,10 +1893,7 @@ class IndexedDbBlockStore {
1861
1893
  if (published !== void 0) {
1862
1894
  throw new Error(`Segment ${segmentId} is published by manifest ${String(published.version)}`);
1863
1895
  }
1864
- await visitObjectStoreSequentially(transactionStore, (candidateValue, key) => {
1865
- if (typeof key !== "string")
1866
- throw corruption("transactions", "record key is invalid");
1867
- const candidate = asTransactionRecord(candidateValue, key);
1896
+ await visitTransactionRecords(transaction, (candidate) => {
1868
1897
  if (candidate.status === "active" && candidate.pendingSegmentIds.includes(segmentId)) {
1869
1898
  throw new Error(`Segment ${segmentId} is referenced by active transaction ${candidate.id}`);
1870
1899
  }
@@ -1903,13 +1932,12 @@ class IndexedDbBlockStore {
1903
1932
  segmentStore.delete(segmentId);
1904
1933
  for (const { next } of rebasedSegments)
1905
1934
  segmentStore.put(next, next.id);
1906
- transactionStore.put(updatedOwner, owner.id);
1935
+ putTransactionRecord(transaction, updatedOwner, journalChunkCountOf(owner));
1907
1936
  await transactionDone(transaction);
1908
1937
  return true;
1909
1938
  } catch (error) {
1910
1939
  abortIfActive(transaction);
1911
- await ignoreAbort(transaction);
1912
- throw error;
1940
+ throw await refusalBehindAbort(transaction, error);
1913
1941
  }
1914
1942
  }
1915
1943
  async adoptAbortedSegment(input) {
@@ -1925,11 +1953,10 @@ class IndexedDbBlockStore {
1925
1953
  const transaction = this.#transaction(["blocks", "catalog", "gc", "manifests", "segments", "statistics", "transactions"], "readwrite");
1926
1954
  try {
1927
1955
  const segmentStore = transaction.objectStore("segments");
1928
- const transactionStore = transaction.objectStore("transactions");
1929
- const [storedValue, abortedValue, replacementValue] = await Promise.all([
1956
+ const [storedValue, aborted, replacement] = await Promise.all([
1930
1957
  requestResult(segmentStore.get(desired.id)),
1931
- requestResult(transactionStore.get(input.expectedAbortedTransactionId)),
1932
- requestResult(transactionStore.get(input.replacementTransactionId))
1958
+ readTransactionRecord(transaction, input.expectedAbortedTransactionId),
1959
+ readTransactionRecord(transaction, input.replacementTransactionId)
1933
1960
  ]);
1934
1961
  if (storedValue === void 0)
1935
1962
  throw new Error(`Segment does not exist: ${desired.id}`);
@@ -1943,11 +1970,9 @@ class IndexedDbBlockStore {
1943
1970
  if (stored.transactionId !== input.expectedAbortedTransactionId || !sameStructuredValue(storedContent, desiredContent)) {
1944
1971
  throw new Error(`Segment ${desired.id} does not match the immutable adoption record`);
1945
1972
  }
1946
- const aborted = abortedValue === void 0 ? void 0 : asTransactionRecord(abortedValue, input.expectedAbortedTransactionId);
1947
1973
  if (aborted?.revision !== input.expectedAbortedTransactionRevision) {
1948
1974
  throw new TransactionRecordConflictError(input.expectedAbortedTransactionId, input.expectedAbortedTransactionRevision, aborted?.revision ?? null);
1949
1975
  }
1950
- const replacement = replacementValue === void 0 ? void 0 : asTransactionRecord(replacementValue, input.replacementTransactionId);
1951
1976
  if (replacement?.revision !== input.expectedReplacementTransactionRevision) {
1952
1977
  throw new TransactionRecordConflictError(input.replacementTransactionId, input.expectedReplacementTransactionRevision, replacement?.revision ?? null);
1953
1978
  }
@@ -1985,10 +2010,7 @@ class IndexedDbBlockStore {
1985
2010
  if (await findReadableManifestBlock(transaction, segmentBlockIds(stored)) !== void 0) {
1986
2011
  throw new Error(`Segment ${desired.id} is still reachable from a readable manifest`);
1987
2012
  }
1988
- await visitObjectStoreSequentially(transactionStore, (candidateValue, key) => {
1989
- if (typeof key !== "string")
1990
- throw corruption("transactions", "record key is invalid");
1991
- const candidate = asTransactionRecord(candidateValue, key);
2013
+ await visitTransactionRecords(transaction, (candidate) => {
1992
2014
  if (candidate.id !== aborted.id && candidate.id !== replacement.id && candidate.status === "active" && candidate.pendingSegmentIds.includes(desired.id)) {
1993
2015
  throw new Error(`Segment ${desired.id} is referenced by active transaction ${candidate.id}`);
1994
2016
  }
@@ -2033,7 +2055,6 @@ class IndexedDbBlockStore {
2033
2055
  revision: incrementSafeInteger(replacement.revision, "Transaction revision"),
2034
2056
  updatedAt: input.updatedAt
2035
2057
  };
2036
- assertTransactionArtifactJournalLimits(updatedReplacement.pendingBlockIds, updatedReplacement.pendingSegmentIds);
2037
2058
  const statistics = transaction.objectStore("statistics");
2038
2059
  await updateTransactionResourceLedger(statistics, aborted.id, {
2039
2060
  blockCount: 0,
@@ -2051,14 +2072,13 @@ class IndexedDbBlockStore {
2051
2072
  segmentStore.put(desired, desired.id);
2052
2073
  for (const { next } of rebasedAbortedSegments)
2053
2074
  segmentStore.put(next, next.id);
2054
- transactionStore.put(updatedAborted, updatedAborted.id);
2055
- transactionStore.put(updatedReplacement, updatedReplacement.id);
2075
+ putTransactionRecord(transaction, updatedAborted, journalChunkCountOf(aborted));
2076
+ putTransactionRecord(transaction, updatedReplacement, journalChunkCountOf(replacement));
2056
2077
  await transactionDone(transaction);
2057
2078
  return structuredClone(updatedReplacement);
2058
2079
  } catch (error) {
2059
2080
  abortIfActive(transaction);
2060
- await ignoreAbort(transaction);
2061
- throw error;
2081
+ throw await refusalBehindAbort(transaction, error);
2062
2082
  }
2063
2083
  }
2064
2084
  async reserveRowIds(tableId, count) {
@@ -2080,8 +2100,7 @@ class IndexedDbBlockStore {
2080
2100
  return { start, endExclusive };
2081
2101
  } catch (error) {
2082
2102
  abortIfActive(transaction);
2083
- await ignoreAbort(transaction);
2084
- throw error;
2103
+ throw await refusalBehindAbort(transaction, error);
2085
2104
  }
2086
2105
  }
2087
2106
  async reserveAutoIncrement(tableId, columnId, count, atLeast) {
@@ -2102,8 +2121,7 @@ class IndexedDbBlockStore {
2102
2121
  return { start, endExclusive };
2103
2122
  } catch (error) {
2104
2123
  abortIfActive(transaction);
2105
- await ignoreAbort(transaction);
2106
- throw error;
2124
+ throw await refusalBehindAbort(transaction, error);
2107
2125
  }
2108
2126
  }
2109
2127
  async getExistingUniqueKeys(tableId, keyTokens) {
@@ -2194,17 +2212,13 @@ class IndexedDbBlockStore {
2194
2212
  }
2195
2213
  segments.sort((left, right) => left.id.localeCompare(right.id));
2196
2214
  const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
2197
- const transactionStore = transaction.objectStore("transactions");
2198
2215
  const transactions = [];
2199
2216
  for (let start = 0; start < transactionIds.length; start += 64) {
2200
2217
  const window = transactionIds.slice(start, start + 64);
2201
- const values = await Promise.all(window.map((id) => requestResult(transactionStore.get(id))));
2202
- values.forEach((value, index) => {
2203
- const id = window[index];
2204
- if (value !== void 0 && id !== void 0) {
2205
- transactions.push(asTransactionRecord(value, id));
2206
- }
2207
- });
2218
+ for (const record of await readTransactionRecords(transaction, window)) {
2219
+ if (record !== void 0)
2220
+ transactions.push(record);
2221
+ }
2208
2222
  }
2209
2223
  await transactionDone(transaction);
2210
2224
  return {
@@ -2216,8 +2230,7 @@ class IndexedDbBlockStore {
2216
2230
  };
2217
2231
  } catch (error) {
2218
2232
  abortIfActive(transaction);
2219
- await ignoreAbort(transaction);
2220
- throw error;
2233
+ throw await refusalBehindAbort(transaction, error);
2221
2234
  }
2222
2235
  }
2223
2236
  async getCurrentManifest() {
@@ -2316,7 +2329,9 @@ class IndexedDbBlockStore {
2316
2329
  currentVersion: current ?? null,
2317
2330
  replacementTransaction: normalized
2318
2331
  });
2319
- transaction.objectStore("transactions").add(normalized, normalized.id);
2332
+ addTransactionRecord(transaction, normalized);
2333
+ if (this.#journalCache?.transactionId === normalized.id)
2334
+ this.#journalCache = void 0;
2320
2335
  let rowIds;
2321
2336
  if (input.reserveRowIds !== void 0) {
2322
2337
  validateCount(input.reserveRowIds.count);
@@ -2352,8 +2367,7 @@ class IndexedDbBlockStore {
2352
2367
  };
2353
2368
  } catch (error) {
2354
2369
  abortIfActive(transaction);
2355
- await ignoreAbort(transaction);
2356
- throw error;
2370
+ throw await refusalBehindAbort(transaction, error);
2357
2371
  }
2358
2372
  }
2359
2373
  async createTransaction(record) {
@@ -2395,65 +2409,68 @@ class IndexedDbBlockStore {
2395
2409
  retainedBytes
2396
2410
  });
2397
2411
  }
2398
- transaction.objectStore("transactions").add(normalized, normalized.id);
2412
+ addTransactionRecord(transaction, normalized);
2399
2413
  await transactionDone(transaction);
2400
2414
  } catch (error) {
2401
2415
  abortIfActive(transaction);
2402
- await ignoreAbort(transaction);
2403
- throw error;
2416
+ throw await refusalBehindAbort(transaction, error);
2404
2417
  }
2405
2418
  }
2406
2419
  async getTransaction(id) {
2407
2420
  validateId(id, "Transaction ID");
2408
2421
  const transaction = this.#transaction("transactions", "readonly");
2409
- const value = await requestResult(transaction.objectStore("transactions").get(id));
2422
+ const record = await readTransactionRecord(transaction, id);
2410
2423
  await transactionDone(transaction);
2411
- return value === void 0 ? void 0 : asTransactionRecord(value, id);
2424
+ return record;
2412
2425
  }
2413
2426
  async getTransactions(ids) {
2414
2427
  assertStorageBulkReadItems(ids, "Transaction read");
2415
2428
  for (const id of ids)
2416
2429
  validateId(id, "Transaction ID");
2417
2430
  const transaction = this.#transaction("transactions", "readonly");
2418
- const store = transaction.objectStore("transactions");
2419
- const values = await Promise.all(ids.map((id) => requestResult(store.get(id))));
2431
+ const records = await readTransactionRecords(transaction, ids);
2420
2432
  await transactionDone(transaction);
2421
- return values.map((value, index) => value === void 0 ? void 0 : asTransactionRecord(value, ids[index]));
2433
+ return records;
2422
2434
  }
2423
2435
  async listTransactionPage(afterId, limit) {
2424
2436
  validatePageLimit(limit);
2425
2437
  const transaction = this.#transaction("transactions", "readonly");
2426
- const records = await readCursorPage(transaction.objectStore("transactions"), limit, (value, key) => {
2438
+ const page = await readCursorPage(transaction.objectStore("transactions"), limit, (value, key) => {
2427
2439
  if (typeof key !== "string")
2428
2440
  throw corruption("transactions", "record key is invalid");
2429
- return asTransactionRecord(value, key);
2441
+ return asStoredTransactionRecord(value, key);
2430
2442
  }, (key) => typeof key === "string" && (afterId === null || key > afterId), afterId ?? void 0);
2443
+ const records = await Promise.all(page.map((stored) => assembleTransactionRecord(transaction, stored)));
2431
2444
  await transactionDone(transaction);
2432
2445
  return { records, nextCursor: records.length === limit ? records.at(-1)?.id ?? null : null };
2433
2446
  }
2434
2447
  async updateTransaction(id, expectedRevision, update) {
2435
2448
  validateId(id, "Transaction ID");
2436
2449
  const transaction = this.#transaction(["transactions", "manifests", "blocks", "catalog", "segments", "statistics"], "readwrite");
2437
- const store = transaction.objectStore("transactions");
2438
- const value = await requestResult(store.get(id));
2439
- const current = value === void 0 ? void 0 : asTransactionRecord(value, id);
2440
- if (current?.revision !== expectedRevision) {
2441
- transaction.abort();
2450
+ const stored = await readStoredTransactionRecord(transaction, id);
2451
+ if (stored?.revision !== expectedRevision) {
2452
+ abortIfActive(transaction);
2442
2453
  await ignoreAbort(transaction);
2443
- throw new TransactionRecordConflictError(id, expectedRevision, current?.revision ?? null);
2454
+ throw new TransactionRecordConflictError(id, expectedRevision, stored?.revision ?? null);
2444
2455
  }
2445
2456
  try {
2457
+ const current = await assembleTransactionRecord(transaction, stored);
2446
2458
  assertGenericTransactionUpdateAllowed(current, update);
2447
2459
  const updated = updateTransactionRecord(current, update);
2448
2460
  if (update.snapshotVersion !== void 0) {
2449
2461
  await assertSnapshotAvailableInTransaction(transaction, updated.snapshotVersion);
2450
2462
  }
2451
2463
  await assertPendingArtifactsAvailableInTransaction(transaction, updated, update.pendingBlockIds !== void 0, update.pendingSegmentIds !== void 0, update.pendingSegmentIds !== void 0);
2452
- const addedBlocks = updated.pendingBlockIds.filter((blockId) => !current.pendingBlockIds.includes(blockId));
2453
- const addedSegments = updated.pendingSegmentIds.filter((segmentId) => !current.pendingSegmentIds.includes(segmentId));
2454
- if (update.pendingBlockIds !== void 0 && current.pendingBlockIds.some((blockId) => !updated.pendingBlockIds.includes(blockId)) || update.pendingSegmentIds !== void 0 && current.pendingSegmentIds.some((segmentId) => !updated.pendingSegmentIds.includes(segmentId))) {
2464
+ const updatedBlocks = new Set(updated.pendingBlockIds);
2465
+ const updatedSegments = new Set(updated.pendingSegmentIds);
2466
+ if (current.pendingBlockIds.some((blockId) => !updatedBlocks.has(blockId)) || current.pendingSegmentIds.some((segmentId) => !updatedSegments.has(segmentId))) {
2455
2467
  throw new TypeError("Use rollbackTransactionArtifacts to remove journaled artifacts");
2456
2468
  }
2469
+ if (!extendsJournalOrder(current.pendingBlockIds, updated.pendingBlockIds) || !extendsJournalOrder(current.pendingSegmentIds, updated.pendingSegmentIds)) {
2470
+ throw new TypeError("Transaction journal updates must append after the journaled ids");
2471
+ }
2472
+ const addedBlocks = updated.pendingBlockIds.slice(current.pendingBlockIds.length);
2473
+ const addedSegments = updated.pendingSegmentIds.slice(current.pendingSegmentIds.length);
2457
2474
  if (addedBlocks.length > 0 || addedSegments.length > 0) {
2458
2475
  let retainedBytes = 0;
2459
2476
  for (const id2 of addedBlocks) {
@@ -2465,13 +2482,15 @@ class IndexedDbBlockStore {
2465
2482
  retainedBytes
2466
2483
  });
2467
2484
  }
2468
- store.put(updated, id);
2485
+ if (addedBlocks.length > 0 || addedSegments.length > 0) {
2486
+ appendTransactionJournalChunks(transaction, stored, await readTransactionJournalTail(transaction, stored), addedBlocks, addedSegments);
2487
+ }
2488
+ putStoredTransactionRecord(transaction, storedTransactionRecord(updated));
2469
2489
  await transactionDone(transaction);
2470
- return structuredClone(updated);
2490
+ return updated;
2471
2491
  } catch (error) {
2472
2492
  abortIfActive(transaction);
2473
- await ignoreAbort(transaction);
2474
- throw error;
2493
+ throw await refusalBehindAbort(transaction, error);
2475
2494
  }
2476
2495
  }
2477
2496
  async renewTransaction(input) {
@@ -2480,13 +2499,11 @@ class IndexedDbBlockStore {
2480
2499
  const cutoff = validateBoundedLeaseExpiration(input.expiresAtCutoff, input.expiresAt, "Transaction");
2481
2500
  const transaction = this.#transaction(["transactions", "catalog", "leases"], "readwrite");
2482
2501
  try {
2483
- const store = transaction.objectStore("transactions");
2484
- const value = await requestResult(store.get(input.transactionId));
2485
- if (value === void 0) {
2502
+ const record = await readStoredTransactionRecord(transaction, input.transactionId);
2503
+ if (record === void 0) {
2486
2504
  await transactionDone(transaction);
2487
2505
  return false;
2488
2506
  }
2489
- const record = asTransactionRecord(value, input.transactionId);
2490
2507
  if (record.status !== "active" || record.ownerId !== input.ownerId || Date.parse(record.expiresAt) <= cutoff) {
2491
2508
  await transactionDone(transaction);
2492
2509
  return false;
@@ -2499,13 +2516,12 @@ class IndexedDbBlockStore {
2499
2516
  replacementTransaction: renewed,
2500
2517
  excludeTransactionId: record.id
2501
2518
  });
2502
- store.put(renewed, input.transactionId);
2519
+ putStoredTransactionRecord(transaction, renewed);
2503
2520
  await transactionDone(transaction);
2504
2521
  return true;
2505
2522
  } catch (error) {
2506
2523
  abortIfActive(transaction);
2507
- await ignoreAbort(transaction);
2508
- throw error;
2524
+ throw await refusalBehindAbort(transaction, error);
2509
2525
  }
2510
2526
  }
2511
2527
  async abortTransactionIfExpired(input) {
@@ -2521,29 +2537,28 @@ class IndexedDbBlockStore {
2521
2537
  const transaction = this.#transaction("transactions", "readwrite");
2522
2538
  try {
2523
2539
  const store = transaction.objectStore("transactions");
2524
- const value = await requestResult(store.get(input.transactionId));
2525
- if (value === void 0) {
2540
+ const stored = await readStoredTransactionRecord(transaction, input.transactionId);
2541
+ if (stored === void 0) {
2526
2542
  await transactionDone(transaction);
2527
2543
  return void 0;
2528
2544
  }
2529
- const current = asTransactionRecord(value, input.transactionId);
2530
- if (current.status !== "active" || current.ownerId !== input.expectedOwnerId || Date.parse(current.expiresAt) > cutoff) {
2545
+ if (stored.status !== "active" || stored.ownerId !== input.expectedOwnerId || Date.parse(stored.expiresAt) > cutoff) {
2531
2546
  await transactionDone(transaction);
2532
2547
  return void 0;
2533
2548
  }
2549
+ const current = await assembleTransactionRecord(transaction, stored);
2534
2550
  const updated = updateTransactionRecord(current, {
2535
2551
  status: "aborted",
2536
2552
  updatedAt: input.updatedAt,
2537
2553
  committedVersion: null
2538
2554
  });
2539
2555
  await assertTerminalTransactionAdmission(store);
2540
- store.put(updated, updated.id);
2556
+ putStoredTransactionRecord(transaction, storedTransactionRecord(updated));
2541
2557
  await transactionDone(transaction);
2542
2558
  return structuredClone(updated);
2543
2559
  } catch (error) {
2544
2560
  abortIfActive(transaction);
2545
- await ignoreAbort(transaction);
2546
- throw error;
2561
+ throw await refusalBehindAbort(transaction, error);
2547
2562
  }
2548
2563
  }
2549
2564
  async stageTransactionArtifacts(input) {
@@ -2551,9 +2566,7 @@ class IndexedDbBlockStore {
2551
2566
  validateId(input.transactionId, "Transaction ID");
2552
2567
  const transaction = this.#transaction(["blocks", "catalog", "segments", "statistics", "transactions"], "readwrite");
2553
2568
  try {
2554
- const transactionStore = transaction.objectStore("transactions");
2555
- const value = await requestResult(transactionStore.get(input.transactionId));
2556
- const current = value === void 0 ? void 0 : asTransactionRecord(value, input.transactionId);
2569
+ const current = await readStoredTransactionRecord(transaction, input.transactionId);
2557
2570
  if (current?.revision !== input.expectedRevision || current.status !== "active") {
2558
2571
  throw new TransactionRecordConflictError(input.transactionId, input.expectedRevision, current?.revision ?? null);
2559
2572
  }
@@ -2577,7 +2590,7 @@ class IndexedDbBlockStore {
2577
2590
  if (normalized.transactionId !== input.transactionId) {
2578
2591
  throw new Error(`Segment ${normalized.id} belongs to another transaction`);
2579
2592
  }
2580
- const expectedCommitOrdinal = current.pendingSegmentIds.length + normalizedSegments.length;
2593
+ const expectedCommitOrdinal = current.pendingSegmentCount + normalizedSegments.length;
2581
2594
  if (normalized.commitOrdinal !== expectedCommitOrdinal) {
2582
2595
  throw new TypeError(`Segment ${normalized.id} commit ordinal must be ${String(expectedCommitOrdinal)}`);
2583
2596
  }
@@ -2585,14 +2598,9 @@ class IndexedDbBlockStore {
2585
2598
  segmentIds.add(normalized.id);
2586
2599
  normalizedSegments.push(normalized);
2587
2600
  }
2588
- const update = {
2589
- pendingBlockIds: [...current.pendingBlockIds, ...input.blocks.map((block) => block.id)],
2590
- pendingSegmentIds: [
2591
- ...current.pendingSegmentIds,
2592
- ...normalizedSegments.map((segment) => segment.id)
2593
- ],
2594
- updatedAt: input.updatedAt
2595
- };
2601
+ const addedBlockIds = input.blocks.map((block) => block.id);
2602
+ const addedSegmentIds = normalizedSegments.map((segment) => segment.id);
2603
+ const update = { updatedAt: input.updatedAt };
2596
2604
  if (current.pendingTable !== void 0) {
2597
2605
  let nextRowId = current.pendingTableNextRowId;
2598
2606
  if (nextRowId === void 0)
@@ -2606,16 +2614,17 @@ class IndexedDbBlockStore {
2606
2614
  update.pendingTableNextRowId = nextRowId;
2607
2615
  }
2608
2616
  assertGenericTransactionUpdateAllowed(current, update);
2609
- const updated = updateTransactionRecord(current, update);
2610
- assertTransactionArtifactJournalLimits(updated.pendingBlockIds, updated.pendingSegmentIds);
2611
- const updatedBlockIds = new Set(updated.pendingBlockIds);
2617
+ const tail = await readTransactionJournalTail(transaction, current);
2618
+ const journal = await this.#journaledIds(transaction, current, tail);
2619
+ const rejournaled = addedBlockIds.find((id) => journal.blockIdSet.has(id));
2620
+ if (rejournaled !== void 0)
2621
+ throw new Error(`Block already exists: ${rejournaled}`);
2612
2622
  for (const segment of normalizedSegments) {
2613
- const unjournaledBlock = segmentBlockIds(segment).find((id) => !updatedBlockIds.has(id));
2623
+ const unjournaledBlock = segmentBlockIds(segment).find((id) => !ids.has(id) && !journal.blockIdSet.has(id));
2614
2624
  if (unjournaledBlock !== void 0) {
2615
2625
  throw new Error(`Segment ${segment.id} references block absent from the transaction journal: ${unjournaledBlock}`);
2616
2626
  }
2617
2627
  }
2618
- await assertPendingArtifactsAvailableInTransaction(transaction, current);
2619
2628
  await updateTransactionResourceLedger(transaction.objectStore("statistics"), input.transactionId, {
2620
2629
  blockCount: input.blocks.length,
2621
2630
  segmentCount: normalizedSegments.length,
@@ -2635,15 +2644,42 @@ class IndexedDbBlockStore {
2635
2644
  for (const segment of normalizedSegments) {
2636
2645
  segmentStore.add(segment, segment.id);
2637
2646
  }
2638
- transactionStore.put(updated, input.transactionId);
2647
+ const updated = {
2648
+ ...appendTransactionJournalChunks(transaction, current, tail, addedBlockIds, addedSegmentIds),
2649
+ ...update.pendingTableNextRowId === void 0 ? {} : { pendingTableNextRowId: update.pendingTableNextRowId },
2650
+ updatedAt: input.updatedAt,
2651
+ revision: incrementSafeInteger(current.revision, "Transaction revision")
2652
+ };
2653
+ putStoredTransactionRecord(transaction, updated);
2639
2654
  await transactionDone(transaction);
2640
- return structuredClone(updated);
2655
+ for (const id of addedBlockIds) {
2656
+ journal.blockIds.push(id);
2657
+ journal.blockIdSet.add(id);
2658
+ }
2659
+ for (const id of addedSegmentIds)
2660
+ journal.segmentIds.push(id);
2661
+ journal.revision = updated.revision;
2662
+ this.#journalCache = journal;
2663
+ return transactionRecordWithJournal(updated, journal.blockIds, journal.segmentIds);
2641
2664
  } catch (error) {
2642
2665
  abortIfActive(transaction);
2643
- await ignoreAbort(transaction);
2644
- throw error;
2666
+ throw await refusalBehindAbort(transaction, error);
2645
2667
  }
2646
2668
  }
2669
+ async #journaledIds(transaction, stored, tail) {
2670
+ const cached = this.#journalCache;
2671
+ if (cached?.transactionId === stored.id && cached.revision === stored.revision && cached.blockIds.length === stored.pendingBlockCount && cached.segmentIds.length === stored.pendingSegmentCount && journalTailMatches(cached, tail)) {
2672
+ return cached;
2673
+ }
2674
+ const record = await assembleTransactionRecord(transaction, stored);
2675
+ return {
2676
+ transactionId: record.id,
2677
+ revision: record.revision,
2678
+ blockIds: record.pendingBlockIds,
2679
+ segmentIds: record.pendingSegmentIds,
2680
+ blockIdSet: new Set(record.pendingBlockIds)
2681
+ };
2682
+ }
2647
2683
  async rollbackTransactionArtifacts(input) {
2648
2684
  validateId(input.transactionId, "Transaction ID");
2649
2685
  for (const id of [...input.pendingBlockIds, ...input.removeBlockIds]) {
@@ -2654,9 +2690,7 @@ class IndexedDbBlockStore {
2654
2690
  }
2655
2691
  const transaction = this.#transaction(["blocks", "catalog", "gc", "manifests", "segments", "statistics", "transactions"], "readwrite");
2656
2692
  try {
2657
- const transactionStore = transaction.objectStore("transactions");
2658
- const value = await requestResult(transactionStore.get(input.transactionId));
2659
- const current = value === void 0 ? void 0 : asTransactionRecord(value, input.transactionId);
2693
+ const current = await readTransactionRecord(transaction, input.transactionId);
2660
2694
  if (current?.revision !== input.expectedRevision || current.status !== "active") {
2661
2695
  throw new TransactionRecordConflictError(input.transactionId, input.expectedRevision, current?.revision ?? null);
2662
2696
  }
@@ -2679,12 +2713,9 @@ class IndexedDbBlockStore {
2679
2713
  if (publishedBlock !== void 0) {
2680
2714
  throw corruption(`transactions/${input.transactionId}`, `rollback journal names block ${publishedBlock.blockId} reachable by manifest ${String(publishedBlock.version)}`);
2681
2715
  }
2682
- await visitObjectStoreSequentially(transactionStore, (recordValue, key) => {
2683
- if (typeof key !== "string")
2684
- throw corruption("transactions", "record key is invalid");
2716
+ await visitTransactionRecords(transaction, (record, key) => {
2685
2717
  if (key === input.transactionId)
2686
2718
  return;
2687
- const record = asTransactionRecord(recordValue, key);
2688
2719
  const shared = record.pendingBlockIds.find((id) => removedBlocks.has(id));
2689
2720
  const sharedSegment = record.pendingSegmentIds.find((id) => removedSegments.has(id));
2690
2721
  if (shared !== void 0 || sharedSegment !== void 0) {
@@ -2744,7 +2775,7 @@ class IndexedDbBlockStore {
2744
2775
  let pendingTableNextRowId;
2745
2776
  if (current.pendingTable !== void 0) {
2746
2777
  const retained = await Promise.all(input.pendingSegmentIds.map((id) => requestResult(segmentStore.get(id))));
2747
- const retainedSegments = retained.map((value2) => value2 === void 0 ? void 0 : asSegmentRecord(value2)).filter((segment) => segment?.tableId === current.pendingTable?.id);
2778
+ const retainedSegments = retained.map((value) => value === void 0 ? void 0 : asSegmentRecord(value)).filter((segment) => segment?.tableId === current.pendingTable?.id);
2748
2779
  pendingTableNextRowId = 1n;
2749
2780
  for (const segment of retainedSegments) {
2750
2781
  if (segment.kind !== "insert" || segment.rowIdStart !== pendingTableNextRowId) {
@@ -2774,13 +2805,19 @@ class IndexedDbBlockStore {
2774
2805
  }
2775
2806
  for (const id of input.removeSegmentIds)
2776
2807
  segmentStore.delete(id);
2777
- transactionStore.put(updated, input.transactionId);
2808
+ putTransactionRecord(transaction, updated, journalChunkCountOf(current));
2778
2809
  await transactionDone(transaction);
2810
+ this.#journalCache = {
2811
+ transactionId: updated.id,
2812
+ revision: updated.revision,
2813
+ blockIds: [...updated.pendingBlockIds],
2814
+ segmentIds: [...updated.pendingSegmentIds],
2815
+ blockIdSet: new Set(updated.pendingBlockIds)
2816
+ };
2779
2817
  return structuredClone(updated);
2780
2818
  } catch (error) {
2781
2819
  abortIfActive(transaction);
2782
- await ignoreAbort(transaction);
2783
- throw error;
2820
+ throw await refusalBehindAbort(transaction, error);
2784
2821
  }
2785
2822
  }
2786
2823
  async commitTransaction(input) {
@@ -2805,14 +2842,13 @@ class IndexedDbBlockStore {
2805
2842
  const transaction = this.#transaction(storeNames2, "readwrite");
2806
2843
  try {
2807
2844
  const record = await readActiveTransactionRecord(transaction, input.transactionId, input.expectedTransactionRevision);
2808
- const outcome = await this.#commitInTransaction(transaction, record, input);
2845
+ const outcome = await this.#commitInTransaction(transaction, record, "stored", input);
2809
2846
  await transactionDone(transaction);
2810
2847
  outcome.settle();
2811
2848
  return outcome.manifest;
2812
2849
  } catch (error) {
2813
2850
  abortIfActive(transaction);
2814
- await ignoreAbort(transaction);
2815
- throw error;
2851
+ throw await refusalBehindAbort(transaction, error);
2816
2852
  }
2817
2853
  }
2818
2854
  async writeTransaction(input) {
@@ -2863,7 +2899,6 @@ class IndexedDbBlockStore {
2863
2899
  storeNames2.push("gc");
2864
2900
  const transaction = this.#transaction(storeNames2, "readwrite");
2865
2901
  try {
2866
- const transactionStore = transaction.objectStore("transactions");
2867
2902
  let base;
2868
2903
  if ("record" in input.transaction) {
2869
2904
  const id = input.transaction.record.id;
@@ -2871,7 +2906,7 @@ class IndexedDbBlockStore {
2871
2906
  ...input.transaction.record,
2872
2907
  snapshotVersion: input.expectedManifestVersion
2873
2908
  });
2874
- if (await requestResult(transactionStore.getKey(id)) !== void 0) {
2909
+ if (await requestResult(transaction.objectStore("transactions").getKey(id)) !== void 0) {
2875
2910
  throw new Error(`Transaction already exists: ${id}`);
2876
2911
  }
2877
2912
  const current = asOptionalManifestVersion(await requestResult(transaction.objectStore("catalog").get(CURRENT_MANIFEST_KEY)), CURRENT_MANIFEST_KEY);
@@ -2917,7 +2952,13 @@ class IndexedDbBlockStore {
2917
2952
  stageUpdate.pendingTableNextRowId = nextRowId;
2918
2953
  }
2919
2954
  const staged = updateTransactionRecord(base, stageUpdate);
2920
- assertTransactionArtifactJournalLimits(staged.pendingBlockIds, staged.pendingSegmentIds);
2955
+ const stagedBlockIds = new Set(staged.pendingBlockIds);
2956
+ for (const segment of normalizedSegments) {
2957
+ const unjournaledBlock = segmentBlockIds(segment).find((id) => !stagedBlockIds.has(id));
2958
+ if (unjournaledBlock !== void 0) {
2959
+ throw new Error(`Segment ${segment.id} references block absent from the transaction journal: ${unjournaledBlock}`);
2960
+ }
2961
+ }
2921
2962
  await assertPendingArtifactsAvailableInTransaction(transaction, base);
2922
2963
  await updateRecordResourceLedger(transaction.objectStore("statistics"), {
2923
2964
  segments: normalizedSegments.map((segment) => ({ next: segment }))
@@ -2933,17 +2974,16 @@ class IndexedDbBlockStore {
2933
2974
  for (const segment of normalizedSegments) {
2934
2975
  segmentStore.add(segment, segment.id);
2935
2976
  }
2936
- const outcome = await this.#commitInTransaction(transaction, staged, input);
2977
+ const outcome = await this.#commitInTransaction(transaction, staged, { replacedChunkCount: journalChunkCountOf(base) }, input);
2937
2978
  await transactionDone(transaction);
2938
2979
  outcome.settle();
2939
2980
  return outcome.manifest;
2940
2981
  } catch (error) {
2941
2982
  abortIfActive(transaction);
2942
- await ignoreAbort(transaction);
2943
- throw error;
2983
+ throw await refusalBehindAbort(transaction, error);
2944
2984
  }
2945
2985
  }
2946
- async #commitInTransaction(transaction, record, input) {
2986
+ async #commitInTransaction(transaction, record, journal, input) {
2947
2987
  incrementSafeInteger(record.revision, "Transaction revision");
2948
2988
  const transactionStore = transaction.objectStore("transactions");
2949
2989
  const catalog = transaction.objectStore("catalog");
@@ -3001,15 +3041,11 @@ class IndexedDbBlockStore {
3001
3041
  if (pendingRemovedBlock !== void 0) {
3002
3042
  throw new Error(`Cannot supersede a pending block: ${pendingRemovedBlock}`);
3003
3043
  }
3004
- let baseBlockIdSet;
3005
3044
  const removedMembership = await manifestBlockMembershipInTransaction(catalog, input.expectedManifestVersion, removedBlockIds);
3006
3045
  const invalidRemovedIndex = removedMembership.findIndex((present) => !present);
3007
3046
  if (invalidRemovedIndex >= 0) {
3008
3047
  throw new Error(`Cannot supersede a block outside the transaction snapshot: ${removedBlockIds[invalidRemovedIndex] ?? ""}`);
3009
3048
  }
3010
- if ((input.levelZeroSegmentLimits?.length ?? 0) > 0) {
3011
- baseBlockIdSet = await resolveManifestBlockSetInTransaction(catalog, input.expectedManifestVersion);
3012
- }
3013
3049
  if (removedBlockIds.length > 0) {
3014
3050
  await assertCompactionRetirementInTransaction(transaction, record, input, removedBlockIds);
3015
3051
  }
@@ -3031,7 +3067,7 @@ class IndexedDbBlockStore {
3031
3067
  throw new Error(`Segment ${foreignSegment.id} belongs to another transaction`);
3032
3068
  }
3033
3069
  await assertCompactionOutputSegmentsInTransaction(transaction, record, input.compactionJobId, pendingSegments);
3034
- await assertLevelZeroSegmentLimits(transaction, input.levelZeroSegmentLimits ?? [], pendingSegments, baseBlockIdSet ?? /* @__PURE__ */ new Set(), pendingTable);
3070
+ await assertLevelZeroSegmentLimits(transaction, input.levelZeroSegmentLimits ?? [], pendingSegments, input.expectedManifestVersion, pendingTable);
3035
3071
  const uniqueKeyEntries = input.uniqueKeyChanges ?? [];
3036
3072
  const coveredUniqueNamespaces = new Set(uniqueKeyEntries.map((entry) => entry.tableId));
3037
3073
  const changedTableIds = /* @__PURE__ */ new Set([
@@ -3378,7 +3414,11 @@ class IndexedDbBlockStore {
3378
3414
  if (ftsDeltaCounts.length > 0)
3379
3415
  manifest.ftsDeltaCounts = ftsDeltaCounts;
3380
3416
  await clearTransactionResourceLedger(transaction.objectStore("statistics"), committed.id);
3381
- transactionStore.put(committed, committed.id);
3417
+ if (journal === "stored") {
3418
+ putStoredTransactionRecord(transaction, storedTransactionRecord(committed));
3419
+ } else {
3420
+ putTransactionRecord(transaction, committed, journal.replacedChunkCount);
3421
+ }
3382
3422
  const settle = () => {
3383
3423
  if (pendingTable !== void 0) {
3384
3424
  this.#tableIdsByName.set(pendingTable.name, pendingTable.id);
@@ -3439,6 +3479,7 @@ class IndexedDbBlockStore {
3439
3479
  this.#uniqueKeyCache.version = manifest.version;
3440
3480
  }
3441
3481
  this.#manifestCache = void 0;
3482
+ this.#journalCache = void 0;
3442
3483
  };
3443
3484
  return { manifest, settle };
3444
3485
  }
@@ -3467,8 +3508,7 @@ class IndexedDbBlockStore {
3467
3508
  await transactionDone(transaction);
3468
3509
  } catch (error) {
3469
3510
  abortIfActive(transaction);
3470
- await ignoreAbort(transaction);
3471
- throw error;
3511
+ throw await refusalBehindAbort(transaction, error);
3472
3512
  }
3473
3513
  }
3474
3514
  async getLease(id) {
@@ -3547,8 +3587,7 @@ class IndexedDbBlockStore {
3547
3587
  return structuredClone(renewed);
3548
3588
  } catch (error) {
3549
3589
  abortIfActive(transaction);
3550
- await ignoreAbort(transaction);
3551
- throw error;
3590
+ throw await refusalBehindAbort(transaction, error);
3552
3591
  }
3553
3592
  }
3554
3593
  async moveLease(input) {
@@ -3589,8 +3628,7 @@ class IndexedDbBlockStore {
3589
3628
  return structuredClone(moved);
3590
3629
  } catch (error) {
3591
3630
  abortIfActive(transaction);
3592
- await ignoreAbort(transaction);
3593
- throw error;
3631
+ throw await refusalBehindAbort(transaction, error);
3594
3632
  }
3595
3633
  }
3596
3634
  async removeLeaseIfExpired(id, expectedRevision, expiresAtCutoff) {
@@ -3776,8 +3814,7 @@ class IndexedDbBlockStore {
3776
3814
  }
3777
3815
  await assertActiveCompactionMarker(jobStore, current);
3778
3816
  incrementSafeInteger(current.revision, "Compaction job revision");
3779
- const transactionValue = current.transactionId === null ? void 0 : await requestResult(transactionStore.get(current.transactionId));
3780
- const linkedTransaction = transactionValue === void 0 ? void 0 : asTransactionRecord(transactionValue);
3817
+ const linkedTransaction = current.transactionId === null ? void 0 : await readTransactionRecord(transaction, current.transactionId);
3781
3818
  let updated;
3782
3819
  let updatedLinkedTransaction;
3783
3820
  try {
@@ -3815,7 +3852,7 @@ class IndexedDbBlockStore {
3815
3852
  const terminalCompactionJobs = checkedQuotaIncrement(quota.terminalCompactionJobs, "Terminal compaction job count");
3816
3853
  assertMaintenanceLimit("terminal compaction job", terminalCompactionJobs, MAX_TERMINAL_COMPACTION_JOB_RECORDS);
3817
3854
  if (updatedLinkedTransaction !== void 0) {
3818
- transactionStore.put(updatedLinkedTransaction, updatedLinkedTransaction.id);
3855
+ putStoredTransactionRecord(transaction, storedTransactionRecord(updatedLinkedTransaction));
3819
3856
  }
3820
3857
  jobStore.put(compactionJobEnvelope(updated), key);
3821
3858
  jobStore.put({
@@ -3858,8 +3895,7 @@ class IndexedDbBlockStore {
3858
3895
  return true;
3859
3896
  } catch (error) {
3860
3897
  abortIfActive(transaction);
3861
- await ignoreAbort(transaction);
3862
- throw error;
3898
+ throw await refusalBehindAbort(transaction, error);
3863
3899
  }
3864
3900
  }
3865
3901
  async createGarbageCollectionJob(input) {
@@ -3899,8 +3935,7 @@ class IndexedDbBlockStore {
3899
3935
  return structuredClone(record);
3900
3936
  } catch (error) {
3901
3937
  abortIfActive(transaction);
3902
- await ignoreAbort(transaction);
3903
- throw error;
3938
+ throw await refusalBehindAbort(transaction, error);
3904
3939
  }
3905
3940
  }
3906
3941
  async updateGarbageCollectionPlanning(input) {
@@ -3928,8 +3963,7 @@ class IndexedDbBlockStore {
3928
3963
  return structuredClone(updated);
3929
3964
  } catch (error) {
3930
3965
  abortIfActive(transaction);
3931
- await ignoreAbort(transaction);
3932
- throw error;
3966
+ throw await refusalBehindAbort(transaction, error);
3933
3967
  }
3934
3968
  }
3935
3969
  async getGarbageCollectionJob(id) {
@@ -3984,7 +4018,6 @@ class IndexedDbBlockStore {
3984
4018
  const manifestStore = transaction.objectStore("manifests");
3985
4019
  const segmentStore = transaction.objectStore("segments");
3986
4020
  const blockStore = transaction.objectStore("blocks");
3987
- const transactionStore = transaction.objectStore("transactions");
3988
4021
  const currentVersionValue = await requestResult(catalog.get(CURRENT_MANIFEST_KEY));
3989
4022
  const currentVersion = asOptionalManifestVersion(currentVersionValue, CURRENT_MANIFEST_KEY) ?? null;
3990
4023
  const leaseCutoff = Date.parse(current.leaseCutoff);
@@ -4082,8 +4115,7 @@ class IndexedDbBlockStore {
4082
4115
  if (id === void 0) {
4083
4116
  throw new Error("Garbage collection transaction cursor is invalid");
4084
4117
  }
4085
- const transactionValue = await requestResult(transactionStore.get(id));
4086
- const record = transactionValue === void 0 ? void 0 : asTransactionRecord(transactionValue, id);
4118
+ const record = await readTransactionRecord(transaction, id);
4087
4119
  const manifestValue = record?.committedVersion === null || record?.committedVersion === void 0 ? void 0 : await requestResult(manifestStore.get(record.committedVersion));
4088
4120
  const manifest = manifestValue === void 0 ? void 0 : asStoredManifestRecord(manifestValue, record?.committedVersion ?? void 0);
4089
4121
  if (record === void 0)
@@ -4092,7 +4124,9 @@ class IndexedDbBlockStore {
4092
4124
  retainedTransactionIds.push(id);
4093
4125
  } else {
4094
4126
  await clearTransactionResourceLedger(transaction.objectStore("statistics"), id);
4095
- transactionStore.delete(id);
4127
+ deleteTransactionRecord(transaction, storedTransactionRecord(record));
4128
+ if (this.#journalCache?.transactionId === id)
4129
+ this.#journalCache = void 0;
4096
4130
  reclaimedTransactionIds.push(id);
4097
4131
  }
4098
4132
  transactionIndex += 1;
@@ -4180,8 +4214,7 @@ class IndexedDbBlockStore {
4180
4214
  };
4181
4215
  } catch (error) {
4182
4216
  abortIfActive(transaction);
4183
- await ignoreAbort(transaction);
4184
- throw error;
4217
+ throw await refusalBehindAbort(transaction, error);
4185
4218
  }
4186
4219
  }
4187
4220
  async removePrunedManifestRecords(maxItems) {
@@ -4243,8 +4276,7 @@ class IndexedDbBlockStore {
4243
4276
  return removed;
4244
4277
  } catch (error) {
4245
4278
  abortIfActive(transaction);
4246
- await ignoreAbort(transaction);
4247
- throw error;
4279
+ throw await refusalBehindAbort(transaction, error);
4248
4280
  }
4249
4281
  }
4250
4282
  async removeGarbageCollectionJob(id) {
@@ -4339,6 +4371,7 @@ class IndexedDbBlockStore {
4339
4371
  this.#tableIdsByName.clear();
4340
4372
  this.#uniqueKeyCache = void 0;
4341
4373
  this.#manifestCache = void 0;
4374
+ this.#journalCache = void 0;
4342
4375
  return {
4343
4376
  identity,
4344
4377
  removedBlockCount: marker.kindItemCounts[SNAPSHOT_FRAME_KINDS.indexOf("block")] ?? 0,
@@ -4346,8 +4379,7 @@ class IndexedDbBlockStore {
4346
4379
  };
4347
4380
  } catch (error) {
4348
4381
  abortIfActive(transaction);
4349
- await ignoreAbort(transaction);
4350
- throw error;
4382
+ throw await refusalBehindAbort(transaction, error);
4351
4383
  }
4352
4384
  }
4353
4385
  async getStorageStats() {
@@ -4899,11 +4931,10 @@ class IndexedDbBlockStore {
4899
4931
  observedSegmentBytes = safeByteSum(observedSegmentBytes, segmentRecordRetainedBytes(segment), "Integrity segment bytes");
4900
4932
  if (segment.id !== key)
4901
4933
  throw corruption(`segments/${key}`, "record id differs from key");
4902
- const ownerValue = await requestResult(transactionStore.get(segment.transactionId));
4903
- if (ownerValue === void 0) {
4934
+ const owner = await readTransactionRecord(transaction, segment.transactionId);
4935
+ if (owner === void 0) {
4904
4936
  throw corruption(`segments/${key}`, `owning transaction ${segment.transactionId} is missing`);
4905
4937
  }
4906
- const owner = asTransactionRecord(ownerValue, segment.transactionId);
4907
4938
  const segmentTable = tablesById.get(segment.tableId) ?? (owner.status === "active" && owner.pendingTable?.id === segment.tableId ? owner.pendingTable : void 0);
4908
4939
  if (segmentTable === void 0) {
4909
4940
  throw corruption(`segments/${key}`, `table ${segment.tableId} is missing`);
@@ -4932,7 +4963,7 @@ class IndexedDbBlockStore {
4932
4963
  try {
4933
4964
  if (typeof key !== "string")
4934
4965
  throw corruption("transactions", "record key is invalid");
4935
- const record = asTransactionRecord(value, key);
4966
+ const record = await assembleTransactionRecord(transaction, asStoredTransactionRecord(value, key));
4936
4967
  for (const [commitOrdinal, segmentId] of record.pendingSegmentIds.entries()) {
4937
4968
  const segmentValue = await requestResult(segmentStore.get(segmentId));
4938
4969
  if (segmentValue === void 0) {
@@ -4961,13 +4992,7 @@ class IndexedDbBlockStore {
4961
4992
  if (alreadyLive) {
4962
4993
  throw corruption(`transactions/${key}`, `pending block ${blockId} is already live in the current manifest`);
4963
4994
  }
4964
- const journaledByAnotherActiveTransaction = await visitObjectStoreSequentially(transactionStore, (candidateValue, candidateKey) => {
4965
- if (typeof candidateKey !== "string") {
4966
- throw corruption("transactions", "record key is invalid");
4967
- }
4968
- const candidate = asTransactionRecord(candidateValue, candidateKey);
4969
- return candidate.id !== record.id && candidate.status === "active" && candidate.pendingBlockIds.includes(blockId);
4970
- });
4995
+ const journaledByAnotherActiveTransaction = await visitTransactionRecords(transaction, (candidate) => candidate.id !== record.id && candidate.status === "active" && candidate.pendingBlockIds.includes(blockId));
4971
4996
  if (journaledByAnotherActiveTransaction) {
4972
4997
  throw corruption(`transactions/${key}`, `pending block ${blockId} is journaled by another active transaction`);
4973
4998
  }
@@ -5053,6 +5078,25 @@ class IndexedDbBlockStore {
5053
5078
  } catch (error) {
5054
5079
  issue("invalid-pending-catalog", "transactions", error);
5055
5080
  }
5081
+ await visitObjectStoreSequentially(transaction.objectStore(TRANSACTION_JOURNAL_STORE), async (value, key) => {
5082
+ checkedRecords += 1;
5083
+ try {
5084
+ if (!Array.isArray(key) || key.length !== 2 || typeof key[0] !== "string" || typeof key[1] !== "number" || !Number.isSafeInteger(key[1]) || key[1] < 0) {
5085
+ throw corruption(TRANSACTION_JOURNAL_STORE, "journal chunk key is invalid");
5086
+ }
5087
+ const [transactionId, chunkIndex] = key;
5088
+ asTransactionJournalChunk(value, `transactions/${transactionId}/journal/${String(chunkIndex)}`);
5089
+ const owner = await readStoredTransactionRecord(transaction, transactionId);
5090
+ if (owner === void 0) {
5091
+ throw corruption(`transactions/${transactionId}`, `journal chunk ${String(chunkIndex)} has no transaction record`);
5092
+ }
5093
+ if (chunkIndex >= owner.journalChunkCount) {
5094
+ throw corruption(`transactions/${transactionId}`, `journal chunk ${String(chunkIndex)} lies beyond the declared ${String(owner.journalChunkCount)}`);
5095
+ }
5096
+ } catch (error) {
5097
+ issue("orphan-transaction-journal", storageKeyLocation(key), error);
5098
+ }
5099
+ });
5056
5100
  const leaseIds = /* @__PURE__ */ new Set();
5057
5101
  await visitObjectStoreSequentially(transaction.objectStore("leases"), async (value, key) => {
5058
5102
  checkedRecords += 1;
@@ -5115,11 +5159,10 @@ class IndexedDbBlockStore {
5115
5159
  }
5116
5160
  }
5117
5161
  if (job.transactionId !== null) {
5118
- const transactionValue = await requestResult(transactionStore.get(job.transactionId));
5119
- if (transactionValue === void 0) {
5162
+ const owner = await readTransactionRecord(transaction, job.transactionId);
5163
+ if (owner === void 0) {
5120
5164
  throw corruption(`gc/${key}`, "linked transaction is missing");
5121
5165
  }
5122
- const owner = asTransactionRecord(transactionValue, job.transactionId);
5123
5166
  if (job.state === "ready" || owner.pendingSegmentIds.length > 0) {
5124
5167
  const table = tablesById.get(job.tableId);
5125
5168
  if (table === void 0)
@@ -5164,11 +5207,8 @@ class IndexedDbBlockStore {
5164
5207
  if (candidate !== void 0)
5165
5208
  asBytes(candidate, `blocks/${id}`);
5166
5209
  }
5167
- for (const id of job.candidateTransactionIds) {
5168
- const candidate = await requestResult(transactionStore.get(id));
5169
- if (candidate !== void 0)
5170
- asTransactionRecord(candidate, id);
5171
- }
5210
+ for (const id of job.candidateTransactionIds)
5211
+ await readTransactionRecord(transaction, id);
5172
5212
  } else if (key.startsWith(ACTIVE_COMPACTION_KEY_PREFIX)) {
5173
5213
  let tableId;
5174
5214
  try {
@@ -5359,11 +5399,10 @@ class IndexedDbBlockStore {
5359
5399
  const ledger = asOptionalTransactionResourceLedger(value, transactionId);
5360
5400
  if (ledger === void 0)
5361
5401
  throw corruption(`statistics/${key}`, "transaction ledger is missing");
5362
- const transactionValue = await requestResult(transactionStore.get(transactionId));
5363
- if (transactionValue === void 0) {
5402
+ const owner = await readTransactionRecord(transaction, transactionId);
5403
+ if (owner === void 0) {
5364
5404
  throw corruption(`statistics/${key}`, "transaction resource ledger has no transaction");
5365
5405
  }
5366
- const owner = asTransactionRecord(transactionValue, transactionId);
5367
5406
  if (owner.status === "committed" || ledger.blockCount !== owner.pendingBlockIds.length || ledger.segmentCount !== owner.pendingSegmentIds.length) {
5368
5407
  throw corruption(`statistics/${key}`, "transaction resource ledger disagrees with its journal");
5369
5408
  }
@@ -5532,8 +5571,7 @@ class IndexedDbBlockStore {
5532
5571
  return { sessionId, ownerId: input.ownerId, expiresAt: input.expiresAt, header };
5533
5572
  } catch (error) {
5534
5573
  abortIfActive(transaction);
5535
- await ignoreAbort(transaction);
5536
- throw error;
5574
+ throw await refusalBehindAbort(transaction, error);
5537
5575
  }
5538
5576
  }
5539
5577
  async readSnapshotExportFrame(input) {
@@ -5609,8 +5647,7 @@ class IndexedDbBlockStore {
5609
5647
  return frame;
5610
5648
  } catch (error) {
5611
5649
  abortIfActive(transaction);
5612
- await ignoreAbort(transaction);
5613
- throw error;
5650
+ throw await refusalBehindAbort(transaction, error);
5614
5651
  }
5615
5652
  }
5616
5653
  async closeSnapshotFrameExport(input) {
@@ -5638,8 +5675,7 @@ class IndexedDbBlockStore {
5638
5675
  return true;
5639
5676
  } catch (error) {
5640
5677
  abortIfActive(transaction);
5641
- await ignoreAbort(transaction);
5642
- throw error;
5678
+ throw await refusalBehindAbort(transaction, error);
5643
5679
  }
5644
5680
  }
5645
5681
  async beginSnapshotFrameImport(input) {
@@ -5739,6 +5775,7 @@ class IndexedDbBlockStore {
5739
5775
  transaction.objectStore("manifests").clear();
5740
5776
  transaction.objectStore("segments").clear();
5741
5777
  transaction.objectStore("transactions").clear();
5778
+ transaction.objectStore(TRANSACTION_JOURNAL_STORE).clear();
5742
5779
  transaction.objectStore(SNAPSHOT_HEADER_STORE).clear();
5743
5780
  const statistics = transaction.objectStore("statistics");
5744
5781
  statistics.clear();
@@ -5769,8 +5806,7 @@ class IndexedDbBlockStore {
5769
5806
  return snapshotFrameImportSession(marker);
5770
5807
  } catch (error) {
5771
5808
  abortIfActive(transaction);
5772
- await ignoreAbort(transaction);
5773
- throw error;
5809
+ throw await refusalBehindAbort(transaction, error);
5774
5810
  }
5775
5811
  }
5776
5812
  async renewSnapshotFrameImport(input) {
@@ -5788,8 +5824,7 @@ class IndexedDbBlockStore {
5788
5824
  return snapshotFrameImportSession(renewed);
5789
5825
  } catch (error) {
5790
5826
  abortIfActive(transaction);
5791
- await ignoreAbort(transaction);
5792
- throw error;
5827
+ throw await refusalBehindAbort(transaction, error);
5793
5828
  }
5794
5829
  }
5795
5830
  async appendSnapshotImportFrames(input) {
@@ -5910,8 +5945,7 @@ class IndexedDbBlockStore {
5910
5945
  return snapshotFrameImportSession(marker);
5911
5946
  } catch (error) {
5912
5947
  abortIfActive(transaction);
5913
- await ignoreAbort(transaction);
5914
- throw error;
5948
+ throw await refusalBehindAbort(transaction, error);
5915
5949
  }
5916
5950
  }
5917
5951
  async finishSnapshotFrameImport(input) {
@@ -5964,10 +5998,10 @@ class IndexedDbBlockStore {
5964
5998
  this.#tableIdsByName.clear();
5965
5999
  this.#uniqueKeyCache = void 0;
5966
6000
  this.#manifestCache = void 0;
6001
+ this.#journalCache = void 0;
5967
6002
  } catch (error) {
5968
6003
  abortIfActive(transaction);
5969
- await ignoreAbort(transaction);
5970
- throw error;
6004
+ throw await refusalBehindAbort(transaction, error);
5971
6005
  }
5972
6006
  }
5973
6007
  async cancelSnapshotFrameImport(input) {
@@ -6002,6 +6036,7 @@ class IndexedDbBlockStore {
6002
6036
  this.#tableIdsByName.clear();
6003
6037
  this.#uniqueKeyCache = void 0;
6004
6038
  this.#manifestCache = void 0;
6039
+ this.#journalCache = void 0;
6005
6040
  return {
6006
6041
  identity: marker.identity,
6007
6042
  removedBlockCount: marker.kindItemCounts[SNAPSHOT_FRAME_KINDS.indexOf("block")] ?? 0,
@@ -6009,8 +6044,7 @@ class IndexedDbBlockStore {
6009
6044
  };
6010
6045
  } catch (error) {
6011
6046
  abortIfActive(transaction);
6012
- await ignoreAbort(transaction);
6013
- throw error;
6047
+ throw await refusalBehindAbort(transaction, error);
6014
6048
  }
6015
6049
  }
6016
6050
  close() {
@@ -6020,6 +6054,7 @@ class IndexedDbBlockStore {
6020
6054
  this.#tableIdsByName.clear();
6021
6055
  this.#uniqueKeyCache = void 0;
6022
6056
  this.#manifestCache = void 0;
6057
+ this.#journalCache = void 0;
6023
6058
  this.#db.close();
6024
6059
  }
6025
6060
  _residentStateForTests() {
@@ -6066,8 +6101,7 @@ class IndexedDbBlockStore {
6066
6101
  return true;
6067
6102
  } catch (error) {
6068
6103
  abortIfActive(transaction);
6069
- await ignoreAbort(transaction);
6070
- throw error;
6104
+ throw await refusalBehindAbort(transaction, error);
6071
6105
  }
6072
6106
  }
6073
6107
  }
@@ -6083,8 +6117,7 @@ class IndexedDbBlockStore {
6083
6117
  return;
6084
6118
  } catch (error) {
6085
6119
  abortIfActive(transaction);
6086
- await ignoreAbort(transaction);
6087
- throw error;
6120
+ throw await refusalBehindAbort(transaction, error);
6088
6121
  }
6089
6122
  }
6090
6123
  }
@@ -6114,8 +6147,7 @@ class IndexedDbBlockStore {
6114
6147
  await transactionDone(transaction);
6115
6148
  } catch (error) {
6116
6149
  abortIfActive(transaction);
6117
- await ignoreAbort(transaction);
6118
- throw error;
6150
+ throw await refusalBehindAbort(transaction, error);
6119
6151
  }
6120
6152
  if (cleanup)
6121
6153
  await this.#cleanupUniqueKeyBuildArtifacts(buildId);
@@ -6146,8 +6178,7 @@ class IndexedDbBlockStore {
6146
6178
  return;
6147
6179
  } catch (error) {
6148
6180
  abortIfActive(transaction);
6149
- await ignoreAbort(transaction);
6150
- throw error;
6181
+ throw await refusalBehindAbort(transaction, error);
6151
6182
  }
6152
6183
  }
6153
6184
  }
@@ -6282,8 +6313,7 @@ class IndexedDbBlockStore {
6282
6313
  await transactionDone(transaction);
6283
6314
  } catch (error) {
6284
6315
  abortIfActive(transaction);
6285
- await ignoreAbort(transaction);
6286
- throw error;
6316
+ throw await refusalBehindAbort(transaction, error);
6287
6317
  }
6288
6318
  }
6289
6319
  async #cleanupFtsRetirementPage() {
@@ -6299,16 +6329,16 @@ class IndexedDbBlockStore {
6299
6329
  await transactionDone(transaction);
6300
6330
  } catch (error) {
6301
6331
  abortIfActive(transaction);
6302
- await ignoreAbort(transaction);
6303
- throw error;
6332
+ throw await refusalBehindAbort(transaction, error);
6304
6333
  }
6305
6334
  }
6306
6335
  #transaction(stores, mode, options = {}) {
6307
6336
  if (this.#closed)
6308
6337
  throw new Error("This IndexedDB store connection is closed");
6309
- const transaction = mode === "readwrite" ? this.#db.transaction(stores, mode, { durability: this.#durability }) : this.#db.transaction(stores, mode);
6338
+ const requested = typeof stores === "string" ? [stores] : stores;
6339
+ const storeList = requested.includes("transactions") && !requested.includes(TRANSACTION_JOURNAL_STORE) ? [...requested, TRANSACTION_JOURNAL_STORE] : requested;
6340
+ const transaction = mode === "readwrite" ? this.#db.transaction(storeList, mode, { durability: this.#durability }) : this.#db.transaction(storeList, mode);
6310
6341
  void transactionDone(transaction).catch(() => void 0);
6311
- const storeList = typeof stores === "string" ? [stores] : stores;
6312
6342
  if (mode === "readwrite" && !options.allowSnapshotImport && storeList.includes("catalog")) {
6313
6343
  const catalog = transaction.objectStore("catalog");
6314
6344
  const marker = catalog.get(SNAPSHOT_FRAME_IMPORT_KEY);
@@ -6333,9 +6363,8 @@ function assertIndexedDbSchemaMigrationRegistry() {
6333
6363
  }
6334
6364
  }
6335
6365
  function createCurrentIndexedDbSchema(database, upgrade) {
6336
- for (const storeName of storeNames)
6366
+ for (const storeName of indexedDbStoreNames)
6337
6367
  database.createObjectStore(storeName);
6338
- database.createObjectStore(SNAPSHOT_HEADER_STORE);
6339
6368
  for (const [storeName, indexes] of Object.entries(indexedDbIndexSchema)) {
6340
6369
  const store = upgrade.objectStore(storeName);
6341
6370
  for (const index of indexes) {
@@ -6348,10 +6377,10 @@ function createCurrentIndexedDbSchema(database, upgrade) {
6348
6377
  upgrade.objectStore("statistics").add(emptyCatalogResourceLedger(), CATALOG_RESOURCE_LEDGER_KEY);
6349
6378
  upgrade.objectStore("statistics").add(emptyRecordResourceLedger(), RECORD_RESOURCE_LEDGER_KEY);
6350
6379
  }
6351
- function applyIndexedDbSchemaMigrations(database, upgrade, oldVersion) {
6380
+ function applyIndexedDbSchemaMigrations(database, upgrade, oldVersion, fail) {
6352
6381
  for (const migration of indexedDbSchemaMigrations) {
6353
6382
  if (migration.targetVersion > oldVersion)
6354
- migration.migrate(database, upgrade);
6383
+ migration.migrate(database, upgrade, fail);
6355
6384
  }
6356
6385
  }
6357
6386
  const indexedDbIndexSchema = {
@@ -6791,7 +6820,7 @@ async function assertTableRemovalAllowed(transaction, tableId, expectedRevision)
6791
6820
  const segments = await readTableSegmentsForRemoval(transaction.objectStore("segments").index(SEGMENT_TABLE_INDEX), tableId);
6792
6821
  const transactionStore = transaction.objectStore("transactions");
6793
6822
  await visitObjectStoreSequentially(transactionStore, (candidateValue, key) => {
6794
- const candidate = asTransactionRecord(candidateValue, storageKeyLocation(key));
6823
+ const candidate = asStoredTransactionRecord(candidateValue, storageKeyLocation(key));
6795
6824
  const pendingTable = candidate.status === "active" ? candidate.pendingTable : void 0;
6796
6825
  const foreignKey = (pendingTable?.foreignKeys ?? []).find((constraint) => constraint.parentTable === record.name);
6797
6826
  if (foreignKey !== void 0) {
@@ -6803,11 +6832,10 @@ async function assertTableRemovalAllowed(transaction, tableId, expectedRevision)
6803
6832
  for (const segment of segments) {
6804
6833
  let owner = owners.get(segment.transactionId);
6805
6834
  if (owner === void 0) {
6806
- const ownerValue = await requestResult(transactionStore.get(segment.transactionId));
6807
- if (ownerValue === void 0) {
6835
+ owner = await readStoredTransactionRecord(transaction, segment.transactionId);
6836
+ if (owner === void 0) {
6808
6837
  throw corruption(`segments/${segment.id}`, `references missing transaction ${segment.transactionId}`);
6809
6838
  }
6810
- owner = asTransactionRecord(ownerValue, segment.transactionId);
6811
6839
  owners.set(segment.transactionId, owner);
6812
6840
  }
6813
6841
  if (owner.status === "active")
@@ -6960,6 +6988,14 @@ async function ignoreAbort(transaction) {
6960
6988
  } catch {
6961
6989
  }
6962
6990
  }
6991
+ async function refusalBehindAbort(transaction, error) {
6992
+ await ignoreAbort(transaction);
6993
+ const refusal = transaction.error;
6994
+ if (refusal !== null && error instanceof DOMException && error.name === "AbortError") {
6995
+ return refusal;
6996
+ }
6997
+ return error;
6998
+ }
6963
6999
  function corruption(location, message) {
6964
7000
  return new StorageCorruptionError("indexeddb", location, message);
6965
7001
  }
@@ -7099,6 +7135,9 @@ function requiredUniqueStringArray(value, location) {
7099
7135
  }
7100
7136
  return [...value];
7101
7137
  }
7138
+ function extendsJournalOrder(current, updated) {
7139
+ return current.length <= updated.length && current.every((id, index) => updated[index] === id);
7140
+ }
7102
7141
  function validateArtifactPartition(retained, removed, current) {
7103
7142
  if (retained.length + removed.length !== current.length)
7104
7143
  return false;
@@ -7813,7 +7852,7 @@ async function stageSnapshotMetadataFrame(transaction, frame) {
7813
7852
  await assertCatalogReservationAdmission(transaction, record.pendingTable);
7814
7853
  await assertTableForeignKeysInTransaction(catalog, record.pendingTable);
7815
7854
  }
7816
- transaction.objectStore("transactions").add(record, record.id);
7855
+ addTransactionRecord(transaction, record);
7817
7856
  return;
7818
7857
  }
7819
7858
  if (item.kind === "unique-generation") {
@@ -7900,7 +7939,6 @@ async function readSnapshotGenerationId(catalog, key, label) {
7900
7939
  async function validateAndPromoteStagedSnapshot(transaction, marker) {
7901
7940
  const catalog = transaction.objectStore("catalog");
7902
7941
  const segments = transaction.objectStore("segments");
7903
- const transactions = transaction.objectStore("transactions");
7904
7942
  const blocks = transaction.objectStore("blocks");
7905
7943
  await visitObjectStoreSequentially(catalog, async (value, key) => {
7906
7944
  if (typeof key !== "string" || !key.startsWith(TABLE_ID_PREFIX))
@@ -7942,10 +7980,7 @@ async function validateAndPromoteStagedSnapshot(transaction, marker) {
7942
7980
  asSnapshotPostingStagingRecord(await requestResult(catalog.get(snapshotPostingStagingKey(generationId))), generationId);
7943
7981
  }
7944
7982
  });
7945
- await visitObjectStoreSequentially(transactions, (value, key) => {
7946
- if (typeof key !== "string")
7947
- throw corruption("transactions", "snapshot transaction key is invalid");
7948
- const record = asTransactionRecord(value, key);
7983
+ await visitTransactionRecords(transaction, (record, key) => {
7949
7984
  if (record.status !== "committed" || record.committedVersion === null || record.committedVersion > marker.version || record.pendingBlockIds.length !== 0 || record.pendingSegmentIds.length !== 0) {
7950
7985
  throw corruption(`transactions/${key}`, "snapshot transaction is not canonical committed history");
7951
7986
  }
@@ -7959,10 +7994,9 @@ async function validateAndPromoteStagedSnapshot(transaction, marker) {
7959
7994
  if (await requestResult(catalog.getKey(`${TABLE_ID_PREFIX}${segment.tableId}`)) === void 0) {
7960
7995
  throw corruption(`segments/${key}`, "snapshot segment table is missing");
7961
7996
  }
7962
- const ownerValue = await requestResult(transactions.get(segment.transactionId));
7963
- if (ownerValue === void 0)
7997
+ if (await readTransactionRecord(transaction, segment.transactionId) === void 0) {
7964
7998
  throw corruption(`segments/${key}`, "snapshot segment owner is missing");
7965
- asTransactionRecord(ownerValue, segment.transactionId);
7999
+ }
7966
8000
  for (const blockId of segmentBlockIds(segment)) {
7967
8001
  const provenance = asManifestBlockRecord(await requestResult(catalog.get(manifestBlockKey(blockId))), blockId);
7968
8002
  if (!manifestBlockVisibleAt(provenance, marker.version)) {
@@ -8242,10 +8276,9 @@ async function writeSnapshotMetadataFramesInTransaction(input) {
8242
8276
  });
8243
8277
  await visitObjectStoreSequentially(input.transaction.objectStore("segments"), async (value) => {
8244
8278
  const record = asSegmentRecord(value);
8245
- const ownerValue = await requestResult(input.transaction.objectStore("transactions").get(record.transactionId));
8246
- if (ownerValue === void 0)
8279
+ const owner = await readStoredTransactionRecord(input.transaction, record.transactionId);
8280
+ if (owner === void 0)
8247
8281
  return;
8248
- const owner = asTransactionRecord(ownerValue, record.transactionId);
8249
8282
  if (owner.status !== "committed" || owner.committedVersion === null || owner.committedVersion > input.version) {
8250
8283
  return;
8251
8284
  }
@@ -8261,14 +8294,14 @@ async function writeSnapshotMetadataFramesInTransaction(input) {
8261
8294
  await visitObjectStoreSequentially(input.transaction.objectStore("transactions"), (value, key) => {
8262
8295
  if (typeof key !== "string")
8263
8296
  throw corruption("transactions", "record key is invalid");
8264
- const record = asTransactionRecord(value, key);
8297
+ const record = asStoredTransactionRecord(value, key);
8265
8298
  if (record.status !== "committed" || record.committedVersion === null || record.committedVersion > input.version) {
8266
8299
  return;
8267
8300
  }
8268
8301
  input.observeRetainedItems(1);
8269
8302
  writeItem("transaction-page", {
8270
8303
  kind: "transaction",
8271
- record: { ...record, pendingBlockIds: [], pendingSegmentIds: [] }
8304
+ record: transactionRecordWithJournal(record, [], [])
8272
8305
  });
8273
8306
  });
8274
8307
  await visitTables(async (table) => {
@@ -8510,34 +8543,74 @@ async function findReadableManifestBlock(transaction, ids, excludedVersions = /*
8510
8543
  });
8511
8544
  return found;
8512
8545
  }
8513
- function asTransactionRecord(value, expectedId) {
8546
+ function journalTailMatches(cached, tail) {
8547
+ if (tail === void 0)
8548
+ return cached.blockIds.length === 0 && cached.segmentIds.length === 0;
8549
+ const endsWith = (ids, suffix) => suffix.every((id, index) => ids[ids.length - suffix.length + index] === id);
8550
+ return endsWith(cached.blockIds, tail.blockIds) && endsWith(cached.segmentIds, tail.segmentIds);
8551
+ }
8552
+ const TRANSACTION_RECORD_FIELDS = [
8553
+ "id",
8554
+ "ownerId",
8555
+ "expiresAt",
8556
+ "snapshotVersion",
8557
+ "status",
8558
+ "revision",
8559
+ "startedAt",
8560
+ "updatedAt",
8561
+ "committedVersion",
8562
+ "schemaEpochGuard",
8563
+ "pendingTable",
8564
+ "pendingTableNextRowId",
8565
+ "catalogEpochGuard"
8566
+ ];
8567
+ function transactionJournalChunkKey(transactionId, chunkIndex) {
8568
+ return [transactionId, chunkIndex];
8569
+ }
8570
+ function transactionJournalEntryCount(blockCount, segmentCount) {
8571
+ const total = blockCount + segmentCount;
8572
+ if (!Number.isSafeInteger(total))
8573
+ throw new RangeError("Transaction journal is too large");
8574
+ return total;
8575
+ }
8576
+ function transactionJournalChunkCount(blockCount, segmentCount) {
8577
+ return Math.ceil(transactionJournalEntryCount(blockCount, segmentCount) / TRANSACTION_JOURNAL_CHUNK_ENTRIES);
8578
+ }
8579
+ function journalChunkCountOf(record) {
8580
+ return transactionJournalChunkCount(record.pendingBlockIds.length, record.pendingSegmentIds.length);
8581
+ }
8582
+ function packTransactionJournal(blockIds, segmentIds) {
8583
+ const chunks = [];
8584
+ let blockOffset = 0;
8585
+ let segmentOffset = 0;
8586
+ while (blockOffset < blockIds.length || segmentOffset < segmentIds.length) {
8587
+ const blocks = blockIds.slice(blockOffset, blockOffset + TRANSACTION_JOURNAL_CHUNK_ENTRIES);
8588
+ blockOffset += blocks.length;
8589
+ const segments = segmentIds.slice(segmentOffset, segmentOffset + TRANSACTION_JOURNAL_CHUNK_ENTRIES - blocks.length);
8590
+ segmentOffset += segments.length;
8591
+ chunks.push({ blockIds: blocks, segmentIds: segments });
8592
+ }
8593
+ return chunks;
8594
+ }
8595
+ function asTransactionJournalChunk(value, location) {
8514
8596
  if (!isRecord(value))
8515
- throw corruption("transactions", "record is not an object");
8516
- assertKnownFields(value, [
8517
- "id",
8518
- "ownerId",
8519
- "expiresAt",
8520
- "snapshotVersion",
8521
- "pendingBlockIds",
8522
- "pendingSegmentIds",
8523
- "status",
8524
- "revision",
8525
- "startedAt",
8526
- "updatedAt",
8527
- "committedVersion",
8528
- "schemaEpochGuard",
8529
- "pendingTable",
8530
- "pendingTableNextRowId",
8531
- "catalogEpochGuard"
8532
- ], "transactions");
8597
+ throw corruption(location, "journal chunk is not an object");
8598
+ assertKnownFields(value, ["blockIds", "segmentIds"], location);
8599
+ const blockIds = requiredUniqueStringArray(value.blockIds, `${location}/blockIds`);
8600
+ const segmentIds = requiredUniqueStringArray(value.segmentIds, `${location}/segmentIds`);
8601
+ const entries = blockIds.length + segmentIds.length;
8602
+ if (entries === 0 || entries > TRANSACTION_JOURNAL_CHUNK_ENTRIES) {
8603
+ throw corruption(location, `journal chunk holds ${String(entries)} ids`);
8604
+ }
8605
+ return { blockIds, segmentIds };
8606
+ }
8607
+ function asTransactionRecordBase(value, expectedId) {
8533
8608
  const id = nonEmptyStoredString(value.id, "transactions/id");
8534
8609
  if (expectedId !== void 0 && id !== expectedId) {
8535
8610
  throw corruption(`transactions/${expectedId}`, `record declares id ${id}`);
8536
8611
  }
8537
8612
  const snapshotVersion = asNullableStoredVersion(value.snapshotVersion, `transactions/${id}/snapshotVersion`);
8538
8613
  const committedVersion = asNullableStoredVersion(value.committedVersion, `transactions/${id}/committedVersion`);
8539
- const pendingBlockIds = requiredUniqueStringArray(value.pendingBlockIds, `transactions/${id}/pendingBlockIds`);
8540
- const pendingSegmentIds = requiredUniqueStringArray(value.pendingSegmentIds, `transactions/${id}/pendingSegmentIds`);
8541
8614
  if (!["active", "committed", "aborted"].includes(value.status)) {
8542
8615
  throw corruption(`transactions/${id}/status`, "status is invalid");
8543
8616
  }
@@ -8568,8 +8641,6 @@ function asTransactionRecord(value, expectedId) {
8568
8641
  ownerId,
8569
8642
  expiresAt,
8570
8643
  snapshotVersion,
8571
- pendingBlockIds,
8572
- pendingSegmentIds,
8573
8644
  status,
8574
8645
  revision,
8575
8646
  startedAt,
@@ -8584,6 +8655,185 @@ function asTransactionRecord(value, expectedId) {
8584
8655
  }
8585
8656
  return { ...base, pendingTable, pendingTableNextRowId, catalogEpochGuard };
8586
8657
  }
8658
+ function asTransactionRecord(value, expectedId) {
8659
+ if (!isRecord(value))
8660
+ throw corruption("transactions", "record is not an object");
8661
+ assertKnownFields(value, [...TRANSACTION_RECORD_FIELDS, "pendingBlockIds", "pendingSegmentIds"], "transactions");
8662
+ const base = asTransactionRecordBase(value, expectedId);
8663
+ const pendingBlockIds = requiredUniqueStringArray(value.pendingBlockIds, `transactions/${base.id}/pendingBlockIds`);
8664
+ const pendingSegmentIds = requiredUniqueStringArray(value.pendingSegmentIds, `transactions/${base.id}/pendingSegmentIds`);
8665
+ transactionJournalEntryCount(pendingBlockIds.length, pendingSegmentIds.length);
8666
+ return { ...base, pendingBlockIds, pendingSegmentIds };
8667
+ }
8668
+ function asStoredTransactionRecord(value, expectedId) {
8669
+ if (!isRecord(value))
8670
+ throw corruption("transactions", "record is not an object");
8671
+ assertKnownFields(value, [...TRANSACTION_RECORD_FIELDS, "journalChunkCount", "pendingBlockCount", "pendingSegmentCount"], "transactions");
8672
+ const base = asTransactionRecordBase(value, expectedId);
8673
+ const pendingBlockCount = nonNegativeStoredInteger(value.pendingBlockCount, `transactions/${base.id}/pendingBlockCount`);
8674
+ const pendingSegmentCount = nonNegativeStoredInteger(value.pendingSegmentCount, `transactions/${base.id}/pendingSegmentCount`);
8675
+ const journalChunkCount = nonNegativeStoredInteger(value.journalChunkCount, `transactions/${base.id}/journalChunkCount`);
8676
+ let expectedChunkCount;
8677
+ try {
8678
+ expectedChunkCount = transactionJournalChunkCount(pendingBlockCount, pendingSegmentCount);
8679
+ } catch {
8680
+ throw corruption(`transactions/${base.id}`, "journal counts are unsafe");
8681
+ }
8682
+ if (journalChunkCount !== expectedChunkCount) {
8683
+ throw corruption(`transactions/${base.id}/journalChunkCount`, `chunk count ${String(journalChunkCount)} disagrees with ${String(pendingBlockCount)} blocks and ${String(pendingSegmentCount)} segments`);
8684
+ }
8685
+ return { ...base, journalChunkCount, pendingBlockCount, pendingSegmentCount };
8686
+ }
8687
+ function storedTransactionRecord(record) {
8688
+ const { pendingBlockIds, pendingSegmentIds, ...header } = record;
8689
+ return {
8690
+ ...header,
8691
+ journalChunkCount: journalChunkCountOf(record),
8692
+ pendingBlockCount: pendingBlockIds.length,
8693
+ pendingSegmentCount: pendingSegmentIds.length
8694
+ };
8695
+ }
8696
+ function transactionRecordWithJournal(stored, pendingBlockIds, pendingSegmentIds) {
8697
+ const { journalChunkCount: _chunkCount, pendingBlockCount: _blockCount, pendingSegmentCount: _segmentCount, ...header } = stored;
8698
+ void _chunkCount;
8699
+ void _blockCount;
8700
+ void _segmentCount;
8701
+ return {
8702
+ ...header,
8703
+ pendingBlockIds: [...pendingBlockIds],
8704
+ pendingSegmentIds: [...pendingSegmentIds]
8705
+ };
8706
+ }
8707
+ function transactionRecordFromJournal(stored, chunks) {
8708
+ const location = `transactions/${stored.id}`;
8709
+ if (chunks.length !== stored.journalChunkCount) {
8710
+ throw corruption(location, "journal chunk count disagrees with its record");
8711
+ }
8712
+ const pendingBlockIds = [];
8713
+ const pendingSegmentIds = [];
8714
+ for (const [index, chunk] of chunks.entries()) {
8715
+ const entries = chunk.blockIds.length + chunk.segmentIds.length;
8716
+ if (index < chunks.length - 1 && entries !== TRANSACTION_JOURNAL_CHUNK_ENTRIES) {
8717
+ throw corruption(`${location}/journal/${String(index)}`, "journal chunk is not packed");
8718
+ }
8719
+ for (const id of chunk.blockIds)
8720
+ pendingBlockIds.push(id);
8721
+ for (const id of chunk.segmentIds)
8722
+ pendingSegmentIds.push(id);
8723
+ }
8724
+ if (pendingBlockIds.length !== stored.pendingBlockCount || pendingSegmentIds.length !== stored.pendingSegmentCount) {
8725
+ throw corruption(location, "journal counts disagree with its chunks");
8726
+ }
8727
+ if (new Set(pendingBlockIds).size !== pendingBlockIds.length || new Set(pendingSegmentIds).size !== pendingSegmentIds.length) {
8728
+ throw corruption(location, "journal repeats an id");
8729
+ }
8730
+ return transactionRecordWithJournal(stored, pendingBlockIds, pendingSegmentIds);
8731
+ }
8732
+ async function readTransactionJournalChunk(journalStore, transactionId, chunkIndex) {
8733
+ const value = await requestResult(journalStore.get(transactionJournalChunkKey(transactionId, chunkIndex)));
8734
+ return value === void 0 ? void 0 : asTransactionJournalChunk(value, `transactions/${transactionId}/journal/${String(chunkIndex)}`);
8735
+ }
8736
+ async function readTransactionJournalChunks(transaction, stored) {
8737
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8738
+ const values = await Promise.all(Array.from({ length: stored.journalChunkCount + 1 }, (_, index) => readTransactionJournalChunk(journalStore, stored.id, index)));
8739
+ const extra = values.pop();
8740
+ if (extra !== void 0) {
8741
+ throw corruption(`transactions/${stored.id}`, "journal has more chunks than its record declares");
8742
+ }
8743
+ return values.map((chunk, index) => {
8744
+ if (chunk === void 0) {
8745
+ throw corruption(`transactions/${stored.id}/journal/${String(index)}`, "chunk is missing");
8746
+ }
8747
+ return chunk;
8748
+ });
8749
+ }
8750
+ async function assembleTransactionRecord(transaction, stored) {
8751
+ return transactionRecordFromJournal(stored, await readTransactionJournalChunks(transaction, stored));
8752
+ }
8753
+ async function readStoredTransactionRecord(transaction, id) {
8754
+ const value = await requestResult(transaction.objectStore("transactions").get(id));
8755
+ return value === void 0 ? void 0 : asStoredTransactionRecord(value, id);
8756
+ }
8757
+ async function readTransactionRecord(transaction, id) {
8758
+ const stored = await readStoredTransactionRecord(transaction, id);
8759
+ return stored === void 0 ? void 0 : assembleTransactionRecord(transaction, stored);
8760
+ }
8761
+ async function readTransactionRecords(transaction, ids) {
8762
+ const stored = await Promise.all(ids.map((id) => readStoredTransactionRecord(transaction, id)));
8763
+ return Promise.all(stored.map((record) => record === void 0 ? Promise.resolve(void 0) : assembleTransactionRecord(transaction, record)));
8764
+ }
8765
+ function visitTransactionRecords(transaction, visit) {
8766
+ return visitObjectStoreSequentially(transaction.objectStore("transactions"), async (value, key) => {
8767
+ if (typeof key !== "string")
8768
+ throw corruption("transactions", "record key is invalid");
8769
+ const record = await assembleTransactionRecord(transaction, asStoredTransactionRecord(value, key));
8770
+ return visit(record, key);
8771
+ });
8772
+ }
8773
+ function visitActiveTransactionRecords(transaction, visit) {
8774
+ return visitIndexPartitionSequentially(transaction.objectStore("transactions").index(TRANSACTION_STATUS_INDEX), "active", async (value, key) => {
8775
+ if (typeof key !== "string")
8776
+ throw corruption("transactions", "record key is invalid");
8777
+ const record = await assembleTransactionRecord(transaction, asStoredTransactionRecord(value, key));
8778
+ return visit(record, key);
8779
+ });
8780
+ }
8781
+ function putStoredTransactionRecord(transaction, stored) {
8782
+ transaction.objectStore("transactions").put(stored, stored.id);
8783
+ }
8784
+ function putTransactionRecord(transaction, record, replacedChunkCount) {
8785
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8786
+ const chunks = packTransactionJournal(record.pendingBlockIds, record.pendingSegmentIds);
8787
+ for (let index = chunks.length; index < replacedChunkCount; index += 1) {
8788
+ journalStore.delete(transactionJournalChunkKey(record.id, index));
8789
+ }
8790
+ chunks.forEach((chunk, index) => {
8791
+ journalStore.put(chunk, transactionJournalChunkKey(record.id, index));
8792
+ });
8793
+ putStoredTransactionRecord(transaction, storedTransactionRecord(record));
8794
+ }
8795
+ function addTransactionRecord(transaction, record) {
8796
+ transaction.objectStore("transactions").add(storedTransactionRecord(record), record.id);
8797
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8798
+ packTransactionJournal(record.pendingBlockIds, record.pendingSegmentIds).forEach((chunk, index) => {
8799
+ journalStore.put(chunk, transactionJournalChunkKey(record.id, index));
8800
+ });
8801
+ }
8802
+ function deleteTransactionRecord(transaction, stored) {
8803
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8804
+ for (let index = 0; index < stored.journalChunkCount; index += 1) {
8805
+ journalStore.delete(transactionJournalChunkKey(stored.id, index));
8806
+ }
8807
+ transaction.objectStore("transactions").delete(stored.id);
8808
+ }
8809
+ async function readTransactionJournalTail(transaction, stored) {
8810
+ const lastIndex = stored.journalChunkCount - 1;
8811
+ if (lastIndex < 0)
8812
+ return void 0;
8813
+ const tail = await readTransactionJournalChunk(transaction.objectStore(TRANSACTION_JOURNAL_STORE), stored.id, lastIndex);
8814
+ if (tail === void 0) {
8815
+ throw corruption(`transactions/${stored.id}/journal/${String(lastIndex)}`, "chunk is missing");
8816
+ }
8817
+ return tail;
8818
+ }
8819
+ function appendTransactionJournalChunks(transaction, stored, tail, addedBlockIds, addedSegmentIds) {
8820
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8821
+ const lastIndex = stored.journalChunkCount - 1;
8822
+ const pendingBlockCount = stored.pendingBlockCount + addedBlockIds.length;
8823
+ const pendingSegmentCount = stored.pendingSegmentCount + addedSegmentIds.length;
8824
+ const journalChunkCount = transactionJournalChunkCount(pendingBlockCount, pendingSegmentCount);
8825
+ const tailEntries = tail === void 0 ? 0 : tail.blockIds.length + tail.segmentIds.length;
8826
+ const topUp = tail !== void 0 && tailEntries < TRANSACTION_JOURNAL_CHUNK_ENTRIES;
8827
+ const firstIndex = topUp ? lastIndex : lastIndex + 1;
8828
+ const chunks = packTransactionJournal([...topUp ? tail.blockIds : [], ...addedBlockIds], [...topUp ? tail.segmentIds : [], ...addedSegmentIds]);
8829
+ if (firstIndex + chunks.length !== journalChunkCount) {
8830
+ throw corruption(`transactions/${stored.id}`, "journal append does not reach the chunk count");
8831
+ }
8832
+ chunks.forEach((chunk, offset) => {
8833
+ journalStore.put(chunk, transactionJournalChunkKey(stored.id, firstIndex + offset));
8834
+ });
8835
+ return { ...stored, journalChunkCount, pendingBlockCount, pendingSegmentCount };
8836
+ }
8587
8837
  function asTableRecord(value, location = "catalog/table") {
8588
8838
  if (!isRecord(value))
8589
8839
  throw corruption(location, "table record is not an object");
@@ -8625,8 +8875,12 @@ function asTableRecord(value, location = "catalog/table") {
8625
8875
  "defaultValue",
8626
8876
  "backfill",
8627
8877
  "enumValues",
8628
- "hidden"
8878
+ "hidden",
8879
+ "generatedValue"
8629
8880
  ], `${location}/columns/${String(index)}`);
8881
+ if (isRecord(column.generatedValue)) {
8882
+ assertKnownFields(column.generatedValue, ["kind", "sql"], `${location}/columns/${String(index)}/generatedValue`);
8883
+ }
8630
8884
  if (isRecord(column.sqlDomain)) {
8631
8885
  const domainFields = column.sqlDomain.kind === "numeric" ? ["kind", "precision", "scale"] : column.sqlDomain.kind === "array" ? ["kind", "element"] : column.sqlDomain.kind === "enum" ? ["kind", "name", "values"] : ["kind"];
8632
8886
  assertKnownFields(column.sqlDomain, domainFields, `${location}/columns/${String(index)}/sqlDomain`);
@@ -8951,8 +9205,7 @@ function asLeaseRecord(value, expectedId) {
8951
9205
  };
8952
9206
  }
8953
9207
  async function readActiveTransactionRecord(transaction, id, expectedRevision) {
8954
- const value = await requestResult(transaction.objectStore("transactions").get(id));
8955
- const record = value === void 0 ? void 0 : asTransactionRecord(value);
9208
+ const record = await readTransactionRecord(transaction, id);
8956
9209
  if (record?.revision !== expectedRevision || record.status !== "active") {
8957
9210
  throw new TransactionRecordConflictError(id, expectedRevision, record?.revision ?? null);
8958
9211
  }
@@ -9012,11 +9265,9 @@ async function assertCompactionJobReferences(transaction, job) {
9012
9265
  }
9013
9266
  }
9014
9267
  if (job.transactionId !== null) {
9015
- const value = await requestResult(transaction.objectStore("transactions").get(job.transactionId));
9016
- if (value === void 0) {
9268
+ if (await readTransactionRecord(transaction, job.transactionId) === void 0) {
9017
9269
  throw new Error(`Compaction job ${job.id} references missing transaction: ${job.transactionId}`);
9018
9270
  }
9019
- asTransactionRecord(value, job.transactionId);
9020
9271
  }
9021
9272
  if (job.state === "ready" || job.state === "published") {
9022
9273
  for (const outputId of compactionOutputSegmentIds(job)) {
@@ -9146,7 +9397,7 @@ async function assertCompactionOutputSegmentsInTransaction(transaction, committi
9146
9397
  }));
9147
9398
  assertCompactionOutputProvenance(job, table, committingTransaction, sourceSegments, pendingSegments);
9148
9399
  }
9149
- async function assertLevelZeroSegmentLimits(transaction, limits, pendingSegments, currentBlockIds, pendingTable) {
9400
+ async function assertLevelZeroSegmentLimits(transaction, limits, pendingSegments, version, pendingTable) {
9150
9401
  const pendingTables = new Set(pendingSegments.filter((segment) => segment.level === 0).map((segment) => segment.tableId));
9151
9402
  if (limits.length !== pendingTables.size) {
9152
9403
  throw new TypeError("Level-zero segment limits must exactly cover pending level-zero tables");
@@ -9171,7 +9422,7 @@ async function assertLevelZeroSegmentLimits(transaction, limits, pendingSegments
9171
9422
  if (table === void 0) {
9172
9423
  throw new Error(`Level-zero segment limit references missing table: ${entry.tableId}`);
9173
9424
  }
9174
- const existing = await countVisibleLevelZeroSegments(segmentIndex, transaction.objectStore("transactions"), entry.tableId, currentBlockIds);
9425
+ const existing = await countVisibleLevelZeroSegments(segmentIndex, transaction.objectStore("transactions"), catalog, version, entry.tableId);
9175
9426
  const added = pendingSegments.reduce((count2, segment) => count2 + (segment.tableId === entry.tableId && segment.level === 0 ? 1 : 0), 0);
9176
9427
  const count = existing + added;
9177
9428
  if (count > entry.limit) {
@@ -9179,66 +9430,54 @@ async function assertLevelZeroSegmentLimits(transaction, limits, pendingSegments
9179
9430
  }
9180
9431
  }
9181
9432
  }
9182
- function countVisibleLevelZeroSegments(index, transactions, tableId, currentBlockIds) {
9183
- return new Promise((resolve, reject) => {
9184
- const statuses = /* @__PURE__ */ new Map();
9185
- let count = 0;
9433
+ async function countVisibleLevelZeroSegments(index, transactions, catalog, version, tableId) {
9434
+ const candidates = await new Promise((resolve, reject) => {
9435
+ const found = [];
9186
9436
  const request = index.openCursor(tableId);
9187
9437
  request.onerror = () => reject(request.error ?? new Error("IndexedDB segment cursor failed"));
9188
9438
  request.onsuccess = () => {
9189
9439
  const cursor = request.result;
9190
9440
  if (cursor === null) {
9191
- resolve(count);
9441
+ resolve(found);
9192
9442
  return;
9193
9443
  }
9194
- let segment;
9195
9444
  try {
9196
- segment = asSegmentRecord(cursor.value);
9445
+ const segment = asSegmentRecord(cursor.value);
9197
9446
  if (segment.id !== cursor.primaryKey || segment.tableId !== tableId) {
9198
9447
  throw corruption(`segments/${segment.id}`, "table index does not match its record");
9199
9448
  }
9200
- if (segment.level !== 0) {
9201
- cursor.continue();
9202
- return;
9203
- }
9204
- const blockIds = segmentBlockIds(segment);
9205
- const visibleBlockCount = blockIds.filter((id) => currentBlockIds.has(id)).length;
9206
- if (visibleBlockCount > 0 && visibleBlockCount < blockIds.length) {
9207
- throw corruption(`segments/${segment.id}`, "only part of the segment is manifest-live");
9208
- }
9209
- if (blockIds.length > 0 && visibleBlockCount === 0) {
9210
- cursor.continue();
9211
- return;
9212
- }
9449
+ if (segment.level === 0)
9450
+ found.push(segment);
9451
+ cursor.continue();
9213
9452
  } catch (error) {
9214
9453
  reject(error instanceof Error ? error : new Error(String(error)));
9215
- return;
9216
- }
9217
- const knownStatus = statuses.get(segment.transactionId);
9218
- if (knownStatus !== void 0) {
9219
- if (knownStatus === "committed")
9220
- count += 1;
9221
- cursor.continue();
9222
- return;
9223
9454
  }
9224
- const ownerRequest = transactions.get(segment.transactionId);
9225
- ownerRequest.onerror = () => reject(ownerRequest.error ?? new Error("IndexedDB transaction lookup failed"));
9226
- ownerRequest.onsuccess = () => {
9227
- try {
9228
- if (ownerRequest.result === void 0) {
9229
- throw corruption(`segments/${segment.id}`, `owning transaction ${segment.transactionId} is missing`);
9230
- }
9231
- const owner = asTransactionRecord(ownerRequest.result, segment.transactionId);
9232
- statuses.set(owner.id, owner.status);
9233
- if (owner.status === "committed")
9234
- count += 1;
9235
- cursor.continue();
9236
- } catch (error) {
9237
- reject(error instanceof Error ? error : new Error(String(error)));
9238
- }
9239
- };
9240
9455
  };
9241
9456
  });
9457
+ const memberships = await Promise.all(candidates.map((segment) => manifestBlockMembershipInTransaction(catalog, version, segmentBlockIds(segment))));
9458
+ const statuses = /* @__PURE__ */ new Map();
9459
+ let count = 0;
9460
+ for (const [position, segment] of candidates.entries()) {
9461
+ const membership = memberships[position] ?? [];
9462
+ const visibleBlockCount = membership.filter((present) => present).length;
9463
+ if (visibleBlockCount > 0 && visibleBlockCount < membership.length) {
9464
+ throw corruption(`segments/${segment.id}`, "only part of the segment is manifest-live");
9465
+ }
9466
+ if (membership.length > 0 && visibleBlockCount === 0)
9467
+ continue;
9468
+ let status = statuses.get(segment.transactionId);
9469
+ if (status === void 0) {
9470
+ const owner = await requestResult(transactions.get(segment.transactionId));
9471
+ if (owner === void 0) {
9472
+ throw corruption(`segments/${segment.id}`, `owning transaction ${segment.transactionId} is missing`);
9473
+ }
9474
+ status = asStoredTransactionRecord(owner, segment.transactionId).status;
9475
+ statuses.set(segment.transactionId, status);
9476
+ }
9477
+ if (status === "committed")
9478
+ count += 1;
9479
+ }
9480
+ return count;
9242
9481
  }
9243
9482
  async function compactionJobRemovalPreservesProvenance(transaction, removedJob) {
9244
9483
  const rootedBlocks = /* @__PURE__ */ new Set();
@@ -9267,10 +9506,7 @@ async function compactionJobRemovalPreservesProvenance(transaction, removedJob)
9267
9506
  for (const blockId of segmentBlockIds(segment))
9268
9507
  rootedBlocks.add(blockId);
9269
9508
  });
9270
- await visitObjectStoreSequentially(transaction.objectStore("transactions"), (value, key) => {
9271
- if (typeof key !== "string")
9272
- throw corruption("transactions", "record key is invalid");
9273
- const record = asTransactionRecord(value, key);
9509
+ await visitTransactionRecords(transaction, (record) => {
9274
9510
  for (const blockId of record.pendingBlockIds)
9275
9511
  rootedBlocks.add(blockId);
9276
9512
  for (const segmentId of record.pendingSegmentIds)
@@ -9320,7 +9556,7 @@ async function garbageCollectionPinsInTransaction(transaction, currentVersion, l
9320
9556
  };
9321
9557
  pin(currentVersion);
9322
9558
  await visitObjectStoreReadBatches(transaction.objectStore("transactions"), (value) => {
9323
- const record = asTransactionRecord(value);
9559
+ const record = asStoredTransactionRecord(value);
9324
9560
  if (record.status === "active") {
9325
9561
  pin(record.snapshotVersion);
9326
9562
  }
@@ -9341,10 +9577,9 @@ async function garbageCollectionPinsInTransaction(transaction, currentVersion, l
9341
9577
  pin(job.sourceManifestVersion);
9342
9578
  if (job.transactionId === null)
9343
9579
  return;
9344
- const transactionValue = await requestResult(transaction.objectStore("transactions").get(job.transactionId));
9345
- if (transactionValue === void 0)
9580
+ const linkedTransaction = await readStoredTransactionRecord(transaction, job.transactionId);
9581
+ if (linkedTransaction === void 0)
9346
9582
  return;
9347
- const linkedTransaction = asTransactionRecord(transactionValue);
9348
9583
  if (linkedTransaction.status !== "committed")
9349
9584
  return;
9350
9585
  if (linkedTransaction.committedVersion === null) {
@@ -9786,8 +10021,7 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9786
10021
  }
9787
10022
  }
9788
10023
  for (const id of candidates.candidateTransactionIds) {
9789
- const value = await requestResult(transaction.objectStore("transactions").get(id));
9790
- const record = value === void 0 ? void 0 : asTransactionRecord(value);
10024
+ const record = await readStoredTransactionRecord(transaction, id);
9791
10025
  if (record === void 0 || record.status !== "aborted" && (record.status !== "committed" || record.committedVersion === null)) {
9792
10026
  throw new Error(`Garbage collection transaction candidate is not terminal: ${id}`);
9793
10027
  }
@@ -9804,10 +10038,7 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9804
10038
  const blockHasProvenance = async (id) => {
9805
10039
  if (manifestProvenBlockIds.has(id))
9806
10040
  return true;
9807
- const transactionProven = await visitObjectStoreSequentially(transaction.objectStore("transactions"), (value) => {
9808
- const record = asTransactionRecord(value);
9809
- return record.status === "aborted" && record.pendingBlockIds.includes(id);
9810
- });
10041
+ const transactionProven = await visitTransactionRecords(transaction, (record) => record.status === "aborted" && record.pendingBlockIds.includes(id));
9811
10042
  if (transactionProven)
9812
10043
  return true;
9813
10044
  return visitObjectStoreSequentially(transaction.objectStore("gc"), (value, key) => {
@@ -9843,8 +10074,7 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9843
10074
  ownerBlockRootsComplete = false;
9844
10075
  }
9845
10076
  };
9846
- await visitIndexPartitionSequentially(transaction.objectStore("transactions").index(TRANSACTION_STATUS_INDEX), "active", (value) => {
9847
- const record = asTransactionRecord(value);
10077
+ await visitActiveTransactionRecords(transaction, (record) => {
9848
10078
  if (record.status !== "active")
9849
10079
  return;
9850
10080
  rememberOwnerBlocks(record.pendingBlockIds);
@@ -9875,10 +10105,7 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9875
10105
  return cached;
9876
10106
  let rooted = ownerBlockRoots.has(id);
9877
10107
  if (!rooted && !ownerBlockRootsComplete) {
9878
- rooted = await visitIndexPartitionSequentially(transaction.objectStore("transactions").index(TRANSACTION_STATUS_INDEX), "active", (value) => {
9879
- const record = asTransactionRecord(value);
9880
- return record.status === "active" && record.pendingBlockIds.includes(id);
9881
- });
10108
+ rooted = await visitActiveTransactionRecords(transaction, (record) => record.status === "active" && record.pendingBlockIds.includes(id));
9882
10109
  if (!rooted) {
9883
10110
  rooted = await visitObjectStoreSequentially(transaction.objectStore("gc"), (value, key) => {
9884
10111
  const job = asCompactionJobAtMaintenanceKey(value, key);
@@ -9899,15 +10126,13 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9899
10126
  const rootedBlockIds = /* @__PURE__ */ new Set();
9900
10127
  const rootedSegmentIds = /* @__PURE__ */ new Set();
9901
10128
  const segmentStore = transaction.objectStore("segments");
9902
- const transactionStore = transaction.objectStore("transactions");
9903
10129
  for (const id of candidateSegmentIds) {
9904
10130
  const value = await requestResult(segmentStore.get(id));
9905
10131
  if (value === void 0)
9906
10132
  continue;
9907
10133
  const segment = asSegmentRecord(value);
9908
10134
  const ids = segmentBlockIds(segment);
9909
- const ownerValue = await requestResult(transactionStore.get(segment.transactionId));
9910
- const owner = ownerValue === void 0 ? void 0 : asTransactionRecord(ownerValue);
10135
+ const owner = await readStoredTransactionRecord(transaction, segment.transactionId);
9911
10136
  let allBlocksRooted = ids.length > 0;
9912
10137
  for (const blockId of ids) {
9913
10138
  if (await isDirectBlockRoot(blockId))
@@ -10728,7 +10953,7 @@ async function pendingCatalogReservations(store, excludeTransactionId) {
10728
10953
  await visitObjectStoreSequentially(store, (value, key) => {
10729
10954
  if (typeof key !== "string")
10730
10955
  throw corruption("transactions", "record key is invalid");
10731
- const transaction = asTransactionRecord(value, key);
10956
+ const transaction = asStoredTransactionRecord(value, key);
10732
10957
  if (transaction.id === excludeTransactionId || transaction.status !== "active" || transaction.pendingTable === void 0) {
10733
10958
  return;
10734
10959
  }
@@ -10951,7 +11176,7 @@ async function assertPinnedHistoryAdmission(transaction, input) {
10951
11176
  throw corruption("transactions", "record key is invalid");
10952
11177
  if (key === input.excludeTransactionId)
10953
11178
  return;
10954
- const record = asTransactionRecord(value, key);
11179
+ const record = asStoredTransactionRecord(value, key);
10955
11180
  if (record.status !== "active") {
10956
11181
  throw corruption(`transactions/${key}`, "status index does not match its record");
10957
11182
  }