@minnowdb/core 0.10.1 → 0.10.2
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/client.js +2 -1
- package/dist/engine/database.js +140 -822
- package/dist/engine/index-terms.js +627 -0
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +2 -1
- package/dist/engine/live-maintenance.js +220 -0
- package/dist/engine/schema.js +2 -1
- package/dist/engine/sql-functions.js +3 -2
- package/dist/engine/sql-quote.js +6 -0
- package/dist/engine/vector.js +1 -3
- package/dist/storage/indexeddb.d.ts +18 -0
- package/dist/storage/indexeddb.js +186 -42
- package/dist/storage/opfs/rpc.js +2 -1
- package/dist/storage/types.d.ts +33 -2
- package/dist/storage/types.js +17 -4
- package/dist/testing/block-store-conformance.js +40 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/interaction-simulator.d.ts +319 -0
- package/dist/testing/interaction-simulator.js +1631 -0
- package/package.json +4 -1
- package/dist/engine/client-audit-harness.js +0 -123
- package/dist/storage/indexeddb-audit-helpers.js +0 -269
- package/dist/storage/opfs/power-loss-model.js +0 -62
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
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, StorageUnresponsiveError, 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";
|
|
@@ -129,11 +129,18 @@ class IndexedDbBlockStore {
|
|
|
129
129
|
#snapshotPeakRetainedItems = 0;
|
|
130
130
|
#snapshotPeakRetainedBytes = 0;
|
|
131
131
|
#closed = false;
|
|
132
|
-
|
|
132
|
+
#unresponsive;
|
|
133
|
+
#stallGuard;
|
|
134
|
+
constructor(db, durability, uniqueKeyCacheBytes, unresponsiveAfterMs) {
|
|
133
135
|
this.#db = db;
|
|
134
136
|
this.liveQueryChannelName = `minnowdb-live:indexeddb:${db.name}`;
|
|
135
137
|
this.#durability = durability;
|
|
136
138
|
this.#uniqueKeyCacheBytes = uniqueKeyCacheBytes;
|
|
139
|
+
this.#stallGuard = new ConnectionStallGuard(db.name, unresponsiveAfterMs, (error) => {
|
|
140
|
+
this.#unresponsive = error;
|
|
141
|
+
this.close();
|
|
142
|
+
});
|
|
143
|
+
stallGuards.set(db, this.#stallGuard);
|
|
137
144
|
db.addEventListener("versionchange", () => {
|
|
138
145
|
this.close();
|
|
139
146
|
});
|
|
@@ -144,6 +151,10 @@ class IndexedDbBlockStore {
|
|
|
144
151
|
if (!Number.isSafeInteger(uniqueKeyCacheBytes) || uniqueKeyCacheBytes < 0) {
|
|
145
152
|
throw new RangeError("Unique-key cache bytes must be a non-negative whole number");
|
|
146
153
|
}
|
|
154
|
+
const unresponsiveAfterMs = options.unresponsiveAfterMs ?? INDEXEDDB_UNRESPONSIVE_AFTER_MS;
|
|
155
|
+
if (!Number.isSafeInteger(unresponsiveAfterMs) || unresponsiveAfterMs <= 0) {
|
|
156
|
+
throw new RangeError("The unresponsive deadline must be a positive whole number of ms");
|
|
157
|
+
}
|
|
147
158
|
const factory = options.indexedDB ?? getGlobalIndexedDb();
|
|
148
159
|
if (factory === void 0)
|
|
149
160
|
throw new Error("IndexedDB is unavailable");
|
|
@@ -177,7 +188,7 @@ class IndexedDbBlockStore {
|
|
|
177
188
|
db = await openDatabaseRequest(request, (event) => {
|
|
178
189
|
abandoned = true;
|
|
179
190
|
return new IndexedDbSchemaUpgradeBlockedError(options.name, event.oldVersion, event.newVersion ?? SCHEMA_VERSION);
|
|
180
|
-
});
|
|
191
|
+
}, options.name, unresponsiveAfterMs);
|
|
181
192
|
} catch (error) {
|
|
182
193
|
if (upgradeError !== void 0)
|
|
183
194
|
throw upgradeError;
|
|
@@ -187,7 +198,7 @@ class IndexedDbBlockStore {
|
|
|
187
198
|
}
|
|
188
199
|
throw error;
|
|
189
200
|
}
|
|
190
|
-
const store = new IndexedDbBlockStore(db, options.durability ?? "strict", uniqueKeyCacheBytes);
|
|
201
|
+
const store = new IndexedDbBlockStore(db, options.durability ?? "strict", uniqueKeyCacheBytes, unresponsiveAfterMs);
|
|
191
202
|
try {
|
|
192
203
|
await validateCurrentIndexedDbSchema(db);
|
|
193
204
|
await store.#validateCatalogResourceLedger();
|
|
@@ -1164,8 +1175,8 @@ class IndexedDbBlockStore {
|
|
|
1164
1175
|
this.#journalCache = void 0;
|
|
1165
1176
|
if (this.#uniqueKeyCache?.tableId === input.tableId || this.#uniqueKeyCache?.tableId.startsWith(`${input.tableId}\0secondary-index\0`) === true) {
|
|
1166
1177
|
this.#uniqueKeyCache = void 0;
|
|
1167
|
-
} else
|
|
1168
|
-
this.#
|
|
1178
|
+
} else {
|
|
1179
|
+
this.#carryUniqueKeyCache(input.expectedManifestVersion, manifest.version);
|
|
1169
1180
|
}
|
|
1170
1181
|
return manifest;
|
|
1171
1182
|
} catch (error) {
|
|
@@ -1302,8 +1313,7 @@ class IndexedDbBlockStore {
|
|
|
1302
1313
|
await transactionDone(transaction);
|
|
1303
1314
|
this.#manifestCache = void 0;
|
|
1304
1315
|
this.#journalCache = void 0;
|
|
1305
|
-
|
|
1306
|
-
this.#uniqueKeyCache.version = manifest.version;
|
|
1316
|
+
this.#carryUniqueKeyCache(input.expectedManifestVersion, manifest.version);
|
|
1307
1317
|
return manifest;
|
|
1308
1318
|
} catch (error) {
|
|
1309
1319
|
abortIfActive(transaction);
|
|
@@ -2124,6 +2134,16 @@ class IndexedDbBlockStore {
|
|
|
2124
2134
|
throw await refusalBehindAbort(transaction, error);
|
|
2125
2135
|
}
|
|
2126
2136
|
}
|
|
2137
|
+
#carryUniqueKeyCache(basedOn, version) {
|
|
2138
|
+
const cache = this.#uniqueKeyCache;
|
|
2139
|
+
if (cache === void 0 || cache.version === version)
|
|
2140
|
+
return;
|
|
2141
|
+
if (basedOn !== null && basedOn !== void 0 && cache.version === basedOn) {
|
|
2142
|
+
cache.version = version;
|
|
2143
|
+
} else {
|
|
2144
|
+
this.#uniqueKeyCache = void 0;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2127
2147
|
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
2128
2148
|
validateId(tableId, "Table ID");
|
|
2129
2149
|
assertStorageBulkReadItems(keyTokens, "Unique-key read");
|
|
@@ -3470,13 +3490,11 @@ class IndexedDbBlockStore {
|
|
|
3470
3490
|
} : void 0;
|
|
3471
3491
|
} else if (this.#uniqueKeyCache?.tableId === uniqueKeyChanges.tableId) {
|
|
3472
3492
|
this.#uniqueKeyCache = void 0;
|
|
3473
|
-
} else
|
|
3474
|
-
this.#
|
|
3493
|
+
} else {
|
|
3494
|
+
this.#carryUniqueKeyCache(input.expectedManifestVersion, manifest.version);
|
|
3475
3495
|
}
|
|
3476
|
-
} else
|
|
3477
|
-
this.#
|
|
3478
|
-
} else if (this.#uniqueKeyCache !== void 0) {
|
|
3479
|
-
this.#uniqueKeyCache.version = manifest.version;
|
|
3496
|
+
} else {
|
|
3497
|
+
this.#carryUniqueKeyCache(input.expectedManifestVersion, manifest.version);
|
|
3480
3498
|
}
|
|
3481
3499
|
this.#manifestCache = void 0;
|
|
3482
3500
|
this.#journalCache = void 0;
|
|
@@ -3910,13 +3928,13 @@ class IndexedDbBlockStore {
|
|
|
3910
3928
|
const quota = await readMaintenanceQuota(gcStore);
|
|
3911
3929
|
let activeGarbageCollectionJobs;
|
|
3912
3930
|
if (record.state !== "completed") {
|
|
3913
|
-
activeGarbageCollectionJobs = checkedQuotaIncrement(quota.activeGarbageCollectionJobs, "Active garbage collection job count");
|
|
3914
|
-
assertMaintenanceLimit("garbage collection job", activeGarbageCollectionJobs, MAX_ACTIVE_GARBAGE_COLLECTION_JOBS);
|
|
3915
3931
|
const markerValue = await requestResult(gcStore.get(ACTIVE_GARBAGE_COLLECTION_KEY));
|
|
3916
3932
|
if (markerValue !== void 0) {
|
|
3917
3933
|
const marker = asActiveGarbageCollectionMarker(markerValue);
|
|
3918
|
-
throw new
|
|
3934
|
+
throw new GarbageCollectionJobConflictError(marker.jobId, record.revision, null);
|
|
3919
3935
|
}
|
|
3936
|
+
activeGarbageCollectionJobs = checkedQuotaIncrement(quota.activeGarbageCollectionJobs, "Active garbage collection job count");
|
|
3937
|
+
assertMaintenanceLimit("garbage collection job", activeGarbageCollectionJobs, MAX_ACTIVE_GARBAGE_COLLECTION_JOBS);
|
|
3920
3938
|
}
|
|
3921
3939
|
await assertGarbageCollectionCandidateProvenanceInTransaction(transaction, record);
|
|
3922
3940
|
if (record.state !== "completed") {
|
|
@@ -3959,6 +3977,9 @@ class IndexedDbBlockStore {
|
|
|
3959
3977
|
candidateTransactionIds: input.candidateTransactionIds ?? []
|
|
3960
3978
|
});
|
|
3961
3979
|
gcStore.put(garbageCollectionJobEnvelope(updated), key);
|
|
3980
|
+
if (updated.state === "completed" && current.state !== "completed") {
|
|
3981
|
+
await releaseGarbageCollectionAdmission(gcStore);
|
|
3982
|
+
}
|
|
3962
3983
|
await transactionDone(transaction);
|
|
3963
3984
|
return structuredClone(updated);
|
|
3964
3985
|
} catch (error) {
|
|
@@ -4183,17 +4204,8 @@ class IndexedDbBlockStore {
|
|
|
4183
4204
|
updatedAt: input.updatedAt
|
|
4184
4205
|
});
|
|
4185
4206
|
gcStore.put(garbageCollectionJobEnvelope(updated), key);
|
|
4186
|
-
if (updated.state === "completed")
|
|
4187
|
-
|
|
4188
|
-
const completedGarbageCollectionJobs = checkedQuotaIncrement(quota.completedGarbageCollectionJobs, "Completed garbage collection job count");
|
|
4189
|
-
assertMaintenanceLimit("completed garbage collection job", completedGarbageCollectionJobs, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS);
|
|
4190
|
-
gcStore.put({
|
|
4191
|
-
...quota,
|
|
4192
|
-
activeGarbageCollectionJobs: checkedQuotaDecrement(quota.activeGarbageCollectionJobs, "active garbage collection job count"),
|
|
4193
|
-
completedGarbageCollectionJobs
|
|
4194
|
-
}, MAINTENANCE_QUOTA_KEY);
|
|
4195
|
-
await requestResult(gcStore.delete(ACTIVE_GARBAGE_COLLECTION_KEY));
|
|
4196
|
-
}
|
|
4207
|
+
if (updated.state === "completed")
|
|
4208
|
+
await releaseGarbageCollectionAdmission(gcStore);
|
|
4197
4209
|
await transactionDone(transaction);
|
|
4198
4210
|
return {
|
|
4199
4211
|
job: structuredClone(updated),
|
|
@@ -6055,6 +6067,7 @@ class IndexedDbBlockStore {
|
|
|
6055
6067
|
this.#uniqueKeyCache = void 0;
|
|
6056
6068
|
this.#manifestCache = void 0;
|
|
6057
6069
|
this.#journalCache = void 0;
|
|
6070
|
+
this.#stallGuard.dispose();
|
|
6058
6071
|
this.#db.close();
|
|
6059
6072
|
}
|
|
6060
6073
|
_residentStateForTests() {
|
|
@@ -6333,6 +6346,8 @@ class IndexedDbBlockStore {
|
|
|
6333
6346
|
}
|
|
6334
6347
|
}
|
|
6335
6348
|
#transaction(stores, mode, options = {}) {
|
|
6349
|
+
if (this.#unresponsive !== void 0)
|
|
6350
|
+
throw this.#unresponsive;
|
|
6336
6351
|
if (this.#closed)
|
|
6337
6352
|
throw new Error("This IndexedDB store connection is closed");
|
|
6338
6353
|
const requested = typeof stores === "string" ? [stores] : stores;
|
|
@@ -6454,49 +6469,156 @@ async function readIndexedDbVersion(factory, name) {
|
|
|
6454
6469
|
return null;
|
|
6455
6470
|
}
|
|
6456
6471
|
}
|
|
6457
|
-
|
|
6472
|
+
const INDEXEDDB_UNRESPONSIVE_AFTER_MS = 3e4;
|
|
6473
|
+
class ConnectionStallGuard {
|
|
6474
|
+
#databaseName;
|
|
6475
|
+
#afterMs;
|
|
6476
|
+
#onTrip;
|
|
6477
|
+
#waiting = /* @__PURE__ */ new Set();
|
|
6478
|
+
#timer;
|
|
6479
|
+
#tripped;
|
|
6480
|
+
#disposed = false;
|
|
6481
|
+
constructor(databaseName, afterMs, onTrip) {
|
|
6482
|
+
this.#databaseName = databaseName;
|
|
6483
|
+
this.#afterMs = afterMs;
|
|
6484
|
+
this.#onTrip = onTrip;
|
|
6485
|
+
}
|
|
6486
|
+
wait(reject) {
|
|
6487
|
+
if (this.#tripped !== void 0) {
|
|
6488
|
+
const tripped = this.#tripped;
|
|
6489
|
+
queueMicrotask(() => reject(tripped));
|
|
6490
|
+
return () => void 0;
|
|
6491
|
+
}
|
|
6492
|
+
if (this.#disposed)
|
|
6493
|
+
return () => void 0;
|
|
6494
|
+
this.#waiting.add(reject);
|
|
6495
|
+
this.#arm();
|
|
6496
|
+
let released = false;
|
|
6497
|
+
return () => {
|
|
6498
|
+
if (released)
|
|
6499
|
+
return;
|
|
6500
|
+
released = true;
|
|
6501
|
+
this.#waiting.delete(reject);
|
|
6502
|
+
this.#progress();
|
|
6503
|
+
};
|
|
6504
|
+
}
|
|
6505
|
+
dispose() {
|
|
6506
|
+
this.#disposed = true;
|
|
6507
|
+
this.#waiting.clear();
|
|
6508
|
+
this.#disarm();
|
|
6509
|
+
}
|
|
6510
|
+
#arm() {
|
|
6511
|
+
if (this.#timer !== void 0 || this.#waiting.size === 0)
|
|
6512
|
+
return;
|
|
6513
|
+
this.#timer = setTimeout(() => {
|
|
6514
|
+
this.#timer = void 0;
|
|
6515
|
+
this.#trip();
|
|
6516
|
+
}, this.#afterMs);
|
|
6517
|
+
this.#timer.unref?.();
|
|
6518
|
+
}
|
|
6519
|
+
#disarm() {
|
|
6520
|
+
if (this.#timer === void 0)
|
|
6521
|
+
return;
|
|
6522
|
+
clearTimeout(this.#timer);
|
|
6523
|
+
this.#timer = void 0;
|
|
6524
|
+
}
|
|
6525
|
+
#progress() {
|
|
6526
|
+
if (this.#tripped !== void 0)
|
|
6527
|
+
return;
|
|
6528
|
+
this.#disarm();
|
|
6529
|
+
this.#arm();
|
|
6530
|
+
}
|
|
6531
|
+
#trip() {
|
|
6532
|
+
if (this.#tripped !== void 0)
|
|
6533
|
+
return;
|
|
6534
|
+
const error = new StorageUnresponsiveError("indexeddb", this.#databaseName, this.#afterMs);
|
|
6535
|
+
this.#tripped = error;
|
|
6536
|
+
const waiting = [...this.#waiting];
|
|
6537
|
+
this.#waiting.clear();
|
|
6538
|
+
this.#onTrip(error);
|
|
6539
|
+
for (const reject of waiting)
|
|
6540
|
+
reject(error);
|
|
6541
|
+
}
|
|
6542
|
+
}
|
|
6543
|
+
const stallGuards = /* @__PURE__ */ new WeakMap();
|
|
6544
|
+
function stallGuardFor(db) {
|
|
6545
|
+
return db === null || db === void 0 ? void 0 : stallGuards.get(db);
|
|
6546
|
+
}
|
|
6547
|
+
function openDatabaseRequest(request, blockedError, databaseName, unresponsiveAfterMs) {
|
|
6458
6548
|
return new Promise((resolve, reject) => {
|
|
6459
6549
|
let settled = false;
|
|
6550
|
+
const timer = setTimeout(() => {
|
|
6551
|
+
if (settled)
|
|
6552
|
+
return;
|
|
6553
|
+
settled = true;
|
|
6554
|
+
reject(new StorageUnresponsiveError("indexeddb", databaseName, unresponsiveAfterMs));
|
|
6555
|
+
}, unresponsiveAfterMs);
|
|
6556
|
+
timer.unref?.();
|
|
6557
|
+
const settle = () => {
|
|
6558
|
+
if (settled)
|
|
6559
|
+
return false;
|
|
6560
|
+
settled = true;
|
|
6561
|
+
clearTimeout(timer);
|
|
6562
|
+
return true;
|
|
6563
|
+
};
|
|
6460
6564
|
request.addEventListener("success", () => {
|
|
6461
|
-
if (
|
|
6565
|
+
if (!settle()) {
|
|
6462
6566
|
request.result.close();
|
|
6463
6567
|
return;
|
|
6464
6568
|
}
|
|
6465
|
-
settled = true;
|
|
6466
6569
|
resolve(request.result);
|
|
6467
6570
|
}, { once: true });
|
|
6468
6571
|
request.addEventListener("error", () => {
|
|
6469
|
-
if (
|
|
6572
|
+
if (!settle())
|
|
6470
6573
|
return;
|
|
6471
|
-
settled = true;
|
|
6472
6574
|
reject(request.error ?? new Error("IndexedDB open failed"));
|
|
6473
6575
|
}, { once: true });
|
|
6474
6576
|
request.addEventListener("blocked", (event) => {
|
|
6475
|
-
if (
|
|
6577
|
+
if (!settle())
|
|
6476
6578
|
return;
|
|
6477
|
-
settled = true;
|
|
6478
6579
|
reject(blockedError(event));
|
|
6479
6580
|
}, { once: true });
|
|
6480
6581
|
});
|
|
6481
6582
|
}
|
|
6482
6583
|
function requestResult(request) {
|
|
6584
|
+
const guard = stallGuardFor(request.transaction?.db);
|
|
6483
6585
|
return new Promise((resolve, reject) => {
|
|
6484
|
-
|
|
6485
|
-
request.addEventListener("
|
|
6586
|
+
const release = guard?.wait(reject);
|
|
6587
|
+
request.addEventListener("success", () => {
|
|
6588
|
+
release?.();
|
|
6589
|
+
resolve(request.result);
|
|
6590
|
+
}, { once: true });
|
|
6591
|
+
request.addEventListener("error", () => {
|
|
6592
|
+
release?.();
|
|
6593
|
+
reject(request.error ?? new Error("IndexedDB request failed"));
|
|
6594
|
+
}, {
|
|
6486
6595
|
once: true
|
|
6487
6596
|
});
|
|
6488
6597
|
});
|
|
6489
6598
|
}
|
|
6490
6599
|
function readCursorPage(store, limit, decode, acceptKey, seekKey) {
|
|
6600
|
+
const guard = stallGuardFor(store.transaction.db);
|
|
6491
6601
|
return new Promise((resolve, reject) => {
|
|
6492
6602
|
const records = [];
|
|
6493
6603
|
const request = store.openCursor();
|
|
6494
6604
|
let seekPending = seekKey !== void 0;
|
|
6495
|
-
|
|
6605
|
+
let release = guard?.wait(reject);
|
|
6606
|
+
const settle = (value) => {
|
|
6607
|
+
release?.();
|
|
6608
|
+
release = void 0;
|
|
6609
|
+
resolve(value);
|
|
6610
|
+
};
|
|
6611
|
+
request.onerror = () => {
|
|
6612
|
+
release?.();
|
|
6613
|
+
release = void 0;
|
|
6614
|
+
reject(request.error ?? new Error("IndexedDB cursor failed"));
|
|
6615
|
+
};
|
|
6496
6616
|
request.onsuccess = () => {
|
|
6617
|
+
release?.();
|
|
6618
|
+
release = guard?.wait(reject);
|
|
6497
6619
|
const cursor = request.result;
|
|
6498
6620
|
if (cursor === null || records.length === limit) {
|
|
6499
|
-
|
|
6621
|
+
settle(records);
|
|
6500
6622
|
return;
|
|
6501
6623
|
}
|
|
6502
6624
|
try {
|
|
@@ -6510,10 +6632,12 @@ function readCursorPage(store, limit, decode, acceptKey, seekKey) {
|
|
|
6510
6632
|
if (acceptKey(cursor.key))
|
|
6511
6633
|
records.push(decode(cursor.value, cursor.key));
|
|
6512
6634
|
if (records.length === limit)
|
|
6513
|
-
|
|
6635
|
+
settle(records);
|
|
6514
6636
|
else
|
|
6515
6637
|
cursor.continue();
|
|
6516
6638
|
} catch (error) {
|
|
6639
|
+
release?.();
|
|
6640
|
+
release = void 0;
|
|
6517
6641
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
6518
6642
|
}
|
|
6519
6643
|
};
|
|
@@ -6973,9 +7097,17 @@ function transactionDone(transaction) {
|
|
|
6973
7097
|
const existing = transactionCompletions.get(transaction);
|
|
6974
7098
|
if (existing !== void 0)
|
|
6975
7099
|
return existing;
|
|
7100
|
+
const guard = stallGuardFor(transaction.db);
|
|
6976
7101
|
const done = new Promise((resolve, reject) => {
|
|
6977
|
-
|
|
6978
|
-
transaction.addEventListener("
|
|
7102
|
+
const release = guard?.wait(reject);
|
|
7103
|
+
transaction.addEventListener("complete", () => {
|
|
7104
|
+
release?.();
|
|
7105
|
+
resolve();
|
|
7106
|
+
}, { once: true });
|
|
7107
|
+
transaction.addEventListener("abort", () => {
|
|
7108
|
+
release?.();
|
|
7109
|
+
reject(transaction.error ?? new Error("Transaction aborted"));
|
|
7110
|
+
}, {
|
|
6979
7111
|
once: true
|
|
6980
7112
|
});
|
|
6981
7113
|
});
|
|
@@ -9737,6 +9869,17 @@ async function assertActiveCompactionMarker(store, record) {
|
|
|
9737
9869
|
throw corruption(`gc/${activeCompactionKey(record.tableId)}`, `marker names job ${marker.jobId} instead of ${record.id}`);
|
|
9738
9870
|
}
|
|
9739
9871
|
}
|
|
9872
|
+
async function releaseGarbageCollectionAdmission(gcStore) {
|
|
9873
|
+
const quota = await readMaintenanceQuota(gcStore);
|
|
9874
|
+
const completedGarbageCollectionJobs = checkedQuotaIncrement(quota.completedGarbageCollectionJobs, "Completed garbage collection job count");
|
|
9875
|
+
assertMaintenanceLimit("completed garbage collection job", completedGarbageCollectionJobs, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS);
|
|
9876
|
+
gcStore.put({
|
|
9877
|
+
...quota,
|
|
9878
|
+
activeGarbageCollectionJobs: checkedQuotaDecrement(quota.activeGarbageCollectionJobs, "active garbage collection job count"),
|
|
9879
|
+
completedGarbageCollectionJobs
|
|
9880
|
+
}, MAINTENANCE_QUOTA_KEY);
|
|
9881
|
+
await requestResult(gcStore.delete(ACTIVE_GARBAGE_COLLECTION_KEY));
|
|
9882
|
+
}
|
|
9740
9883
|
async function assertActiveGarbageCollectionMarker(store, record) {
|
|
9741
9884
|
const marker = asActiveGarbageCollectionMarker(await requestResult(store.get(ACTIVE_GARBAGE_COLLECTION_KEY)));
|
|
9742
9885
|
if (marker.jobId !== record.id) {
|
|
@@ -12897,5 +13040,6 @@ function utf8Bytes(value) {
|
|
|
12897
13040
|
return storageTextEncoder.encode(value).byteLength;
|
|
12898
13041
|
}
|
|
12899
13042
|
export {
|
|
13043
|
+
INDEXEDDB_UNRESPONSIVE_AFTER_MS,
|
|
12900
13044
|
IndexedDbBlockStore
|
|
12901
13045
|
};
|
package/dist/storage/opfs/rpc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, ConnectionLostError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError, PostingBuildConflictError, SchemaConflictError, SnapshotImportConflictError, SnapshotManifestMissingError, StorageCorruptionError, StorageFormatVersionError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueIndexCoverageError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UnknownOutcomeError, WriteConflictError } from "../types.js";
|
|
1
|
+
import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, ConnectionLostError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError, PostingBuildConflictError, SchemaConflictError, SnapshotImportConflictError, SnapshotManifestMissingError, StorageCorruptionError, StorageUnresponsiveError, StorageFormatVersionError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueIndexCoverageError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UnknownOutcomeError, WriteConflictError } from "../types.js";
|
|
2
2
|
import { dateIsoString } from "../../date-value.js";
|
|
3
3
|
import { MAX_SERIALIZED_CAUSE_DEPTH, rehydrateError, serializeError } from "../../worker-protocol/index.js";
|
|
4
4
|
const MAX_OPFS_RPC_STACK_CHARACTERS = 64 * 1024;
|
|
@@ -203,6 +203,7 @@ const errorRegistry = new Map([
|
|
|
203
203
|
PostingBuildConflictError,
|
|
204
204
|
StorageCorruptionError,
|
|
205
205
|
StorageFormatVersionError,
|
|
206
|
+
StorageUnresponsiveError,
|
|
206
207
|
OpfsCoordinationError,
|
|
207
208
|
OpfsDatabaseInUseError,
|
|
208
209
|
OpfsUncertainOutcomeError,
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -254,8 +254,16 @@ export interface SecondaryIndexRecord {
|
|
|
254
254
|
unique?: true;
|
|
255
255
|
/** The membership set has been seeded and must be enforced, independent of postings state. */
|
|
256
256
|
uniqueEnforced?: true;
|
|
257
|
-
/**
|
|
258
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Prefix-free composite term encoding.
|
|
259
|
+
*
|
|
260
|
+
* `tuple-v1` encodes non-null components only: a row with a NULL in any indexed column has no
|
|
261
|
+
* posting at all, so a prefix lookup through the index cannot see it. `tuple-v2` keeps every
|
|
262
|
+
* non-null component byte-identical and adds a one-character marker for a NULL component, so a
|
|
263
|
+
* row with a NULL trailing component is named under its non-null prefix. Both stay readable;
|
|
264
|
+
* only `tuple-v2` may serve a prefix lookup whose unconstrained trailing columns are nullable.
|
|
265
|
+
*/
|
|
266
|
+
termEncoding: "tuple-v1" | "tuple-v2";
|
|
259
267
|
storage: "postings-v1";
|
|
260
268
|
storageColumnId: string;
|
|
261
269
|
locator: "row-id" | "key-hash-v1";
|
|
@@ -1361,6 +1369,29 @@ export declare class UnknownOutcomeError extends Error {
|
|
|
1361
1369
|
export declare class ConnectionLostError extends Error {
|
|
1362
1370
|
readonly name: string;
|
|
1363
1371
|
}
|
|
1372
|
+
/**
|
|
1373
|
+
* The store stopped answering: the whole connection went the adapter's unresponsive deadline
|
|
1374
|
+
* without a single storage event while it had work outstanding. The remedy is to reload the
|
|
1375
|
+
* page, not to retry — the connection cannot recover, and a fresh one in the same page may be
|
|
1376
|
+
* wedged by the same cause.
|
|
1377
|
+
*
|
|
1378
|
+
* The known cause is a worker terminated with a write in flight. WebKit keeps the dead worker's
|
|
1379
|
+
* IndexedDB connection, and its unfinished transaction, registered until the owning document
|
|
1380
|
+
* goes away, and while it is registered every connection to that database blocks — including
|
|
1381
|
+
* connections opened afterwards in other tabs. A store cannot unwedge the browser, so it bounds
|
|
1382
|
+
* the wait and says so instead of hanging.
|
|
1383
|
+
*
|
|
1384
|
+
* It extends `UnknownOutcomeError` because a write that stalled may still be committed by the
|
|
1385
|
+
* browser once the wedge clears, and carries a `ConnectionLostError` cause so `classifyError`
|
|
1386
|
+
* reports the connection as unusable.
|
|
1387
|
+
*/
|
|
1388
|
+
export declare class StorageUnresponsiveError extends UnknownOutcomeError {
|
|
1389
|
+
readonly backend: string;
|
|
1390
|
+
readonly databaseName: string;
|
|
1391
|
+
readonly waitedMs: number;
|
|
1392
|
+
readonly name = "StorageUnresponsiveError";
|
|
1393
|
+
constructor(backend: string, databaseName: string, waitedMs: number);
|
|
1394
|
+
}
|
|
1364
1395
|
/**
|
|
1365
1396
|
* A remote OPFS leader may have committed a mutation whose acknowledgement was lost.
|
|
1366
1397
|
* Reconcile stable identities or revisions before retrying the named operation.
|
package/dist/storage/types.js
CHANGED
|
@@ -226,7 +226,7 @@ function secondaryIndexWriteContractChanged(previous, next) {
|
|
|
226
226
|
if (rightEntry?.[0] !== id)
|
|
227
227
|
return true;
|
|
228
228
|
const right = rightEntry[1];
|
|
229
|
-
return left.name !== right.name || left.columnId !== right.columnId || left.columnIds.length !== right.columnIds.length || left.columnIds.some((columnId, index) => columnId !== right.columnIds[index]) || left.directions.length !== right.directions.length || left.directions.some((direction, index) => direction !== right.directions[index]) || left.unique !== right.unique || left.uniqueEnforced !== right.uniqueEnforced || left.storageColumnId !== right.storageColumnId || left.locator !== right.locator;
|
|
229
|
+
return left.name !== right.name || left.columnId !== right.columnId || left.columnIds.length !== right.columnIds.length || left.columnIds.some((columnId, index) => columnId !== right.columnIds[index]) || left.directions.length !== right.directions.length || left.directions.some((direction, index) => direction !== right.directions[index]) || left.termEncoding !== right.termEncoding || left.unique !== right.unique || left.uniqueEnforced !== right.uniqueEnforced || left.storageColumnId !== right.storageColumnId || left.locator !== right.locator;
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
232
|
function validateTableForeignKey(childTable, key, parentTable) {
|
|
@@ -461,7 +461,7 @@ function validateSecondaryIndexes(record) {
|
|
|
461
461
|
}
|
|
462
462
|
const directions = index.directions;
|
|
463
463
|
const termEncoding = index.termEncoding;
|
|
464
|
-
if (directions.length !== indexedColumnIds.length || directions.some((direction) => direction !== "asc" && direction !== "desc") || termEncoding !== "tuple-v1") {
|
|
464
|
+
if (directions.length !== indexedColumnIds.length || directions.some((direction) => direction !== "asc" && direction !== "desc") || termEncoding !== "tuple-v1" && termEncoding !== "tuple-v2") {
|
|
465
465
|
throw new TypeError(`Secondary index ${index.name} has invalid key metadata`);
|
|
466
466
|
}
|
|
467
467
|
if (index.uniqueEnforced === true && index.unique !== true) {
|
|
@@ -993,6 +993,18 @@ class UnknownOutcomeError extends Error {
|
|
|
993
993
|
class ConnectionLostError extends Error {
|
|
994
994
|
name = "ConnectionLostError";
|
|
995
995
|
}
|
|
996
|
+
class StorageUnresponsiveError extends UnknownOutcomeError {
|
|
997
|
+
backend;
|
|
998
|
+
databaseName;
|
|
999
|
+
waitedMs;
|
|
1000
|
+
name = "StorageUnresponsiveError";
|
|
1001
|
+
constructor(backend, databaseName, waitedMs) {
|
|
1002
|
+
super(`The ${backend} database ${databaseName} answered nothing for ${String(waitedMs)}ms and is treated as unresponsive; reload the page to open it again`, { cause: new ConnectionLostError(`The ${backend} connection stopped answering`) });
|
|
1003
|
+
this.backend = backend;
|
|
1004
|
+
this.databaseName = databaseName;
|
|
1005
|
+
this.waitedMs = waitedMs;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
996
1008
|
class OpfsUncertainOutcomeError extends UnknownOutcomeError {
|
|
997
1009
|
method;
|
|
998
1010
|
name = "OpfsUncertainOutcomeError";
|
|
@@ -2428,7 +2440,7 @@ function normalizeMergeCompactionRewritePlan(value) {
|
|
|
2428
2440
|
for (let index = 1; index < sourceSegments.length; index += 1) {
|
|
2429
2441
|
const previous = sourceSegments[index - 1];
|
|
2430
2442
|
const current = sourceSegments[index];
|
|
2431
|
-
if (previous !== void 0 && current !== void 0 && compareMergeSourceSegments(previous, current)
|
|
2443
|
+
if (previous !== void 0 && current !== void 0 && compareMergeSourceSegments(previous, current) > 0) {
|
|
2432
2444
|
throw new TypeError("Merge source segments must use canonical logical order");
|
|
2433
2445
|
}
|
|
2434
2446
|
}
|
|
@@ -2739,7 +2751,7 @@ function normalizeRowIdSpans(value, totalRows, rowIdStart, rowIdEndExclusive, la
|
|
|
2739
2751
|
return spans;
|
|
2740
2752
|
}
|
|
2741
2753
|
function compareMergeSourceSegments(left, right) {
|
|
2742
|
-
return left.logicalOrder - right.logicalOrder || left.committedVersion - right.committedVersion
|
|
2754
|
+
return left.logicalOrder - right.logicalOrder || left.committedVersion - right.committedVersion;
|
|
2743
2755
|
}
|
|
2744
2756
|
function normalizeCompactionOutputCursor(value, plan) {
|
|
2745
2757
|
if (plan.kind === "copy-v1") {
|
|
@@ -3065,6 +3077,7 @@ export {
|
|
|
3065
3077
|
StorageCorruptionError,
|
|
3066
3078
|
StorageFormatVersionError,
|
|
3067
3079
|
StorageResourceLimitError,
|
|
3080
|
+
StorageUnresponsiveError,
|
|
3068
3081
|
TableInUseError,
|
|
3069
3082
|
TableRecordConflictError,
|
|
3070
3083
|
TempOwnerConflictError,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompactionJobConflictError, LeaseConflictError, LeaseOwnerConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_BULK_READ_ITEMS, PostingBuildConflictError, SnapshotManifestMissingError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, secondaryUniqueKeyNamespace, WriteConflictError } from "../storage/types.js";
|
|
1
|
+
import { CompactionJobConflictError, GarbageCollectionJobConflictError, LeaseConflictError, LeaseOwnerConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_BULK_READ_ITEMS, PostingBuildConflictError, SnapshotManifestMissingError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, secondaryUniqueKeyNamespace, WriteConflictError } from "../storage/types.js";
|
|
2
2
|
async function runBlockStoreConformance(target) {
|
|
3
3
|
for (const conformanceCase of blockStoreConformanceCases()) {
|
|
4
4
|
try {
|
|
@@ -768,6 +768,36 @@ function blockStoreConformanceCases() {
|
|
|
768
768
|
limit: 1
|
|
769
769
|
});
|
|
770
770
|
checkEqual(retiredBeforeCollection.records.map((record) => record.blockId), [first.blockId], "retired provenance must remain independently pageable before collection");
|
|
771
|
+
const discovering = await store.createGarbageCollectionJob({
|
|
772
|
+
id: "gc-discovering",
|
|
773
|
+
candidateManifestVersions: [],
|
|
774
|
+
candidateSegmentIds: [],
|
|
775
|
+
candidateBlockIds: [],
|
|
776
|
+
leaseCutoff: LATER,
|
|
777
|
+
createdAt: LATER,
|
|
778
|
+
discovery: {
|
|
779
|
+
phase: "manifests",
|
|
780
|
+
currentManifestVersion: null,
|
|
781
|
+
retainAboveVersion: 0,
|
|
782
|
+
retainAfter: 0,
|
|
783
|
+
maxPlanningItems: 16,
|
|
784
|
+
manifestCursor: null,
|
|
785
|
+
segmentCursor: null,
|
|
786
|
+
transactionCursor: null,
|
|
787
|
+
compactionCursor: null,
|
|
788
|
+
visitedRecords: 0,
|
|
789
|
+
resumePhase: null,
|
|
790
|
+
postManifestPhase: null,
|
|
791
|
+
artifactCursor: null
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
const discovered = await store.updateGarbageCollectionPlanning({
|
|
795
|
+
jobId: discovering.id,
|
|
796
|
+
expectedRevision: discovering.revision,
|
|
797
|
+
discovery: { ...discovering.discovery ?? {}, phase: "complete" },
|
|
798
|
+
updatedAt: LATER
|
|
799
|
+
});
|
|
800
|
+
check(discovered.state === "completed", "a discovery that found nothing must complete its job");
|
|
771
801
|
const job = await store.createGarbageCollectionJob({
|
|
772
802
|
id: "gc-1",
|
|
773
803
|
candidateManifestVersions: [first.version],
|
|
@@ -778,6 +808,15 @@ function blockStoreConformanceCases() {
|
|
|
778
808
|
});
|
|
779
809
|
await checkThrows(() => store.removeGarbageCollectionJob(job.id), Error, "removing a non-completed garbage-collection job");
|
|
780
810
|
check((await store.getGarbageCollectionJob(job.id))?.state === "planned", "a refused garbage-collection job removal mutated the record");
|
|
811
|
+
await checkThrows(() => store.createGarbageCollectionJob({
|
|
812
|
+
id: "gc-2",
|
|
813
|
+
candidateManifestVersions: [first.version],
|
|
814
|
+
candidateSegmentIds: [],
|
|
815
|
+
candidateBlockIds: [first.blockId],
|
|
816
|
+
leaseCutoff: LATER,
|
|
817
|
+
createdAt: LATER
|
|
818
|
+
}), GarbageCollectionJobConflictError, "a second active garbage-collection job");
|
|
819
|
+
check(await store.getGarbageCollectionJob("gc-2") === void 0, "a refused second garbage-collection job left a record behind");
|
|
781
820
|
const step = await store.runGarbageCollectionStep({
|
|
782
821
|
jobId: job.id,
|
|
783
822
|
expectedRevision: job.revision,
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -96,4 +96,5 @@ export declare class FaultInjectingBlockStore implements BlockStore {
|
|
|
96
96
|
export { blockStoreConformanceCases, runBlockStoreConformance, type BlockStoreConformanceCase, type BlockStoreConformanceTarget, } from "./block-store-conformance.js";
|
|
97
97
|
export { MemoryOpfs, type TransferLimit, type WriteFault } from "./opfs-shim.js";
|
|
98
98
|
export { DeterministicScheduler, generateSimulationPlan, parseSimulationPlan, runSimulation, scheduledBlockStore, type CollectionPassSummary, type SimulationDelete, type SimulationMutation, type SimulationOptions, type SimulationPlan, type SimulationPut, type SimulationResult, type SimulationStep, type SimulationTraceEvent, } from "./simulator.js";
|
|
99
|
+
export { InteractionFailure, createDatabaseDriver, evaluate as evaluatePlanPredicate, generateInteractionPlan, parseInteractionPlan, renderLiteral as renderPlanLiteral, renderPredicate as renderPlanPredicate, runInteractionPlan, type Assignment, type ColumnType, type CompareOperator, type DatabaseDriverOptions, type DriverStoreSource, type FaultPointName, type Interaction, type InteractionPlan, type InteractionPlanOptions, type InteractionRunOptions, type InteractionRunResult, type KeyedMutation, type PlanColumn, type PlanRow, type PlanTable, type PlanValue, type Predicate, type SimulatedConnection, type SimulatedExecuteResult, type SimulatedFaults, type SimulatedQueryResult, type SimulationDriver, type TransactionStatement, } from "./interaction-simulator.js";
|
|
99
100
|
export { SqlLogicFailure, SqlLogicParseError, md5Hex, parseSqlLogicTest, parseSqlLogicTestLines, renderSqlLogicValue, runSqlLogicTest, type SqlLogicCondition, type SqlLogicDatabase, type SqlLogicExpectedHash, type SqlLogicExpectedValues, type SqlLogicHalt, type SqlLogicHashThreshold, type SqlLogicLocation, type SqlLogicQuery, type SqlLogicRecord, type SqlLogicRunStatistics, type SqlLogicRunOptions, type SqlLogicSortMode, type SqlLogicStatement, type SqlLogicType, } from "./sqllogictest.js";
|
package/dist/testing/index.js
CHANGED
|
@@ -290,22 +290,31 @@ class FaultInjectingBlockStore {
|
|
|
290
290
|
import { blockStoreConformanceCases, runBlockStoreConformance } from "./block-store-conformance.js";
|
|
291
291
|
import { MemoryOpfs } from "./opfs-shim.js";
|
|
292
292
|
import { DeterministicScheduler, generateSimulationPlan, parseSimulationPlan, runSimulation, scheduledBlockStore } from "./simulator.js";
|
|
293
|
+
import { InteractionFailure, createDatabaseDriver, evaluate, generateInteractionPlan, parseInteractionPlan, renderLiteral, renderPredicate, runInteractionPlan } from "./interaction-simulator.js";
|
|
293
294
|
import { SqlLogicFailure, SqlLogicParseError, md5Hex, parseSqlLogicTest, parseSqlLogicTestLines, renderSqlLogicValue, runSqlLogicTest } from "./sqllogictest.js";
|
|
294
295
|
export {
|
|
295
296
|
DeterministicScheduler,
|
|
296
297
|
FaultInjectingBlockStore,
|
|
298
|
+
InteractionFailure,
|
|
297
299
|
MemoryOpfs,
|
|
298
300
|
SqlLogicFailure,
|
|
299
301
|
SqlLogicParseError,
|
|
300
302
|
blockStoreConformanceCases,
|
|
303
|
+
createDatabaseDriver,
|
|
304
|
+
evaluate as evaluatePlanPredicate,
|
|
301
305
|
faultPoints,
|
|
306
|
+
generateInteractionPlan,
|
|
302
307
|
generateSimulationPlan,
|
|
303
308
|
md5Hex,
|
|
309
|
+
parseInteractionPlan,
|
|
304
310
|
parseSimulationPlan,
|
|
305
311
|
parseSqlLogicTest,
|
|
306
312
|
parseSqlLogicTestLines,
|
|
313
|
+
renderLiteral as renderPlanLiteral,
|
|
314
|
+
renderPredicate as renderPlanPredicate,
|
|
307
315
|
renderSqlLogicValue,
|
|
308
316
|
runBlockStoreConformance,
|
|
317
|
+
runInteractionPlan,
|
|
309
318
|
runSimulation,
|
|
310
319
|
runSqlLogicTest,
|
|
311
320
|
scheduledBlockStore
|