@minnowdb/core 0.10.0 → 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/auto-store.d.ts +15 -3
- package/dist/engine/auto-store.js +48 -6
- package/dist/engine/client.js +23 -7
- package/dist/engine/database.js +347 -862
- 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/engine/worker-host.js +2 -0
- package/dist/engine/worker-server.js +4 -5
- package/dist/engine/worker-store-auto.js +2 -2
- package/dist/engine/write-coordinator.js +21 -1
- package/dist/storage/indexeddb.d.ts +18 -0
- package/dist/storage/indexeddb.js +293 -211
- package/dist/storage/opfs/coordination-helpers.js +54 -0
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +3 -2
- package/dist/storage/opfs/leader.js +44 -5
- package/dist/storage/opfs/rpc.js +3 -1
- package/dist/storage/opfs/store.d.ts +12 -0
- package/dist/storage/opfs/store.js +59 -12
- package/dist/storage/toolkit/wal.js +16 -0
- package/dist/storage/toolkit/wire.js +3 -3
- package/dist/storage/types.d.ts +35 -4
- 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/dist/transactions/index.d.ts +7 -0
- package/dist/transactions/index.js +17 -3
- package/package.json +4 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { OpfsBlockStore } from "./store.js";
|
|
2
|
+
function table(name) {
|
|
3
|
+
return {
|
|
4
|
+
id: `table-${name}`,
|
|
5
|
+
name,
|
|
6
|
+
columns: [{ id: "c1", name: "id", type: "number", nullable: false }],
|
|
7
|
+
managed: false,
|
|
8
|
+
revision: 0,
|
|
9
|
+
createdAt: "2026-08-19T00:00:00.000Z"
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
async function waitFor(condition, what, attempts = 800) {
|
|
16
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
17
|
+
if (await condition())
|
|
18
|
+
return;
|
|
19
|
+
await sleep(5);
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Timed out waiting for ${what}`);
|
|
22
|
+
}
|
|
23
|
+
function opener(shim, name, overrides = {}) {
|
|
24
|
+
return () => OpfsBlockStore.open({ name, root: shim.root, rpcTimeoutMs: 200, ...overrides });
|
|
25
|
+
}
|
|
26
|
+
async function outcome(work) {
|
|
27
|
+
return work.then(() => "ok", (error) => error instanceof Error ? error.name : String(error));
|
|
28
|
+
}
|
|
29
|
+
async function outcomeMessage(work) {
|
|
30
|
+
return work.then(() => "ok", (error) => error instanceof Error ? `${error.name}: ${error.message}` : String(error));
|
|
31
|
+
}
|
|
32
|
+
async function delaySyncHandleOpens(shim, matches, delayMs) {
|
|
33
|
+
const probe = await shim.root.getFileHandle("__delay-probe__", { create: true });
|
|
34
|
+
const prototype = Object.getPrototypeOf(probe);
|
|
35
|
+
const original = prototype.createSyncAccessHandle;
|
|
36
|
+
prototype.createSyncAccessHandle = async function patched() {
|
|
37
|
+
if (matches(this.name))
|
|
38
|
+
await sleep(delayMs);
|
|
39
|
+
return original.call(this);
|
|
40
|
+
};
|
|
41
|
+
await shim.root.removeEntry("__delay-probe__");
|
|
42
|
+
return () => {
|
|
43
|
+
prototype.createSyncAccessHandle = original;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
delaySyncHandleOpens,
|
|
48
|
+
opener,
|
|
49
|
+
outcome,
|
|
50
|
+
outcomeMessage,
|
|
51
|
+
sleep,
|
|
52
|
+
table,
|
|
53
|
+
waitFor
|
|
54
|
+
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { OpfsBlockStore, deleteOpfsDatabase, type OpfsBlockStoreOptions } from "./store.js";
|
|
1
|
+
export { OpfsBlockStore, deleteOpfsDatabase, opfsDatabaseExists, type OpfsBlockStoreOptions, } from "./store.js";
|
|
2
2
|
export { OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError, } from "../types.js";
|
|
3
3
|
export { OpfsTree, type ReadFileOptions, type WriteFileOptions } from "./files.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OpfsBlockStore, deleteOpfsDatabase } from "./store.js";
|
|
1
|
+
import { OpfsBlockStore, deleteOpfsDatabase, opfsDatabaseExists } from "./store.js";
|
|
2
2
|
import { OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError } from "../types.js";
|
|
3
3
|
import { OpfsTree } from "./files.js";
|
|
4
4
|
export {
|
|
@@ -7,5 +7,6 @@ export {
|
|
|
7
7
|
OpfsDatabaseInUseError,
|
|
8
8
|
OpfsTree,
|
|
9
9
|
OpfsUncertainOutcomeError,
|
|
10
|
-
deleteOpfsDatabase
|
|
10
|
+
deleteOpfsDatabase,
|
|
11
|
+
opfsDatabaseExists
|
|
11
12
|
};
|
|
@@ -85,6 +85,36 @@ const LOGGED_BODY_BUILDERS = {
|
|
|
85
85
|
removeGarbageCollectionJob: ([id]) => ({ op: "removeGarbageCollectionJob", id }),
|
|
86
86
|
removePrunedManifestRecords: ([maxItems]) => ({ op: "removePrunedManifestRecords", maxItems })
|
|
87
87
|
};
|
|
88
|
+
const STATE_SHRINKING_OPS = /* @__PURE__ */ new Set([
|
|
89
|
+
"rollbackTransactionArtifacts",
|
|
90
|
+
"abortTransactionIfExpired",
|
|
91
|
+
"removeLease",
|
|
92
|
+
"removeLeaseIfExpired",
|
|
93
|
+
"removeTempRun",
|
|
94
|
+
"removeTempOwner",
|
|
95
|
+
"removeTempOwnerIfExpired",
|
|
96
|
+
"removeCompactionJob",
|
|
97
|
+
"cancelCompactionJob",
|
|
98
|
+
"removeGarbageCollectionJob",
|
|
99
|
+
"removePrunedManifestRecords",
|
|
100
|
+
"removeAbortedSegment",
|
|
101
|
+
"abortFtsBaseBuild",
|
|
102
|
+
"abortUniqueKeyBuild",
|
|
103
|
+
"cancelSnapshotFrameImport",
|
|
104
|
+
"closeSnapshotFrameExport",
|
|
105
|
+
"garbageCollectionStep"
|
|
106
|
+
]);
|
|
107
|
+
function shrinksLoggedState(body) {
|
|
108
|
+
if (body.op === "updateTransaction")
|
|
109
|
+
return body.update.status === "aborted";
|
|
110
|
+
return STATE_SHRINKING_OPS.has(body.op);
|
|
111
|
+
}
|
|
112
|
+
class OpfsLeaderClosedError extends Error {
|
|
113
|
+
name = "OpfsLeaderClosedError";
|
|
114
|
+
constructor() {
|
|
115
|
+
super("This OPFS store connection is closed");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
88
118
|
class OpfsLeader {
|
|
89
119
|
#tree;
|
|
90
120
|
#strict;
|
|
@@ -425,7 +455,7 @@ class OpfsLeader {
|
|
|
425
455
|
#run(work) {
|
|
426
456
|
const result = this.#chain.then(async () => {
|
|
427
457
|
if (this.#closed)
|
|
428
|
-
throw new
|
|
458
|
+
throw new OpfsLeaderClosedError();
|
|
429
459
|
if (this.#poisoned)
|
|
430
460
|
await this.#loadFromDisk();
|
|
431
461
|
return work();
|
|
@@ -470,11 +500,13 @@ class OpfsLeader {
|
|
|
470
500
|
return this.#servedLedger.get(key);
|
|
471
501
|
}
|
|
472
502
|
async completeServed(key, result) {
|
|
473
|
-
|
|
474
|
-
if (entry === void 0 || entry.settled)
|
|
503
|
+
if (!this.#servedLedger.has(key))
|
|
475
504
|
return;
|
|
476
505
|
const bytes = estimateRpcValueBytes(result);
|
|
477
506
|
await this.#run(() => {
|
|
507
|
+
const entry = this.#servedLedger.get(key);
|
|
508
|
+
if (entry === void 0 || entry.settled)
|
|
509
|
+
return;
|
|
478
510
|
if (bytes > this.#servedLedgerResultBytes) {
|
|
479
511
|
this.#appendFrame({ op: "servedResult", key, withheld: true }, false);
|
|
480
512
|
entry.settled = true;
|
|
@@ -499,7 +531,13 @@ class OpfsLeader {
|
|
|
499
531
|
}
|
|
500
532
|
#logged(body) {
|
|
501
533
|
if (this.#wal.byteLength >= MAX_OPFS_WAL_BYTES - 64 * 1024 * 1024) {
|
|
502
|
-
|
|
534
|
+
try {
|
|
535
|
+
this.checkpointNow();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (!shrinksLoggedState(body) || this.#wal.byteLength >= MAX_OPFS_WAL_BYTES - 16 * 1024 * 1024) {
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
503
541
|
}
|
|
504
542
|
if (this.#strict) {
|
|
505
543
|
try {
|
|
@@ -1569,7 +1607,7 @@ class OpfsLeader {
|
|
|
1569
1607
|
};
|
|
1570
1608
|
const bytes = encodeSyncCheckpoint(state);
|
|
1571
1609
|
if (bytes.byteLength > MAX_OPFS_CHECKPOINT_BYTES) {
|
|
1572
|
-
throw new
|
|
1610
|
+
throw new StorageResourceLimitError("checkpoint byte", bytes.byteLength, MAX_OPFS_CHECKPOINT_BYTES);
|
|
1573
1611
|
}
|
|
1574
1612
|
const slotIndex = this.#newestSlot === 0 ? 1 : 0;
|
|
1575
1613
|
const mirrorIndex = slotIndex === 0 ? 1 : 0;
|
|
@@ -4802,5 +4840,6 @@ export {
|
|
|
4802
4840
|
MAX_OPFS_CHECKPOINT_BYTES,
|
|
4803
4841
|
MAX_OPFS_WAL_BYTES,
|
|
4804
4842
|
OpfsLeader,
|
|
4843
|
+
OpfsLeaderClosedError,
|
|
4805
4844
|
assertBlockReadBatchByteLimit
|
|
4806
4845
|
};
|
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;
|
|
@@ -109,6 +109,7 @@ function parseStoreRpcMessage(value, knownMethods) {
|
|
|
109
109
|
case "state":
|
|
110
110
|
return exactKeys(record, ["kind", "leaderId", "foreground"]) && boundedRpcString(record.leaderId) && typeof record.foreground === "boolean" ? record : void 0;
|
|
111
111
|
case "leader":
|
|
112
|
+
case "wait":
|
|
112
113
|
case "released":
|
|
113
114
|
return exactKeys(record, ["kind", "leaderId"]) && boundedRpcString(record.leaderId) ? record : void 0;
|
|
114
115
|
case "ping":
|
|
@@ -202,6 +203,7 @@ const errorRegistry = new Map([
|
|
|
202
203
|
PostingBuildConflictError,
|
|
203
204
|
StorageCorruptionError,
|
|
204
205
|
StorageFormatVersionError,
|
|
206
|
+
StorageUnresponsiveError,
|
|
205
207
|
OpfsCoordinationError,
|
|
206
208
|
OpfsDatabaseInUseError,
|
|
207
209
|
OpfsUncertainOutcomeError,
|
|
@@ -27,6 +27,8 @@ export interface OpfsBlockStoreOptions {
|
|
|
27
27
|
hiddenIdleReleaseMs?: number;
|
|
28
28
|
/** @internal Test seam: how long an ex-leader stays out of elections (default 1500ms). */
|
|
29
29
|
handoverGraceMs?: number;
|
|
30
|
+
/** @internal Test seam: how long an operation looks for a leader (default 10000ms). */
|
|
31
|
+
dispatchBudgetMs?: number;
|
|
30
32
|
/**
|
|
31
33
|
* Hears failures no operation reports: a background checkpoint or cleanup that failed, an
|
|
32
34
|
* election or handover that threw, a served request that could not be answered. The worker
|
|
@@ -108,3 +110,13 @@ export declare function deleteOpfsDatabase(options: {
|
|
|
108
110
|
name: string;
|
|
109
111
|
root?: FileSystemDirectoryHandle;
|
|
110
112
|
}): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Whether a database directory of this name exists in the origin's private file system (or
|
|
115
|
+
* under `root`). Nothing is created: an `auto` descriptor asks this before choosing a store for
|
|
116
|
+
* a name it has no record of, so a database an explicit `opfs` descriptor created is found
|
|
117
|
+
* rather than shadowed by an empty one.
|
|
118
|
+
*/
|
|
119
|
+
export declare function opfsDatabaseExists(options: {
|
|
120
|
+
name: string;
|
|
121
|
+
root?: FileSystemDirectoryHandle;
|
|
122
|
+
}): Promise<boolean>;
|
|
@@ -2,11 +2,11 @@ import { assertStorageBulkReadItems, assertTempRunPageBatchLimits, OpfsCoordinat
|
|
|
2
2
|
import { validateTempRunPage, validateTempRunPageIdentity } from "../toolkit/record-core.js";
|
|
3
3
|
import { OpfsTree, encodeSegment, isDomError } from "./files.js";
|
|
4
4
|
import { LOG_FORMAT_VERSION } from "../toolkit/wire.js";
|
|
5
|
-
import { OpfsLeader } from "./leader.js";
|
|
5
|
+
import { OpfsLeader, OpfsLeaderClosedError } from "./leader.js";
|
|
6
6
|
import { rehydrateStoreError, estimateRpcValueBytes, fingerprintStoreRequest, MAX_OPFS_RPC_HOLD_MS, MAX_OPFS_RPC_MESSAGE_BYTES, parseStoreRpcMessage, serializeStoreError } from "./rpc.js";
|
|
7
7
|
const RPC_TIMEOUT_MS = 1e3;
|
|
8
8
|
const DISCOVERY_WAIT_MS = 150;
|
|
9
|
-
const
|
|
9
|
+
const DISPATCH_BUDGET_MS = 1e4;
|
|
10
10
|
const YIELD_COOLDOWN_MS = 3e3;
|
|
11
11
|
const HANDOVER_GRACE_MS = 1500;
|
|
12
12
|
const HIDDEN_IDLE_RELEASE_MS = 15e3;
|
|
@@ -185,6 +185,7 @@ class OpfsBlockStore {
|
|
|
185
185
|
#yieldCooldownMs;
|
|
186
186
|
#hiddenIdleReleaseMs;
|
|
187
187
|
#handoverGraceMs;
|
|
188
|
+
#dispatchBudgetMs;
|
|
188
189
|
#onDiagnostic;
|
|
189
190
|
#instanceId = crypto.randomUUID();
|
|
190
191
|
#channelName;
|
|
@@ -197,6 +198,8 @@ class OpfsBlockStore {
|
|
|
197
198
|
#closed = false;
|
|
198
199
|
#lastYieldAt = 0;
|
|
199
200
|
#electing;
|
|
201
|
+
#recovering = false;
|
|
202
|
+
#waitHeardAt = 0;
|
|
200
203
|
#pending = /* @__PURE__ */ new Map();
|
|
201
204
|
#inFlightMutations = /* @__PURE__ */ new Map();
|
|
202
205
|
#settledMutations = /* @__PURE__ */ new Map();
|
|
@@ -230,6 +233,7 @@ class OpfsBlockStore {
|
|
|
230
233
|
this.#yieldCooldownMs = options.yieldCooldownMs ?? YIELD_COOLDOWN_MS;
|
|
231
234
|
this.#hiddenIdleReleaseMs = options.hiddenIdleReleaseMs ?? HIDDEN_IDLE_RELEASE_MS;
|
|
232
235
|
this.#handoverGraceMs = options.handoverGraceMs ?? HANDOVER_GRACE_MS;
|
|
236
|
+
this.#dispatchBudgetMs = options.dispatchBudgetMs ?? DISPATCH_BUDGET_MS;
|
|
233
237
|
this.#onDiagnostic = options.onDiagnostic;
|
|
234
238
|
this.#channelName = `minnowdb-store:${options.name}`;
|
|
235
239
|
this.liveQueryChannelName = `minnowdb-live:opfs:${options.name}`;
|
|
@@ -311,7 +315,9 @@ class OpfsBlockStore {
|
|
|
311
315
|
}
|
|
312
316
|
let slotA;
|
|
313
317
|
let slotB;
|
|
318
|
+
this.#recovering = true;
|
|
314
319
|
try {
|
|
320
|
+
this.#post({ kind: "wait", leaderId: this.#instanceId });
|
|
315
321
|
slotA = await this.#openWithRetry(["checkpoint-a"]);
|
|
316
322
|
slotB = await this.#openWithRetry(["checkpoint-b"]);
|
|
317
323
|
this.#leader = await OpfsLeader.recover(this.#tree, this.#durability === "strict", { wal, slotA, slotB }, this.#checkpointEntries, this.#cleanupLimitBytes, this.#onDiagnostic, this.#servedLedgerAgeMs, this.#servedLedgerResultBytes);
|
|
@@ -322,6 +328,8 @@ class OpfsBlockStore {
|
|
|
322
328
|
if (isLockContention(error))
|
|
323
329
|
return false;
|
|
324
330
|
throw error;
|
|
331
|
+
} finally {
|
|
332
|
+
this.#recovering = false;
|
|
325
333
|
}
|
|
326
334
|
if (this.#closed) {
|
|
327
335
|
const leader = this.#leader;
|
|
@@ -409,7 +417,7 @@ class OpfsBlockStore {
|
|
|
409
417
|
return;
|
|
410
418
|
this.#leader = void 0;
|
|
411
419
|
this.#knownLeader = void 0;
|
|
412
|
-
const shutdown = leader
|
|
420
|
+
const shutdown = this.#shutdownAfterMutations(leader).catch((error) => {
|
|
413
421
|
this.#diagnostic(error, "opfs idle release shutdown");
|
|
414
422
|
leader.crash();
|
|
415
423
|
});
|
|
@@ -494,7 +502,7 @@ class OpfsBlockStore {
|
|
|
494
502
|
this.#leader = void 0;
|
|
495
503
|
this.#knownLeader = void 0;
|
|
496
504
|
this.#lastYieldAt = Date.now();
|
|
497
|
-
const shutdown = leader
|
|
505
|
+
const shutdown = this.#shutdownAfterMutations(leader).catch((error) => {
|
|
498
506
|
this.#diagnostic(error, "opfs yield shutdown");
|
|
499
507
|
leader.crash();
|
|
500
508
|
});
|
|
@@ -516,6 +524,14 @@ class OpfsBlockStore {
|
|
|
516
524
|
}, this.#handoverGraceMs);
|
|
517
525
|
}
|
|
518
526
|
#yielding;
|
|
527
|
+
#shutdownAfterMutations(leader) {
|
|
528
|
+
return this.#withMutationTurn(() => leader.shutdown());
|
|
529
|
+
}
|
|
530
|
+
async #servedDrained() {
|
|
531
|
+
const deadline = Date.now() + DECLINE_AFTER_CLOSE_MS;
|
|
532
|
+
while (this.#served.size > 0 && Date.now() < deadline)
|
|
533
|
+
await sleep(5);
|
|
534
|
+
}
|
|
519
535
|
#onMessage(message) {
|
|
520
536
|
if (this.#closed) {
|
|
521
537
|
if (message.kind === "op")
|
|
@@ -645,9 +661,16 @@ class OpfsBlockStore {
|
|
|
645
661
|
case "ping": {
|
|
646
662
|
if (this.#leader !== void 0) {
|
|
647
663
|
this.#post({ kind: "leader", leaderId: this.#instanceId });
|
|
664
|
+
} else if (this.#recovering || this.#yielding !== void 0) {
|
|
665
|
+
this.#post({ kind: "wait", leaderId: this.#instanceId });
|
|
648
666
|
}
|
|
649
667
|
return;
|
|
650
668
|
}
|
|
669
|
+
case "wait": {
|
|
670
|
+
if (message.leaderId !== this.#instanceId)
|
|
671
|
+
this.#waitHeardAt = Date.now();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
651
674
|
case "bid": {
|
|
652
675
|
if (message.foreground && message.bidderId !== this.#instanceId) {
|
|
653
676
|
this.#considerBid(message.bidderId);
|
|
@@ -1036,7 +1059,7 @@ class OpfsBlockStore {
|
|
|
1036
1059
|
let sentRemotely = false;
|
|
1037
1060
|
let mayHaveRun = false;
|
|
1038
1061
|
this.#lastActivityAt = sentAt;
|
|
1039
|
-
|
|
1062
|
+
while (Date.now() - Math.max(sentAt, this.#waitHeardAt) < this.#dispatchBudgetMs) {
|
|
1040
1063
|
this.#assertOpen();
|
|
1041
1064
|
const leader = this.#leader;
|
|
1042
1065
|
if (leader !== void 0) {
|
|
@@ -1058,7 +1081,16 @@ class OpfsBlockStore {
|
|
|
1058
1081
|
if (sentAt < leader.servedCoverageSince)
|
|
1059
1082
|
throw new OpfsUncertainOutcomeError(method);
|
|
1060
1083
|
}
|
|
1061
|
-
return await this.#withMutationTurn(() =>
|
|
1084
|
+
return await this.#withMutationTurn(() => {
|
|
1085
|
+
if (leader.isClosed() || !this.#leads(leader))
|
|
1086
|
+
throw new OpfsLeaderClosedError();
|
|
1087
|
+
return bound.apply(leader, args);
|
|
1088
|
+
});
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
if (!this.#leads(leader) && (isRead || error instanceof OpfsLeaderClosedError)) {
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
throw error;
|
|
1062
1094
|
} finally {
|
|
1063
1095
|
this.#lastActivityAt = Date.now();
|
|
1064
1096
|
}
|
|
@@ -1187,10 +1219,11 @@ class OpfsBlockStore {
|
|
|
1187
1219
|
const leader = this.#leader;
|
|
1188
1220
|
this.#leader = void 0;
|
|
1189
1221
|
if (leader !== void 0) {
|
|
1190
|
-
void leader
|
|
1222
|
+
void this.#shutdownAfterMutations(leader).catch((error) => {
|
|
1191
1223
|
this.#diagnostic(error, "opfs close shutdown");
|
|
1192
1224
|
leader.crash();
|
|
1193
|
-
}).then(() => {
|
|
1225
|
+
}).then(() => this.#servedDrained()).then(() => {
|
|
1226
|
+
this.#stopKeepalive();
|
|
1194
1227
|
this.#post({ kind: "released", leaderId: this.#instanceId });
|
|
1195
1228
|
this.#closeChannels();
|
|
1196
1229
|
this.#releaseWhenHandlesClose();
|
|
@@ -1198,13 +1231,15 @@ class OpfsBlockStore {
|
|
|
1198
1231
|
return;
|
|
1199
1232
|
}
|
|
1200
1233
|
if (this.#yielding !== void 0) {
|
|
1201
|
-
void this.#yielding.then(() => {
|
|
1234
|
+
void this.#yielding.then(() => this.#servedDrained()).then(() => {
|
|
1235
|
+
this.#stopKeepalive();
|
|
1202
1236
|
this.#post({ kind: "released", leaderId: this.#instanceId });
|
|
1203
1237
|
this.#closeChannels();
|
|
1204
1238
|
this.#releaseWhenHandlesClose();
|
|
1205
1239
|
});
|
|
1206
1240
|
return;
|
|
1207
1241
|
}
|
|
1242
|
+
this.#stopKeepalive();
|
|
1208
1243
|
this.#closeChannels();
|
|
1209
1244
|
this.#releaseWhenHandlesClose();
|
|
1210
1245
|
}
|
|
@@ -1212,10 +1247,8 @@ class OpfsBlockStore {
|
|
|
1212
1247
|
if (this.#reacquireTimer !== void 0)
|
|
1213
1248
|
clearTimeout(this.#reacquireTimer);
|
|
1214
1249
|
this.#reacquireTimer = void 0;
|
|
1215
|
-
this.#stopKeepalive();
|
|
1216
1250
|
this.#clearDeferredBid();
|
|
1217
1251
|
this.#clearHiddenIdleTimer();
|
|
1218
|
-
this.#served.clear();
|
|
1219
1252
|
}
|
|
1220
1253
|
#releaseWhenHandlesClose() {
|
|
1221
1254
|
const release = this.#releaseConnectionLock;
|
|
@@ -1275,6 +1308,8 @@ class OpfsBlockStore {
|
|
|
1275
1308
|
_crashForTests() {
|
|
1276
1309
|
this.#closed = true;
|
|
1277
1310
|
this.#clearCoordinationTimers();
|
|
1311
|
+
this.#stopKeepalive();
|
|
1312
|
+
this.#served.clear();
|
|
1278
1313
|
for (const pending of this.#pending.values())
|
|
1279
1314
|
clearTimeout(pending.timer);
|
|
1280
1315
|
this.#pending.clear();
|
|
@@ -1423,6 +1458,17 @@ async function resolveDatabaseRoot(options) {
|
|
|
1423
1458
|
const namespace = await root.getDirectoryHandle("minnowdb", { create: true });
|
|
1424
1459
|
return namespace.getDirectoryHandle(encodedName, { create: true });
|
|
1425
1460
|
}
|
|
1461
|
+
async function opfsDatabaseExists(options) {
|
|
1462
|
+
const encodedName = encodeSegment(validateStorageDatabaseName(options.name));
|
|
1463
|
+
try {
|
|
1464
|
+
const root = options.root ?? await navigator.storage.getDirectory();
|
|
1465
|
+
const namespace = await root.getDirectoryHandle("minnowdb");
|
|
1466
|
+
await namespace.getDirectoryHandle(encodedName);
|
|
1467
|
+
return true;
|
|
1468
|
+
} catch {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1426
1472
|
function isLockContention(error) {
|
|
1427
1473
|
return isDomError(error, "NoModificationAllowedError") || isDomError(error, "InvalidStateError");
|
|
1428
1474
|
}
|
|
@@ -1431,5 +1477,6 @@ function sleep(ms) {
|
|
|
1431
1477
|
}
|
|
1432
1478
|
export {
|
|
1433
1479
|
OpfsBlockStore,
|
|
1434
|
-
deleteOpfsDatabase
|
|
1480
|
+
deleteOpfsDatabase,
|
|
1481
|
+
opfsDatabaseExists
|
|
1435
1482
|
};
|
|
@@ -79,6 +79,8 @@ function* iterateWalFrames(handle) {
|
|
|
79
79
|
while (offset + FRAME_HEADER_BYTES <= size) {
|
|
80
80
|
readFully(handle, header, offset, "reading a WAL frame header for recovery");
|
|
81
81
|
if (headerView.getUint32(0, true) !== FRAME_MAGIC) {
|
|
82
|
+
if (isZeroFilled(handle, offset, size))
|
|
83
|
+
break;
|
|
82
84
|
throw new Error(`WAL frame marker mismatch at offset ${String(offset)}`);
|
|
83
85
|
}
|
|
84
86
|
const length = headerView.getUint32(4, true);
|
|
@@ -92,12 +94,26 @@ function* iterateWalFrames(handle) {
|
|
|
92
94
|
const payloadBytes = new Uint8Array(length);
|
|
93
95
|
readFully(handle, payloadBytes, offset + FRAME_HEADER_BYTES, "reading a WAL frame payload for recovery");
|
|
94
96
|
if (crc32(payloadBytes) !== checksum) {
|
|
97
|
+
if (payloadBytes.every((byte) => byte === 0) && isZeroFilled(handle, end, size))
|
|
98
|
+
break;
|
|
95
99
|
throw new Error(`WAL frame checksum mismatch at offset ${String(offset)}`);
|
|
96
100
|
}
|
|
97
101
|
yield { payload: decodeRecordJson(payloadBytes), frameEnd: end };
|
|
98
102
|
offset = end;
|
|
99
103
|
}
|
|
100
104
|
}
|
|
105
|
+
function isZeroFilled(handle, offset, size) {
|
|
106
|
+
const chunk = new Uint8Array(Math.min(64 * 1024, Math.max(0, size - offset)));
|
|
107
|
+
for (let at = offset; at < size; at += chunk.byteLength) {
|
|
108
|
+
const length = Math.min(chunk.byteLength, size - at);
|
|
109
|
+
const window = length === chunk.byteLength ? chunk : chunk.subarray(0, length);
|
|
110
|
+
readFully(handle, window, at, "reading a WAL tail for recovery");
|
|
111
|
+
for (let index = 0; index < length; index += 1)
|
|
112
|
+
if (window[index] !== 0)
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
101
117
|
export {
|
|
102
118
|
MAX_WAL_FRAME_BYTES,
|
|
103
119
|
WalWriter,
|
|
@@ -58,15 +58,15 @@ function decodeEnvelope(magic, bytes) {
|
|
|
58
58
|
}
|
|
59
59
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
60
60
|
const version = view.getUint32(8, true);
|
|
61
|
-
if (version !== LOG_FORMAT_VERSION) {
|
|
62
|
-
throw new StorageFormatVersionError("opfs", `envelope/${magic}`, version, LOG_FORMAT_VERSION, version < LOG_FORMAT_VERSION ? "older" : "newer");
|
|
63
|
-
}
|
|
64
61
|
const payloadLength = view.getUint32(12, true);
|
|
65
62
|
if (bytes.byteLength !== ENVELOPE_HEADER_BYTES + payloadLength)
|
|
66
63
|
return void 0;
|
|
67
64
|
const payload = bytes.subarray(ENVELOPE_HEADER_BYTES, ENVELOPE_HEADER_BYTES + payloadLength);
|
|
68
65
|
if (crc32(payload) !== view.getUint32(16, true))
|
|
69
66
|
return void 0;
|
|
67
|
+
if (version !== LOG_FORMAT_VERSION) {
|
|
68
|
+
throw new StorageFormatVersionError("opfs", `envelope/${magic}`, version, LOG_FORMAT_VERSION, version < LOG_FORMAT_VERSION ? "older" : "newer");
|
|
69
|
+
}
|
|
70
70
|
return payload;
|
|
71
71
|
}
|
|
72
72
|
const CHUNK_MAGIC = "MNWCHNK1";
|
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";
|
|
@@ -502,11 +510,11 @@ export declare const MAX_PINNED_RETIRED_BYTES: number;
|
|
|
502
510
|
export declare const MAX_RETIRED_HISTORY_BYTES: number;
|
|
503
511
|
/** A durable resource family reached its fixed corruption/growth safety ceiling. */
|
|
504
512
|
export declare class StorageResourceLimitError extends Error {
|
|
505
|
-
readonly resource: "lease" | "temp owner" | "temp run" | "temp page" | "temporary run total" | "temporary page total" | "temp owner byte" | "temporary byte" | "compaction job" | "garbage collection job" | "unique-key build" | "full-text build" | "secondary-index build" | "accelerator build byte" | "accelerator build entry" | "transaction" | "staged artifact byte" | "staged block" | "staged segment" | "catalog record" | "catalog byte" | "manifest record" | "manifest byte" | "segment record" | "segment byte" | "terminal transaction" | "terminal compaction job" | "completed garbage collection job" | "pinned manifest version lag" | "pinned retired block" | "pinned retired byte" | "snapshot accelerator byte" | "snapshot accelerator entry" | "retired history byte";
|
|
513
|
+
readonly resource: "lease" | "temp owner" | "temp run" | "temp page" | "temporary run total" | "temporary page total" | "temp owner byte" | "temporary byte" | "compaction job" | "garbage collection job" | "unique-key build" | "full-text build" | "secondary-index build" | "accelerator build byte" | "accelerator build entry" | "transaction" | "staged artifact byte" | "staged block" | "staged segment" | "catalog record" | "catalog byte" | "manifest record" | "manifest byte" | "segment record" | "segment byte" | "terminal transaction" | "terminal compaction job" | "completed garbage collection job" | "pinned manifest version lag" | "pinned retired block" | "pinned retired byte" | "snapshot accelerator byte" | "snapshot accelerator entry" | "retired history byte" | "checkpoint byte";
|
|
506
514
|
readonly count: number;
|
|
507
515
|
readonly limit: number;
|
|
508
516
|
readonly name = "StorageResourceLimitError";
|
|
509
|
-
constructor(resource: "lease" | "temp owner" | "temp run" | "temp page" | "temporary run total" | "temporary page total" | "temp owner byte" | "temporary byte" | "compaction job" | "garbage collection job" | "unique-key build" | "full-text build" | "secondary-index build" | "accelerator build byte" | "accelerator build entry" | "transaction" | "staged artifact byte" | "staged block" | "staged segment" | "catalog record" | "catalog byte" | "manifest record" | "manifest byte" | "segment record" | "segment byte" | "terminal transaction" | "terminal compaction job" | "completed garbage collection job" | "pinned manifest version lag" | "pinned retired block" | "pinned retired byte" | "snapshot accelerator byte" | "snapshot accelerator entry" | "retired history byte", count: number, limit: number);
|
|
517
|
+
constructor(resource: "lease" | "temp owner" | "temp run" | "temp page" | "temporary run total" | "temporary page total" | "temp owner byte" | "temporary byte" | "compaction job" | "garbage collection job" | "unique-key build" | "full-text build" | "secondary-index build" | "accelerator build byte" | "accelerator build entry" | "transaction" | "staged artifact byte" | "staged block" | "staged segment" | "catalog record" | "catalog byte" | "manifest record" | "manifest byte" | "segment record" | "segment byte" | "terminal transaction" | "terminal compaction job" | "completed garbage collection job" | "pinned manifest version lag" | "pinned retired block" | "pinned retired byte" | "snapshot accelerator byte" | "snapshot accelerator entry" | "retired history byte" | "checkpoint byte", count: number, limit: number);
|
|
510
518
|
}
|
|
511
519
|
export interface LeaseRecord {
|
|
512
520
|
id: string;
|
|
@@ -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,
|