@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,9 +1,9 @@
1
1
  import { createLiveQueryPatch } from "./live-patch.js";
2
- import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, PostingBuildConflictError, SnapshotManifestMissingError, SnapshotImportConflictError, SchemaConflictError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, StorageCorruptionError, StorageFormatVersionError, OpfsCoordinationError, OpfsDatabaseInUseError, OpfsUncertainOutcomeError } from "../storage/types.js";
2
+ 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 "../storage/types.js";
3
3
  import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
4
- import { parseRpcResponse, MAX_DATABASE_RPC_IN_FLIGHT, protocolVersion } from "../worker-protocol/index.js";
4
+ import { MAX_DATABASE_RPC_IN_FLIGHT, WORKER_DIAGNOSTIC_HANDLE_ID, isWorkerErrorReport, parseRpcResponse, MIN_WORKER_KEEPALIVE_INTERVAL_MS, WORKER_KEEPALIVE_INTERVAL_MS, protocolVersion, rehydrateError as rehydrateSerializedError } from "../worker-protocol/index.js";
5
5
  import { definedVectors, toColumnarBatch } from "./batch.js";
6
- import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, DatabaseReadBacklogError, TransactionExpiredError, DatabaseWorkerTimeoutError, DatabaseWorkerOutcomeUnknownError, LiveQueryLimitError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
6
+ import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, DatabaseReadBacklogError, TransactionExpiredError, DatabaseWorkerTimeoutError, DatabaseWorkerFailedError, DatabaseStoreUnavailableError, DatabaseWorkerOutcomeUnknownError, LiveQueryLimitError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
7
7
  import { QueryMemoryBudgetError } from "./memory.js";
8
8
  import { decodeQueryResult } from "./result-wire.js";
9
9
  import { serializeSchema } from "./schema-wire.js";
@@ -23,6 +23,7 @@ function throwIfClientSnapshotAborted(signal) {
23
23
  error.name = "AbortError";
24
24
  throw error;
25
25
  }
