@minnowdb/core 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/engine/auto-store.d.ts +40 -0
  2. package/dist/engine/auto-store.js +115 -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.d.ts +55 -6
  6. package/dist/engine/client.js +155 -40
  7. package/dist/engine/database.d.ts +15 -1
  8. package/dist/engine/database.js +1085 -227
  9. package/dist/engine/errors.d.ts +61 -2
  10. package/dist/engine/errors.js +116 -3
  11. package/dist/engine/index.d.ts +1 -0
  12. package/dist/engine/index.js +2 -0
  13. package/dist/engine/live.d.ts +24 -1
  14. package/dist/engine/live.js +33 -9
  15. package/dist/engine/scope-write-set.js +36 -0
  16. package/dist/engine/worker-auto.d.ts +1 -0
  17. package/dist/engine/worker-auto.js +3 -0
  18. package/dist/engine/worker-host.d.ts +2 -1
  19. package/dist/engine/worker-host.js +17 -1
  20. package/dist/engine/worker-server.d.ts +53 -1
  21. package/dist/engine/worker-server.js +118 -14
  22. package/dist/engine/worker-store-auto.js +36 -0
  23. package/dist/engine/worker-store-opfs.js +3 -2
  24. package/dist/engine/write-coordinator.js +24 -2
  25. package/dist/storage/indexeddb.js +494 -207
  26. package/dist/storage/opfs/leader.js +201 -14
  27. package/dist/storage/opfs/rpc.js +23 -43
  28. package/dist/storage/opfs/store.d.ts +20 -0
  29. package/dist/storage/opfs/store.js +479 -60
  30. package/dist/storage/toolkit/record-core.js +67 -38
  31. package/dist/storage/toolkit/wire.d.ts +1 -1
  32. package/dist/storage/toolkit/wire.js +1 -1
  33. package/dist/storage/types.d.ts +29 -8
  34. package/dist/storage/types.js +27 -16
  35. package/dist/testing/opfs-shim.js +14 -6
  36. package/dist/transactions/index.d.ts +12 -0
  37. package/dist/transactions/index.js +83 -23
  38. package/dist/worker-protocol/index.d.ts +50 -2
  39. package/dist/worker-protocol/index.js +106 -4
  40. 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)
@@ -831,7 +882,9 @@ class DatabaseRpcServer {
831
882
  handle.idleTimer = void 0;
832
883
  if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
833
884
  return;
834
- void this.#settleWriteHandle(handleId, handle, false).catch(() => void 0);
885
+ void this.#settleWriteHandle(handleId, handle, false).catch((error) => {
886
+ this.#report("maintenance", error, "idle write handle rollback");
887
+ });
835
888
  }, this.#writeHandleIdleTimeoutMs);
836
889
  unrefTimer(handle.idleTimer);
837
890
  }
@@ -897,7 +950,8 @@ class DatabaseRpcServer {
897
950
  handleCleanup.push(handle.task.catch(() => void 0));
898
951
  } else if (handle.type === "live-set")
899
952
  this.#closeLiveSet(handleId, handle);
900
- } catch {
953
+ } catch (error) {
954
+ this.#report("maintenance", error, "dispose handle");
901
955
  }
902
956
  }
903
957
  await Promise.allSettled(handleCleanup);
@@ -922,6 +976,7 @@ class DatabaseRpcServer {
922
976
  }
