@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
@@ -9,14 +9,35 @@ export declare class TransactionExpiredError extends Error {
9
9
  constructor();
10
10
  }
11
11
  /** The worker stopped responding within the configured request deadline. */
12
- export declare class DatabaseWorkerTimeoutError extends Error {
12
+ export declare class DatabaseWorkerTimeoutError extends ConnectionLostError {
13
13
  readonly method: string;
14
14
  readonly timeoutMs: number;
15
15
  readonly name = "DatabaseWorkerTimeoutError";
16
16
  constructor(method: string, timeoutMs: number);
17
17
  }
18
+ /**
19
+ * The worker's channel failed underneath every pending call: the worker raised a script error it
20
+ * did not handle, or sent a frame that could not be read. `reason` says which; `cause` carries
21
+ * the worker's own error when the browser handed it over.
22
+ */
23
+ export declare class DatabaseWorkerFailedError extends ConnectionLostError {
24
+ readonly reason: "error" | "messageerror" | "reopened";
25
+ readonly name = "DatabaseWorkerFailedError";
26
+ constructor(reason: "error" | "messageerror" | "reopened", message: string, options?: ErrorOptions);
27
+ }
28
+ /**
29
+ * The store an `auto` descriptor remembered for this database cannot be opened here — OPFS in a
30
+ * context without synchronous access handles, typically — and the database is not reopened,
31
+ * empty, on the other store.
32
+ */
33
+ export declare class DatabaseStoreUnavailableError extends Error {
34
+ readonly store: "opfs" | "indexeddb";
35
+ readonly databaseName: string;
36
+ readonly name = "DatabaseStoreUnavailableError";
37
+ constructor(store: "opfs" | "indexeddb", databaseName: string, message: string);
38
+ }
18
39
  /** A transport failure cannot prove whether this operation published before its reply was lost. */
19
- export declare class DatabaseWorkerOutcomeUnknownError extends Error {
40
+ export declare class DatabaseWorkerOutcomeUnknownError extends UnknownOutcomeError {
20
41
  readonly method: string;
21
42
  readonly requestId: string;
22
43
  readonly name = "DatabaseWorkerOutcomeUnknownError";
@@ -108,4 +129,42 @@ export declare class SqlCompileError extends TypeError {
108
129
  readonly name = "SqlCompileError";
109
130
  constructor(message: string, offset: number, length: number);
110
131
  }
132
+ import { ConnectionLostError, UnknownOutcomeError } from "../storage/types.js";
133
+ /**
134
+ * What an error means for the caller, in the three terms that decide what to do next. Every
135
+ * error Minnow throws maps to exactly one kind; an unrecognized error is `"other"` with the
136
+ * cautious answers.
137
+ */
138
+ export type ErrorKind =
139
+ /** The operation may have happened. Reconcile a stable id or revision before retrying. */
140
+ "unknown-outcome"
141
+ /** This connection is finished; open a new one. Pending mutations were reported separately. */
142
+ | "connection-lost"
143
+ /** Lost a race with another writer or a schema change; nothing happened. Retry as is. */
144
+ | "conflict"
145
+ /** The request itself is wrong (a duplicate key, a bad table, a compile error). Fix it. */
146
+ | "rejected"
147
+ /** A momentary condition: backpressure, coordination, an expired scope. Retry with backoff. */
148
+ | "transient"
149
+ /** A limit was reached: quota, memory, a resource ceiling. Free something, then retry. */
150
+ | "resource"
151
+ /** The stored data is unreadable by this build. Do not retry, and do not delete. */
152
+ | "corruption"
153
+ /** The caller cancelled it; nothing happened. */
154
+ | "cancelled" | "other";
155
+ export interface ErrorClassification {
156
+ kind: ErrorKind;
157
+ /** True when the operation may have published despite the error. Never replay when true. */
158
+ mayHavePublished: boolean;
159
+ /** Whether repeating the same call is sound: as is, only after reconciling, or not at all. */
160
+ retry: "safe" | "after-reconcile" | "never";
161
+ /** False when every later call on the same connection will fail the same way. */
162
+ connectionUsable: boolean;
163
+ }
164
+ /**
165
+ * Classifies any error into what it means for the caller: may the operation have published, is
166
+ * a retry sound, and is the connection still usable. Works on errors rehydrated from the worker
167
+ * or a follower hop, and on the platform's own `QuotaExceededError` and `AbortError`.
168
+ */
169
+ export declare function classifyError(error: unknown): ErrorClassification;
111
170
  export {};
@@ -4,7 +4,7 @@ class TransactionExpiredError extends Error {
4
4
  super("The SQL transaction expired and rolled back; issue ROLLBACK or BEGIN before continuing");
5
5
  }
6
6
  }
7
- class DatabaseWorkerTimeoutError extends Error {
7
+ class DatabaseWorkerTimeoutError extends ConnectionLostError {
8
8
  method;
9
9
  timeoutMs;
10
10
  name = "DatabaseWorkerTimeoutError";
@@ -14,7 +14,25 @@ class DatabaseWorkerTimeoutError extends Error {
14
14
  this.timeoutMs = timeoutMs;
15
15
  }
16
16
  }
17
- class DatabaseWorkerOutcomeUnknownError extends Error {
17
+ class DatabaseWorkerFailedError extends ConnectionLostError {
18
+ reason;
19
+ name = "DatabaseWorkerFailedError";
20
+ constructor(reason, message, options) {
21
+ super(message, options);
22
+ this.reason = reason;
23
+ }
24
+ }
25
+ class DatabaseStoreUnavailableError extends Error {
26
+ store;
27
+ databaseName;
28
+ name = "DatabaseStoreUnavailableError";
29
+ constructor(store, databaseName, message) {
30
+ super(message);
31
+ this.store = store;
32
+ this.databaseName = databaseName;
33
+ }
34
+ }
35
+ class DatabaseWorkerOutcomeUnknownError extends UnknownOutcomeError {
18
36
  method;
19
37
  requestId;
20
38
  name = "DatabaseWorkerOutcomeUnknownError";
@@ -138,11 +156,105 @@ class SqlCompileError extends TypeError {
138
156
  }
139
157
  }
140
158
  import { dateIsoString } from "../date-value.js";
159
+ import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, ConnectionLostError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, OpfsCoordinationError, OpfsDatabaseInUseError, PostingBuildConflictError, SchemaConflictError, SnapshotImportConflictError, SnapshotManifestMissingError, StorageCorruptionError, StorageFormatVersionError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueIndexCoverageError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UnknownOutcomeError, WriteConflictError } from "../storage/types.js";
160
+ const CONFLICT_ERRORS = [
161
+ WriteConflictError,
162
+ SchemaConflictError,
163
+ TableRecordConflictError,
164
+ TransactionRecordConflictError,
165
+ LeaseConflictError,
166
+ LeaseOwnerConflictError,
167
+ CompactionJobConflictError,
168
+ GarbageCollectionJobConflictError,
169
+ TempOwnerConflictError,
170
+ UniqueKeyBuildConflictError,
171
+ PostingBuildConflictError,
172
+ SnapshotImportConflictError,
173
+ SnapshotManifestMissingError,
174
+ TableInUseError,
175
+ OpfsDatabaseInUseError
176
+ ];
177
+ const TRANSIENT_ERRORS = [
178
+ OpfsCoordinationError,
179
+ DatabaseReadBacklogError,
180
+ MaintenanceBacklogError,
181
+ CompactionBacklogError,
182
+ LiveQueryLimitError,
183
+ LeaseExpiredError,
184
+ TransactionExpiredError,
185
+ IndexedDbSchemaUpgradeBlockedError,
186
+ VisibleSegmentCursorStaleError
187
+ ];
188
+ const RESOURCE_ERRORS = [
189
+ StorageResourceLimitError,
190
+ BlockReadBatchTooLargeError,
191
+ CompactionMemoryBudgetError,
192
+ CompactionWriteAmplificationError
193
+ ];
194
+ const REJECTED_ERRORS = [
195
+ UniqueConstraintError,
196
+ UniqueKeyConflictError,
197
+ UniqueIndexCoverageError,
198
+ MissingKeyError,
199
+ UnknownTableError,
200
+ SqlCompileError,
201
+ CompactionJobCancelledError
202
+ ];
203
+ function isAnyOf(error, classes) {
204
+ return classes.some((constructor) => error instanceof constructor);
205
+ }
206
+ function hasErrorName(error, name) {
207
+ return typeof error === "object" && error !== null && error.name === name;
208
+ }
209
+ function classifyError(error) {
210
+ if (error instanceof UnknownOutcomeError) {
211
+ return {
212
+ kind: "unknown-outcome",
213
+ mayHavePublished: true,
214
+ retry: "after-reconcile",
215
+ connectionUsable: !(error.cause instanceof ConnectionLostError)
216
+ };
217
+ }
218
+ if (error instanceof ConnectionLostError) {
219
+ return {
220
+ kind: "connection-lost",
221
+ mayHavePublished: false,
222
+ retry: "never",
223
+ connectionUsable: false
224
+ };
225
+ }
226
+ if (error instanceof StorageCorruptionError || error instanceof StorageFormatVersionError) {
227
+ return { kind: "corruption", mayHavePublished: false, retry: "never", connectionUsable: true };
228
+ }
229
+ if (isAnyOf(error, CONFLICT_ERRORS)) {
230
+ return { kind: "conflict", mayHavePublished: false, retry: "safe", connectionUsable: true };
231
+ }
232
+ if (isAnyOf(error, TRANSIENT_ERRORS)) {
233
+ return { kind: "transient", mayHavePublished: false, retry: "safe", connectionUsable: true };
234
+ }
235
+ if (isAnyOf(error, RESOURCE_ERRORS) || hasErrorName(error, "QuotaExceededError")) {
236
+ return {
237
+ kind: "resource",
238
+ mayHavePublished: false,
239
+ retry: "after-reconcile",
240
+ connectionUsable: true
241
+ };
242
+ }
243
+ if (isAnyOf(error, REJECTED_ERRORS) || error instanceof TypeError || error instanceof RangeError || error instanceof SyntaxError) {
244
+ return { kind: "rejected", mayHavePublished: false, retry: "never", connectionUsable: true };
245
+ }
246
+ if (hasErrorName(error, "AbortError")) {
247
+ return { kind: "cancelled", mayHavePublished: false, retry: "safe", connectionUsable: true };
248
+ }
249
+ return { kind: "other", mayHavePublished: false, retry: "never", connectionUsable: true };
250
+ }
141
251
  export {
142
252
  CompactionJobCancelledError,
143
253
  CompactionMemoryBudgetError,
144
254
  CompactionWriteAmplificationError,
145
255
  DatabaseReadBacklogError,
256
+ DatabaseStoreUnavailableError,
257
+ DatabaseWorkerFailedError,
146
258
  DatabaseWorkerOutcomeUnknownError,
147
259
  DatabaseWorkerTimeoutError,
148
260
  LiveQueryLimitError,
@@ -152,5 +264,6 @@ export {
152
264
  TransactionExpiredError,
153
265
  UniqueConstraintError,
154
266
  UnknownTableError,
155
- VisibleSegmentCursorStaleError
267
+ VisibleSegmentCursorStaleError,
268
+ classifyError
156
269
  };
@@ -5,6 +5,7 @@
5
5
  export * from "./catalog.js";
6
6
  export * from "./database.js";
7
7
  export * from "./errors.js";
8
+ export { forgetStoreChoice } from "./auto-store.js";
8
9
  export { QueryMemoryBudgetError, type QueryMemoryUsage } from "./memory.js";
9
10
  export { MAX_SQL_NESTING_DEPTH, MAX_SQL_NUMERIC_DIGITS, MAX_SQL_PARAMETERS, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_STRUCTURED_VALUE_DEPTH, MAX_SQL_STRUCTURED_VALUE_ITEMS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS, } from "./cache-limits.js";
10
11
  /**
@@ -1,6 +1,7 @@
1
1
  export * from "./catalog.js";
2
2
  export * from "./database.js";
3
3
  export * from "./errors.js";
4
+ import { forgetStoreChoice } from "./auto-store.js";
4
5
  import { QueryMemoryBudgetError } from "./memory.js";
5
6
  import { MAX_SQL_NESTING_DEPTH, MAX_SQL_NUMERIC_DIGITS, MAX_SQL_PARAMETERS, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_STRUCTURED_VALUE_DEPTH, MAX_SQL_STRUCTURED_VALUE_ITEMS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS } from "./cache-limits.js";
6
7
  import { column, foreignKeyName, isDestructiveStep, planMigration, schema, table, view } from "./schema.js";
@@ -22,6 +23,7 @@ export {
22
23
  QueryMemoryBudgetError,
23
24
  column,
24
25
  foreignKeyName,
26
+ forgetStoreChoice,
25
27
  isDestructiveStep,
26
28
  planMigration,
27
29
  schema,
@@ -125,6 +125,25 @@ export interface LiveQueryStats {
125
125
  /** Work avoided because equal statements shared one query group or in-flight execution. */
126
126
  sharedExecutions: number;
127
127
  lastSweepMs: number;
128
+ /** One entry per distinct statement, with why it re-executes when it does. */
129
+ groups: readonly LiveQueryGroupStats[];
130
+ }
131
+ /** What one distinct live statement costs, and why. */
132
+ export interface LiveQueryGroupStats {
133
+ /** The statement's SQL; undefined for a compiled-plan envelope. */
134
+ readonly sql: string | undefined;
135
+ readonly subscribers: number;
136
+ /** Whether the engine maintains this statement incrementally; false before its first run too. */
137
+ readonly maintainable: boolean;
138
+ /** Why it re-executes on every relevant commit; empty when it is maintained. */
139
+ readonly reasons: readonly string[];
140
+ /** Full executions after the first. */
141
+ readonly reruns: number;
142
+ /** Deliveries answered by patching the retained result. */
143
+ readonly maintained: number;
144
+ /** Full executions of a maintainable statement that a particular commit could not be patched for. */
145
+ readonly fallbacks: number;
146
+ readonly retainedRows: number;
128
147
  }
129
148
  /** A live query: plain SQL, parameterized SQL, or a compiled-plan envelope. */
130
149
  export type LiveQueryInput = string | {
@@ -157,6 +176,10 @@ export interface LiveMaintainedExecution {
157
176
  /** Modeled total bytes retained by the result and opaque maintenance state. */
158
177
  readonly retainedBytes?: number;
159
178
  }
179
+ /** The host will not maintain this statement, and says why, in the terms a developer fixes. */
180
+ export interface LiveMaintenanceDeclined {
181
+ readonly declined: readonly string[];
182
+ }
160
183
  export interface LiveMaintainedChange extends LiveMaintainedExecution {
161
184
  readonly changed: boolean;
162
185
  /** For each row of `result`, its index in the previous result, or -1; see `LiveQueryDelivery`. */
@@ -175,7 +198,7 @@ export interface LiveQueryHost {
175
198
  * Executes a statement the host can later maintain incrementally, or returns undefined when
176
199
  * the statement's shape rules that out; the set then executes it in full from then on.
177
200
  */
178
- executeMaintainable?(query: LiveQueryInput, context?: LiveQueryExecuteContext): Promise<LiveMaintainedExecution | undefined>;
201
+ executeMaintainable?(query: LiveQueryInput, context?: LiveQueryExecuteContext): Promise<LiveMaintainedExecution | LiveMaintenanceDeclined | undefined>;
179
202
  /**
180
203
  * Patches a retained result with the row changes the commits in (after, until] made to
181
204
  * `tableIds`, or returns undefined when only a full execution can answer.
@@ -111,7 +111,8 @@ class LiveQuerySet {
111
111
  retainedRows: 0,
112
112
  retainedBytes: 0,
113
113
  sharedExecutions: 0,
114
- lastSweepMs: 0
114
+ lastSweepMs: 0,
115
+ groups: []
115
116
  };
116
117
  #lastProbe;
117
118
  #pendingProbe;
@@ -159,9 +160,22 @@ class LiveQuerySet {
159
160
  }
160
161
  get stats() {
161
162
  let retainedRows = 0;
162
- for (const group of this.#groups.values())
163
- retainedRows += group.result?.rows.length ?? 0;
164
- return { ...this.#stats, retainedRows, retainedBytes: this.#retainedBytes };
163
+ const groups = [];
164
+ for (const group of this.#groups.values()) {
165
+ const rows = group.result?.rows.length ?? 0;
166
+ retainedRows += rows;
167
+ groups.push({
168
+ sql: typeof group.query === "string" ? group.query : group.query.kind === "sql-query" ? group.query.sql : void 0,
169
+ subscribers: group.subscribers.size,
170
+ maintainable: group.maintenance !== void 0,
171
+ reasons: group.reasons,
172
+ reruns: group.reruns,
173
+ maintained: group.maintained,
174
+ fallbacks: group.fallbacks,
175
+ retainedRows: rows
176
+ });
177
+ }
178
+ return { ...this.#stats, retainedRows, retainedBytes: this.#retainedBytes, groups };
165
179
  }
166
180
  #freshProbe() {
167
181
  let pending = this.#pendingProbe;
@@ -334,7 +348,11 @@ class LiveQuerySet {
334
348
  execution: void 0,
335
349
  deliveries: 0,
336
350
  maintenance: void 0,
337
- unmaintainable: false
351
+ unmaintainable: false,
352
+ reasons: [],
353
+ reruns: 0,
354
+ maintained: 0,
355
+ fallbacks: 0
338
356
  };
339
357
  this.#groups.set(key, group);
340
358
  this.#indexGroup(group, dependencies);
@@ -399,9 +417,10 @@ class LiveQuerySet {
399
417
  try {
400
418
  if (this.#incremental && this.#host.executeMaintainable !== void 0 && !group.unmaintainable) {
401
419
  const maintained = await this.#host.executeMaintainable(group.query, context);
402
- if (maintained === void 0)
420
+ if (maintained === void 0 || "declined" in maintained) {
403
421
  group.unmaintainable = true;
404
- else {
422
+ group.reasons = maintained?.declined ?? ["the host declined to maintain it"];
423
+ } else {
405
424
  candidate = maintained;
406
425
  executed = maintained.result;
407
426
  maintenance = maintained.state;
@@ -665,6 +684,7 @@ class LiveQuerySet {
665
684
  if (catalogChanged) {
666
685
  group.maintenance = void 0;
667
686
  group.unmaintainable = false;
687
+ group.reasons = [];
668
688
  try {
669
689
  await this.#refreshDependencies(group);
670
690
  } catch (error) {
@@ -719,10 +739,14 @@ class LiveQuerySet {
719
739
  if (!this.#stillOpen() || this.#groups.get(group.key) !== group)
720
740
  return;
721
741
  }
722
- if (execution !== void 0)
742
+ if (execution !== void 0) {
723
743
  this.#stats.maintained += 1;
724
- else {
744
+ group.maintained += 1;
745
+ } else {
725
746
  this.#stats.reruns += 1;
747
+ group.reruns += 1;
748
+ if (group.maintenance !== void 0)
749
+ group.fallbacks += 1;
726
750
  execution = await this.#executeGroup(group, {
727
751
  probe: current,
728
752
  memoize: groupMemoizes(group)
@@ -0,0 +1,36 @@
1
+ const DEFAULT_SCOPE_WRITE_SET_BUDGET_BYTES = 64 * 1024 * 1024;
2
+ const scopeWriteSetTestHooks = {
3
+ defaultBudgetBytes: DEFAULT_SCOPE_WRITE_SET_BUDGET_BYTES,
4
+ budgetBytes: DEFAULT_SCOPE_WRITE_SET_BUDGET_BYTES
5
+ };
6
+ function composeScopeEffect(base, next) {
7
+ switch (next.kind) {
8
+ case "delete":
9
+ return next;
10
+ case "insert":
11
+ return base?.kind === "delete" ? { kind: "upsert", values: next.values } : next;
12
+ case "upsert":
13
+ return base?.kind === "insert" ? { kind: "insert", values: next.values } : next;
14
+ case "update": {
15
+ if (base === void 0)
16
+ return next;
17
+ if (base.kind === "delete")
18
+ throw new Error("A deleted key cannot take an update");
19
+ const values = base.values.slice();
20
+ next.values.forEach((value, position) => {
21
+ if (value !== void 0)
22
+ values[position] = value;
23
+ });
24
+ return { kind: base.kind, values };
25
+ }
26
+ }
27
+ }
28
+ function netScopeEffect(entry) {
29
+ return entry.pending === void 0 ? entry.staged : composeScopeEffect(entry.staged, entry.pending);
30
+ }
31
+ export {
32
+ DEFAULT_SCOPE_WRITE_SET_BUDGET_BYTES,
33
+ composeScopeEffect,
34
+ netScopeEffect,
35
+ scopeWriteSetTestHooks
36
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { attachWorkerHost } from "./worker-server.js";
2
+ import { autoWorkerStore } from "./worker-store-auto.js";
3
+ attachWorkerHost(self, autoWorkerStore);
@@ -1,5 +1,6 @@
1
1
  import { type RpcScope, type WorkerStoreFactory, type WorkerStoreOptions } from "./worker-server.js";
2
- export { exposeDatabase, singleStoreFactory, MAX_WORKER_HANDLES_PER_CONNECTION, type DatabaseInitPayload, type ExposeDatabaseOptions, type RpcScope, type StoreDescriptor, type WireDatabaseOptions, type WorkerStoreFactory, type WorkerStoreOptions, } from "./worker-server.js";
2
+ export { forgetStoreChoice, openAutoStore, resolveAutoStoreKind, type AutoStoreKind, } from "./auto-store.js";
3
+ export { exposeDatabase, singleStoreFactory, MAX_WORKER_HANDLES_PER_CONNECTION, type DatabaseInitPayload, type ExposeDatabaseOptions, type OpenedStore, type OpenedStoreKind, type RpcScope, type StoreDescriptor, type WireDatabaseOptions, type WorkerStoreFactory, type WorkerStoreOptions, } from "./worker-server.js";
3
4
  export interface AttachDatabaseWorkerOptions extends WorkerStoreOptions {
4
5
  /**
5
6
  * Replaces the stock store factory. `singleStoreFactory()` builds one that opens a single
@@ -1,9 +1,23 @@
1
1
  import { attachWorkerHost } from "./worker-server.js";
2
+ import { forgetStoreChoice, openAutoStore, resolveAutoStoreKind } from "./auto-store.js";
2
3
  import { exposeDatabase, singleStoreFactory, MAX_WORKER_HANDLES_PER_CONNECTION } from "./worker-server.js";
3
4
  function attachDatabaseWorker(scope, options = {}) {
4
5
  attachWorkerHost(scope, options.createStore ?? createStore, options);
5
6
  }
6
7
  async function createStore(descriptor, options) {
8
+ if (descriptor.kind === "auto") {
9
+ const { openAutoStore: openAutoStore2 } = await import("./auto-store.js");
10
+ return openAutoStore2(descriptor.name, async (kind) => {
11
+ const opened = await createStore(kind === "opfs" ? {
12
+ kind,
13
+ name: descriptor.name,
14
+ ...descriptor.opfs?.durability === void 0 ? {} : { durability: descriptor.opfs.durability }
15
+ } : { kind, name: descriptor.name, ...descriptor.indexeddb }, options);
16
+ return opened;
17
+ }, {
18
+ opfsDatabaseExists: async (name) => (await import("../storage/opfs/index.js")).opfsDatabaseExists({ name })
19
+ });
20
+ }
7
21
  if (descriptor.kind === "memory") {
8
22
  const { MemoryBlockStore } = await import("../storage/memory.js");
9
23
  return new MemoryBlockStore();
@@ -12,7 +26,8 @@ async function createStore(descriptor, options) {
12
26
  const { OpfsBlockStore } = await import("../storage/opfs/index.js");
13
27
  return OpfsBlockStore.open({
14
28
  name: descriptor.name,
15
- ...descriptor.durability === void 0 ? {} : { durability: descriptor.durability }
29
+ ...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
30
+ ...options.onDiagnostic === void 0 ? {} : { onDiagnostic: options.onDiagnostic }
16
31
  });
17
32
  }
18
33
  const { IndexedDbBlockStore } = await import("../storage/indexeddb.js");
@@ -27,5 +42,8 @@ export {
27
42
  MAX_WORKER_HANDLES_PER_CONNECTION,
28
43
  attachDatabaseWorker,
29
44
  exposeDatabase,
45
+ forgetStoreChoice,
46
+ openAutoStore,
47
+ resolveAutoStoreKind,
30
48
  singleStoreFactory
31
49
  };
@@ -1,4 +1,5 @@
1
1
  import type { BlockStore } from "../storage/types.js";
2
+ import { type WorkerErrorKind } from "../worker-protocol/index.js";
2
3
  import { MinnowDatabase, type MinnowDatabaseOptions } from "./database.js";
3
4
  /**
4
5
  * Worker-side host for the main-thread client. exposeDatabase() answers the client's RPC frames
@@ -24,12 +25,40 @@ export type StoreDescriptor = {
24
25
  kind: "opfs";
25
26
  name: string;
26
27
  durability?: "relaxed" | "strict";
28
+ }
29
+ /**
30
+ * OPFS where the worker can hold synchronous access handles, IndexedDB elsewhere. The choice
31
+ * is made once per name and remembered, so the database never reopens empty on the other
32
+ * store; `client.storeKind()` reports which one opened.
33
+ */
34
+ | {
35
+ kind: "auto";
36
+ name: string;
37
+ opfs?: {
38
+ durability?: "relaxed" | "strict";
39
+ };
40
+ indexeddb?: {
41
+ durability?: IDBTransactionDurability;
42
+ uniqueKeyCacheBytes?: number;
43
+ };
27
44
  };
45
+ /** The store kinds a worker can actually open; `auto` resolves to one of the durable two. */
46
+ export type OpenedStoreKind = "indexeddb" | "opfs" | "memory";
47
+ /** What a store factory may return when the descriptor did not name the kind it opened. */
48
+ export interface OpenedStore {
49
+ store: BlockStore;
50
+ kind: OpenedStoreKind;
51
+ }
28
52
  /** The cloneable subset of MinnowDatabaseOptions; function-valued seams stay worker-side. */
29
53
  export type WireDatabaseOptions = Pick<MinnowDatabaseOptions, "compression" | "targetBlockBytes" | "rowsPerBlock" | "maxCommitRetries" | "coordinateWrites" | "spillOwnerLeaseMs" | "transactionOwnerLeaseMs" | "transactionIdleTimeoutMs" | "bufferPoolBytes" | "executionMemoryBudgetBytes" | "autoCollectDebtLimitCommits" | "autoCollect" | "autoCompact">;
30
54
  export interface DatabaseInitPayload {
31
55
  store: StoreDescriptor;
32
56
  options?: WireDatabaseOptions;
57
+ /**
58
+ * How often the worker reports on a call it is still working on; the client derives it
59
+ * from its own deadline so a short deadline still hears from a long call in time.
60
+ */
61
+ keepaliveIntervalMs?: number;
33
62
  }
34
63
  /** The slice of DedicatedWorkerGlobalScope (or MessagePort) the host needs. */
35
64
  export interface RpcScope {
@@ -42,6 +71,17 @@ export interface RpcScope {
42
71
  transfer: ArrayBuffer[];
43
72
  }): void;
44
73
  }
74
+ /** Reports a worker failure that belongs to no request to the main thread. */
75
+ export type WorkerErrorReporter = (kind: WorkerErrorKind, error: unknown, context: string) => void;
76
+ /**
77
+ * The one place every failure that is nobody's call answer goes. It posts a diagnostic event
78
+ * frame the client turns into `onWorkerError` (or a `console.error` on the main thread), and on
79
+ * a real worker global it also catches what would otherwise be an uncaught exception, an
80
+ * unhandled rejection, or an unreadable inbound frame. Those three are marked handled so the
81
+ * browser does not log them a second time under the worker's own name; the main thread's copy
82
+ * carries the same message, stack, and cause.
83
+ */
84
+ export declare function workerErrorReporter(scope: RpcScope): WorkerErrorReporter;
45
85
  export interface ExposeDatabaseOptions {
46
86
  /** Called when the client sends its dispose frame, after every handle has been closed. */
47
87
  onDispose?: () => void | Promise<void>;
@@ -56,18 +96,28 @@ export interface ExposeDatabaseOptions {
56
96
  * on the tab the user is looking at.
57
97
  */
58
98
  onVisibility?: (visible: boolean) => void;
99
+ /** The kind of store behind the database, reported to the client when it connects. */
100
+ storeKind?: OpenedStoreKind;
101
+ /** Milliseconds between keepalive reports on a call in progress; defaults to five seconds. */
102
+ keepaliveIntervalMs?: number;
59
103
  }
60
104
  /** What a worker store factory may need beyond the descriptor itself. */
61
105
  export interface WorkerStoreOptions {
62
106
  /** Test seam for environments without a global IndexedDB (e.g. fake-indexeddb). */
63
107
  indexedDB?: IDBFactory;
108
+ /**
109
+ * Hears a store's background failures — a failed checkpoint, a broken election — so they reach
110
+ * the main thread. The stock host wires it to the worker error reporter; a factory passes it
111
+ * to a store that accepts one (`OpfsBlockStoreOptions.onDiagnostic`).
112
+ */
113
+ onDiagnostic?: (error: unknown, context: string) => void;
64
114
  }
65
115
  /**
66
116
  * Builds the block store named by a client's init frame. The stock entry's factory loads the
67
117
  * adapter for any of the three kinds on demand; a per-store entry supplies one that imports a
68
118
  * single adapter statically and refuses the rest.
69
119
  */
70
- export type WorkerStoreFactory = (descriptor: StoreDescriptor, options: WorkerStoreOptions) => BlockStore | Promise<BlockStore>;
120
+ export type WorkerStoreFactory = (descriptor: StoreDescriptor, options: WorkerStoreOptions) => BlockStore | OpenedStore | Promise<BlockStore | OpenedStore>;
71
121
  /** Hard per-connection ceiling for resident worker resources abandoned by a client. */
72
122
  export declare const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
73
123
  /**
@@ -91,3 +141,5 @@ export declare function attachWorkerHost(scope: RpcScope, createStore: WorkerSto
91
141
  export declare function singleStoreFactory<Kind extends StoreDescriptor["kind"]>(kind: Kind, open: (descriptor: Extract<StoreDescriptor, {
92
142
  kind: Kind;
93
143
  }>, options: WorkerStoreOptions) => BlockStore | Promise<BlockStore>): WorkerStoreFactory;
144
+ /** The refusal a worker entry gives an init frame for a store it does not bundle. */
145
+ export declare function unsupportedStoreKindError(bundled: StoreDescriptor["kind"], requested: string): Error;