@minnowdb/core 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/engine/artifact-cache.js +3 -2
  2. package/dist/engine/buffered-writer.js +12 -2
  3. package/dist/engine/client.d.ts +8 -2
  4. package/dist/engine/client.js +106 -22
  5. package/dist/engine/database.d.ts +5 -3
  6. package/dist/engine/database.js +2044 -1674
  7. package/dist/engine/errors.d.ts +19 -0
  8. package/dist/engine/errors.js +29 -0
  9. package/dist/engine/index.d.ts +1 -0
  10. package/dist/engine/index.js +4 -0
  11. package/dist/engine/live-accept.js +13 -0
  12. package/dist/engine/live-aggregate.js +54 -16
  13. package/dist/engine/live.d.ts +2 -0
  14. package/dist/engine/live.js +109 -108
  15. package/dist/engine/query-cache.js +17 -2
  16. package/dist/engine/query.d.ts +1 -2
  17. package/dist/engine/query.js +4 -7
  18. package/dist/engine/result-state.d.ts +7 -0
  19. package/dist/engine/result-state.js +15 -0
  20. package/dist/engine/vector.d.ts +4 -0
  21. package/dist/engine/vector.js +14 -14
  22. package/dist/engine/windows.js +15 -14
  23. package/dist/engine/worker-server.d.ts +1 -1
  24. package/dist/engine/worker-server.js +28 -15
  25. package/dist/engine/write-coordinator.js +54 -0
  26. package/dist/storage/indexeddb.js +134 -89
  27. package/dist/storage/opfs/index.d.ts +1 -1
  28. package/dist/storage/opfs/index.js +3 -1
  29. package/dist/storage/opfs/leader.js +20 -9
  30. package/dist/storage/opfs/rpc.js +3 -1
  31. package/dist/storage/opfs/store.d.ts +2 -0
  32. package/dist/storage/opfs/store.js +128 -40
  33. package/dist/storage/toolkit/record-core.js +2 -1
  34. package/dist/storage/types.d.ts +14 -0
  35. package/dist/storage/types.js +21 -0
  36. package/dist/transactions/index.d.ts +3 -0
  37. package/dist/transactions/index.js +4 -1
  38. package/dist/worker-protocol/index.d.ts +1 -1
  39. package/dist/worker-protocol/index.js +1 -1
  40. package/package.json +2 -2
  41. package/dist/date-value.d.ts +0 -20
  42. package/dist/engine/artifact-cache.d.ts +0 -29
  43. package/dist/engine/byte-estimates.d.ts +0 -11
  44. package/dist/engine/cancellation.d.ts +0 -2
  45. package/dist/engine/defaults.d.ts +0 -29
  46. package/dist/engine/group-index.d.ts +0 -33
  47. package/dist/engine/join-index.d.ts +0 -10
  48. package/dist/engine/live-aggregate.d.ts +0 -12
  49. package/dist/engine/live-equal.d.ts +0 -7
  50. package/dist/engine/point-read.d.ts +0 -59
  51. package/dist/engine/query-cache.d.ts +0 -21
  52. package/dist/engine/query-generations.d.ts +0 -8
  53. package/dist/engine/query-identity.d.ts +0 -3
  54. package/dist/engine/result-wire.d.ts +0 -70
  55. package/dist/engine/sort-keys.d.ts +0 -73
  56. package/dist/engine/sql-domains.d.ts +0 -97
  57. package/dist/engine/sql-functions.d.ts +0 -11
  58. package/dist/engine/sql-json.d.ts +0 -40
  59. package/dist/engine/sql-semantics.d.ts +0 -66
  60. package/dist/engine/worker-store-indexeddb.d.ts +0 -2
  61. package/dist/engine/worker-store-memory.d.ts +0 -2
  62. package/dist/engine/worker-store-opfs.d.ts +0 -2
  63. package/dist/engine/write-block-planner.d.ts +0 -19
  64. package/dist/storage/opfs/leader.d.ts +0 -460
  65. package/dist/storage/opfs/rpc.d.ts +0 -82
  66. package/dist/storage/opfs/snapshot-ledger.d.ts +0 -41
@@ -1,6 +1,6 @@
1
1
  import { QueryMemoryContext } from "./memory.js";
2
2
  import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
3
- import { compareSqlValues } from "./sql-semantics.js";
3
+ import { compareSqlValues, defineSqlResultProperty } from "./sql-semantics.js";
4
4
  import { exactNumericBinary, exactNumericValue, isExactNumeric } from "./sql-domains.js";
5
5
  import { throwIfAborted } from "./cancellation.js";
6
6
  class FrameAggregate {
@@ -246,13 +246,12 @@ function* applyPartition(rows, partition, window, result, memory) {
246
246
  }
247
247
  }
248
248
  const row = rows[indexes[position] ?? -1];
249
- if (row !== void 0)
250
- Object.defineProperty(row, window.alias, {
251
- value,
252
- enumerable: true,
253
- writable: true,
254
- configurable: true
255
- });
249
+ if (row !== void 0) {
250
+ if (window.alias in row)
251
+ defineSqlResultProperty(row, window.alias, value);
252
+ else
253
+ row[window.alias] = value;
254
+ }
256
255
  }