923
977
  function exposeDatabase(database, scope, options = {}) {
924
978
  const server = new DatabaseRpcServer(database, scope, options);
979
+ const report = workerErrorReporter(scope);
925
980
  scope.addEventListener("message", (event) => {
926
981
  let request;
927
982
  try {
@@ -930,15 +985,28 @@ function exposeDatabase(database, scope, options = {}) {
930
985
  const requestId = requestIdOf(event.data);
931
986
  if (requestId !== void 0)
932
987
  scope.postMessage(rpcFailure(requestId, error));
988
+ else
989
+ report("messageerror", error, "unreadable request frame");
933
990
  return;
934
991
  }
935
- if (request !== null)
936
- void server.handle(request);
992
+ if (request !== null) {
993
+ void server.handle(request).catch((error) => {
994
+ report("uncaught", error, `${request.kind} dispatch`);
995
+ });
996
+ }
937
997
  });
938
998
  }
939
999
  function attachWorkerHost(scope, createStore, options = {}) {
940
1000
  let initialized;
941
1001
  let initFailure;
1002
+ const report = workerErrorReporter(scope);
1003
+ const storeOptions = {
1004
+ ...options,
1005
+ onDiagnostic: (error, context) => {
1006
+ report("coordination", error, context);
1007
+ options.onDiagnostic?.(error, context);
1008
+ }
1009
+ };
942
1010
  scope.addEventListener("message", (event) => {
943
1011
  let request;
944
1012
  try {
@@ -947,13 +1015,15 @@ function attachWorkerHost(scope, createStore, options = {}) {
947
1015
  const requestId = requestIdOf(event.data);
948
1016
  if (requestId !== void 0)
949
1017
  scope.postMessage(rpcFailure(requestId, error));
1018
+ else
1019
+ report("messageerror", error, "unreadable request frame");
950
1020
  return;
951
1021
  }
952
1022
  if (request === null)
953
1023
  return;
954
1024
  if (request.kind === "rpc-init" && initialized === void 0) {
955
1025
  initFailure = void 0;
956
- const attempt = createServer(scope, request.payload, createStore, options);
1026
+ const attempt = createServer(scope, request.payload, createStore, storeOptions, report);
957
1027
  initialized = attempt;
958
1028
  attempt.catch((error) => {
959
1029
  if (initialized === attempt) {
@@ -969,25 +1039,54 @@ function attachWorkerHost(scope, createStore, options = {}) {
969
1039
  return;
970
1040
  }
971
1041
  void pending.then((server) => server.handle(request)).catch((error) => {
972
- scope.postMessage(rpcFailure(request.requestId, error));
1042
+ try {
1043
+ scope.postMessage(rpcFailure(request.requestId, error));
1044
+ } catch (postError) {
1045
+ report("uncaught", postError, `${request.kind} failure frame`);
1046
+ }
973
1047
  });
974
1048
  });
975
1049
  }
976
- async function createServer(scope, payload, createStore, options) {
977
- const store = await createStore(payload.store, options);
978
- const database = new MinnowDatabase(store, payload.options ?? {});
1050
+ async function createServer(scope, payload, createStore, options, report) {
1051
+ const opened = await createStore(payload.store, options);
1052
+ const { store, kind } = isOpenedStore(opened) ? opened : { store: opened, kind: payload.store.kind === "auto" ? void 0 : payload.store.kind };
1053
+ let database;
1054
+ try {
1055
+ database = new MinnowDatabase(store, {
1056
+ ...payload.options ?? {},
1057
+ onBackgroundError: (error, context) => report("maintenance", error, context)
1058
+ });
1059
+ } catch (error) {
1060
+ try {
1061
+ store.close();
1062
+ } catch (closeError) {
1063
+ report("maintenance", closeError, "store close after failed construction");
1064
+ }
1065
+ throw error;
1066
+ }
979
1067
  return new DatabaseRpcServer(database, scope, {
980
1068
  ...payload.options?.transactionIdleTimeoutMs === void 0 ? {} : { writeHandleIdleTimeoutMs: payload.options.transactionIdleTimeoutMs },
981
1069
  onDispose: () => store.close(),
982
1070
  onVisibility: (visible) => {
983
1071
  store.setForeground?.(visible);
984
- }
1072
+ },
1073
+ ...kind === void 0 ? {} : { storeKind: kind },
1074
+ ...payload.keepaliveIntervalMs === void 0 ? {} : { keepaliveIntervalMs: payload.keepaliveIntervalMs }
985
1075
  });
986
1076
  }
1077
+ function keepaliveInterval(value) {
1078
+ if (typeof value !== "number" || !Number.isFinite(value))
1079
+ return WORKER_KEEPALIVE_INTERVAL_MS;
1080
+ return Math.min(WORKER_KEEPALIVE_INTERVAL_MS, Math.max(MIN_WORKER_KEEPALIVE_INTERVAL_MS, value));
1081
+ }
1082
+ function isOpenedStore(value) {
1083
+ return typeof value.kind === "string" && typeof value.store === "object";
1084
+ }
987
1085
  const storeKindLabels = {
988
1086
  indexeddb: "IndexedDB",
989
1087
  opfs: "OPFS",
990
- memory: "memory"
1088
+ memory: "memory",
1089
+ auto: "OPFS-or-IndexedDB"
991
1090
  };
992
1091
  function singleStoreFactory(kind, open) {
993
1092
  return (descriptor, options) => {
@@ -997,6 +1096,9 @@ function singleStoreFactory(kind, open) {
997
1096
  return open(descriptor, options);
998
1097
  };
999
1098
  }
1099
+ function unsupportedStoreKindError(bundled, requested) {
1100
+ return new Error(unsupportedStoreKindMessage(bundled, requested));
1101
+ }
1000
1102
  function unsupportedStoreKindMessage(bundled, requested) {
1001
1103
  const requestedLabel = Object.hasOwn(storeKindLabels, requested) ? storeKindLabels[requested] : void 0;
1002
1104
  const requestedEntry = requestedLabel === void 0 ? "" : ` or "@minnowdb/core/worker/${requested}" for the ${requestedLabel} store`;
@@ -1018,5 +1120,7 @@ export {
1018
1120
  MAX_WORKER_HANDLES_PER_CONNECTION,
1019
1121
  attachWorkerHost,
1020
1122
  exposeDatabase,
1021
- singleStoreFactory
1123
+ singleStoreFactory,
1124
+ unsupportedStoreKindError,
1125
+ workerErrorReporter
1022
1126
  };
@@ -0,0 +1,36 @@
1
+ import { IndexedDbBlockStore } from "../storage/indexeddb.js";
2
+ import { OpfsBlockStore } 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
+ }));
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,7 @@
1
1
  const anonymous = /* @__PURE__ */ new WeakMap();
2
2
  const named = /* @__PURE__ */ new Map();
3
- async function coordinateWrite(store, run, signal) {
3
+ const WRITE_ADMISSION_WAIT_MS = 1e4;
4
+ async function coordinateWrite(store, run, signal, options = {}) {
4
5
  signal.throwIfAborted();
5
6
  const name = store.liveQueryChannelName;
6
7
  let queue = name === void 0 ? anonymous.get(store) : named.get(name);
@@ -19,9 +20,29 @@ async function coordinateWrite(store, run, signal) {
19
20
  admitted = true;
20
21
  return run();
21
22
  };
23
+ const admissionWaitMs = options.admissionWaitMs ?? WRITE_ADMISSION_WAIT_MS;
22
24
  const operation = queue.tail.then(async () => {
23
25
  signal.throwIfAborted();
24
- return name !== void 0 && locks !== void 0 ? await locks.request(`minnowdb-write:${name}`, { signal: lockController.signal }, enter) : await enter();
26
+ if (name === void 0 || locks === void 0)
27
+ return enter();
28
+ const startedAt = Date.now();
29
+ const wait = { ranOut: false };
30
+ const waitTimer = setTimeout(() => {
31
+ wait.ranOut = true;
32
+ lockController.abort(new Error("Write admission wait ran out"));
33
+ }, admissionWaitMs);
34
+ waitTimer.unref?.();
35
+ try {
36
+ return await locks.request(`minnowdb-write:${name}`, { signal: lockController.signal }, enter);
37
+ } catch (error) {
38
+ if (!wait.ranOut || admitted)
39
+ throw error;
40
+ signal.throwIfAborted();
41
+ options.onAdmissionWaitExceeded?.(Date.now() - startedAt);
42
+ return await enter();
43
+ } finally {
44
+ clearTimeout(waitTimer);
45
+ }
25
46
  });
26
47
  const settled = operation.then(() => void 0, () => void 0);
27
48
  queue.tail = settled;
@@ -50,5 +71,6 @@ async function coordinateWrite(store, run, signal) {
50
71
  }
51
72
  }
52
73
  export {
74
+ WRITE_ADMISSION_WAIT_MS,
53
75
  coordinateWrite
54
76
  };