@minnowdb/core 0.9.1 → 0.10.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 (47) hide show
  1. package/dist/engine/auto-store.d.ts +52 -0
  2. package/dist/engine/auto-store.js +157 -0
  3. package/dist/engine/buffered-writer.d.ts +2 -0
  4. package/dist/engine/buffered-writer.js +15 -2
  5. package/dist/engine/client-audit-harness.js +123 -0
  6. package/dist/engine/client.d.ts +55 -6
  7. package/dist/engine/client.js +176 -46
  8. package/dist/engine/database.d.ts +15 -1
  9. package/dist/engine/database.js +1276 -251
  10. package/dist/engine/errors.d.ts +61 -2
  11. package/dist/engine/errors.js +116 -3
  12. package/dist/engine/index.d.ts +1 -0
  13. package/dist/engine/index.js +2 -0
  14. package/dist/engine/live.d.ts +24 -1
  15. package/dist/engine/live.js +33 -9
  16. package/dist/engine/scope-write-set.js +36 -0
  17. package/dist/engine/worker-auto.d.ts +1 -0
  18. package/dist/engine/worker-auto.js +3 -0
  19. package/dist/engine/worker-host.d.ts +2 -1
  20. package/dist/engine/worker-host.js +19 -1
  21. package/dist/engine/worker-server.d.ts +53 -1
  22. package/dist/engine/worker-server.js +122 -19
  23. package/dist/engine/worker-store-auto.js +36 -0
  24. package/dist/engine/worker-store-opfs.js +3 -2
  25. package/dist/engine/write-coordinator.js +44 -2
  26. package/dist/storage/indexeddb-audit-helpers.js +269 -0
  27. package/dist/storage/indexeddb.js +599 -374
  28. package/dist/storage/opfs/coordination-helpers.js +54 -0
  29. package/dist/storage/opfs/index.d.ts +1 -1
  30. package/dist/storage/opfs/index.js +3 -2
  31. package/dist/storage/opfs/leader.js +243 -17
  32. package/dist/storage/opfs/power-loss-model.js +62 -0
  33. package/dist/storage/opfs/rpc.js +24 -43
  34. package/dist/storage/opfs/store.d.ts +32 -0
  35. package/dist/storage/opfs/store.js +531 -65
  36. package/dist/storage/toolkit/record-core.js +67 -38
  37. package/dist/storage/toolkit/wal.js +16 -0
  38. package/dist/storage/toolkit/wire.d.ts +1 -1
  39. package/dist/storage/toolkit/wire.js +4 -4
  40. package/dist/storage/types.d.ts +31 -10
  41. package/dist/storage/types.js +27 -16
  42. package/dist/testing/opfs-shim.js +14 -6
  43. package/dist/transactions/index.d.ts +19 -0
  44. package/dist/transactions/index.js +99 -25
  45. package/dist/worker-protocol/index.d.ts +50 -2
  46. package/dist/worker-protocol/index.js +106 -4
  47. package/package.json +7 -2
@@ -1,5 +1,5 @@
1
1
  import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
2
- import { parseRpcRequest, MAX_DATABASE_RPC_IN_FLIGHT, rpcEvent, rpcFailure, rpcResult, serializeError } from "../worker-protocol/index.js";
2
+ import { MAX_DATABASE_RPC_IN_FLIGHT, parseRpcRequest, rpcEvent, rpcFailure, rpcResult, serializeError, MIN_WORKER_KEEPALIVE_INTERVAL_MS, WORKER_KEEPALIVE_INTERVAL_MS, workerErrorEvent, workerKeepaliveEvent } from "../worker-protocol/index.js";
3
3
  import { MinnowDatabase } from "./database.js";
4
4
  import { encodeQueryResult, encodeQueryRows } from "./result-wire.js";
5
5
  import { deserializeSchema, serializeMigrationSteps } from "./schema-wire.js";
@@ -156,6 +156,40 @@ function abortError(message) {
156
156
  error.name = "AbortError";
157
157
  return error;
158
158
  }
