@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.
- package/dist/engine/auto-store.d.ts +40 -0
- package/dist/engine/auto-store.js +115 -0
- package/dist/engine/buffered-writer.d.ts +2 -0
- package/dist/engine/buffered-writer.js +15 -2
- package/dist/engine/client.d.ts +55 -6
- package/dist/engine/client.js +155 -40
- package/dist/engine/database.d.ts +15 -1
- package/dist/engine/database.js +1085 -227
- package/dist/engine/errors.d.ts +61 -2
- package/dist/engine/errors.js +116 -3
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/live.d.ts +24 -1
- package/dist/engine/live.js +33 -9
- package/dist/engine/scope-write-set.js +36 -0
- package/dist/engine/worker-auto.d.ts +1 -0
- package/dist/engine/worker-auto.js +3 -0
- package/dist/engine/worker-host.d.ts +2 -1
- package/dist/engine/worker-host.js +17 -1
- package/dist/engine/worker-server.d.ts +53 -1
- package/dist/engine/worker-server.js +118 -14
- package/dist/engine/worker-store-auto.js +36 -0
- package/dist/engine/worker-store-opfs.js +3 -2
- package/dist/engine/write-coordinator.js +24 -2
- package/dist/storage/indexeddb.js +494 -207
- package/dist/storage/opfs/leader.js +201 -14
- package/dist/storage/opfs/rpc.js +23 -43
- package/dist/storage/opfs/store.d.ts +20 -0
- package/dist/storage/opfs/store.js +479 -60
- package/dist/storage/toolkit/record-core.js +67 -38
- package/dist/storage/toolkit/wire.d.ts +1 -1
- package/dist/storage/toolkit/wire.js +1 -1
- package/dist/storage/types.d.ts +29 -8
- package/dist/storage/types.js +27 -16
- package/dist/testing/opfs-shim.js +14 -6
- package/dist/transactions/index.d.ts +12 -0
- package/dist/transactions/index.js +83 -23
- package/dist/worker-protocol/index.d.ts +50 -2
- package/dist/worker-protocol/index.js +106 -4
- 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,
|
|
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 =
|
|
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
|
|
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
|
|
1849
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1929
|
-
const [storedValue, abortedValue, replacementValue] = await Promise.all([
|
|
1978
|
+
const [storedValue, aborted, replacement] = await Promise.all([
|
|
1930
1979
|
requestResult(segmentStore.get(desired.id)),
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
|
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
|
-
|
|
2055
|
-
|
|
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
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
|
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
|
|
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
|
|
2450
|
+
const record = await readTransactionRecord(transaction, id);
|
|
2410
2451
|
await transactionDone(transaction);
|
|
2411
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2438
|
-
|
|
2439
|
-
|
|
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,
|
|
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
|
|
2453
|
-
const
|
|
2454
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
2484
|
-
|
|
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
|
-
|
|
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
|
|
2525
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
2589
|
-
|
|
2590
|
-
|
|
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
|
|
2610
|
-
|
|
2611
|
-
const
|
|
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) => !
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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((
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
4903
|
-
if (
|
|
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 =
|
|
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
|
|
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
|
|
5119
|
-
if (
|
|
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
|
-
|
|
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
|
|
5363
|
-
if (
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
6807
|
-
if (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8246
|
-
if (
|
|
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 =
|
|
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:
|
|
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
|
|
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(
|
|
8516
|
-
assertKnownFields(value, [
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
9345
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
}
|