@minnowdb/core 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/engine/auto-store.d.ts +40 -0
  2. package/dist/engine/auto-store.js +115 -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.d.ts +55 -6
  6. package/dist/engine/client.js +155 -40
  7. package/dist/engine/database.d.ts +15 -1
  8. package/dist/engine/database.js +1085 -227
  9. package/dist/engine/errors.d.ts +61 -2
  10. package/dist/engine/errors.js +116 -3
  11. package/dist/engine/index.d.ts +1 -0
  12. package/dist/engine/index.js +2 -0
  13. package/dist/engine/live.d.ts +24 -1
  14. package/dist/engine/live.js +33 -9
  15. package/dist/engine/scope-write-set.js +36 -0
  16. package/dist/engine/worker-auto.d.ts +1 -0
  17. package/dist/engine/worker-auto.js +3 -0
  18. package/dist/engine/worker-host.d.ts +2 -1
  19. package/dist/engine/worker-host.js +17 -1
  20. package/dist/engine/worker-server.d.ts +53 -1
  21. package/dist/engine/worker-server.js +118 -14
  22. package/dist/engine/worker-store-auto.js +36 -0
  23. package/dist/engine/worker-store-opfs.js +3 -2
  24. package/dist/engine/write-coordinator.js +24 -2
  25. package/dist/storage/indexeddb.js +494 -207
  26. package/dist/storage/opfs/leader.js +201 -14
  27. package/dist/storage/opfs/rpc.js +23 -43
  28. package/dist/storage/opfs/store.d.ts +20 -0
  29. package/dist/storage/opfs/store.js +479 -60
  30. package/dist/storage/toolkit/record-core.js +67 -38
  31. package/dist/storage/toolkit/wire.d.ts +1 -1
  32. package/dist/storage/toolkit/wire.js +1 -1
  33. package/dist/storage/types.d.ts +29 -8
  34. package/dist/storage/types.js +27 -16
  35. package/dist/testing/opfs-shim.js +14 -6
  36. package/dist/transactions/index.d.ts +12 -0
  37. package/dist/transactions/index.js +83 -23
  38. package/dist/worker-protocol/index.d.ts +50 -2
  39. package/dist/worker-protocol/index.js +106 -4
  40. 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();