159
+ const errorSinks = /* @__PURE__ */ new WeakMap();
160
+ function workerErrorReporter(scope) {
161
+ const existing = errorSinks.get(scope);
162
+ if (existing !== void 0)
163
+ return existing;
164
+ const report = (kind, error, context) => {
165
+ try {
166
+ scope.postMessage(workerErrorEvent({ kind, context, error: serializeError(error) }));
167
+ } catch {
168
+ }
169
+ };
170
+ errorSinks.set(scope, report);
171
+ const listen = (type, listener) => {
172
+ try {
173
+ scope.addEventListener(type, listener);
174
+ } catch {
175
+ }
176
+ };
177
+ listen("error", (event) => {
178
+ const where = typeof event.filename === "string" && event.filename !== "" ? `${event.filename}:${String(event.lineno)}:${String(event.colno)}` : "worker global scope";
179
+ const error = event.error instanceof Error ? event.error : new Error(typeof event.message === "string" ? event.message : "Uncaught error");
180
+ report("uncaught", error, where);
181
+ event.preventDefault?.();
182
+ });
183
+ listen("unhandledrejection", (event) => {
184
+ report("unhandled-rejection", event.reason, "worker global scope");
185
+ event.preventDefault?.();
186
+ });
187
+ listen("messageerror", (event) => {
188
+ report("messageerror", new Error("A frame sent to the database worker could not be deserialized"), "worker inbound channel");
189
+ event.preventDefault?.();
190
+ });
191
+ return report;
192
+ }
159
193
  const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
160
194
  const DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS = 3e4;
161
195
  const MAX_TIMER_DELAY_MS = 2147483647;