257
256
  }
258
257
  function* windowSteps(result, windows, options) {
@@ -286,7 +285,9 @@ function* windowSteps(result, windows, options) {
286
285
  ...window.partitionAliases,
287
286
  ...window.orderAliases.map(({ alias }) => alias)
288
287
  ];
289
- work.tally(rows.length * (48 + aliases.length * 40), "Window sort and partition buffers");
288
+ const peers = group.some((member) => !["ROW_NUMBER", "NTILE", "LAG", "LEAD"].includes(member.name) && (["RANK", "DENSE_RANK", "PERCENT_RANK", "CUME_DIST"].includes(member.name) || member.frame?.unit !== "rows" || member.frame.exclude === "group" || member.frame.exclude === "ties"));
289
+ const range = group.some((member) => member.frame?.unit === "range");
290
+ work.tally(rows.length * (20 + (peers ? 20 : 0) + (range ? 8 : 0) + aliases.length * 40), "Window sort and partition buffers");
290
291
  const columns = aliases.map((alias) => buildSortKeyColumn(rows.length, (index) => rows[index]?.[alias] ?? null));
291
292
  yield;
292
293
  const indexes = sortKeyIndexes(rows.length, columns.map((column, index) => {
@@ -306,14 +307,14 @@ function* windowSteps(result, windows, options) {
306
307
  const size = end - start;
307
308
  const partition = {
308
309
  indexes: indexes.subarray(start, end),
309
- peerStart: new Int32Array(size),
310
- peerEnd: new Int32Array(size),
311
- groupOrdinal: new Int32Array(size),
310
+ peerStart: new Int32Array(peers ? size : 0),
311
+ peerEnd: new Int32Array(peers ? size : 0),
312
+ groupOrdinal: new Int32Array(peers ? size : 0),
312
313
  groupStarts: [],
313
- orderValues: Array.from(indexes.subarray(start, end), (index) => rows[index]?.[window.orderAliases[0]?.alias ?? ""] ?? null)
314
+ orderValues: range ? Array.from(indexes.subarray(start, end), (index) => rows[index]?.[window.orderAliases[0]?.alias ?? ""] ?? null) : []
314
315
  };
315
316
  let begin = 0;
316
- for (let position = 1; position <= size; position += 1) {
317
+ for (let position = 1; peers && position <= size; position += 1) {
317
318
  if (position % 2048 === 0)
318
319
  yield;
319
320
  if (position === size || orders.some((column) => column.compare(indexes[start + begin] ?? 0, indexes[start + position] ?? 0) !== 0)) {
@@ -26,7 +26,7 @@ export type StoreDescriptor = {
26
26
  durability?: "relaxed" | "strict";
27
27
  };
28
28
  /** The cloneable subset of MinnowDatabaseOptions; function-valued seams stay worker-side. */
29
- export type WireDatabaseOptions = Pick<MinnowDatabaseOptions, "compression" | "targetBlockBytes" | "rowsPerBlock" | "maxCommitRetries" | "spillOwnerLeaseMs" | "transactionOwnerLeaseMs" | "transactionIdleTimeoutMs" | "bufferPoolBytes" | "executionMemoryBudgetBytes" | "autoCollectDebtLimitCommits" | "autoCollect" | "autoCompact">;
29
+ export type WireDatabaseOptions = Pick<MinnowDatabaseOptions, "compression" | "targetBlockBytes" | "rowsPerBlock" | "maxCommitRetries" | "coordinateWrites" | "spillOwnerLeaseMs" | "transactionOwnerLeaseMs" | "transactionIdleTimeoutMs" | "bufferPoolBytes" | "executionMemoryBudgetBytes" | "autoCollectDebtLimitCommits" | "autoCollect" | "autoCompact">;
30
30
  export interface DatabaseInitPayload {
31
31
  store: StoreDescriptor;
32
32
  options?: WireDatabaseOptions;
@@ -455,7 +455,9 @@ class DatabaseRpcServer {
455
455
  resolveVersion = resolve;
456
456
  rejectVersion = reject;
457
457
  });
458
+ let snapshotSession;
458
459
  const done = this.database.snapshot(async (session) => {
460
+ snapshotSession = session;
459
461
  resolveVersion(session.version);
460
462
  await new Promise((resolveRelease) => {
461
463
  release = resolveRelease;
@@ -466,7 +468,7 @@ class DatabaseRpcServer {
466
468
  const version = await versionReady;
467
469
  if (release === void 0)
468
470
  throw new Error("Snapshot scope did not install its release");
469
- this.#publishHandle(handleId, { type: "snapshot", release, done });
471
+ this.#publishHandle(handleId, { type: "snapshot", session: snapshotSession, release, done });
470
472
  return { handleId, version };
471
473
  } catch (error) {
472
474
  release?.();
@@ -494,6 +496,9 @@ class DatabaseRpcServer {
494
496
  }
495
497
  switch (handle.type) {
496
498
  case "snapshot": {
499
+ if (method === "query") {
500
+ return this.#querySession(handle.session, args, context);
501
+ }
497
502
  if (method !== "close")
498
503
  throw new Error(`Unsupported snapshot method: ${method}`);
499
504
  handle.release();
@@ -537,19 +542,21 @@ class DatabaseRpcServer {
537
542
  }
538
543
  }
539
544
  }
545
+ async #querySession(session, args, context) {
546
+ const [sql, options, reportStats = false] = args;
547
+ return new ColumnarResult(encodeQueryResult(await session.query(sql, {
548
+ ...options,
549
+ signal: context.signal,
550
+ ...reportStats ? {
551
+ onStats: (stats) => {
552
+ this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
553
+ }
554
+ } : {}
555
+ })));
556
+ }
540
557
  async #callWriteHandle(handleId, handle, method, args, context) {
541
- if (method === "query") {
542
- const [sql, options, reportStats = false] = args;
543
- return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, {
544
- ...options,
545
- signal: context.signal,
546
- ...reportStats ? {
547
- onStats: (stats) => {
548
- this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
549
- }
550
- } : {}
551
- })));
552
- }
558
+ if (method === "query")
559
+ return this.#querySession(handle.session, args, context);
553
560
  if (method === "execute") {
554
561
  const [sql, params] = args;
555
562
  return handle.session.execute(sql, params);
@@ -665,15 +672,21 @@ class DatabaseRpcServer {
665
672
  case "subscribe": {
666
673
  const subscriptionId = this.#claimHandleId(args[0]);
667
674
  const query = args[1];
675
+ const patches = args[2]?.patches === true;
668
676
  let subscription;
669
677
  try {
670
678
  subscription = await handle.set.subscribe(query, {
671
679
  onChange: (result, delivery) => {
672
- const encoded = encodeQueryResult(result);
680
+ const patch = patches && !delivery.initial && delivery.retained !== void 0;
681
+ const encoded = encodeQueryResult(patch ? {
682
+ columns: result.columns,
683
+ columnDomains: result.columnDomains,
684
+ rows: result.rows.filter((_, index) => (delivery.retained?.[index] ?? -1) < 0)
685
+ } : result);
673
686
  const retained = delivery.retained?.slice();
674
687
  if (retained !== void 0)
675
688
  encoded.transfer.push(retained.buffer);
676
- this.scope.postMessage(rpcEvent(subscriptionId, "change", {
689
+ this.scope.postMessage(rpcEvent(subscriptionId, patch ? "patch" : "change", {
677
690
  result: encoded.payload,
678
691
  delivery: retained === void 0 ? delivery : { ...delivery, retained }
679
692
  }), { transfer: encoded.transfer });
@@ -0,0 +1,54 @@
1
+ const anonymous = /* @__PURE__ */ new WeakMap();
2
+ const named = /* @__PURE__ */ new Map();
3
+ async function coordinateWrite(store, run, signal) {
4
+ signal.throwIfAborted();
5
+ const name = store.liveQueryChannelName;
6
+ let queue = name === void 0 ? anonymous.get(store) : named.get(name);
7
+ if (queue === void 0) {
8
+ queue = { tail: Promise.resolve() };
9
+ if (name === void 0)
10
+ anonymous.set(store, queue);
11
+ else
12
+ named.set(name, queue);
13
+ }
14
+ const locks = typeof navigator === "undefined" ? void 0 : navigator.locks;
15
+ const lockController = new AbortController();
16
+ let admitted = false;
17
+ const enter = () => {
18
+ signal.throwIfAborted();
19
+ admitted = true;
20
+ return run();
21
+ };
22
+ const operation = queue.tail.then(async () => {
23
+ signal.throwIfAborted();
24
+ return name !== void 0 && locks !== void 0 ? await locks.request(`minnowdb-write:${name}`, { signal: lockController.signal }, enter) : await enter();
25
+ });
26
+ const settled = operation.then(() => void 0, () => void 0);
27
+ queue.tail = settled;
28
+ void settled.then(() => {
29
+ if (queue.tail !== settled)
30
+ return;
31
+ if (name === void 0)
32
+ anonymous.delete(store);
33
+ else
34
+ named.delete(name);
35
+ });
36
+ let abort = () => void 0;
37
+ const cancelled = new Promise((_resolve, reject) => {
38
+ abort = () => {
39
+ if (!admitted) {
40
+ lockController.abort(signal.reason);
41
+ reject(signal.reason instanceof Error ? signal.reason : new Error("Write admission cancelled", { cause: signal.reason }));
42
+ }
43
+ };
44
+ signal.addEventListener("abort", abort, { once: true });
45
+ });
46
+ try {
47
+ return await Promise.race([operation, cancelled]);
48
+ } finally {
49
+ signal.removeEventListener("abort", abort);
50
+ }
51
+ }
52
+ export {
53
+ coordinateWrite
54
+ };
@@ -1791,11 +1791,9 @@ class IndexedDbBlockStore {
1791
1791
  const transaction = this.#transaction("catalog", "readonly");
1792
1792
  const store = transaction.objectStore("catalog");
1793
1793
  const records = [];
1794
- await visitObjectStoreSequentially(store, (value, key) => {
1795
- if (typeof key === "string" && key.startsWith(TABLE_ID_PREFIX)) {
1796
- records.push(asTableRecord(value, key));
1797
- }
1798
- });
1794
+ await visitObjectStoreReadBatches(store, (value, key) => {
1795
+ records.push(asTableRecord(value, storageKeyLocation(key)));
1796
+ }, { prefix: TABLE_ID_PREFIX });
1799
1797
  await transactionDone(transaction);
1800
1798
  return records.sort((left, right) => left.name.localeCompare(right.name));
1801
1799
  }
@@ -3990,7 +3988,7 @@ class IndexedDbBlockStore {
3990
3988
  const currentVersionValue = await requestResult(catalog.get(CURRENT_MANIFEST_KEY));
3991
3989
  const currentVersion = asOptionalManifestVersion(currentVersionValue, CURRENT_MANIFEST_KEY) ?? null;
3992
3990
  const leaseCutoff = Date.parse(current.leaseCutoff);
3993
- await assertGarbageCollectionPinsAvailableInTransaction(transaction, currentVersion, leaseCutoff);
3991
+ const pinnedVersions = await garbageCollectionPinsInTransaction(transaction, currentVersion, leaseCutoff);
3994
3992
  const prunedManifestVersions = [];
3995
3993
  const alreadyPrunedManifestVersions = [];
3996
3994
  const retainedManifestVersions = [];
@@ -4018,7 +4016,7 @@ class IndexedDbBlockStore {
4018
4016
  missingManifestVersions.push(version);
4019
4017
  else if (asStoredManifestRecord(manifestValue, version).prunedAt !== void 0) {
4020
4018
  alreadyPrunedManifestVersions.push(version);
4021
- } else if (await isManifestVersionPinnedInTransaction(transaction, version, currentVersion, leaseCutoff))
4019
+ } else if (pinnedVersions.has(version))
4022
4020
  retainedManifestVersions.push(version);
4023
4021
  else {
4024
4022
  const previous = asStoredManifestRecord(manifestValue, version);
@@ -4031,12 +4029,12 @@ class IndexedDbBlockStore {
4031
4029
  remaining -= 1;
4032
4030
  }
4033
4031
  const prunedManifestVersionSet = new Set(prunedManifestVersions);
4034
- await assertRemainingManifestRecordsAvailable(transaction, prunedManifestVersionSet);
4032
+ const readableVersions = await assertRemainingManifestRecordsAvailable(transaction, prunedManifestVersionSet);
4035
4033
  let segmentIndex = current.cursor.segmentIndex;
4036
4034
  const segmentIdsToExamine = manifestIndex === current.candidateManifestVersions.length ? current.candidateSegmentIds.slice(segmentIndex, segmentIndex + remaining) : [];
4037
4035
  const blockCapacity = Math.max(0, remaining - segmentIdsToExamine.length);
4038
4036
  const blockIdsToExamine = manifestIndex === current.candidateManifestVersions.length && segmentIndex + segmentIdsToExamine.length === current.candidateSegmentIds.length ? current.candidateBlockIds.slice(current.cursor.blockIndex, current.cursor.blockIndex + blockCapacity) : [];
4039
- const roots = await collectBoundedPhysicalRootsInTransaction(transaction, segmentIdsToExamine, blockIdsToExamine, prunedManifestVersionSet);
4037
+ const roots = await collectBoundedPhysicalRootsInTransaction(transaction, segmentIdsToExamine, blockIdsToExamine, readableVersions);
4040
4038
  while (remaining > 0 && manifestIndex === current.candidateManifestVersions.length && segmentIndex < current.candidateSegmentIds.length) {
4041
4039
  const id = current.candidateSegmentIds[segmentIndex];
4042
4040
  if (id === void 0)
@@ -4065,7 +4063,7 @@ class IndexedDbBlockStore {
4065
4063
  if (remaining > 0 && manifestIndex === current.candidateManifestVersions.length && segmentIndex === current.candidateSegmentIds.length && blockIndex === current.candidateBlockIds.length) {
4066
4064
  const candidates = new Set(current.candidateTransactionIds.slice(transactionIndex));
4067
4065
  const segmentOwners = /* @__PURE__ */ new Set();
4068
- await visitObjectStoreSequentially(segmentStore, (value2) => {
4066
+ await visitObjectStoreReadBatches(segmentStore, (value2) => {
4069
4067
  const owner = asSegmentRecord(value2).transactionId;
4070
4068
  if (candidates.has(owner))
4071
4069
  segmentOwners.add(owner);
@@ -6309,6 +6307,7 @@ class IndexedDbBlockStore {
6309
6307
  if (this.#closed)
6310
6308
  throw new Error("This IndexedDB store connection is closed");
6311
6309
  const transaction = mode === "readwrite" ? this.#db.transaction(stores, mode, { durability: this.#durability }) : this.#db.transaction(stores, mode);
6310
+ void transactionDone(transaction).catch(() => void 0);
6312
6311
  const storeList = typeof stores === "string" ? [stores] : stores;
6313
6312
  if (mode === "readwrite" && !options.allowSnapshotImport && storeList.includes("catalog")) {
6314
6313
  const catalog = transaction.objectStore("catalog");
@@ -6378,6 +6377,7 @@ async function validateCurrentIndexedDbSchema(database) {
6378
6377
  let transaction;
6379
6378
  try {
6380
6379
  transaction = database.transaction([...indexedDbStoreNames], "readonly");
6380
+ void transactionDone(transaction).catch(() => void 0);
6381
6381
  for (const storeName of indexedDbStoreNames) {
6382
6382
  const store = transaction.objectStore(storeName);
6383
6383
  if (store.keyPath !== null || store.autoIncrement) {
@@ -6674,6 +6674,44 @@ function visitObjectStoreSequentially(store, visit, direction = "next") {
6674
6674
  };
6675
6675
  });
6676
6676
  }
6677
+ async function visitObjectStoreReadBatches(store, visit, partition) {
6678
+ const kind = partition !== void 0 && "kind" in partition ? partition.kind : void 0;
6679
+ const prefix = partition !== void 0 && "prefix" in partition ? partition.prefix : void 0;
6680
+ if (typeof IDBKeyRange === "undefined") {
6681
+ if (kind === MANIFEST_BLOCK) {
6682
+ await visitManifestBlockRecords(store, (record) => Promise.resolve(visit(record, [kind, record.blockId])));
6683
+ return;
6684
+ }
6685
+ await visitObjectStoreSequentially(store, (value, key) => {
6686
+ if (prefix !== void 0) {
6687
+ if (typeof key === "string" && key.startsWith(prefix))
6688
+ return visit(value, key);
6689
+ } else if (kind === void 0 || compareStructuredKind(key, kind) === 0)
6690
+ return visit(value, key);
6691
+ });
6692
+ return;
6693
+ }
6694
+ const limit = 128;
6695
+ const upper = prefix !== void 0 ? prefix.slice(0, -1) + String.fromCharCode(prefix.charCodeAt(prefix.length - 1) + 1) : kind === void 0 ? void 0 : [kind + "\0"];
6696
+ let lower = prefix ?? (kind === void 0 ? void 0 : [kind]);
6697
+ let exclusive = false;
6698
+ for (; ; ) {
6699
+ const range = lower === void 0 ? void 0 : upper === void 0 ? IDBKeyRange.lowerBound(lower, exclusive) : IDBKeyRange.bound(lower, upper, exclusive, true);
6700
+ const [keys, values] = await Promise.all([
6701
+ requestResult(store.getAllKeys(range, limit)),
6702
+ requestResult(store.getAll(range, limit))
6703
+ ]);
6704
+ if (keys.length !== values.length)
6705
+ throw corruption(store.name, "batch keys do not match values");
6706
+ await Promise.all(keys.map(async (key, index) => {
6707
+ await visit(values[index], key);
6708
+ }));
6709
+ if (keys.length < limit)
6710
+ return;
6711
+ lower = keys.at(-1);
6712
+ exclusive = true;
6713
+ }
6714
+ }
6677
6715
  function visitIndexPartitionSequentially(index, key, visit) {
6678
6716
  return new Promise((resolve, reject) => {
6679
6717
  const request = index.openCursor(key);
@@ -6902,16 +6940,19 @@ function transactionFailure(transaction) {
6902
6940
  transaction.addEventListener("error", () => reject(transaction.error ?? new Error("Transaction failed")), { once: true });
6903
6941
  });
6904
6942
  }
6943
+ const transactionCompletions = /* @__PURE__ */ new WeakMap();
6905
6944
  function transactionDone(transaction) {
6906
- return new Promise((resolve, reject) => {
6945
+ const existing = transactionCompletions.get(transaction);
6946
+ if (existing !== void 0)
6947
+ return existing;
6948
+ const done = new Promise((resolve, reject) => {
6907
6949
  transaction.addEventListener("complete", () => resolve(), { once: true });
6908
6950
  transaction.addEventListener("abort", () => reject(transaction.error ?? new Error("Transaction aborted")), {
6909
6951
  once: true
6910
6952
  });
6911
- transaction.addEventListener("error", () => reject(transaction.error ?? new Error("Transaction failed")), {
6912
- once: true
6913
- });
6914
6953
  });
6954
+ transactionCompletions.set(transaction, done);
6955
+ return done;
6915
6956
  }
6916
6957
  async function ignoreAbort(transaction) {
6917
6958
  try {
@@ -8367,13 +8408,21 @@ async function resolveManifestBlockSetInTransaction(catalog, version) {
8367
8408
  const blockIds = /* @__PURE__ */ new Set();
8368
8409
  if (version === null)
8369
8410
  return blockIds;
8370
- await visitManifestBlockRecords(catalog, (record) => {
8411
+ await visitManifestBlockReadBatches(catalog, (record) => {
8371
8412
  if (manifestBlockVisibleAt(record, version))
8372
8413
  blockIds.add(record.blockId);
8373
8414
  return void 0;
8374
8415
  });
8375
8416
  return blockIds;
8376
8417
  }
8418
+ async function visitManifestBlockReadBatches(catalog, visit) {
8419
+ await visitObjectStoreReadBatches(catalog, (value, key) => {
8420
+ if (!Array.isArray(key) || key.length !== 2 || typeof key[1] !== "string") {
8421
+ throw corruption(MANIFEST_BLOCK, "record key is invalid");
8422
+ }
8423
+ visit(asManifestBlockRecord(value, key[1]));
8424
+ }, { kind: MANIFEST_BLOCK });
8425
+ }
8377
8426
  function visitManifestBlockRecords(catalog, visit) {
8378
8427
  return new Promise((resolve, reject) => {
8379
8428
  const request = catalog.openCursor();
@@ -9263,19 +9312,24 @@ async function compactionJobRemovalPreservesProvenance(transaction, removedJob)
9263
9312
  }
9264
9313
  return true;
9265
9314
  }
9266
- async function assertGarbageCollectionPinsAvailableInTransaction(transaction, currentVersion, leaseCutoff) {
9267
- await assertSnapshotAvailableInTransaction(transaction, currentVersion);
9268
- await visitObjectStoreSequentially(transaction.objectStore("transactions"), async (value) => {
9315
+ async function garbageCollectionPinsInTransaction(transaction, currentVersion, leaseCutoff) {
9316
+ const versions = /* @__PURE__ */ new Set();
9317
+ const pin = (version) => {
9318
+ if (version !== null)
9319
+ versions.add(version);
9320
+ };
9321
+ pin(currentVersion);
9322
+ await visitObjectStoreReadBatches(transaction.objectStore("transactions"), (value) => {
9269
9323
  const record = asTransactionRecord(value);
9270
9324
  if (record.status === "active") {
9271
- await assertSnapshotAvailableInTransaction(transaction, record.snapshotVersion);
9325
+ pin(record.snapshotVersion);
9272
9326
  }
9273
9327
  });
9274
- await visitObjectStoreSequentially(transaction.objectStore("leases"), async (value) => {
9328
+ await visitObjectStoreSequentially(transaction.objectStore("leases"), (value) => {
9275
9329
  const lease = asLeaseRecord(value);
9276
9330
  const expiresAt = Date.parse(lease.expiresAt);
9277
9331
  if (!Number.isFinite(expiresAt) || expiresAt > leaseCutoff) {
9278
- await assertSnapshotAvailableInTransaction(transaction, lease.manifestVersion);
9332
+ pin(lease.manifestVersion);
9279
9333
  }
9280
9334
  });
9281
9335
  await visitObjectStoreSequentially(transaction.objectStore("gc"), async (value, key) => {
@@ -9284,7 +9338,7 @@ async function assertGarbageCollectionPinsAvailableInTransaction(transaction, cu
9284
9338
  return;
9285
9339
  if (isTerminalCompactionJob(job))
9286
9340
  return;
9287
- await assertSnapshotAvailableInTransaction(transaction, job.sourceManifestVersion);
9341
+ pin(job.sourceManifestVersion);
9288
9342
  if (job.transactionId === null)
9289
9343
  return;
9290
9344
  const transactionValue = await requestResult(transaction.objectStore("transactions").get(job.transactionId));
@@ -9296,8 +9350,11 @@ async function assertGarbageCollectionPinsAvailableInTransaction(transaction, cu
9296
9350
  if (linkedTransaction.committedVersion === null) {
9297
9351
  throw new Error(`Committed transaction has no manifest version: ${linkedTransaction.id}`);
9298
9352
  }
9299
- await assertSnapshotAvailableInTransaction(transaction, linkedTransaction.committedVersion);
9353
+ pin(linkedTransaction.committedVersion);
9300
9354
  });
9355
+ for (const version of versions)
9356
+ await assertSnapshotAvailableInTransaction(transaction, version);
9357
+ return versions;
9301
9358
  }
9302
9359
  async function assertPendingArtifactsAvailableInTransaction(transaction, record, validateBlocks = true, validateSegments = true, validateCurrentTable = false) {
9303
9360
  const [blockKeys, segmentValues] = await Promise.all([
@@ -9349,27 +9406,25 @@ async function assertPendingArtifactsAvailableInTransaction(transaction, record,
9349
9406
  }
9350
9407
  async function assertRemainingManifestRecordsAvailable(transaction, newlyPrunedVersions) {
9351
9408
  const readableVersions = [];
9352
- await visitObjectStoreSequentially(transaction.objectStore("manifests"), (value, key) => {
9409
+ await visitObjectStoreReadBatches(transaction.objectStore("manifests"), (value, key) => {
9353
9410
  if (typeof key !== "number")
9354
9411
  throw corruption("manifests", "record key is invalid");
9355
9412
  const record = asStoredManifestRecord(value, key);
9356
- if (record.prunedAt !== void 0 || newlyPrunedVersions.has(record.version))
9357
- return;
9358
- readableVersions.push(record.version);
9413
+ if (record.prunedAt === void 0 && !newlyPrunedVersions.has(record.version)) {
9414
+ readableVersions.push(record.version);
9415
+ }
9359
9416
  });
9360
- const referenced = /* @__PURE__ */ new Map();
9361
- await visitManifestBlockRecords(transaction.objectStore("catalog"), (record) => {
9417
+ await visitObjectStoreReadBatches(transaction.objectStore("catalog"), async (value, key) => {
9418
+ if (!Array.isArray(key) || key.length !== 2 || typeof key[1] !== "string") {
9419
+ throw corruption(MANIFEST_BLOCK, "record key is invalid");
9420
+ }
9421
+ const record = asManifestBlockRecord(value, key[1]);
9362
9422
  const version = firstOverlappingManifestVersion(record, readableVersions);
9363
- if (version !== void 0)
9364
- referenced.set(record.blockId, version);
9365
- return void 0;
9366
- });
9367
- const blockStore = transaction.objectStore("blocks");
9368
- for (const [id, version] of referenced) {
9369
- if (await requestResult(blockStore.getKey(id)) === void 0) {
9423
+ if (version !== void 0 && await requestResult(transaction.objectStore("blocks").getKey(record.blockId)) === void 0) {
9370
9424
  throw new SnapshotManifestMissingError(version);
9371
9425
  }
9372
- }
9426
+ }, { kind: MANIFEST_BLOCK });
9427
+ return readableVersions;
9373
9428
  }
9374
9429
  function compactionJobKey(id) {
9375
9430
  return `${COMPACTION_JOB_KEY_PREFIX}${id}`;
@@ -9774,52 +9829,25 @@ async function assertGarbageCollectionCandidateProvenanceInTransaction(transacti
9774
9829
  throw new Error(`Garbage collection segment candidate has no persisted provenance: ${id}`);
9775
9830
  }
9776
9831
  }
9777
- async function isManifestVersionPinnedInTransaction(transaction, version, currentVersion, leaseCutoff) {
9778
- if (currentVersion === version)
9779
- return true;
9780
- const transactionPinned = await visitObjectStoreSequentially(transaction.objectStore("transactions"), (value) => {
9781
- const record = asTransactionRecord(value);
9782
- return record.status === "active" && record.snapshotVersion === version;
9783
- });
9784
- if (transactionPinned)
9785
- return true;
9786
- const leasePinned = await visitObjectStoreSequentially(transaction.objectStore("leases"), (value) => {
9787
- const lease = asLeaseRecord(value);
9788
- const expiresAt = Date.parse(lease.expiresAt);
9789
- return lease.manifestVersion === version && (!Number.isFinite(expiresAt) || expiresAt > leaseCutoff);
9790
- });
9791
- if (leasePinned)
9792
- return true;
9793
- return visitObjectStoreSequentially(transaction.objectStore("gc"), async (value, key) => {
9794
- const job = asCompactionJobAtMaintenanceKey(value, key);
9795
- if (job === void 0)
9796
- return false;
9797
- if (isTerminalCompactionJob(job))
9798
- return false;
9799
- if (job.sourceManifestVersion === version)
9800
- return true;
9801
- if (job.transactionId === null)
9802
- return false;
9803
- const transactionValue = await requestResult(transaction.objectStore("transactions").get(job.transactionId));
9804
- if (transactionValue === void 0)
9805
- return false;
9806
- const linkedTransaction = asTransactionRecord(transactionValue);
9807
- if (linkedTransaction.status !== "committed")
9808
- return false;
9809
- if (linkedTransaction.committedVersion === null) {
9810
- throw new Error(`Committed transaction has no manifest version: ${linkedTransaction.id}`);
9811
- }
9812
- return linkedTransaction.committedVersion === version;
9813
- });
9814
- }
9815
- async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSegmentIds, candidateBlockIds, newlyPrunedVersions) {
9832
+ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSegmentIds, candidateBlockIds, readableVersions) {
9816
9833
  const candidateSegments = new Set(candidateSegmentIds);
9817
9834
  const candidateBlocks = new Set(candidateBlockIds);
9818
9835
  const directSegmentRoots = /* @__PURE__ */ new Set();
9819
- await visitObjectStoreSequentially(transaction.objectStore("transactions"), (value) => {
9836
+ const ownerBlockRoots = /* @__PURE__ */ new Set();
9837
+ let ownerBlockRootsComplete = true;
9838
+ const rememberOwnerBlocks = (ids) => {
9839
+ for (const id of ids) {
9840
+ if (ownerBlockRoots.size < 4096)
9841
+ ownerBlockRoots.add(id);
9842
+ else if (!ownerBlockRoots.has(id))
9843
+ ownerBlockRootsComplete = false;
9844
+ }
9845
+ };
9846
+ await visitIndexPartitionSequentially(transaction.objectStore("transactions").index(TRANSACTION_STATUS_INDEX), "active", (value) => {
9820
9847
  const record = asTransactionRecord(value);
9821
9848
  if (record.status !== "active")
9822
9849
  return;
9850
+ rememberOwnerBlocks(record.pendingBlockIds);
9823
9851
  for (const id of record.pendingSegmentIds)
9824
9852
  if (candidateSegments.has(id))
9825
9853
  directSegmentRoots.add(id);
@@ -9830,6 +9858,8 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9830
9858
  return;
9831
9859
  if (isTerminalCompactionJob(job))
9832
9860
  return;
9861
+ rememberOwnerBlocks(job.sourceBlockIds);
9862
+ rememberOwnerBlocks(job.outputBlockIds);
9833
9863
  for (const id of job.sourceSegmentIds)
9834
9864
  if (candidateSegments.has(id))
9835
9865
  directSegmentRoots.add(id);
@@ -9843,20 +9873,24 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9843
9873
  const cached = directBlockRootCache.get(id);
9844
9874
  if (cached !== void 0)
9845
9875
  return cached;
9846
- let rooted = await visitObjectStoreSequentially(transaction.objectStore("transactions"), (value) => {
9847
- const record = asTransactionRecord(value);
9848
- return record.status === "active" && record.pendingBlockIds.includes(id);
9849
- });
9850
- if (!rooted) {
9851
- rooted = await visitObjectStoreSequentially(transaction.objectStore("gc"), (value, key) => {
9852
- const job = asCompactionJobAtMaintenanceKey(value, key);
9853
- if (job === void 0)
9854
- return false;
9855
- return !isTerminalCompactionJob(job) && (job.sourceBlockIds.includes(id) || job.outputBlockIds.includes(id));
9876
+ let rooted = ownerBlockRoots.has(id);
9877
+ if (!rooted && !ownerBlockRootsComplete) {
9878
+ rooted = await visitIndexPartitionSequentially(transaction.objectStore("transactions").index(TRANSACTION_STATUS_INDEX), "active", (value) => {
9879
+ const record = asTransactionRecord(value);
9880
+ return record.status === "active" && record.pendingBlockIds.includes(id);
9856
9881
  });
9882
+ if (!rooted) {
9883
+ rooted = await visitObjectStoreSequentially(transaction.objectStore("gc"), (value, key) => {
9884
+ const job = asCompactionJobAtMaintenanceKey(value, key);
9885
+ if (job === void 0)
9886
+ return false;
9887
+ return !isTerminalCompactionJob(job) && (job.sourceBlockIds.includes(id) || job.outputBlockIds.includes(id));
9888
+ });
9889
+ }
9857
9890
  }
9858
9891
  if (!rooted) {
9859
- rooted = await findReadableManifestBlock(transaction, [id], newlyPrunedVersions) !== void 0;
9892
+ const value = await requestResult(transaction.objectStore("catalog").get(manifestBlockKey(id)));
9893
+ rooted = value !== void 0 && firstOverlappingManifestVersion(asManifestBlockRecord(value, id), readableVersions) !== void 0;
9860
9894
  }
9861
9895
  if (directBlockRootCache.size < 4096)
9862
9896
  directBlockRootCache.set(id, rooted);
@@ -9893,6 +9927,17 @@ async function collectBoundedPhysicalRootsInTransaction(transaction, candidateSe
9893
9927
  rootedBlockIds.add(id);
9894
9928
  for (const id of directSegmentRoots)
9895
9929
  rootedSegmentIds.add(id);
9930
+ if (candidateBlocks.size > 0) {
9931
+ await visitObjectStoreReadBatches(segmentStore, (value) => {
9932
+ const segment = asSegmentRecord(value);
9933
+ if (candidateSegments.has(segment.id) && !rootedSegmentIds.has(segment.id))
9934
+ return;
9935
+ for (const id of segmentBlockIds(segment)) {
9936
+ if (candidateBlocks.has(id))
9937
+ rootedBlockIds.add(id);
9938
+ }
9939
+ });
9940
+ }
9896
9941
  return { blockIds: rootedBlockIds, segmentIds: rootedSegmentIds };
9897
9942
  }
9898
9943
  function segmentBlockIds(segment) {
@@ -10929,7 +10974,7 @@ async function assertPinnedHistoryAdmission(transaction, input) {
10929
10974
  return;
10930
10975
  let pinnedBlockCount = 0;
10931
10976
  let pinnedBytes = 0;
10932
- await visitManifestBlockRecords(transaction.objectStore("catalog"), (record) => {
10977
+ await visitManifestBlockReadBatches(transaction.objectStore("catalog"), (record) => {
10933
10978
  const removedVersion = input.prospectiveRemovedBlockIds?.has(record.blockId) ? input.currentVersion : record.removedVersion;
10934
10979
  if (removedVersion === null)
10935
10980
  return void 0;
@@ -1,3 +1,3 @@
1
1
  export { OpfsBlockStore, deleteOpfsDatabase, type OpfsBlockStoreOptions } from "./store.js";
2
- export { OpfsUncertainOutcomeError } from "../types.js";
2
+ export { OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError, } from "../types.js";
3
3
  export { OpfsTree, type ReadFileOptions, type WriteFileOptions } from "./files.js";
@@ -1,8 +1,10 @@
1
1
  import { OpfsBlockStore, deleteOpfsDatabase } from "./store.js";
2
- import { OpfsUncertainOutcomeError } from "../types.js";
2
+ import { OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError } from "../types.js";
3
3
  import { OpfsTree } from "./files.js";
4
4
  export {
5
5
  OpfsBlockStore,
6
+ OpfsCoordinationError,
7
+ OpfsDatabaseInUseError,
6
8
  OpfsTree,
7
9
  OpfsUncertainOutcomeError,
8
10
  deleteOpfsDatabase