@@ -1125,6 +1178,7 @@ class IndexedDbBlockStore {
1125
1178
  await bumpSchemaEpoch(catalog);
1126
1179
  await transactionDone(transaction);
1127
1180
  this.#manifestCache = void 0;
1181
+ this.#journalCache = void 0;
1128
1182
  if (this.#uniqueKeyCache?.tableId === input.tableId || this.#uniqueKeyCache?.tableId.startsWith(`${input.tableId}\0secondary-index\0`) === true) {
1129
1183
  this.#uniqueKeyCache = void 0;
1130
1184
  } else if (this.#uniqueKeyCache !== void 0) {
@@ -1265,6 +1319,7 @@ class IndexedDbBlockStore {
1265
1319
  await bumpSchemaEpoch(catalog);
1266
1320
  await transactionDone(transaction);
1267
1321
  this.#manifestCache = void 0;
1322
+ this.#journalCache = void 0;
1268
1323
  if (this.#uniqueKeyCache !== void 0)
1269
1324
  this.#uniqueKeyCache.version = manifest.version;
1270
1325
  return manifest;
@@ -1845,12 +1900,10 @@ class IndexedDbBlockStore {
1845
1900
  if (segment.id !== segmentId || segment.transactionId !== expectedTransactionId) {
1846
1901
  throw new Error(`Segment ${segmentId} is not owned by transaction ${expectedTransactionId}`);
1847
1902
  }
1848
- const transactionStore = transaction.objectStore("transactions");
1849
- const ownerValue = await requestResult(transactionStore.get(expectedTransactionId));
1850
- if (ownerValue === void 0) {
1903
+ const owner = await readTransactionRecord(transaction, expectedTransactionId);
1904
+ if (owner === void 0) {
1851
1905
  throw corruption(`segments/${segmentId}`, `owning transaction ${expectedTransactionId} is missing`);
1852
1906
  }
1853
- const owner = asTransactionRecord(ownerValue, expectedTransactionId);
1854
1907
  if (owner.status !== "aborted") {
1855
1908
  throw new Error(`Segment ${segmentId} owner must be aborted; found ${owner.status}`);
1856
1909
  }
@@ -1861,10 +1914,7 @@ class IndexedDbBlockStore {
1861
1914
  if (published !== void 0) {
1862
1915
  throw new Error(`Segment ${segmentId} is published by manifest ${String(published.version)}`);
1863
1916
  }
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);
1917
+ await visitTransactionRecords(transaction, (candidate) => {
1868
1918
  if (candidate.status === "active" && candidate.pendingSegmentIds.includes(segmentId)) {
1869
1919
  throw new Error(`Segment ${segmentId} is referenced by active transaction ${candidate.id}`);
1870
1920
  }
@@ -1903,7 +1953,7 @@ class IndexedDbBlockStore {
1903
1953
  segmentStore.delete(segmentId);
1904
1954
  for (const { next } of rebasedSegments)
1905
1955
  segmentStore.put(next, next.id);
1906
- transactionStore.put(updatedOwner, owner.id);
1956
+ putTransactionRecord(transaction, updatedOwner, journalChunkCountOf(owner));
1907
1957
  await transactionDone(transaction);
1908
1958
  return true;
1909
1959
  } catch (error) {
@@ -1925,11 +1975,10 @@ class IndexedDbBlockStore {
1925
1975
  const transaction = this.#transaction(["blocks", "catalog", "gc", "manifests", "segments", "statistics", "transactions"], "readwrite");
1926
1976
  try {
1927
1977
  const segmentStore = transaction.objectStore("segments");
1928
- const transactionStore = transaction.objectStore("transactions");
1929
- const [storedValue, abortedValue, replacementValue] = await Promise.all([
1978
+ const [storedValue, aborted, replacement] = await Promise.all([
1930
1979
  requestResult(segmentStore.get(desired.id)),
1931
- requestResult(transactionStore.get(input.expectedAbortedTransactionId)),
1932
- requestResult(transactionStore.get(input.replacementTransactionId))
1980
+ readTransactionRecord(transaction, input.expectedAbortedTransactionId),
1981
+ readTransactionRecord(transaction, input.replacementTransactionId)
1933
1982
  ]);
1934
1983
  if (storedValue === void 0)
1935
1984
  throw new Error(`Segment does not exist: ${desired.id}`);
@@ -1943,11 +1992,9 @@ class IndexedDbBlockStore {
1943
1992
  if (stored.transactionId !== input.expectedAbortedTransactionId || !sameStructuredValue(storedContent, desiredContent)) {
1944
1993
  throw new Error(`Segment ${desired.id} does not match the immutable adoption record`);
1945
1994
  }
1946
- const aborted = abortedValue === void 0 ? void 0 : asTransactionRecord(abortedValue, input.expectedAbortedTransactionId);
1947
1995
  if (aborted?.revision !== input.expectedAbortedTransactionRevision) {
1948
1996
  throw new TransactionRecordConflictError(input.expectedAbortedTransactionId, input.expectedAbortedTransactionRevision, aborted?.revision ?? null);
1949
1997
  }
1950
- const replacement = replacementValue === void 0 ? void 0 : asTransactionRecord(replacementValue, input.replacementTransactionId);
1951
1998
  if (replacement?.revision !== input.expectedReplacementTransactionRevision) {
1952
1999
  throw new TransactionRecordConflictError(input.replacementTransactionId, input.expectedReplacementTransactionRevision, replacement?.revision ?? null);
1953
2000
  }
@@ -1985,10 +2032,7 @@ class IndexedDbBlockStore {
1985
2032
  if (await findReadableManifestBlock(transaction, segmentBlockIds(stored)) !== void 0) {
1986
2033
  throw new Error(`Segment ${desired.id} is still reachable from a readable manifest`);
1987
2034
  }
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);
2035
+ await visitTransactionRecords(transaction, (candidate) => {
1992
2036
  if (candidate.id !== aborted.id && candidate.id !== replacement.id && candidate.status === "active" && candidate.pendingSegmentIds.includes(desired.id)) {
1993
2037
  throw new Error(`Segment ${desired.id} is referenced by active transaction ${candidate.id}`);
1994
2038
  }
@@ -2033,7 +2077,6 @@ class IndexedDbBlockStore {
2033
2077
  revision: incrementSafeInteger(replacement.revision, "Transaction revision"),
2034
2078
  updatedAt: input.updatedAt
2035
2079
  };
2036
- assertTransactionArtifactJournalLimits(updatedReplacement.pendingBlockIds, updatedReplacement.pendingSegmentIds);
2037
2080
  const statistics = transaction.objectStore("statistics");
2038
2081
  await updateTransactionResourceLedger(statistics, aborted.id, {
2039
2082
  blockCount: 0,
@@ -2051,8 +2094,8 @@ class IndexedDbBlockStore {
2051
2094
  segmentStore.put(desired, desired.id);
2052
2095
  for (const { next } of rebasedAbortedSegments)
2053
2096
  segmentStore.put(next, next.id);
2054
- transactionStore.put(updatedAborted, updatedAborted.id);
2055
- transactionStore.put(updatedReplacement, updatedReplacement.id);
2097
+ putTransactionRecord(transaction, updatedAborted, journalChunkCountOf(aborted));
2098
+ putTransactionRecord(transaction, updatedReplacement, journalChunkCountOf(replacement));
2056
2099
  await transactionDone(transaction);
2057
2100
  return structuredClone(updatedReplacement);
2058
2101
  } catch (error) {
@@ -2194,17 +2237,13 @@ class IndexedDbBlockStore {
2194
2237
  }
2195
2238
  segments.sort((left, right) => left.id.localeCompare(right.id));
2196
2239
  const transactionIds = [...new Set(segments.map((segment) => segment.transactionId))];
2197
- const transactionStore = transaction.objectStore("transactions");
2198
2240
  const transactions = [];
2199
2241
  for (let start = 0; start < transactionIds.length; start += 64) {
2200
2242
  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
- });
2243
+ for (const record of await readTransactionRecords(transaction, window)) {
2244
+ if (record !== void 0)
2245
+ transactions.push(record);
2246
+ }
2208
2247
  }
2209
2248
  await transactionDone(transaction);
2210
2249
  return {
@@ -2316,7 +2355,9 @@ class IndexedDbBlockStore {
2316
2355
  currentVersion: current ?? null,
2317
2356
  replacementTransaction: normalized
2318
2357
  });
2319
- transaction.objectStore("transactions").add(normalized, normalized.id);
2358
+ addTransactionRecord(transaction, normalized);
2359
+ if (this.#journalCache?.transactionId === normalized.id)
2360
+ this.#journalCache = void 0;
2320
2361
  let rowIds;
2321
2362
  if (input.reserveRowIds !== void 0) {
2322
2363
  validateCount(input.reserveRowIds.count);
@@ -2395,7 +2436,7 @@ class IndexedDbBlockStore {
2395
2436
  retainedBytes
2396
2437
  });
2397
2438
  }
2398
- transaction.objectStore("transactions").add(normalized, normalized.id);
2439
+ addTransactionRecord(transaction, normalized);
2399
2440
  await transactionDone(transaction);
2400
2441
  } catch (error) {
2401
2442
  abortIfActive(transaction);
@@ -2406,54 +2447,58 @@ class IndexedDbBlockStore {
2406
2447
  async getTransaction(id) {
2407
2448
  validateId(id, "Transaction ID");
2408
2449
  const transaction = this.#transaction("transactions", "readonly");
2409
- const value = await requestResult(transaction.objectStore("transactions").get(id));
2450
+ const record = await readTransactionRecord(transaction, id);
2410
2451
  await transactionDone(transaction);
2411
- return value === void 0 ? void 0 : asTransactionRecord(value, id);
2452
+ return record;
2412
2453
  }
2413
2454
  async getTransactions(ids) {
2414
2455
  assertStorageBulkReadItems(ids, "Transaction read");
2415
2456
  for (const id of ids)
2416
2457
  validateId(id, "Transaction ID");
2417
2458
  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))));
2459
+ const records = await readTransactionRecords(transaction, ids);
2420
2460
  await transactionDone(transaction);
2421
- return values.map((value, index) => value === void 0 ? void 0 : asTransactionRecord(value, ids[index]));
2461
+ return records;
2422
2462
  }
2423
2463
  async listTransactionPage(afterId, limit) {
2424
2464
  validatePageLimit(limit);
2425
2465
  const transaction = this.#transaction("transactions", "readonly");
2426
- const records = await readCursorPage(transaction.objectStore("transactions"), limit, (value, key) => {
2466
+ const page = await readCursorPage(transaction.objectStore("transactions"), limit, (value, key) => {
2427
2467
  if (typeof key !== "string")
2428
2468
  throw corruption("transactions", "record key is invalid");
2429
- return asTransactionRecord(value, key);
2469
+ return asStoredTransactionRecord(value, key);
2430
2470
  }, (key) => typeof key === "string" && (afterId === null || key > afterId), afterId ?? void 0);
2471
+ const records = await Promise.all(page.map((stored) => assembleTransactionRecord(transaction, stored)));
2431
2472
  await transactionDone(transaction);
2432
2473
  return { records, nextCursor: records.length === limit ? records.at(-1)?.id ?? null : null };
2433
2474
  }
2434
2475
  async updateTransaction(id, expectedRevision, update) {
2435
2476
  validateId(id, "Transaction ID");
2436
2477
  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();
2478
+ const stored = await readStoredTransactionRecord(transaction, id);
2479
+ if (stored?.revision !== expectedRevision) {
2480
+ abortIfActive(transaction);
2442
2481
  await ignoreAbort(transaction);
2443
- throw new TransactionRecordConflictError(id, expectedRevision, current?.revision ?? null);
2482
+ throw new TransactionRecordConflictError(id, expectedRevision, stored?.revision ?? null);
2444
2483
  }
2445
2484
  try {
2485
+ const current = await assembleTransactionRecord(transaction, stored);
2446
2486
  assertGenericTransactionUpdateAllowed(current, update);
2447
2487
  const updated = updateTransactionRecord(current, update);
2448
2488
  if (update.snapshotVersion !== void 0) {
2449
2489
  await assertSnapshotAvailableInTransaction(transaction, updated.snapshotVersion);
2450
2490
  }
2451
2491
  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))) {
2492
+ const updatedBlocks = new Set(updated.pendingBlockIds);
2493
+ const updatedSegments = new Set(updated.pendingSegmentIds);
2494
+ if (current.pendingBlockIds.some((blockId) => !updatedBlocks.has(blockId)) || current.pendingSegmentIds.some((segmentId) => !updatedSegments.has(segmentId))) {
2455
2495
  throw new TypeError("Use rollbackTransactionArtifacts to remove journaled artifacts");
2456
2496
  }
2497
+ if (!extendsJournalOrder(current.pendingBlockIds, updated.pendingBlockIds) || !extendsJournalOrder(current.pendingSegmentIds, updated.pendingSegmentIds)) {
2498
+ throw new TypeError("Transaction journal updates must append after the journaled ids");
2499
+ }
2500
+ const addedBlocks = updated.pendingBlockIds.slice(current.pendingBlockIds.length);
2501
+ const addedSegments = updated.pendingSegmentIds.slice(current.pendingSegmentIds.length);
2457
2502
  if (addedBlocks.length > 0 || addedSegments.length > 0) {
2458
2503
  let retainedBytes = 0;
2459
2504
  for (const id2 of addedBlocks) {
@@ -2465,9 +2510,12 @@ class IndexedDbBlockStore {
2465
2510
  retainedBytes
2466
2511
  });
2467
2512
  }
2468
- store.put(updated, id);
2513
+ if (addedBlocks.length > 0 || addedSegments.length > 0) {
2514
+ appendTransactionJournalChunks(transaction, stored, packTransactionJournal(current.pendingBlockIds, current.pendingSegmentIds).at(-1), addedBlocks, addedSegments);
2515
+ }
2516
+ putStoredTransactionRecord(transaction, storedTransactionRecord(updated));
2469
2517
  await transactionDone(transaction);
2470
- return structuredClone(updated);
2518
+ return updated;
2471
2519
  } catch (error) {
2472
2520
  abortIfActive(transaction);
2473
2521
  await ignoreAbort(transaction);
@@ -2480,13 +2528,11 @@ class IndexedDbBlockStore {
2480
2528
  const cutoff = validateBoundedLeaseExpiration(input.expiresAtCutoff, input.expiresAt, "Transaction");
2481
2529
  const transaction = this.#transaction(["transactions", "catalog", "leases"], "readwrite");
2482
2530
  try {
2483
- const store = transaction.objectStore("transactions");
2484
- const value = await requestResult(store.get(input.transactionId));
2485
- if (value === void 0) {
2531
+ const record = await readStoredTransactionRecord(transaction, input.transactionId);
2532
+ if (record === void 0) {
2486
2533
  await transactionDone(transaction);
2487
2534
  return false;
2488
2535
  }
2489
- const record = asTransactionRecord(value, input.transactionId);
2490
2536
  if (record.status !== "active" || record.ownerId !== input.ownerId || Date.parse(record.expiresAt) <= cutoff) {
2491
2537
  await transactionDone(transaction);
2492
2538
  return false;
@@ -2499,7 +2545,7 @@ class IndexedDbBlockStore {
2499
2545
  replacementTransaction: renewed,
2500
2546
  excludeTransactionId: record.id
2501
2547
  });
2502
- store.put(renewed, input.transactionId);
2548
+ putStoredTransactionRecord(transaction, renewed);
2503
2549
  await transactionDone(transaction);
2504
2550
  return true;
2505
2551
  } catch (error) {
@@ -2521,23 +2567,23 @@ class IndexedDbBlockStore {
2521
2567
  const transaction = this.#transaction("transactions", "readwrite");
2522
2568
  try {
2523
2569
  const store = transaction.objectStore("transactions");
2524
- const value = await requestResult(store.get(input.transactionId));
2525
- if (value === void 0) {
2570
+ const stored = await readStoredTransactionRecord(transaction, input.transactionId);
2571
+ if (stored === void 0) {
2526
2572
  await transactionDone(transaction);
2527
2573
  return void 0;
2528
2574
  }
2529
- const current = asTransactionRecord(value, input.transactionId);
2530
- if (current.status !== "active" || current.ownerId !== input.expectedOwnerId || Date.parse(current.expiresAt) > cutoff) {
2575
+ if (stored.status !== "active" || stored.ownerId !== input.expectedOwnerId || Date.parse(stored.expiresAt) > cutoff) {
2531
2576
  await transactionDone(transaction);
2532
2577
  return void 0;
2533
2578
  }
2579
+ const current = await assembleTransactionRecord(transaction, stored);
2534
2580
  const updated = updateTransactionRecord(current, {
2535
2581
  status: "aborted",
2536
2582
  updatedAt: input.updatedAt,
2537
2583
  committedVersion: null
2538
2584
  });
2539
2585
  await assertTerminalTransactionAdmission(store);
2540
- store.put(updated, updated.id);
2586
+ putStoredTransactionRecord(transaction, storedTransactionRecord(updated));
2541
2587
  await transactionDone(transaction);
2542
2588
  return structuredClone(updated);
2543
2589
  } catch (error) {
@@ -2551,9 +2597,7 @@ class IndexedDbBlockStore {
2551
2597
  validateId(input.transactionId, "Transaction ID");
2552
2598
  const transaction = this.#transaction(["blocks", "catalog", "segments", "statistics", "transactions"], "readwrite");
2553
2599
  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);
2600
+ const current = await readStoredTransactionRecord(transaction, input.transactionId);
2557
2601
  if (current?.revision !== input.expectedRevision || current.status !== "active") {
2558
2602
  throw new TransactionRecordConflictError(input.transactionId, input.expectedRevision, current?.revision ?? null);
2559
2603
  }
@@ -2577,7 +2621,7 @@ class IndexedDbBlockStore {
2577
2621
  if (normalized.transactionId !== input.transactionId) {
2578
2622
  throw new Error(`Segment ${normalized.id} belongs to another transaction`);
2579
2623
  }
2580
- const expectedCommitOrdinal = current.pendingSegmentIds.length + normalizedSegments.length;
2624
+ const expectedCommitOrdinal = current.pendingSegmentCount + normalizedSegments.length;
2581
2625
  if (normalized.commitOrdinal !== expectedCommitOrdinal) {
2582
2626
  throw new TypeError(`Segment ${normalized.id} commit ordinal must be ${String(expectedCommitOrdinal)}`);
2583
2627
  }
@@ -2585,14 +2629,9 @@ class IndexedDbBlockStore {
2585
2629
  segmentIds.add(normalized.id);
2586
2630
  normalizedSegments.push(normalized);
2587
2631
  }
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
- };
2632
+ const addedBlockIds = input.blocks.map((block) => block.id);
2633
+ const addedSegmentIds = normalizedSegments.map((segment) => segment.id);
2634
+ const update = { updatedAt: input.updatedAt };
2596
2635
  if (current.pendingTable !== void 0) {
2597
2636
  let nextRowId = current.pendingTableNextRowId;
2598
2637
  if (nextRowId === void 0)
@@ -2606,16 +2645,17 @@ class IndexedDbBlockStore {
2606
2645
  update.pendingTableNextRowId = nextRowId;
2607
2646
  }
2608
2647
  assertGenericTransactionUpdateAllowed(current, update);
2609
- const updated = updateTransactionRecord(current, update);
2610
- assertTransactionArtifactJournalLimits(updated.pendingBlockIds, updated.pendingSegmentIds);
2611
- const updatedBlockIds = new Set(updated.pendingBlockIds);
2648
+ const tail = await readTransactionJournalTail(transaction, current);
2649
+ const journal = await this.#journaledIds(transaction, current, tail);
2650
+ const rejournaled = addedBlockIds.find((id) => journal.blockIdSet.has(id));
2651
+ if (rejournaled !== void 0)
2652
+ throw new Error(`Block already exists: ${rejournaled}`);
2612
2653
  for (const segment of normalizedSegments) {
2613
- const unjournaledBlock = segmentBlockIds(segment).find((id) => !updatedBlockIds.has(id));
2654
+ const unjournaledBlock = segmentBlockIds(segment).find((id) => !ids.has(id) && !journal.blockIdSet.has(id));
2614
2655
  if (unjournaledBlock !== void 0) {
2615
2656
  throw new Error(`Segment ${segment.id} references block absent from the transaction journal: ${unjournaledBlock}`);
2616
2657
  }
2617
2658
  }
2618
- await assertPendingArtifactsAvailableInTransaction(transaction, current);
2619
2659
  await updateTransactionResourceLedger(transaction.objectStore("statistics"), input.transactionId, {
2620
2660
  blockCount: input.blocks.length,
2621
2661
  segmentCount: normalizedSegments.length,
@@ -2635,15 +2675,43 @@ class IndexedDbBlockStore {
2635
2675
  for (const segment of normalizedSegments) {
2636
2676
  segmentStore.add(segment, segment.id);
2637
2677
  }
2638
- transactionStore.put(updated, input.transactionId);
2678
+ const updated = {
2679
+ ...appendTransactionJournalChunks(transaction, current, tail, addedBlockIds, addedSegmentIds),
2680
+ ...update.pendingTableNextRowId === void 0 ? {} : { pendingTableNextRowId: update.pendingTableNextRowId },
2681
+ updatedAt: input.updatedAt,
2682
+ revision: incrementSafeInteger(current.revision, "Transaction revision")
2683
+ };
2684
+ putStoredTransactionRecord(transaction, updated);
2639
2685
  await transactionDone(transaction);
2640
- return structuredClone(updated);
2686
+ for (const id of addedBlockIds) {
2687
+ journal.blockIds.push(id);
2688
+ journal.blockIdSet.add(id);
2689
+ }
2690
+ for (const id of addedSegmentIds)
2691
+ journal.segmentIds.push(id);
2692
+ journal.revision = updated.revision;
2693
+ this.#journalCache = journal;
2694
+ return transactionRecordWithJournal(updated, journal.blockIds, journal.segmentIds);
2641
2695
  } catch (error) {
2642
2696
  abortIfActive(transaction);
2643
2697
  await ignoreAbort(transaction);
2644
2698
  throw error;
2645
2699
  }
2646
2700
  }
2701
+ async #journaledIds(transaction, stored, tail) {
2702
+ const cached = this.#journalCache;
2703
+ if (cached?.transactionId === stored.id && cached.revision === stored.revision && cached.blockIds.length === stored.pendingBlockCount && cached.segmentIds.length === stored.pendingSegmentCount && journalTailMatches(cached, tail)) {
2704
+ return cached;
2705
+ }
2706
+ const record = await assembleTransactionRecord(transaction, stored);
2707
+ return {
2708
+ transactionId: record.id,
2709
+ revision: record.revision,
2710
+ blockIds: record.pendingBlockIds,
2711
+ segmentIds: record.pendingSegmentIds,
2712
+ blockIdSet: new Set(record.pendingBlockIds)
2713
+ };
2714
+ }
2647
2715
  async rollbackTransactionArtifacts(input) {
2648
2716
  validateId(input.transactionId, "Transaction ID");
2649
2717
  for (const id of [...input.pendingBlockIds, ...input.removeBlockIds]) {
@@ -2654,9 +2722,7 @@ class IndexedDbBlockStore {
2654
2722
  }
2655
2723
  const transaction = this.#transaction(["blocks", "catalog", "gc", "manifests", "segments", "statistics", "transactions"], "readwrite");
2656
2724
  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);
2725
+ const current = await readTransactionRecord(transaction, input.transactionId);
2660
2726
  if (current?.revision !== input.expectedRevision || current.status !== "active") {
2661
2727
  throw new TransactionRecordConflictError(input.transactionId, input.expectedRevision, current?.revision ?? null);
2662
2728
  }
@@ -2679,12 +2745,9 @@ class IndexedDbBlockStore {
2679
2745
  if (publishedBlock !== void 0) {
2680
2746
  throw corruption(`transactions/${input.transactionId}`, `rollback journal names block ${publishedBlock.blockId} reachable by manifest ${String(publishedBlock.version)}`);
2681
2747
  }
2682
- await visitObjectStoreSequentially(transactionStore, (recordValue, key) => {
2683
- if (typeof key !== "string")
2684
- throw corruption("transactions", "record key is invalid");
2748
+ await visitTransactionRecords(transaction, (record, key) => {
2685
2749
  if (key === input.transactionId)
2686
2750
  return;
2687
- const record = asTransactionRecord(recordValue, key);
2688
2751
  const shared = record.pendingBlockIds.find((id) => removedBlocks.has(id));
2689
2752
  const sharedSegment = record.pendingSegmentIds.find((id) => removedSegments.has(id));
2690
2753
  if (shared !== void 0 || sharedSegment !== void 0) {
@@ -2744,7 +2807,7 @@ class IndexedDbBlockStore {
2744
2807
  let pendingTableNextRowId;
2745
2808
  if (current.pendingTable !== void 0) {
2746
2809
  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);
2810
+ const retainedSegments = retained.map((value) => value === void 0 ? void 0 : asSegmentRecord(value)).filter((segment) => segment?.tableId === current.pendingTable?.id);
2748
2811
  pendingTableNextRowId = 1n;
2749
2812
  for (const segment of retainedSegments) {
2750
2813
  if (segment.kind !== "insert" || segment.rowIdStart !== pendingTableNextRowId) {
@@ -2774,8 +2837,15 @@ class IndexedDbBlockStore {
2774
2837
  }
2775
2838
  for (const id of input.removeSegmentIds)
2776
2839
  segmentStore.delete(id);
2777
- transactionStore.put(updated, input.transactionId);
2840
+ putTransactionRecord(transaction, updated, journalChunkCountOf(current));
2778
2841
  await transactionDone(transaction);
2842
+ this.#journalCache = {
2843
+ transactionId: updated.id,
2844
+ revision: updated.revision,
2845
+ blockIds: [...updated.pendingBlockIds],
2846
+ segmentIds: [...updated.pendingSegmentIds],
2847
+ blockIdSet: new Set(updated.pendingBlockIds)
2848
+ };
2779
2849
  return structuredClone(updated);
2780
2850
  } catch (error) {
2781
2851
  abortIfActive(transaction);
@@ -2805,7 +2875,7 @@ class IndexedDbBlockStore {
2805
2875
  const transaction = this.#transaction(storeNames2, "readwrite");
2806
2876
  try {
2807
2877
  const record = await readActiveTransactionRecord(transaction, input.transactionId, input.expectedTransactionRevision);
2808
- const outcome = await this.#commitInTransaction(transaction, record, input);
2878
+ const outcome = await this.#commitInTransaction(transaction, record, "stored", input);
2809
2879
  await transactionDone(transaction);
2810
2880
  outcome.settle();
2811
2881
  return outcome.manifest;
@@ -2863,7 +2933,6 @@ class IndexedDbBlockStore {
2863
2933
  storeNames2.push("gc");
2864
2934
  const transaction = this.#transaction(storeNames2, "readwrite");
2865
2935
  try {
2866
- const transactionStore = transaction.objectStore("transactions");
2867
2936
  let base;
2868
2937
  if ("record" in input.transaction) {
2869
2938
  const id = input.transaction.record.id;
@@ -2871,7 +2940,7 @@ class IndexedDbBlockStore {
2871
2940
  ...input.transaction.record,
2872
2941
  snapshotVersion: input.expectedManifestVersion
2873
2942
  });
2874
- if (await requestResult(transactionStore.getKey(id)) !== void 0) {
2943
+ if (await requestResult(transaction.objectStore("transactions").getKey(id)) !== void 0) {
2875
2944
  throw new Error(`Transaction already exists: ${id}`);
2876
2945
  }
2877
2946
  const current = asOptionalManifestVersion(await requestResult(transaction.objectStore("catalog").get(CURRENT_MANIFEST_KEY)), CURRENT_MANIFEST_KEY);
@@ -2917,7 +2986,13 @@ class IndexedDbBlockStore {
2917
2986
  stageUpdate.pendingTableNextRowId = nextRowId;
2918
2987
  }
2919
2988
  const staged = updateTransactionRecord(base, stageUpdate);
2920
- assertTransactionArtifactJournalLimits(staged.pendingBlockIds, staged.pendingSegmentIds);
2989
+ const stagedBlockIds = new Set(staged.pendingBlockIds);
2990
+ for (const segment of normalizedSegments) {
2991
+ const unjournaledBlock = segmentBlockIds(segment).find((id) => !stagedBlockIds.has(id));
2992
+ if (unjournaledBlock !== void 0) {
2993
+ throw new Error(`Segment ${segment.id} references block absent from the transaction journal: ${unjournaledBlock}`);
2994
+ }
2995
+ }
2921
2996
  await assertPendingArtifactsAvailableInTransaction(transaction, base);
2922
2997
  await updateRecordResourceLedger(transaction.objectStore("statistics"), {
2923
2998
  segments: normalizedSegments.map((segment) => ({ next: segment }))
@@ -2933,7 +3008,7 @@ class IndexedDbBlockStore {
2933
3008
  for (const segment of normalizedSegments) {
2934
3009
  segmentStore.add(segment, segment.id);
2935
3010
  }
2936
- const outcome = await this.#commitInTransaction(transaction, staged, input);
3011
+ const outcome = await this.#commitInTransaction(transaction, staged, { replacedChunkCount: journalChunkCountOf(base) }, input);
2937
3012
  await transactionDone(transaction);
2938
3013
  outcome.settle();
2939
3014
  return outcome.manifest;
@@ -2943,7 +3018,7 @@ class IndexedDbBlockStore {
2943
3018
  throw error;
2944
3019
  }
2945
3020
  }
2946
- async #commitInTransaction(transaction, record, input) {
3021
+ async #commitInTransaction(transaction, record, journal, input) {
2947
3022
  incrementSafeInteger(record.revision, "Transaction revision");
2948
3023
  const transactionStore = transaction.objectStore("transactions");
2949
3024
  const catalog = transaction.objectStore("catalog");
@@ -3378,7 +3453,11 @@ class IndexedDbBlockStore {
3378
3453
  if (ftsDeltaCounts.length > 0)
3379
3454
  manifest.ftsDeltaCounts = ftsDeltaCounts;
3380
3455
  await clearTransactionResourceLedger(transaction.objectStore("statistics"), committed.id);
3381
- transactionStore.put(committed, committed.id);
3456
+ if (journal === "stored") {
3457
+ putStoredTransactionRecord(transaction, storedTransactionRecord(committed));
3458
+ } else {
3459
+ putTransactionRecord(transaction, committed, journal.replacedChunkCount);
3460
+ }
3382
3461
  const settle = () => {
3383
3462
  if (pendingTable !== void 0) {
3384
3463
  this.#tableIdsByName.set(pendingTable.name, pendingTable.id);
@@ -3439,6 +3518,7 @@ class IndexedDbBlockStore {
3439
3518
  this.#uniqueKeyCache.version = manifest.version;
3440
3519
  }
3441
3520
  this.#manifestCache = void 0;
3521
+ this.#journalCache = void 0;
3442
3522
  };
3443
3523
  return { manifest, settle };
3444
3524
  }
@@ -3776,8 +3856,7 @@ class IndexedDbBlockStore {
3776
3856
  }
3777
3857
  await assertActiveCompactionMarker(jobStore, current);
3778
3858
  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);
3859
+ const linkedTransaction = current.transactionId === null ? void 0 : await readTransactionRecord(transaction, current.transactionId);
3781
3860
  let updated;
3782
3861
  let updatedLinkedTransaction;
3783
3862
  try {
@@ -3815,7 +3894,7 @@ class IndexedDbBlockStore {
3815
3894
  const terminalCompactionJobs = checkedQuotaIncrement(quota.terminalCompactionJobs, "Terminal compaction job count");
3816
3895
  assertMaintenanceLimit("terminal compaction job", terminalCompactionJobs, MAX_TERMINAL_COMPACTION_JOB_RECORDS);
3817
3896
  if (updatedLinkedTransaction !== void 0) {
3818
- transactionStore.put(updatedLinkedTransaction, updatedLinkedTransaction.id);
3897
+ putStoredTransactionRecord(transaction, storedTransactionRecord(updatedLinkedTransaction));
3819
3898
  }
3820
3899
  jobStore.put(compactionJobEnvelope(updated), key);
3821
3900
  jobStore.put({
@@ -3984,7 +4063,6 @@ class IndexedDbBlockStore {
3984
4063
  const manifestStore = transaction.objectStore("manifests");
3985
4064
  const segmentStore = transaction.objectStore("segments");
3986
4065
  const blockStore = transaction.objectStore("blocks");
3987
- const transactionStore = transaction.objectStore("transactions");
3988
4066
  const currentVersionValue = await requestResult(catalog.get(CURRENT_MANIFEST_KEY));
3989
4067
  const currentVersion = asOptionalManifestVersion(currentVersionValue, CURRENT_MANIFEST_KEY) ?? null;
3990
4068
  const leaseCutoff = Date.parse(current.leaseCutoff);
@@ -4082,8 +4160,7 @@ class IndexedDbBlockStore {
4082
4160
  if (id === void 0) {
4083
4161
  throw new Error("Garbage collection transaction cursor is invalid");
4084
4162
  }
4085
- const transactionValue = await requestResult(transactionStore.get(id));
4086
- const record = transactionValue === void 0 ? void 0 : asTransactionRecord(transactionValue, id);
4163
+ const record = await readTransactionRecord(transaction, id);
4087
4164
  const manifestValue = record?.committedVersion === null || record?.committedVersion === void 0 ? void 0 : await requestResult(manifestStore.get(record.committedVersion));
4088
4165
  const manifest = manifestValue === void 0 ? void 0 : asStoredManifestRecord(manifestValue, record?.committedVersion ?? void 0);
4089
4166
  if (record === void 0)
@@ -4092,7 +4169,9 @@ class IndexedDbBlockStore {
4092
4169
  retainedTransactionIds.push(id);
4093
4170
  } else {
4094
4171
  await clearTransactionResourceLedger(transaction.objectStore("statistics"), id);
4095
- transactionStore.delete(id);
4172
+ deleteTransactionRecord(transaction, storedTransactionRecord(record));
4173
+ if (this.#journalCache?.transactionId === id)
4174
+ this.#journalCache = void 0;
4096
4175
  reclaimedTransactionIds.push(id);
4097
4176
  }
4098
4177
  transactionIndex += 1;
@@ -4339,6 +4418,7 @@ class IndexedDbBlockStore {
4339
4418
  this.#tableIdsByName.clear();
4340
4419
  this.#uniqueKeyCache = void 0;
4341
4420
  this.#manifestCache = void 0;
4421
+ this.#journalCache = void 0;
4342
4422
  return {
4343
4423
  identity,
4344
4424
  removedBlockCount: marker.kindItemCounts[SNAPSHOT_FRAME_KINDS.indexOf("block")] ?? 0,
@@ -4899,11 +4979,10 @@ class IndexedDbBlockStore {
4899
4979
  observedSegmentBytes = safeByteSum(observedSegmentBytes, segmentRecordRetainedBytes(segment), "Integrity segment bytes");
4900
4980
  if (segment.id !== key)
4901
4981
  throw corruption(`segments/${key}`, "record id differs from key");
4902
- const ownerValue = await requestResult(transactionStore.get(segment.transactionId));
4903
- if (ownerValue === void 0) {
4982
+ const owner = await readTransactionRecord(transaction, segment.transactionId);
4983
+ if (owner === void 0) {
4904
4984
  throw corruption(`segments/${key}`, `owning transaction ${segment.transactionId} is missing`);
4905
4985
  }
4906
- const owner = asTransactionRecord(ownerValue, segment.transactionId);
4907
4986
  const segmentTable = tablesById.get(segment.tableId) ?? (owner.status === "active" && owner.pendingTable?.id === segment.tableId ? owner.pendingTable : void 0);
4908
4987
  if (segmentTable === void 0) {
4909
4988
  throw corruption(`segments/${key}`, `table ${segment.tableId} is missing`);
@@ -4932,7 +5011,7 @@ class IndexedDbBlockStore {
4932
5011
  try {
4933
5012
  if (typeof key !== "string")
4934
5013
  throw corruption("transactions", "record key is invalid");
4935
- const record = asTransactionRecord(value, key);
5014
+ const record = await assembleTransactionRecord(transaction, asStoredTransactionRecord(value, key));
4936
5015
  for (const [commitOrdinal, segmentId] of record.pendingSegmentIds.entries()) {
4937
5016
  const segmentValue = await requestResult(segmentStore.get(segmentId));
4938
5017
  if (segmentValue === void 0) {
@@ -4961,13 +5040,7 @@ class IndexedDbBlockStore {
4961
5040
  if (alreadyLive) {
4962
5041
  throw corruption(`transactions/${key}`, `pending block ${blockId} is already live in the current manifest`);
4963
5042
  }
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
- });
5043
+ const journaledByAnotherActiveTransaction = await visitTransactionRecords(transaction, (candidate) => candidate.id !== record.id && candidate.status === "active" && candidate.pendingBlockIds.includes(blockId));
4971
5044
  if (journaledByAnotherActiveTransaction) {
4972
5045
  throw corruption(`transactions/${key}`, `pending block ${blockId} is journaled by another active transaction`);
4973
5046
  }
@@ -5053,6 +5126,25 @@ class IndexedDbBlockStore {
5053
5126
  } catch (error) {
5054
5127
  issue("invalid-pending-catalog", "transactions", error);
5055
5128
  }
5129
+ await visitObjectStoreSequentially(transaction.objectStore(TRANSACTION_JOURNAL_STORE), async (value, key) => {
5130
+ checkedRecords += 1;
5131
+ try {
5132
+ if (!Array.isArray(key) || key.length !== 2 || typeof key[0] !== "string" || typeof key[1] !== "number" || !Number.isSafeInteger(key[1]) || key[1] < 0) {
5133
+ throw corruption(TRANSACTION_JOURNAL_STORE, "journal chunk key is invalid");
5134
+ }
5135
+ const [transactionId, chunkIndex] = key;
5136
+ asTransactionJournalChunk(value, `transactions/${transactionId}/journal/${String(chunkIndex)}`);
5137
+ const owner = await readStoredTransactionRecord(transaction, transactionId);
5138
+ if (owner === void 0) {
5139
+ throw corruption(`transactions/${transactionId}`, `journal chunk ${String(chunkIndex)} has no transaction record`);
5140
+ }
5141
+ if (chunkIndex >= owner.journalChunkCount) {
5142
+ throw corruption(`transactions/${transactionId}`, `journal chunk ${String(chunkIndex)} lies beyond the declared ${String(owner.journalChunkCount)}`);
5143
+ }
5144
+ } catch (error) {
5145
+ issue("orphan-transaction-journal", storageKeyLocation(key), error);
5146
+ }
5147
+ });
5056
5148
  const leaseIds = /* @__PURE__ */ new Set();
5057
5149
  await visitObjectStoreSequentially(transaction.objectStore("leases"), async (value, key) => {
5058
5150
  checkedRecords += 1;
@@ -5115,11 +5207,10 @@ class IndexedDbBlockStore {
5115
5207
  }
5116
5208
  }
5117
5209
  if (job.transactionId !== null) {
5118
- const transactionValue = await requestResult(transactionStore.get(job.transactionId));
5119
- if (transactionValue === void 0) {
5210
+ const owner = await readTransactionRecord(transaction, job.transactionId);
5211
+ if (owner === void 0) {
5120
5212
  throw corruption(`gc/${key}`, "linked transaction is missing");
5121
5213
  }
5122
- const owner = asTransactionRecord(transactionValue, job.transactionId);
5123
5214
  if (job.state === "ready" || owner.pendingSegmentIds.length > 0) {
5124
5215
  const table = tablesById.get(job.tableId);
5125
5216
  if (table === void 0)
@@ -5164,11 +5255,8 @@ class IndexedDbBlockStore {
5164
5255
  if (candidate !== void 0)
5165
5256
  asBytes(candidate, `blocks/${id}`);
5166
5257
  }
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
- }
5258
+ for (const id of job.candidateTransactionIds)
5259
+ await readTransactionRecord(transaction, id);
5172
5260
  } else if (key.startsWith(ACTIVE_COMPACTION_KEY_PREFIX)) {
5173
5261
  let tableId;
5174
5262
  try {
@@ -5359,11 +5447,10 @@ class IndexedDbBlockStore {
5359
5447
  const ledger = asOptionalTransactionResourceLedger(value, transactionId);
5360
5448
  if (ledger === void 0)
5361
5449
  throw corruption(`statistics/${key}`, "transaction ledger is missing");
5362
- const transactionValue = await requestResult(transactionStore.get(transactionId));
5363
- if (transactionValue === void 0) {
5450
+ const owner = await readTransactionRecord(transaction, transactionId);
5451
+ if (owner === void 0) {
5364
5452
  throw corruption(`statistics/${key}`, "transaction resource ledger has no transaction");
5365
5453
  }
5366
- const owner = asTransactionRecord(transactionValue, transactionId);
5367
5454
  if (owner.status === "committed" || ledger.blockCount !== owner.pendingBlockIds.length || ledger.segmentCount !== owner.pendingSegmentIds.length) {
5368
5455
  throw corruption(`statistics/${key}`, "transaction resource ledger disagrees with its journal");
5369
5456
  }
@@ -5739,6 +5826,7 @@ class IndexedDbBlockStore {
5739
5826
  transaction.objectStore("manifests").clear();
5740
5827
  transaction.objectStore("segments").clear();
5741
5828
  transaction.objectStore("transactions").clear();
5829
+ transaction.objectStore(TRANSACTION_JOURNAL_STORE).clear();
5742
5830
  transaction.objectStore(SNAPSHOT_HEADER_STORE).clear();
5743
5831
  const statistics = transaction.objectStore("statistics");
5744
5832
  statistics.clear();
@@ -5964,6 +6052,7 @@ class IndexedDbBlockStore {
5964
6052
  this.#tableIdsByName.clear();
5965
6053
  this.#uniqueKeyCache = void 0;
5966
6054
  this.#manifestCache = void 0;
6055
+ this.#journalCache = void 0;
5967
6056
  } catch (error) {
5968
6057
  abortIfActive(transaction);
5969
6058
  await ignoreAbort(transaction);
@@ -6002,6 +6091,7 @@ class IndexedDbBlockStore {
6002
6091
  this.#tableIdsByName.clear();
6003
6092
  this.#uniqueKeyCache = void 0;
6004
6093
  this.#manifestCache = void 0;
6094
+ this.#journalCache = void 0;
6005
6095
  return {
6006
6096
  identity: marker.identity,
6007
6097
  removedBlockCount: marker.kindItemCounts[SNAPSHOT_FRAME_KINDS.indexOf("block")] ?? 0,
@@ -6020,6 +6110,7 @@ class IndexedDbBlockStore {
6020
6110
  this.#tableIdsByName.clear();
6021
6111
  this.#uniqueKeyCache = void 0;
6022
6112
  this.#manifestCache = void 0;
6113
+ this.#journalCache = void 0;
6023
6114
  this.#db.close();
6024
6115
  }
6025
6116
  _residentStateForTests() {
@@ -6306,9 +6397,10 @@ class IndexedDbBlockStore {
6306
6397
  #transaction(stores, mode, options = {}) {
6307
6398
  if (this.#closed)
6308
6399
  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);
6400
+ const requested = typeof stores === "string" ? [stores] : stores;
6401
+ const storeList = requested.includes("transactions") && !requested.includes(TRANSACTION_JOURNAL_STORE) ? [...requested, TRANSACTION_JOURNAL_STORE] : requested;
6402
+ const transaction = mode === "readwrite" ? this.#db.transaction(storeList, mode, { durability: this.#durability }) : this.#db.transaction(storeList, mode);
6310
6403
  void transactionDone(transaction).catch(() => void 0);
6311
- const storeList = typeof stores === "string" ? [stores] : stores;
6312
6404
  if (mode === "readwrite" && !options.allowSnapshotImport && storeList.includes("catalog")) {
6313
6405
  const catalog = transaction.objectStore("catalog");
6314
6406
  const marker = catalog.get(SNAPSHOT_FRAME_IMPORT_KEY);
@@ -6333,9 +6425,8 @@ function assertIndexedDbSchemaMigrationRegistry() {
6333
6425
  }
6334
6426
  }
6335
6427
  function createCurrentIndexedDbSchema(database, upgrade) {
6336
- for (const storeName of storeNames)
6428
+ for (const storeName of indexedDbStoreNames)
6337
6429
  database.createObjectStore(storeName);
6338
- database.createObjectStore(SNAPSHOT_HEADER_STORE);
6339
6430
  for (const [storeName, indexes] of Object.entries(indexedDbIndexSchema)) {
6340
6431
  const store = upgrade.objectStore(storeName);
6341
6432
  for (const index of indexes) {
@@ -6348,10 +6439,10 @@ function createCurrentIndexedDbSchema(database, upgrade) {
6348
6439
  upgrade.objectStore("statistics").add(emptyCatalogResourceLedger(), CATALOG_RESOURCE_LEDGER_KEY);
6349
6440
  upgrade.objectStore("statistics").add(emptyRecordResourceLedger(), RECORD_RESOURCE_LEDGER_KEY);
6350
6441
  }
6351
- function applyIndexedDbSchemaMigrations(database, upgrade, oldVersion) {
6442
+ function applyIndexedDbSchemaMigrations(database, upgrade, oldVersion, fail) {
6352
6443
  for (const migration of indexedDbSchemaMigrations) {
6353
6444
  if (migration.targetVersion > oldVersion)
6354
- migration.migrate(database, upgrade);
6445
+ migration.migrate(database, upgrade, fail);
6355
6446
  }
6356
6447
  }
6357
6448
  const indexedDbIndexSchema = {
@@ -6791,7 +6882,7 @@ async function assertTableRemovalAllowed(transaction, tableId, expectedRevision)
6791
6882
  const segments = await readTableSegmentsForRemoval(transaction.objectStore("segments").index(SEGMENT_TABLE_INDEX), tableId);
6792
6883
  const transactionStore = transaction.objectStore("transactions");
6793
6884
  await visitObjectStoreSequentially(transactionStore, (candidateValue, key) => {
6794
- const candidate = asTransactionRecord(candidateValue, storageKeyLocation(key));
6885
+ const candidate = asStoredTransactionRecord(candidateValue, storageKeyLocation(key));
6795
6886
  const pendingTable = candidate.status === "active" ? candidate.pendingTable : void 0;
6796
6887
  const foreignKey = (pendingTable?.foreignKeys ?? []).find((constraint) => constraint.parentTable === record.name);
6797
6888
  if (foreignKey !== void 0) {
@@ -6803,11 +6894,10 @@ async function assertTableRemovalAllowed(transaction, tableId, expectedRevision)
6803
6894
  for (const segment of segments) {
6804
6895
  let owner = owners.get(segment.transactionId);
6805
6896
  if (owner === void 0) {
6806
- const ownerValue = await requestResult(transactionStore.get(segment.transactionId));
6807
- if (ownerValue === void 0) {
6897
+ owner = await readStoredTransactionRecord(transaction, segment.transactionId);
6898
+ if (owner === void 0) {
6808
6899
  throw corruption(`segments/${segment.id}`, `references missing transaction ${segment.transactionId}`);
6809
6900
  }
6810
- owner = asTransactionRecord(ownerValue, segment.transactionId);
6811
6901
  owners.set(segment.transactionId, owner);
6812
6902
  }
6813
6903
  if (owner.status === "active")
@@ -7099,6 +7189,9 @@ function requiredUniqueStringArray(value, location) {
7099
7189
  }
7100
7190
  return [...value];
7101
7191
  }
7192
+ function extendsJournalOrder(current, updated) {
7193
+ return current.length <= updated.length && current.every((id, index) => updated[index] === id);
7194
+ }
7102
7195
  function validateArtifactPartition(retained, removed, current) {
7103
7196
  if (retained.length + removed.length !== current.length)
7104
7197
  return false;
@@ -7813,7 +7906,7 @@ async function stageSnapshotMetadataFrame(transaction, frame) {
7813
7906
  await assertCatalogReservationAdmission(transaction, record.pendingTable);
7814
7907
  await assertTableForeignKeysInTransaction(catalog, record.pendingTable);
7815
7908
  }
7816
- transaction.objectStore("transactions").add(record, record.id);
7909
+ addTransactionRecord(transaction, record);
7817
7910
  return;
7818
7911
  }
7819
7912
  if (item.kind === "unique-generation") {
@@ -7900,7 +7993,6 @@ async function readSnapshotGenerationId(catalog, key, label) {
7900
7993
  async function validateAndPromoteStagedSnapshot(transaction, marker) {
7901
7994
  const catalog = transaction.objectStore("catalog");
7902
7995
  const segments = transaction.objectStore("segments");
7903
- const transactions = transaction.objectStore("transactions");
7904
7996
  const blocks = transaction.objectStore("blocks");
7905
7997
  await visitObjectStoreSequentially(catalog, async (value, key) => {
7906
7998
  if (typeof key !== "string" || !key.startsWith(TABLE_ID_PREFIX))
@@ -7942,10 +8034,7 @@ async function validateAndPromoteStagedSnapshot(transaction, marker) {
7942
8034
  asSnapshotPostingStagingRecord(await requestResult(catalog.get(snapshotPostingStagingKey(generationId))), generationId);
7943
8035
  }
7944
8036
  });
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);
8037
+ await visitTransactionRecords(transaction, (record, key) => {
7949
8038
  if (record.status !== "committed" || record.committedVersion === null || record.committedVersion > marker.version || record.pendingBlockIds.length !== 0 || record.pendingSegmentIds.length !== 0) {
7950
8039
  throw corruption(`transactions/${key}`, "snapshot transaction is not canonical committed history");
7951
8040
  }
@@ -7959,10 +8048,9 @@ async function validateAndPromoteStagedSnapshot(transaction, marker) {
7959
8048
  if (await requestResult(catalog.getKey(`${TABLE_ID_PREFIX}${segment.tableId}`)) === void 0) {
7960
8049
  throw corruption(`segments/${key}`, "snapshot segment table is missing");
7961
8050
  }
7962
- const ownerValue = await requestResult(transactions.get(segment.transactionId));
7963
- if (ownerValue === void 0)
8051
+ if (await readTransactionRecord(transaction, segment.transactionId) === void 0) {
7964
8052
  throw corruption(`segments/${key}`, "snapshot segment owner is missing");
7965
- asTransactionRecord(ownerValue, segment.transactionId);
8053
+ }
7966
8054
  for (const blockId of segmentBlockIds(segment)) {
7967
8055
  const provenance = asManifestBlockRecord(await requestResult(catalog.get(manifestBlockKey(blockId))), blockId);
7968
8056
  if (!manifestBlockVisibleAt(provenance, marker.version)) {
@@ -8242,10 +8330,9 @@ async function writeSnapshotMetadataFramesInTransaction(input) {
8242
8330
  });
8243
8331
  await visitObjectStoreSequentially(input.transaction.objectStore("segments"), async (value) => {
8244
8332
  const record = asSegmentRecord(value);
8245
- const ownerValue = await requestResult(input.transaction.objectStore("transactions").get(record.transactionId));
8246
- if (ownerValue === void 0)
8333
+ const owner = await readStoredTransactionRecord(input.transaction, record.transactionId);
8334
+ if (owner === void 0)
8247
8335
  return;
8248
- const owner = asTransactionRecord(ownerValue, record.transactionId);
8249
8336
  if (owner.status !== "committed" || owner.committedVersion === null || owner.committedVersion > input.version) {
8250
8337
  return;
8251
8338
  }
@@ -8261,14 +8348,14 @@ async function writeSnapshotMetadataFramesInTransaction(input) {
8261
8348
  await visitObjectStoreSequentially(input.transaction.objectStore("transactions"), (value, key) => {
8262
8349
  if (typeof key !== "string")
8263
8350
  throw corruption("transactions", "record key is invalid");
8264
- const record = asTransactionRecord(value, key);
8351
+ const record = asStoredTransactionRecord(value, key);
8265
8352
  if (record.status !== "committed" || record.committedVersion === null || record.committedVersion > input.version) {
8266
8353
  return;
8267
8354
  }
8268
8355
  input.observeRetainedItems(1);
8269
8356
  writeItem("transaction-page", {
8270
8357
  kind: "transaction",
8271
- record: { ...record, pendingBlockIds: [], pendingSegmentIds: [] }
8358
+ record: transactionRecordWithJournal(record, [], [])
8272
8359
  });
8273
8360
  });
8274
8361
  await visitTables(async (table) => {
@@ -8510,34 +8597,74 @@ async function findReadableManifestBlock(transaction, ids, excludedVersions = /*
8510
8597
  });
8511
8598
  return found;
8512
8599
  }
8513
- function asTransactionRecord(value, expectedId) {
8600
+ function journalTailMatches(cached, tail) {
8601
+ if (tail === void 0)
8602
+ return cached.blockIds.length === 0 && cached.segmentIds.length === 0;
8603
+ const endsWith = (ids, suffix) => suffix.every((id, index) => ids[ids.length - suffix.length + index] === id);
8604
+ return endsWith(cached.blockIds, tail.blockIds) && endsWith(cached.segmentIds, tail.segmentIds);
8605
+ }
8606
+ const TRANSACTION_RECORD_FIELDS = [
8607
+ "id",
8608
+ "ownerId",
8609
+ "expiresAt",
8610
+ "snapshotVersion",
8611
+ "status",
8612
+ "revision",
8613
+ "startedAt",
8614
+ "updatedAt",
8615
+ "committedVersion",
8616
+ "schemaEpochGuard",
8617
+ "pendingTable",
8618
+ "pendingTableNextRowId",
8619
+ "catalogEpochGuard"
8620
+ ];
8621
+ function transactionJournalChunkKey(transactionId, chunkIndex) {
8622
+ return [transactionId, chunkIndex];
8623
+ }
8624
+ function transactionJournalEntryCount(blockCount, segmentCount) {
8625
+ const total = blockCount + segmentCount;
8626
+ if (!Number.isSafeInteger(total))
8627
+ throw new RangeError("Transaction journal is too large");
8628
+ return total;
8629
+ }
8630
+ function transactionJournalChunkCount(blockCount, segmentCount) {
8631
+ return Math.ceil(transactionJournalEntryCount(blockCount, segmentCount) / TRANSACTION_JOURNAL_CHUNK_ENTRIES);
8632
+ }
8633
+ function journalChunkCountOf(record) {
8634
+ return transactionJournalChunkCount(record.pendingBlockIds.length, record.pendingSegmentIds.length);
8635
+ }
8636
+ function packTransactionJournal(blockIds, segmentIds) {
8637
+ const chunks = [];
8638
+ let blockOffset = 0;
8639
+ let segmentOffset = 0;
8640
+ while (blockOffset < blockIds.length || segmentOffset < segmentIds.length) {
8641
+ const blocks = blockIds.slice(blockOffset, blockOffset + TRANSACTION_JOURNAL_CHUNK_ENTRIES);
8642
+ blockOffset += blocks.length;
8643
+ const segments = segmentIds.slice(segmentOffset, segmentOffset + TRANSACTION_JOURNAL_CHUNK_ENTRIES - blocks.length);
8644
+ segmentOffset += segments.length;
8645
+ chunks.push({ blockIds: blocks, segmentIds: segments });
8646
+ }
8647
+ return chunks;
8648
+ }
8649
+ function asTransactionJournalChunk(value, location) {
8514
8650
  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");
8651
+ throw corruption(location, "journal chunk is not an object");
8652
+ assertKnownFields(value, ["blockIds", "segmentIds"], location);
8653
+ const blockIds = requiredUniqueStringArray(value.blockIds, `${location}/blockIds`);
8654
+ const segmentIds = requiredUniqueStringArray(value.segmentIds, `${location}/segmentIds`);
8655
+ const entries = blockIds.length + segmentIds.length;
8656
+ if (entries === 0 || entries > TRANSACTION_JOURNAL_CHUNK_ENTRIES) {
8657
+ throw corruption(location, `journal chunk holds ${String(entries)} ids`);
8658
+ }
8659
+ return { blockIds, segmentIds };
8660
+ }
8661
+ function asTransactionRecordBase(value, expectedId) {
8533
8662
  const id = nonEmptyStoredString(value.id, "transactions/id");
8534
8663
  if (expectedId !== void 0 && id !== expectedId) {
8535
8664
  throw corruption(`transactions/${expectedId}`, `record declares id ${id}`);
8536
8665
  }
8537
8666
  const snapshotVersion = asNullableStoredVersion(value.snapshotVersion, `transactions/${id}/snapshotVersion`);
8538
8667
  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
8668
  if (!["active", "committed", "aborted"].includes(value.status)) {
8542
8669
  throw corruption(`transactions/${id}/status`, "status is invalid");
8543
8670
  }
@@ -8568,8 +8695,6 @@ function asTransactionRecord(value, expectedId) {
8568
8695
  ownerId,
8569
8696
  expiresAt,
8570
8697
  snapshotVersion,
8571
- pendingBlockIds,
8572
- pendingSegmentIds,
8573
8698
  status,
8574
8699
  revision,
8575
8700
  startedAt,
@@ -8584,6 +8709,185 @@ function asTransactionRecord(value, expectedId) {
8584
8709
  }
8585
8710
  return { ...base, pendingTable, pendingTableNextRowId, catalogEpochGuard };
8586
8711
  }
8712
+ function asTransactionRecord(value, expectedId) {
8713
+ if (!isRecord(value))
8714
+ throw corruption("transactions", "record is not an object");
8715
+ assertKnownFields(value, [...TRANSACTION_RECORD_FIELDS, "pendingBlockIds", "pendingSegmentIds"], "transactions");
8716
+ const base = asTransactionRecordBase(value, expectedId);
8717
+ const pendingBlockIds = requiredUniqueStringArray(value.pendingBlockIds, `transactions/${base.id}/pendingBlockIds`);
8718
+ const pendingSegmentIds = requiredUniqueStringArray(value.pendingSegmentIds, `transactions/${base.id}/pendingSegmentIds`);
8719
+ transactionJournalEntryCount(pendingBlockIds.length, pendingSegmentIds.length);
8720
+ return { ...base, pendingBlockIds, pendingSegmentIds };
8721
+ }
8722
+ function asStoredTransactionRecord(value, expectedId) {
8723
+ if (!isRecord(value))
8724
+ throw corruption("transactions", "record is not an object");
8725
+ assertKnownFields(value, [...TRANSACTION_RECORD_FIELDS, "journalChunkCount", "pendingBlockCount", "pendingSegmentCount"], "transactions");
8726
+ const base = asTransactionRecordBase(value, expectedId);
8727
+ const pendingBlockCount = nonNegativeStoredInteger(value.pendingBlockCount, `transactions/${base.id}/pendingBlockCount`);
8728
+ const pendingSegmentCount = nonNegativeStoredInteger(value.pendingSegmentCount, `transactions/${base.id}/pendingSegmentCount`);
8729
+ const journalChunkCount = nonNegativeStoredInteger(value.journalChunkCount, `transactions/${base.id}/journalChunkCount`);
8730
+ let expectedChunkCount;
8731
+ try {
8732
+ expectedChunkCount = transactionJournalChunkCount(pendingBlockCount, pendingSegmentCount);
8733
+ } catch {
8734
+ throw corruption(`transactions/${base.id}`, "journal counts are unsafe");
8735
+ }
8736
+ if (journalChunkCount !== expectedChunkCount) {
8737
+ throw corruption(`transactions/${base.id}/journalChunkCount`, `chunk count ${String(journalChunkCount)} disagrees with ${String(pendingBlockCount)} blocks and ${String(pendingSegmentCount)} segments`);
8738
+ }
8739
+ return { ...base, journalChunkCount, pendingBlockCount, pendingSegmentCount };
8740
+ }
8741
+ function storedTransactionRecord(record) {
8742
+ const { pendingBlockIds, pendingSegmentIds, ...header } = record;
8743
+ return {
8744
+ ...header,
8745
+ journalChunkCount: journalChunkCountOf(record),
8746
+ pendingBlockCount: pendingBlockIds.length,
8747
+ pendingSegmentCount: pendingSegmentIds.length
8748
+ };
8749
+ }
8750
+ function transactionRecordWithJournal(stored, pendingBlockIds, pendingSegmentIds) {
8751
+ const { journalChunkCount: _chunkCount, pendingBlockCount: _blockCount, pendingSegmentCount: _segmentCount, ...header } = stored;
8752
+ void _chunkCount;
8753
+ void _blockCount;
8754
+ void _segmentCount;
8755
+ return {
8756
+ ...header,
8757
+ pendingBlockIds: [...pendingBlockIds],
8758
+ pendingSegmentIds: [...pendingSegmentIds]
8759
+ };
8760
+ }
8761
+ function transactionRecordFromJournal(stored, chunks) {
8762
+ const location = `transactions/${stored.id}`;
8763
+ if (chunks.length !== stored.journalChunkCount) {
8764
+ throw corruption(location, "journal chunk count disagrees with its record");
8765
+ }
8766
+ const pendingBlockIds = [];
8767
+ const pendingSegmentIds = [];
8768
+ for (const [index, chunk] of chunks.entries()) {
8769
+ const entries = chunk.blockIds.length + chunk.segmentIds.length;
8770
+ if (index < chunks.length - 1 && entries !== TRANSACTION_JOURNAL_CHUNK_ENTRIES) {
8771
+ throw corruption(`${location}/journal/${String(index)}`, "journal chunk is not packed");
8772
+ }
8773
+ for (const id of chunk.blockIds)
8774
+ pendingBlockIds.push(id);
8775
+ for (const id of chunk.segmentIds)
8776
+ pendingSegmentIds.push(id);
8777
+ }
8778
+ if (pendingBlockIds.length !== stored.pendingBlockCount || pendingSegmentIds.length !== stored.pendingSegmentCount) {
8779
+ throw corruption(location, "journal counts disagree with its chunks");
8780
+ }
8781
+ if (new Set(pendingBlockIds).size !== pendingBlockIds.length || new Set(pendingSegmentIds).size !== pendingSegmentIds.length) {
8782
+ throw corruption(location, "journal repeats an id");
8783
+ }
8784
+ return transactionRecordWithJournal(stored, pendingBlockIds, pendingSegmentIds);
8785
+ }
8786
+ async function readTransactionJournalChunk(journalStore, transactionId, chunkIndex) {
8787
+ const value = await requestResult(journalStore.get(transactionJournalChunkKey(transactionId, chunkIndex)));
8788
+ return value === void 0 ? void 0 : asTransactionJournalChunk(value, `transactions/${transactionId}/journal/${String(chunkIndex)}`);
8789
+ }
8790
+ async function readTransactionJournalChunks(transaction, stored) {
8791
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8792
+ const values = await Promise.all(Array.from({ length: stored.journalChunkCount + 1 }, (_, index) => readTransactionJournalChunk(journalStore, stored.id, index)));
8793
+ const extra = values.pop();
8794
+ if (extra !== void 0) {
8795
+ throw corruption(`transactions/${stored.id}`, "journal has more chunks than its record declares");
8796
+ }
8797
+ return values.map((chunk, index) => {
8798
+ if (chunk === void 0) {
8799
+ throw corruption(`transactions/${stored.id}/journal/${String(index)}`, "chunk is missing");
8800
+ }
8801
+ return chunk;
8802
+ });
8803
+ }
8804
+ async function assembleTransactionRecord(transaction, stored) {
8805
+ return transactionRecordFromJournal(stored, await readTransactionJournalChunks(transaction, stored));
8806
+ }
8807
+ async function readStoredTransactionRecord(transaction, id) {
8808
+ const value = await requestResult(transaction.objectStore("transactions").get(id));
8809
+ return value === void 0 ? void 0 : asStoredTransactionRecord(value, id);
8810
+ }
8811
+ async function readTransactionRecord(transaction, id) {
8812
+ const stored = await readStoredTransactionRecord(transaction, id);
8813
+ return stored === void 0 ? void 0 : assembleTransactionRecord(transaction, stored);
8814
+ }
8815
+ async function readTransactionRecords(transaction, ids) {
8816
+ const stored = await Promise.all(ids.map((id) => readStoredTransactionRecord(transaction, id)));
8817
+ return Promise.all(stored.map((record) => record === void 0 ? Promise.resolve(void 0) : assembleTransactionRecord(transaction, record)));
8818
+ }
8819
+ function visitTransactionRecords(transaction, visit) {
8820
+ return visitObjectStoreSequentially(transaction.objectStore("transactions"), async (value, key) => {
8821
+ if (typeof key !== "string")
8822
+ throw corruption("transactions", "record key is invalid");
8823
+ const record = await assembleTransactionRecord(transaction, asStoredTransactionRecord(value, key));
8824
+ return visit(record, key);
8825
+ });
8826
+ }
8827
+ function visitActiveTransactionRecords(transaction, visit) {
8828
+ return visitIndexPartitionSequentially(transaction.objectStore("transactions").index(TRANSACTION_STATUS_INDEX), "active", async (value, key) => {
8829
+ if (typeof key !== "string")
8830
+ throw corruption("transactions", "record key is invalid");
8831
+ const record = await assembleTransactionRecord(transaction, asStoredTransactionRecord(value, key));
8832
+ return visit(record, key);
8833
+ });
8834
+ }
8835
+ function putStoredTransactionRecord(transaction, stored) {
8836
+ transaction.objectStore("transactions").put(stored, stored.id);
8837
+ }
8838
+ function putTransactionRecord(transaction, record, replacedChunkCount) {
8839
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8840
+ const chunks = packTransactionJournal(record.pendingBlockIds, record.pendingSegmentIds);
8841
+ for (let index = chunks.length; index < replacedChunkCount; index += 1) {
8842
+ journalStore.delete(transactionJournalChunkKey(record.id, index));
8843
+ }
8844
+ chunks.forEach((chunk, index) => {
8845
+ journalStore.put(chunk, transactionJournalChunkKey(record.id, index));
8846
+ });
8847
+ putStoredTransactionRecord(transaction, storedTransactionRecord(record));
8848
+ }
8849
+ function addTransactionRecord(transaction, record) {
8850
+ transaction.objectStore("transactions").add(storedTransactionRecord(record), record.id);
8851
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8852
+ packTransactionJournal(record.pendingBlockIds, record.pendingSegmentIds).forEach((chunk, index) => {
8853
+ journalStore.put(chunk, transactionJournalChunkKey(record.id, index));
8854
+ });
8855
+ }
8856
+ function deleteTransactionRecord(transaction, stored) {
8857
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8858
+ for (let index = 0; index < stored.journalChunkCount; index += 1) {
8859
+ journalStore.delete(transactionJournalChunkKey(stored.id, index));
8860
+ }
8861
+ transaction.objectStore("transactions").delete(stored.id);
8862
+ }
8863
+ async function readTransactionJournalTail(transaction, stored) {
8864
+ const lastIndex = stored.journalChunkCount - 1;
8865
+ if (lastIndex < 0)
8866
+ return void 0;
8867
+ const tail = await readTransactionJournalChunk(transaction.objectStore(TRANSACTION_JOURNAL_STORE), stored.id, lastIndex);
8868
+ if (tail === void 0) {
8869
+ throw corruption(`transactions/${stored.id}/journal/${String(lastIndex)}`, "chunk is missing");
8870
+ }
8871
+ return tail;
8872
+ }
8873
+ function appendTransactionJournalChunks(transaction, stored, tail, addedBlockIds, addedSegmentIds) {
8874
+ const journalStore = transaction.objectStore(TRANSACTION_JOURNAL_STORE);
8875
+ const lastIndex = stored.journalChunkCount - 1;
8876
+ const pendingBlockCount = stored.pendingBlockCount + addedBlockIds.length;
8877
+ const pendingSegmentCount = stored.pendingSegmentCount + addedSegmentIds.length;
8878
+ const journalChunkCount = transactionJournalChunkCount(pendingBlockCount, pendingSegmentCount);
8879
+ const tailEntries = tail === void 0 ? 0 : tail.blockIds.length + tail.segmentIds.length;
8880
+ const topUp = tail !== void 0 && tailEntries < TRANSACTION_JOURNAL_CHUNK_ENTRIES;
8881
+ const firstIndex = topUp ? lastIndex : lastIndex + 1;
8882
+ const chunks = packTransactionJournal([...topUp ? tail.blockIds : [], ...addedBlockIds], [...topUp ? tail.segmentIds : [], ...addedSegmentIds]);
8883
+ if (firstIndex + chunks.length !== journalChunkCount) {
8884
+ throw corruption(`transactions/${stored.id}`, "journal append does not reach the chunk count");
8885
+ }
8886
+ chunks.forEach((chunk, offset) => {
8887
+ journalStore.put(chunk, transactionJournalChunkKey(stored.id, firstIndex + offset));
8888
+ });
8889
+ return { ...stored, journalChunkCount, pendingBlockCount, pendingSegmentCount };
8890
+ }
8587
8891
  function asTableRecord(value, location = "catalog/table") {
8588
8892
  if (!isRecord(value))
8589
8893
  throw corruption(location, "table record is not an object");
@@ -8951,8 +9255,7 @@ function asLeaseRecord(value, expectedId) {
8951
9255
  };
8952
9256
  }
8953
9257
  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);
9258
+ const record = await readTransactionRecord(transaction, id);
8956
9259
  if (record?.revision !== expectedRevision || record.status !== "active") {
8957
9260
  throw new TransactionRecordConflictError(id, expectedRevision, record?.revision ?? null);
8958
9261
  }
@@ -9012,11 +9315,9 @@ async function assertCompactionJobReferences(transaction, job) {
9012
9315
  }
9013
9316
  }
9014
9317
  if (job.transactionId !== null) {
9015
- const value = await requestResult(transaction.objectStore("transactions").get(job.transactionId));
9016
- if (value === void 0) {
9318
+ if (await readTransactionRecord(transaction, job.transactionId) === void 0) {
9017
9319
  throw new Error(`Compaction job ${job.id} references missing transaction: ${job.transactionId}`);
9018
9320
  }
9019
- asTransactionRecord(value, job.transactionId);
9020
9321
  }
9021
9322
  if (job.state === "ready" || job.state === "published") {
9022
9323
  for (const outputId of compactionOutputSegmentIds(job)) {
@@ -9228,7 +9529,7 @@ function countVisibleLevelZeroSegments(index, transactions, tableId, currentBloc
9228
9529
  if (ownerRequest.result === void 0) {
9229
9530
  throw corruption(`segments/${segment.id}`, `owning transaction ${segment.transactionId} is missing`);
9230
9531
  }
9231
- const owner = asTransactionRecord(ownerRequest.result, segment.transactionId);
9532
+ const owner = asStoredTransactionRecord(ownerRequest.result, segment.transactionId);
9232
9533
  statuses.set(owner.id, owner.status);
9233
9534
  if (owner.status === "committed")
9234
9535
  count += 1;
@@ -9267,10 +9568,7 @@ async function compactionJobRemovalPreservesProvenance(transaction, removedJob)
9267
9568
  for (const blockId of segmentBlockIds(segment))
9268
9569
  rootedBlocks.add(blockId);
9269
9570
  });
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);
9571
+ await visitTransactionRecords(transaction, (record) => {
9274
9572
  for (const blockId of record.pendingBlockIds)
9275
9573
  rootedBlocks.add(blockId);
9276
9574
  for (const segmentId of record.pendingSegmentIds)
@@ -9320,7 +9618,7 @@ async function garbageCollectionPinsInTransaction(transaction, currentVersion, l
9320
9618
  };
9321
9619
  pin(currentVersion);
9322
9620
  await visitObjectStoreReadBatches(transaction.objectStore("transactions"), (value) => {
9323
- const record = asTransactionRecord(value);
9621
+ const record = asStoredTransactionRecord(value);
9324
9622
  if (record.status === "active") {
9325
9623
  pin(record.snapshotVersion);
9326
9624
  }
@@ -9341,10 +9639,9 @@ async function garbageCollectionPinsInTransaction(transaction, currentVersion, l
9341
9639
  pin(job.sourceManifestVersion);
9342
9640
  if (job.transactionId === null)
9343
9641
  return;
9344
- const transactionValue = await requestResult(transaction.objectStore("transactions").get(job.transactionId));
9345
- if (transactionValue === void 0)
9642
+ const linkedTransaction = await readStoredTransactionRecord(transaction, job.transactionId);
9643
+ if (linkedTransaction === void 0)
9346
9644
  return;
9347
- const linkedTransaction = asTransactionRecord(transactionValue);
9348
9645
  if (linkedTransaction.status !== "committed")
9349
9646
  return;
9350
9647
  if (linkedTransaction.committedVersion === null) {
@@ -9786,8 +10083,7 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9786
10083
  }
9787
10084
  }
9788
10085
  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);
10086
+ const record = await readStoredTransactionRecord(transaction, id);
9791
10087
  if (record === void 0 || record.status !== "aborted" && (record.status !== "committed" || record.committedVersion === null)) {
9792
10088
  throw new Error(`Garbage collection transaction candidate is not terminal: ${id}`);
9793
10089
  }
@@ -9804,10 +10100,7 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9804
10100
  const blockHasProvenance = async (id) => {
9805
10101
  if (manifestProvenBlockIds.has(id))
9806
10102
  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
- });
10103
+ const transactionProven = await visitTransactionRecords(transaction, (record) => record.status === "aborted" && record.pendingBlockIds.includes(id));
9811
10104
  if (transactionProven)
9812
10105
  return true;
9813
10106
  return visitObjectStoreSequentially(transaction.objectStore("gc"), (value, key) => {
@@ -9843,8 +10136,7 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9843
10136
  ownerBlockRootsComplete = false;
9844
10137
  }
9845
10138
  };
9846
- await visitIndexPartitionSequentially(transaction.objectStore("transactions").index(TRANSACTION_STATUS_INDEX), "active", (value) => {
9847
- const record = asTransactionRecord(value);
10139
+ await visitActiveTransactionRecords(transaction, (record) => {
9848
10140
  if (record.status !== "active")
9849
10141
  return;
9850
10142
  rememberOwnerBlocks(record.pendingBlockIds);
@@ -9875,10 +10167,7 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9875
10167
  return cached;
9876
10168
  let rooted = ownerBlockRoots.has(id);
9877
10169
  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
- });
10170
+ rooted = await visitActiveTransactionRecords(transaction, (record) => record.status === "active" && record.pendingBlockIds.includes(id));
9882
10171
  if (!rooted) {
9883
10172
  rooted = await visitObjectStoreSequentially(transaction.objectStore("gc"), (value, key) => {
9884
10173
  const job = asCompactionJobAtMaintenanceKey(value, key);
@@ -9899,15 +10188,13 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9899
10188
  const rootedBlockIds = /* @__PURE__ */ new Set();
9900
10189
  const rootedSegmentIds = /* @__PURE__ */ new Set();
9901
10190
  const segmentStore = transaction.objectStore("segments");
9902
- const transactionStore = transaction.objectStore("transactions");
9903
10191
  for (const id of candidateSegmentIds) {
9904
10192
  const value = await requestResult(segmentStore.get(id));
9905
10193
  if (value === void 0)
9906
10194
  continue;
9907
10195
  const segment = asSegmentRecord(value);
9908
10196
  const ids = segmentBlockIds(segment);
9909
- const ownerValue = await requestResult(transactionStore.get(segment.transactionId));
9910
- const owner = ownerValue === void 0 ? void 0 : asTransactionRecord(ownerValue);
10197
+ const owner = await readStoredTransactionRecord(transaction, segment.transactionId);
9911
10198
  let allBlocksRooted = ids.length > 0;
9912
10199
  for (const blockId of ids) {
9913
10200
  if (await isDirectBlockRoot(blockId))
@@ -10728,7 +11015,7 @@ async function pendingCatalogReservations(store, excludeTransactionId) {
10728
11015
  await visitObjectStoreSequentially(store, (value, key) => {
10729
11016
  if (typeof key !== "string")
10730
11017
  throw corruption("transactions", "record key is invalid");
10731
- const transaction = asTransactionRecord(value, key);
11018
+ const transaction = asStoredTransactionRecord(value, key);
10732
11019
  if (transaction.id === excludeTransactionId || transaction.status !== "active" || transaction.pendingTable === void 0) {
10733
11020
  return;
10734
11021
  }
@@ -10951,7 +11238,7 @@ async function assertPinnedHistoryAdmission(transaction, input) {
10951
11238
  throw corruption("transactions", "record key is invalid");
10952
11239
  if (key === input.excludeTransactionId)
10953
11240
  return;
10954
- const record = asTransactionRecord(value, key);
11241
+ const record = asStoredTransactionRecord(value, key);
10955
11242
  if (record.status !== "active") {
10956
11243
  throw corruption(`transactions/${key}`, "status index does not match its record");
10957
11244
  }