@@ -174,10 +208,16 @@ class DatabaseRpcServer {
174
208
  #inFlightRpcDrain;
175
209
  #resolveInFlightRpcDrain;
176
210
  #writeHandleIdleTimeoutMs;
211
+ #report;
212
+ #storeKind;
213
+ #keepaliveIntervalMs;
177
214
  constructor(database, scope, options) {
178
215
  this.database = database;
179
216
  this.scope = scope;
180
217
  this.options = options;
218
+ this.#report = workerErrorReporter(scope);
219
+ this.#storeKind = options.storeKind;
220
+ this.#keepaliveIntervalMs = keepaliveInterval(options.keepaliveIntervalMs);
181
221
  this.#writeHandleIdleTimeoutMs = options.writeHandleIdleTimeoutMs ?? DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS;
182
222
  if (!Number.isSafeInteger(this.#writeHandleIdleTimeoutMs) || this.#writeHandleIdleTimeoutMs <= 0 || this.#writeHandleIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
183
223
  throw new RangeError(`Worker write-handle idle timeout must be a positive whole number no greater than ${String(MAX_TIMER_DELAY_MS)}`);
@@ -185,7 +225,10 @@ class DatabaseRpcServer {
185
225
  }
186
226
  async handle(request) {
187
227
  if (request.kind === "rpc-init") {
188
- this.scope.postMessage(rpcResult(request.requestId, { ready: true }));
228
+ this.scope.postMessage(rpcResult(request.requestId, {
229
+ ready: true,
230
+ ...this.#storeKind === void 0 ? {} : { store: this.#storeKind }
231
+ }));
189
232
  return;
190
233
  }
191
234
  if (request.kind === "rpc-cancel") {
@@ -199,6 +242,13 @@ class DatabaseRpcServer {
199
242
  }
200
243
  if (!bypassLimit)
201
244
  this.#inFlightRpcCount += 1;
245
+ const keepalive = setInterval(() => {
246
+ try {
247
+ this.scope.postMessage(workerKeepaliveEvent(request.requestId));
248
+ } catch {
249
+ }
250
+ }, this.#keepaliveIntervalMs);
251
+ unrefTimer(keepalive);
202
252
  const abort = request.method === "query" || request.method === "execute" ? new AbortController() : void 0;
203
253
  if (abort !== void 0)
204
254
  this.#requestAborts.set(request.requestId, abort);
@@ -226,6 +276,7 @@ class DatabaseRpcServer {
226
276
  } catch (error) {
227
277
  this.scope.postMessage(rpcFailure(request.requestId, error));
228
278
  } finally {
279
+ clearInterval(keepalive);
229
280
  if (abort !== void 0)
230
281
  this.#requestAborts.delete(request.requestId);
231
282
  if (!bypassLimit)
@@ -487,7 +538,7 @@ class DatabaseRpcServer {
487
538
  if (handle === void 0)
488
539
  throw new Error(`Unknown handle: ${handleId}`);
489
540
  if (handle.type === "write") {
490
- this.#beginWriteHandleCall(handle);
541
+ await this.#beginWriteHandleCall(handle);
491
542
  try {
492
543
  return await this.#callWriteHandle(handleId, handle, method, args, context);
493
544
  } finally {
@@ -796,12 +847,11 @@ class DatabaseRpcServer {
796
847
  #releaseHandleId(id) {
797
848
  this.#reservedHandleIds.delete(id);
798
849
  }
799
- #beginWriteHandleCall(handle) {
850
+ async #beginWriteHandleCall(handle) {
851
+ while (handle.activeCalls !== 0)
852
+ await handle.activeCallDone;
800
853
  if (!handle.open)
801
854
  throw new Error("Write handle is closed");
802
- if (handle.activeCalls !== 0) {
803
- throw new Error("Write handle already has a call in flight");
804
- }
805
855
  handle.activeCalls = 1;
806
856
  handle.activeCallDone = new Promise((resolve) => {
807
857
  handle.finishActiveCall = resolve;
@@ -831,7 +881,9 @@ class DatabaseRpcServer {
831
881
  handle.idleTimer = void 0;
832
882
  if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
833
883
  return;
834
- void this.#settleWriteHandle(handleId, handle, false).catch(() => void 0);
884
+ void this.#settleWriteHandle(handleId, handle, false).catch((error) => {
885
+ this.#report("maintenance", error, "idle write handle rollback");
886
+ });
835
887
  }, this.#writeHandleIdleTimeoutMs);
836
888
  unrefTimer(handle.idleTimer);
837
889
  }
@@ -897,7 +949,8 @@ class DatabaseRpcServer {
897
949
  handleCleanup.push(handle.task.catch(() => void 0));
898
950
  } else if (handle.type === "live-set")
899
951
  this.#closeLiveSet(handleId, handle);
900
- } catch {
952
+ } catch (error) {
953
+ this.#report("maintenance", error, "dispose handle");
901
954
  }
902
955
  }
903
956
  await Promise.allSettled(handleCleanup);
@@ -922,6 +975,7 @@ class DatabaseRpcServer {
922
975
  }
923
976
  function exposeDatabase(database, scope, options = {}) {
924
977
  const server = new DatabaseRpcServer(database, scope, options);
978
+ const report = workerErrorReporter(scope);
925
979
  scope.addEventListener("message", (event) => {
926
980
  let request;
927
981
  try {
@@ -930,15 +984,28 @@ function exposeDatabase(database, scope, options = {}) {
930
984
  const requestId = requestIdOf(event.data);
931
985
  if (requestId !== void 0)
932
986
  scope.postMessage(rpcFailure(requestId, error));
987
+ else
988
+ report("messageerror", error, "unreadable request frame");
933
989
  return;
934
990
  }
935
- if (request !== null)
936
- void server.handle(request);
991
+ if (request !== null) {
992
+ void server.handle(request).catch((error) => {
993
+ report("uncaught", error, `${request.kind} dispatch`);
994
+ });
995
+ }
937
996
  });
938
997
  }
939
998
  function attachWorkerHost(scope, createStore, options = {}) {
940
999
  let initialized;
941
1000
  let initFailure;
1001
+ const report = workerErrorReporter(scope);
1002
+ const storeOptions = {
1003
+ ...options,
1004
+ onDiagnostic: (error, context) => {
1005
+ report("coordination", error, context);
1006
+ options.onDiagnostic?.(error, context);
1007
+ }
1008
+ };
942
1009
  scope.addEventListener("message", (event) => {
943
1010
  let request;
944
1011
  try {
@@ -947,13 +1014,15 @@ function attachWorkerHost(scope, createStore, options = {}) {
947
1014
  const requestId = requestIdOf(event.data);
948
1015
  if (requestId !== void 0)
949
1016
  scope.postMessage(rpcFailure(requestId, error));
1017
+ else
1018
+ report("messageerror", error, "unreadable request frame");
950
1019
  return;
951
1020
  }
952
1021
  if (request === null)
953
1022
  return;
954
1023
  if (request.kind === "rpc-init" && initialized === void 0) {
955
1024
  initFailure = void 0;
956
- const attempt = createServer(scope, request.payload, createStore, options);
1025
+ const attempt = createServer(scope, request.payload, createStore, storeOptions, report);
957
1026
  initialized = attempt;
958
1027
  attempt.catch((error) => {
959
1028
  if (initialized === attempt) {
@@ -969,25 +1038,54 @@ function attachWorkerHost(scope, createStore, options = {}) {
969
1038
  return;
970
1039
  }
971
1040
  void pending.then((server) => server.handle(request)).catch((error) => {
972
- scope.postMessage(rpcFailure(request.requestId, error));
1041
+ try {
1042
+ scope.postMessage(rpcFailure(request.requestId, error));
1043
+ } catch (postError) {
1044
+ report("uncaught", postError, `${request.kind} failure frame`);
1045
+ }
973
1046
  });
974
1047
  });
975
1048
  }
976
- async function createServer(scope, payload, createStore, options) {
977
- const store = await createStore(payload.store, options);
978
- const database = new MinnowDatabase(store, payload.options ?? {});
1049
+ async function createServer(scope, payload, createStore, options, report) {
1050
+ const opened = await createStore(payload.store, options);
1051
+ const { store, kind } = isOpenedStore(opened) ? opened : { store: opened, kind: payload.store.kind === "auto" ? void 0 : payload.store.kind };
1052
+ let database;
1053
+ try {
1054
+ database = new MinnowDatabase(store, {
1055
+ ...payload.options ?? {},
1056
+ onBackgroundError: (error, context) => report("maintenance", error, context)
1057
+ });
1058
+ } catch (error) {
1059
+ try {
1060
+ store.close();
1061
+ } catch (closeError) {
1062
+ report("maintenance", closeError, "store close after failed construction");
1063
+ }
1064
+ throw error;
1065
+ }
979
1066
  return new DatabaseRpcServer(database, scope, {
980
1067
  ...payload.options?.transactionIdleTimeoutMs === void 0 ? {} : { writeHandleIdleTimeoutMs: payload.options.transactionIdleTimeoutMs },
981
1068
  onDispose: () => store.close(),
982
1069
  onVisibility: (visible) => {
983
1070
  store.setForeground?.(visible);
984
- }
1071
+ },
1072
+ ...kind === void 0 ? {} : { storeKind: kind },
1073
+ ...payload.keepaliveIntervalMs === void 0 ? {} : { keepaliveIntervalMs: payload.keepaliveIntervalMs }
985
1074
  });
986
1075
  }
1076
+ function keepaliveInterval(value) {
1077
+ if (typeof value !== "number" || !Number.isFinite(value))
1078
+ return WORKER_KEEPALIVE_INTERVAL_MS;
1079
+ return Math.min(WORKER_KEEPALIVE_INTERVAL_MS, Math.max(MIN_WORKER_KEEPALIVE_INTERVAL_MS, value));
1080
+ }
1081
+ function isOpenedStore(value) {
1082
+ return typeof value.kind === "string" && typeof value.store === "object";
1083
+ }
987
1084
  const storeKindLabels = {
988
1085
  indexeddb: "IndexedDB",
989
1086
  opfs: "OPFS",
990
- memory: "memory"
1087
+ memory: "memory",
1088
+ auto: "OPFS-or-IndexedDB"
991
1089
  };
992
1090
  function singleStoreFactory(kind, open) {
993
1091
  return (descriptor, options) => {
@@ -997,6 +1095,9 @@ function singleStoreFactory(kind, open) {
997
1095
  return open(descriptor, options);
998
1096
  };
999
1097
  }
1098
+ function unsupportedStoreKindError(bundled, requested) {
1099
+ return new Error(unsupportedStoreKindMessage(bundled, requested));
1100
+ }
1000
1101
  function unsupportedStoreKindMessage(bundled, requested) {
1001
1102
  const requestedLabel = Object.hasOwn(storeKindLabels, requested) ? storeKindLabels[requested] : void 0;
1002
1103
  const requestedEntry = requestedLabel === void 0 ? "" : ` or "@minnowdb/core/worker/${requested}" for the ${requestedLabel} store`;
@@ -1018,5 +1119,7 @@ export {
1018
1119
  MAX_WORKER_HANDLES_PER_CONNECTION,
1019
1120
  attachWorkerHost,
1020
1121
  exposeDatabase,
1021
- singleStoreFactory
1122
+ singleStoreFactory,
1123
+ unsupportedStoreKindError,
1124
+ workerErrorReporter
1022
1125
  };
@@ -0,0 +1,36 @@
1
+ import { IndexedDbBlockStore } from "../storage/indexeddb.js";
2
+ import { OpfsBlockStore, opfsDatabaseExists } from "../storage/opfs/index.js";
3
+ import { openAutoStore } from "./auto-store.js";
4
+ import { unsupportedStoreKindError } from "./worker-server.js";
5
+ const autoWorkerStore = async (descriptor, options) => {
6
+ const diagnostic = options.onDiagnostic === void 0 ? {} : { onDiagnostic: options.onDiagnostic };
7
+ switch (descriptor.kind) {
8
+ case "opfs":
9
+ return OpfsBlockStore.open({
10
+ name: descriptor.name,
11
+ ...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
12
+ ...diagnostic
13
+ });
14
+ case "indexeddb":
15
+ return IndexedDbBlockStore.open({
16
+ name: descriptor.name,
17
+ ...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
18
+ ...descriptor.uniqueKeyCacheBytes === void 0 ? {} : { uniqueKeyCacheBytes: descriptor.uniqueKeyCacheBytes }
19
+ });
20
+ case "auto":
21
+ return openAutoStore(descriptor.name, (kind) => kind === "opfs" ? OpfsBlockStore.open({
22
+ name: descriptor.name,
23
+ ...descriptor.opfs?.durability === void 0 ? {} : { durability: descriptor.opfs.durability },
24
+ ...diagnostic
25
+ }) : IndexedDbBlockStore.open({
26
+ name: descriptor.name,
27
+ ...descriptor.indexeddb?.durability === void 0 ? {} : { durability: descriptor.indexeddb.durability },
28
+ ...descriptor.indexeddb?.uniqueKeyCacheBytes === void 0 ? {} : { uniqueKeyCacheBytes: descriptor.indexeddb.uniqueKeyCacheBytes }
29
+ }), { opfsDatabaseExists: (name) => opfsDatabaseExists({ name }) });
30
+ default:
31
+ throw unsupportedStoreKindError("auto", descriptor.kind);
32
+ }
33
+ };
34
+ export {
35
+ autoWorkerStore
36
+ };
@@ -1,8 +1,9 @@
1
1
  import { OpfsBlockStore } from "../storage/opfs/index.js";
2
2
  import { singleStoreFactory } from "./worker-server.js";
3
- const opfsWorkerStore = singleStoreFactory("opfs", (descriptor) => OpfsBlockStore.open({
3
+ const opfsWorkerStore = singleStoreFactory("opfs", (descriptor, options) => OpfsBlockStore.open({
4
4
  name: descriptor.name,
5
- ...descriptor.durability === void 0 ? {} : { durability: descriptor.durability }
5
+ ...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
6
+ ...options.onDiagnostic === void 0 ? {} : { onDiagnostic: options.onDiagnostic }
6
7
  }));
7
8
  export {
8
9
  opfsWorkerStore
@@ -1,6 +1,12 @@
1
1
  const anonymous = /* @__PURE__ */ new WeakMap();
2
2
  const named = /* @__PURE__ */ new Map();
3
- async function coordinateWrite(store, run, signal) {
3
+ const bypassing = /* @__PURE__ */ new Set();
4
+ const NOT_GRANTED = /* @__PURE__ */ Symbol("write admission lock not granted");
5
+ const WRITE_ADMISSION_WAIT_MS = 1e4;
6
+ function _resetWriteAdmissionForTests() {
7
+ bypassing.clear();
8
+ }
9
+ async function coordinateWrite(store, run, signal, options = {}) {
4
10
  signal.throwIfAborted();
5
11
  const name = store.liveQueryChannelName;
6
12
  let queue = name === void 0 ? anonymous.get(store) : named.get(name);
@@ -19,9 +25,43 @@ async function coordinateWrite(store, run, signal) {
19
25
  admitted = true;
20
26
  return run();
21
27
  };
28
+ const admissionWaitMs = options.admissionWaitMs ?? WRITE_ADMISSION_WAIT_MS;
22
29
  const operation = queue.tail.then(async () => {
23
30
  signal.throwIfAborted();
24
- return name !== void 0 && locks !== void 0 ? await locks.request(`minnowdb-write:${name}`, { signal: lockController.signal }, enter) : await enter();
31
+ if (name === void 0 || locks === void 0)
32
+ return enter();
33
+ const lockName = `minnowdb-write:${name}`;
34
+ if (bypassing.has(name)) {
35
+ const result = await locks.request(lockName, { ifAvailable: true }, async (lock) => {
36
+ if (lock === null)
37
+ return NOT_GRANTED;
38
+ bypassing.delete(name);
39
+ return enter();
40
+ });
41
+ if (result !== NOT_GRANTED)
42
+ return result;
43
+ signal.throwIfAborted();
44
+ return await enter();
45
+ }
46
+ const startedAt = Date.now();
47
+ const wait = { ranOut: false };
48
+ const waitTimer = setTimeout(() => {
49
+ wait.ranOut = true;
50
+ lockController.abort(new Error("Write admission wait ran out"));
51
+ }, admissionWaitMs);
52
+ waitTimer.unref?.();
53
+ try {
54
+ return await locks.request(lockName, { signal: lockController.signal }, enter);
55
+ } catch (error) {
56
+ if (!wait.ranOut || admitted)
57
+ throw error;
58
+ signal.throwIfAborted();
59
+ bypassing.add(name);
60
+ options.onAdmissionWaitExceeded?.(Date.now() - startedAt);
61
+ return await enter();
62
+ } finally {
63
+ clearTimeout(waitTimer);
64
+ }
25
65
  });
26
66
  const settled = operation.then(() => void 0, () => void 0);
27
67
  queue.tail = settled;
@@ -50,5 +90,7 @@ async function coordinateWrite(store, run, signal) {
50
90
  }
51
91
  }
52
92
  export {
93
+ WRITE_ADMISSION_WAIT_MS,
94
+ _resetWriteAdmissionForTests,
53
95
  coordinateWrite
54
96
  };
@@ -0,0 +1,269 @@
1
+ import { IndexedDbBlockStore } from "./indexeddb.js";
2
+ const NOW = "2026-09-12T12:00:00.000Z";
3
+ async function openStore(indexedDB, name = crypto.randomUUID(), durability) {
4
+ return IndexedDbBlockStore.open({ name, indexedDB, ...durability ? { durability } : {} });
5
+ }
6
+ const EVENTS_TABLE = {
7
+ managed: false,
8
+ id: "events",
9
+ name: "events",
10
+ columns: [{ id: "value", name: "value", type: "number", nullable: false }],
11
+ revision: 0,
12
+ createdAt: NOW
13
+ };
14
+ function activeTransaction(id, snapshotVersion) {
15
+ return {
16
+ id,
17
+ ownerId: `owner-${id}`,
18
+ expiresAt: "2026-09-12T12:30:00.000Z",
19
+ snapshotVersion,
20
+ pendingBlockIds: [],
21
+ pendingSegmentIds: [],
22
+ status: "active",
23
+ revision: 0,
24
+ startedAt: NOW,
25
+ updatedAt: NOW,
26
+ committedVersion: null
27
+ };
28
+ }
29
+ function segment(id, transactionId, blockId, commitOrdinal = 0, rowIdStart = 1n) {
30
+ return {
31
+ id,
32
+ tableId: "events",
33
+ transactionId,
34
+ rowCount: 1,
35
+ rowIdStart,
36
+ rowIdEndExclusive: rowIdStart + 1n,
37
+ columnBlockIds: { value: [blockId] },
38
+ kind: "insert",
39
+ level: 0,
40
+ logicalOrder: 0,
41
+ commitOrdinal,
42
+ rowIdSpans: [],
43
+ createdAt: NOW
44
+ };
45
+ }
46
+ async function rawOpen(indexedDB, name) {
47
+ return new Promise((resolve, reject) => {
48
+ const request = indexedDB.open(name);
49
+ request.onsuccess = () => resolve(request.result);
50
+ request.onerror = () => reject(request.error ?? new Error("raw IndexedDB open failed"));
51
+ });
52
+ }
53
+ async function readRawValue(indexedDB, name, storeName, key) {
54
+ const database = await rawOpen(indexedDB, name);
55
+ try {
56
+ const transaction = database.transaction(storeName, "readonly");
57
+ const value = await new Promise((resolve, reject) => {
58
+ const request = transaction.objectStore(storeName).get(key);
59
+ request.onsuccess = () => resolve(request.result);
60
+ request.onerror = () => reject(request.error ?? new Error("raw IndexedDB read failed"));
61
+ });
62
+ await new Promise((resolve, reject) => {
63
+ transaction.oncomplete = () => resolve();
64
+ transaction.onabort = () => reject(transaction.error ?? new Error("raw read aborted"));
65
+ });
66
+ return value;
67
+ } finally {
68
+ database.close();
69
+ }
70
+ }
71
+ async function readRawKeys(indexedDB, name, storeName) {
72
+ const database = await rawOpen(indexedDB, name);
73
+ try {
74
+ const transaction = database.transaction(storeName, "readonly");
75
+ const keys = await new Promise((resolve, reject) => {
76
+ const request = transaction.objectStore(storeName).getAllKeys();
77
+ request.onsuccess = () => resolve(request.result);
78
+ request.onerror = () => reject(request.error ?? new Error("raw IndexedDB read failed"));
79
+ });
80
+ await new Promise((resolve, reject) => {
81
+ transaction.oncomplete = () => resolve();
82
+ transaction.onabort = () => reject(transaction.error ?? new Error("raw read aborted"));
83
+ });
84
+ return keys;
85
+ } finally {
86
+ database.close();
87
+ }
88
+ }
89
+ function instrumentFactory(indexedDB) {
90
+ const state = {
91
+ factory: indexedDB,
92
+ transactions: [],
93
+ requestCounts: /* @__PURE__ */ new Map(),
94
+ setHook(hook) {
95
+ currentHook = hook;
96
+ },
97
+ reset() {
98
+ state.transactions.length = 0;
99
+ state.requestCounts.clear();
100
+ }
101
+ };
102
+ let currentHook;
103
+ const wrappedScanners = /* @__PURE__ */ new WeakSet();
104
+ const countScans = (target, label) => {
105
+ if (wrappedScanners.has(target))
106
+ return;
107
+ wrappedScanners.add(target);
108
+ for (const method of [
109
+ "openCursor",
110
+ "openKeyCursor",
111
+ "getAll",
112
+ "getAllKeys",
113
+ "count"
114
+ ]) {
115
+ const original = target[method].bind(target);
116
+ Object.defineProperty(target, method, {
117
+ configurable: true,
118
+ value: (...args) => {
119
+ const key = `${label}:${method}`;
120
+ state.requestCounts.set(key, (state.requestCounts.get(key) ?? 0) + 1);
121
+ const request = original(...args);
122
+ if (method === "openCursor" || method === "openKeyCursor") {
123
+ request.addEventListener("success", () => {
124
+ const stepKey = `${label}:cursor-steps`;
125
+ if (request.result !== null && request.result !== void 0) {
126
+ state.requestCounts.set(stepKey, (state.requestCounts.get(stepKey) ?? 0) + 1);
127
+ }
128
+ });
129
+ }
130
+ return request;
131
+ }
132
+ });
133
+ }
134
+ };
135
+ const wrappedStores = /* @__PURE__ */ new WeakSet();
136
+ const originalOpen = indexedDB.open.bind(indexedDB);
137
+ Object.defineProperty(indexedDB, "open", {
138
+ configurable: true,
139
+ value: (name, version) => {
140
+ const request = version === void 0 ? originalOpen(name) : originalOpen(name, version);
141
+ request.addEventListener("success", () => {
142
+ const database = request.result;
143
+ const originalTransaction = database.transaction.bind(database);
144
+ Object.defineProperty(database, "transaction", {
145
+ configurable: true,
146
+ value: (stores, mode, options) => {
147
+ const transaction = originalTransaction(stores, mode, options);
148
+ const entry = {
149
+ transaction,
150
+ stores: typeof stores === "string" ? [stores] : [...stores],
151
+ mode: mode ?? "readonly",
152
+ options,
153
+ completed: false,
154
+ aborted: false
155
+ };
156
+ transaction.addEventListener("complete", () => {
157
+ entry.completed = true;
158
+ });
159
+ transaction.addEventListener("abort", () => {
160
+ entry.aborted = true;
161
+ });
162
+ state.transactions.push(entry);
163
+ const originalObjectStore = transaction.objectStore.bind(transaction);
164
+ Object.defineProperty(transaction, "objectStore", {
165
+ configurable: true,
166
+ value: (storeName) => {
167
+ const store = originalObjectStore(storeName);
168
+ if (wrappedStores.has(store))
169
+ return store;
170
+ wrappedStores.add(store);
171
+ countScans(store, storeName);
172
+ const originalIndex = store.index.bind(store);
173
+ Object.defineProperty(store, "index", {
174
+ configurable: true,
175
+ value: (indexName) => {
176
+ const index = originalIndex(indexName);
177
+ countScans(index, `${storeName}.${indexName}`);
178
+ return index;
179
+ }
180
+ });
181
+ for (const method of ["put", "add", "delete", "get", "getKey"]) {
182
+ const original = store[method].bind(store);
183
+ Object.defineProperty(store, method, {
184
+ configurable: true,
185
+ value: (...args) => {
186
+ state.requestCounts.set(storeName, (state.requestCounts.get(storeName) ?? 0) + 1);
187
+ const [first, second] = args;
188
+ const isWrite = method === "put" || method === "add";
189
+ const decision = currentHook?.({
190
+ transaction,
191
+ storeName,
192
+ method,
193
+ key: isWrite ? second : first,
194
+ value: isWrite ? first : void 0
195
+ });
196
+ if (decision === "throw-quota") {
197
+ const failing = original(...args);
198
+ const queue = transaction._requests;
199
+ const entry2 = queue.find((candidate) => candidate.request === failing);
200
+ if (entry2 === void 0)
201
+ throw new Error("request not queued");
202
+ entry2.operation = () => {
203
+ throw new DOMException("The quota has been exceeded.", "QuotaExceededError");
204
+ };
205
+ return failing;
206
+ }
207
+ const result = original(...args);
208
+ if (decision === "abort") {
209
+ try {
210
+ transaction.abort();
211
+ } catch {
212
+ }
213
+ }
214
+ return result;
215
+ }
216
+ });
217
+ }
218
+ return store;
219
+ }
220
+ });
221
+ return transaction;
222
+ }
223
+ });
224
+ });
225
+ return request;
226
+ }
227
+ });
228
+ return state;
229
+ }
230
+ async function stageBlocks(store, record, ids) {
231
+ let current = record;
232
+ for (let start = 0; start < ids.length; start += 64) {
233
+ const slice = ids.slice(start, start + 64);
234
+ current = await store.stageTransactionArtifacts({
235
+ transactionId: record.id,
236
+ expectedRevision: current.revision,
237
+ blocks: slice.map((id) => ({ id, bytes: Uint8Array.of(1) })),
238
+ segments: [],
239
+ updatedAt: NOW
240
+ });
241
+ }
242
+ return current;
243
+ }
244
+ async function stageSegments(store, record, ids, blockId) {
245
+ let current = record;
246
+ for (let start = 0; start < ids.length; start += 64) {
247
+ const slice = ids.slice(start, start + 64);
248
+ current = await store.stageTransactionArtifacts({
249
+ transactionId: record.id,
250
+ expectedRevision: current.revision,
251
+ blocks: [],
252
+ segments: slice.map((id, offset) => segment(id, record.id, blockId, current.pendingSegmentIds.length + offset, BigInt(current.pendingSegmentIds.length + offset + 1))),
253
+ updatedAt: NOW
254
+ });
255
+ }
256
+ return current;
257
+ }
258
+ export {
259
+ EVENTS_TABLE,
260
+ NOW,
261
+ activeTransaction,
262
+ instrumentFactory,
263
+ openStore,
264
+ readRawKeys,
265
+ readRawValue,
266
+ segment,
267
+ stageBlocks,
268
+ stageSegments
269
+ };