26
+ const MAX_KEEPALIVE_EXTENSION = 10;
26
27
  const errorRegistry = new Map([
27
28
  UniqueConstraintError,
28
29
  MissingKeyError,
@@ -36,6 +37,8 @@ const errorRegistry = new Map([
36
37
  TransactionExpiredError,
37
38
  DatabaseWorkerTimeoutError,
38
39
  DatabaseWorkerOutcomeUnknownError,
40
+ DatabaseWorkerFailedError,
41
+ DatabaseStoreUnavailableError,
39
42
  LiveQueryLimitError,
40
43
  SqlCompileError,
41
44
  QueryMemoryBudgetError,
@@ -64,31 +67,12 @@ const errorRegistry = new Map([
64
67
  StorageFormatVersionError,
65
68
  OpfsCoordinationError,
66
69
  OpfsDatabaseInUseError,
67
- OpfsUncertainOutcomeError
70
+ OpfsUncertainOutcomeError,
71
+ UnknownOutcomeError,
72
+ ConnectionLostError
68
73
  ].map((constructor) => [constructor.name, constructor]));
69
74
  function rehydrateError(serialized) {
70
- const constructor = errorRegistry.get(serialized.name);
71
- const error = constructor === void 0 ? new Error(serialized.message) : Object.create(constructor.prototype);
72
- Object.defineProperty(error, "message", {
73
- value: serialized.message,
74
- writable: true,
75
- configurable: true
76
- });
77
- Object.defineProperty(error, "name", {
78
- value: serialized.name,
79
- writable: true,
80
- configurable: true
81
- });
82
- if (serialized.stack !== void 0) {
83
- Object.defineProperty(error, "stack", {
84
- value: serialized.stack,
85
- writable: true,
86
- configurable: true
87
- });
88
- }
89
- if (serialized.props !== void 0)
90
- Object.assign(error, serialized.props);
91
- return error;
75
+ return rehydrateSerializedError(serialized, errorRegistry);
92
76
  }
93
77
  function rehydrateResponseError(payload) {
94
78
  const candidate = payload;
@@ -103,36 +87,57 @@ class MinnowDatabaseClient {
103
87
  return this;
104
88
  }
105
89
  #transport;
90
+ #transportFactory;
91
+ #initPayload;
92
+ #onConnectionLost;
106
93
  #requestTimeoutMs;
107
94
  #closePromise;
108
95
  #pending = /* @__PURE__ */ new Map();
109
96
  #events = /* @__PURE__ */ new Map();
110
97
  #ready;
98
+ #storeKind;
111
99
  #fatal;
112
100
  #closed = false;
113
101
  #onVisibilityChange;
114
102
  #onMessage = (event) => {
115
103
  this.#receive(event.data);
116
104
  };
117
- #onError = () => {
118
- this.#fail(new Error("The database worker failed; see the worker's own error output"));
105
+ #onWorkerError;
106
+ #onError = (event) => {
107
+ const detail = event !== void 0 && "filename" in event ? event : void 0;
108
+ const where = detail?.filename === void 0 || detail.filename === "" ? "" : ` at ${detail.filename}:${String(detail.lineno)}:${String(detail.colno)}`;
109
+ const message = detail?.message === void 0 || detail.message === "" ? "" : `: ${detail.message}`;
110
+ const error = new DatabaseWorkerFailedError("error", `The database worker failed: it raised an error it did not handle${message}${where}`, detail?.error === void 0 ? void 0 : { cause: detail.error });
111
+ this.#reportWorkerError({ kind: "transport", context: "worker error event", error });
112
+ this.#fail(error);
119
113
  };
120
114
  #onMessageError = () => {
121
- this.#fail(new Error("A database worker message could not be deserialized"));
115
+ const error = new DatabaseWorkerFailedError("messageerror", "A database worker message could not be deserialized");
116
+ this.#reportWorkerError({ kind: "transport", context: "worker messageerror event", error });
117
+ this.#fail(error);
122
118
  };
119
+ #reportWorkerError(event) {
120
+ if (this.#onWorkerError !== void 0) {
121
+ this.#onWorkerError(event);
122
+ return;
123
+ }
124
+ if (typeof console === "undefined")
125
+ return;
126
+ console.error(`[minnowdb] worker ${event.kind} (${event.context}):`, event.error);
127
+ }
123
128
  constructor(transport, options = {}) {
124
129
  this.#requestTimeoutMs = clientDeadline(options.requestTimeoutMs ?? 6e4);
125
130
  this.#schema = options.schema;
126
- this.#transport = transport;
127
- transport.addEventListener("message", this.#onMessage);
128
- transport.addEventListener("error", this.#onError);
129
- transport.addEventListener("messageerror", this.#onMessageError);
130
- const payload = {
131
+ this.#onWorkerError = options.onWorkerError;
132
+ this.#onConnectionLost = options.onConnectionLost;
133
+ this.#transportFactory = typeof transport === "function" ? transport : void 0;
134
+ this.#transport = typeof transport === "function" ? transport() : transport;
135
+ this.#initPayload = {
131
136
  store: options.store ?? { kind: "indexeddb", name: "minnow" },
132
- ...options.databaseOptions === void 0 ? {} : { options: options.databaseOptions }
137
+ ...options.databaseOptions === void 0 ? {} : { options: options.databaseOptions },
138
+ keepaliveIntervalMs: keepalivePace(this.#requestTimeoutMs)
133
139
  };
134
- this.#ready = this.#post("rpc-init", null, "init", [payload]).then(() => void 0);
135
- this.#ready.catch(() => void 0);
140
+ this.#ready = this.#attach(this.#transport);
136
141
  if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
137
142
  const report = () => {
138
143
  if (this.#closed)
@@ -146,9 +151,56 @@ class MinnowDatabaseClient {
146
151
  report();
147
152
  }
148
153
  }
154
+ #attach(transport) {
155
+ transport.addEventListener("message", this.#onMessage);
156
+ transport.addEventListener("error", this.#onError);
157
+ transport.addEventListener("messageerror", this.#onMessageError);
158
+ const ready = this.#post("rpc-init", null, "init", [this.#initPayload]).then((result) => {
159
+ const kind = result?.store;
160
+ this.#storeKind = kind === "indexeddb" || kind === "opfs" || kind === "memory" ? kind : void 0;
161
+ });
162
+ ready.catch(() => void 0);
163
+ return ready;
164
+ }
165
+ #detach(transport) {
166
+ transport.removeEventListener?.("message", this.#onMessage);
167
+ transport.removeEventListener?.("error", this.#onError);
168
+ transport.removeEventListener?.("messageerror", this.#onMessageError);
169
+ }
170
+ async reopen(transport) {
171
+ const next = transport ?? this.#transportFactory?.();
172
+ if (next === void 0) {
173
+ throw new TypeError("reopen() needs a transport: pass one, or construct the client with a transport factory");
174
+ }
175
+ const previous = this.#transport;
176
+ this.#detach(previous);
177
+ this.#transport = next;
178
+ if (this.#fatal === void 0) {
179
+ this.#fail(new DatabaseWorkerFailedError("reopened", "The database client was reopened"));
180
+ }
181
+ try {
182
+ previous.terminate?.();
183
+ } catch {
184
+ }
185
+ this.#fatal = void 0;
186
+ this.#closed = false;
187
+ this.#closePromise = void 0;
188
+ this.#events.clear();
189
+ this.#transport = next;
190
+ this.#ready = this.#attach(next);
191
+ if (this.#onVisibilityChange !== void 0 && typeof document !== "undefined") {
192
+ document.addEventListener("visibilitychange", this.#onVisibilityChange);
193
+ }
194
+ this.#onVisibilityChange?.();
195
+ await this.#ready;
196
+ }
149
197
  async ready() {
150
198
  return this.#ready;
151
199
  }
200
+ async storeKind() {
201
+ await this.#ready;
202
+ return this.#storeKind;
203
+ }
152
204
  async createTable(input) {
153
205
  await this.#call("createTable", [input]);
154
206
  }
@@ -223,7 +275,24 @@ class MinnowDatabaseClient {
223
275
  ...onError === void 0 ? {} : { onError }
224
276
  });
225
277
  const created = this.#call("bufferedWriter", [handleId, tableName, wireOptions]);
226
- return new ClientBufferedWriter(this.#erased, handleId, created);
278
+ const writer = new ClientBufferedWriter(this.#erased, handleId, created);
279
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
280
+ const onHidden = () => {
281
+ if (document.visibilityState === "hidden")
282
+ writer.requestFlush();
283
+ };
284
+ const onPageHide = () => {
285
+ writer.requestFlush();
286
+ };
287
+ document.addEventListener("visibilitychange", onHidden);
288
+ const page = typeof window === "undefined" ? void 0 : window;
289
+ page?.addEventListener("pagehide", onPageHide);
290
+ writer._onClose(() => {
291
+ document.removeEventListener("visibilitychange", onHidden);
292
+ page?.removeEventListener("pagehide", onPageHide);
293
+ });
294
+ }
295
+ return writer;
227
296
  }
228
297
  async readTable(tableName, versionOrOptions) {
229
298
  return decodeQueryResult(await this.#call("readTable", versionOrOptions === void 0 ? [tableName] : [tableName, versionOrOptions])).rows;
@@ -512,7 +581,7 @@ class MinnowDatabaseClient {
512
581
  try {
513
582
  await this.#post("rpc-call", null, "dispose", [], void 0, true, { timeoutMs });
514
583
  } finally {
515
- this.#fail(new Error("Database client is closed"));
584
+ this.#fail(new Error("Database client is closed"), true);
516
585
  this.#transport.removeEventListener?.("message", this.#onMessage);
517
586
  this.#transport.removeEventListener?.("error", this.#onError);
518
587
  this.#transport.removeEventListener?.("messageerror", this.#onMessageError);
@@ -552,8 +621,19 @@ class MinnowDatabaseClient {
552
621
  const timeoutMs = controls.timeoutMs ?? this.#requestTimeoutMs;
553
622
  const mayPublish = rpcMayPublish(method, args);
554
623
  return new Promise((resolve, reject) => {
555
- const timer = setTimeout(() => this.#fail(new DatabaseWorkerTimeoutError(method, timeoutMs)), timeoutMs);
624
+ const startedAt = Date.now();
625
+ const expire = () => this.#fail(new DatabaseWorkerTimeoutError(method, timeoutMs));
626
+ let timer = setTimeout(expire, timeoutMs);
556
627
  timer.unref?.();
628
+ const keepalive = () => {
629
+ const elapsed = Date.now() - startedAt;
630
+ const remaining = timeoutMs * MAX_KEEPALIVE_EXTENSION - elapsed;
631
+ if (remaining <= 0)
632
+ return;
633
+ clearTimeout(timer);
634
+ timer = setTimeout(expire, Math.min(timeoutMs, remaining));
635
+ timer.unref?.();
636
+ };
557
637
  const onAbort = () => {
558
638
  try {
559
639
  this.#transport.postMessage({ version: protocolVersion, requestId, kind: "rpc-cancel" });
@@ -580,7 +660,15 @@ class MinnowDatabaseClient {
580
660
  if (controls.onStats !== void 0) {
581
661
  this.#events.set(requestId, { onStats: controls.onStats });
582
662
  }
583
- this.#pending.set(requestId, { resolve, reject, cleanup, method, requestId, mayPublish });
663
+ this.#pending.set(requestId, {
664
+ resolve,
665
+ reject,
666
+ cleanup,
667
+ method,
668
+ requestId,
669
+ mayPublish,
670
+ keepalive
671
+ });
584
672
  try {
585
673
  this.#transport.postMessage(kind === "rpc-init" ? { version: protocolVersion, requestId, kind, payload: args[0] } : { version: protocolVersion, requestId, kind, handleId, method, args }, transfer === void 0 ? void 0 : { transfer });
586
674
  } catch (error) {
@@ -601,6 +689,20 @@ class MinnowDatabaseClient {
601
689
  if (response === null)
602
690
  return;
603
691
  if (response.kind === "rpc-event") {
692
+ if (response.handleId === WORKER_DIAGNOSTIC_HANDLE_ID) {
693
+ if (response.event === "keepalive" && typeof response.requestId === "string") {
694
+ this.#pending.get(response.requestId)?.keepalive?.();
695
+ return;
696
+ }
697
+ if (response.event === "error" && isWorkerErrorReport(response.payload)) {
698
+ this.#reportWorkerError({
699
+ kind: response.payload.kind,
700
+ context: response.payload.context,
701
+ error: rehydrateError(response.payload.error)
702
+ });
703
+ }
704
+ return;
705
+ }
604
706
  const route = this.#events.get(response.handleId);
605
707
  if (route === void 0)
606
708
  return;
@@ -651,30 +753,52 @@ class MinnowDatabaseClient {
651
753
  }
652
754
  #rejectUnreadable(message, cause) {
653
755
  const reason = cause instanceof Error ? cause.message : String(cause);
654
- const error = new Error(`The database worker sent a frame this client cannot read: ${reason}`, {
655
- cause
656
- });
756
+ const text = `The database worker sent a frame this client cannot read: ${reason}`;
657
757
  const requestId = message.requestId;
658
758
  const pending = typeof requestId === "string" ? this.#pending.get(requestId) : void 0;
659
759
  if (pending === void 0 || typeof requestId !== "string") {
660
- this.#fail(error);
760
+ this.#fail(new DatabaseWorkerFailedError("messageerror", text, { cause }));
661
761
  return;
662
762
  }
763
+ const error = new Error(text, { cause });
663
764
  this.#pending.delete(requestId);
664
765
  pending.cleanup?.();
665
766
  pending.reject(pending.mayPublish ? new DatabaseWorkerOutcomeUnknownError(pending.method, requestId, { cause: error }) : error);
666
767
  }
667
- #fail(error) {
768
+ #fail(error, closing = false) {
769
+ const first = this.#fatal === void 0;
668
770
  this.#fatal = error;
669
771
  const pending = [...this.#pending.values()];
670
772
  this.#pending.clear();
773
+ const routes = [...this.#events.values()];
671
774
  this.#events.clear();
775
+ for (const route of routes) {
776
+ if (!closing) {
777
+ try {
778
+ route.onError?.(error);
779
+ } catch {
780
+ }
781
+ }
782
+ try {
783
+ route.onComplete?.();
784
+ } catch {
785
+ }
786
+ }
672
787
  for (const call of pending) {
673
788
  call.cleanup?.();
674
789
  call.reject(call.mayPublish ? new DatabaseWorkerOutcomeUnknownError(call.method, call.requestId, { cause: error }) : error);
675
790
  }
791
+ if (first && error instanceof ConnectionLostError && !(error instanceof DatabaseWorkerFailedError && error.reason === "reopened") && this.#onConnectionLost !== void 0) {
792
+ try {
793
+ this.#onConnectionLost(error);
794
+ } catch {
795
+ }
796
+ }
676
797
  }
677
798
  }
799
+ function keepalivePace(deadlineMs) {
800
+ return Math.min(WORKER_KEEPALIVE_INTERVAL_MS, Math.max(MIN_WORKER_KEEPALIVE_INTERVAL_MS, Math.floor(deadlineMs / 3)));
801
+ }
678
802
  function clientDeadline(value) {
679
803
  if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647)
680
804
  throw new RangeError("Worker timeout must be a positive timer interval");
@@ -695,6 +819,10 @@ class ClientBufferedWriter {
695
819
  created.catch(() => void 0);
696
820
  }
697
821
  #created;
822
+ #onClose;
823
+ _onClose(cleanup) {
824
+ this.#onClose = cleanup;
825
+ }
698
826
  async add(row) {
699
827
  await this.#created;
700
828
  return await this.client._invoke(this.handleId, "add", [row]);
@@ -720,6 +848,8 @@ class ClientBufferedWriter {
720
848
  return await this.client._invoke(this.handleId, "close", []);
721
849
  } finally {
722
850
  this.client._unrouteEvents(this.handleId);
851
+ this.#onClose?.();
852
+ this.#onClose = void 0;
723
853
  }
724
854
  }
725
855
  }
@@ -4,7 +4,7 @@ export { attachLifecycleFlush, BufferedTableWriter, MAX_BUFFERED_WRITER_PENDING_
4
4
  import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
5
5
  export { DatabaseReadBacklogError } from "./errors.js";
6
6
  import { type Compression } from "../block-format/index.js";
7
- import { type BlockStore, type ColumnDefault, type ColumnGenerated, CompactionBacklogError, type CompactionJobRecord, type CompactionJobState, type GarbageCollectionJobRecord, type GarbageCollectionJobState, type SimpleDataType, type SqlDomain, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult, TableInUseError } from "../storage/types.js";
7
+ import { type BlockStore, type ColumnDefault, type ColumnGenerated, CompactionBacklogError, type CompactionJobRecord, type CompactionJobState, type GarbageCollectionJobRecord, type GarbageCollectionJobState, type SimpleDataType, type SqlDomain, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult, type TableColumnRecord, type TableRecord, TableInUseError } from "../storage/types.js";
8
8
  import type { SnapshotExportProgress, SnapshotLoadProgress } from "../storage/snapshot.js";
9
9
  import { type ComparisonOperator, type CompiledQuery, type CompiledStatement, type ForeignKeyDefinition, type QueryResult, type QueryRow, type QueryValue, type UniqueConstraintDefinition } from "./query.js";
10
10
  import { LiveQuerySet, type LiveQuerySetOptions } from "./live.js";
@@ -476,6 +476,13 @@ export interface MinnowDatabaseOptions<TSchema extends AnySchema = UntypedSchema
476
476
  coordinateWrites?: boolean;
477
477
  now?: () => Date;
478
478
  createId?: () => string;
479
+ /**
480
+ * Hears failures in work no caller awaits: a background collection pass that failed, a live
481
+ * sweep that failed with nobody subscribed. `maintenanceStatus().lastError` still records the
482
+ * last one; this hook sees every one, as it happens. Inside the worker host it feeds the
483
+ * client's `onWorkerError`.
484
+ */
485
+ onBackgroundError?: (error: unknown, context: string) => void;
479
486
  /** Durable spill-owner lease lifetime; renewed while a spilling query runs. */
480
487
  spillOwnerLeaseMs?: number;
481
488
  /** Durable active-writer deadline; renewed every third while the writer is live. */
@@ -657,6 +664,13 @@ export interface RunStatementOptions {
657
664
  */
658
665
  export interface StatementWriter extends WriteSession {
659
666
  queryPlan(plan: CompiledQuery): Promise<QueryResult>;
667
+ /**
668
+ * Which of the given keys the scope sees as present — staged by it and not removed, or
669
+ * committed — as key tokens, answered from the scope's key ledger and a keyed probe so a plain
670
+ * INSERT or an `ON CONFLICT DO NOTHING` never has to stage its buffered predecessors just to
671
+ * find a duplicate. Undefined when the writer cannot answer without a read.
672
+ */
673
+ stagedKeyPresence?(table: TableRecord, keyColumn: TableColumnRecord, keys: ReadonlyArray<Exclude<BatchValue, null>>): Promise<ReadonlySet<string> | undefined>;
660
674
  }
661
675
  export interface VisibleSegment {
662
676
  id: string;