@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
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What the composition root knows that this module must not import: whether an OPFS database
3
+ * directory of a name exists. The OPFS adapter provides it (`opfsDatabaseExists`); a root that
4
+ * cannot bundle OPFS leaves it out, and OPFS is then never chosen for an unknown name anyway.
5
+ */
6
+ export interface AutoStoreProbes {
7
+ opfsDatabaseExists?: (name: string) => Promise<boolean>;
8
+ }
9
+ /**
10
+ * The `{ kind: "auto" }` store: OPFS where this context can hold synchronous access handles,
11
+ * IndexedDB where it cannot (Safari's private browsing, a page context, an older build).
12
+ *
13
+ * The choice is made once per database name and remembered in a small IndexedDB record, so a
14
+ * database created on one store is never silently reopened, empty, on the other: a remembered
15
+ * store that cannot open now is reported as `DatabaseStoreUnavailableError` instead. Delete the
16
+ * record with `forgetStoreChoice` when the database itself is deleted.
17
+ */
18
+ export type AutoStoreKind = "opfs" | "indexeddb";
19
+ /** Test seams: the OPFS probe and the IndexedDB factory that keeps the choices. */
20
+ export declare const autoStoreTestHooks: {
21
+ opfsAvailable?: () => Promise<boolean>;
22
+ /** Whether an OPFS database directory of this name exists; Node has no OPFS to ask. */
23
+ opfsDatabaseExists?: (name: string) => Promise<boolean>;
24
+ indexedDB?: IDBFactory;
25
+ };
26
+ /**
27
+ * Whether OPFS is usable here: the directory handle resolves, and file handles expose
28
+ * synchronous access — which only a dedicated worker has, and Safari's private browsing never.
29
+ */
30
+ export declare function opfsAvailable(): Promise<boolean>;
31
+ /**
32
+ * Resolves `auto` for one database name: the remembered store, or the best one available.
33
+ * A fresh choice is reserved with an insert-only write, so two workers deciding at the same
34
+ * instant in contexts that disagree still end up on the one store the record names; the
35
+ * caller reports through `settleAutoStoreChoice` whether that reservation produced a database.
36
+ */
37
+ export declare function resolveAutoStoreKind(name: string, probes?: AutoStoreProbes): Promise<{
38
+ kind: AutoStoreKind;
39
+ reserved: boolean;
40
+ }>;
41
+ /**
42
+ * Opens the store `auto` resolves to. A reservation that produced no database is released
43
+ * again, so a first open that failed does not pin the name to a store that never held data —
44
+ * but only when the store really holds none: another connection may have opened the same
45
+ * reservation successfully in the meantime, and its database must keep its memory.
46
+ */
47
+ export declare function openAutoStore<Store>(name: string, open: (kind: AutoStoreKind) => Promise<Store>, probes?: AutoStoreProbes): Promise<{
48
+ store: Store;
49
+ kind: AutoStoreKind;
50
+ }>;
51
+ /** Forgets which store `auto` chose for a database; call it when deleting the database. */
52
+ export declare function forgetStoreChoice(name: string): Promise<void>;
@@ -0,0 +1,157 @@
1
+ import { DatabaseStoreUnavailableError } from "./errors.js";
2
+ const CHOICE_DATABASE = "minnowdb-store-choice";
3
+ const CHOICE_STORE = "choices";
4
+ const autoStoreTestHooks = {};
5
+ async function opfsAvailable() {
6
+ if (autoStoreTestHooks.opfsAvailable !== void 0)
7
+ return autoStoreTestHooks.opfsAvailable();
8
+ const storage = globalThis.navigator?.storage;
9
+ if (typeof storage?.getDirectory !== "function")
10
+ return false;
11
+ const fileHandle = globalThis.FileSystemFileHandle;
12
+ if (fileHandle?.prototype === void 0 || typeof fileHandle.prototype.createSyncAccessHandle !== "function") {
13
+ return false;
14
+ }
15
+ try {
16
+ await storage.getDirectory();
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+ async function resolveAutoStoreKind(name, probes = {}) {
23
+ const remembered = await readChoice(name);
24
+ if (remembered === "indexeddb")
25
+ return { kind: "indexeddb", reserved: false };
26
+ if (remembered === "opfs") {
27
+ if (await opfsAvailable())
28
+ return { kind: "opfs", reserved: false };
29
+ throw new DatabaseStoreUnavailableError("opfs", name, `Database "${name}" lives on the OPFS store, which this context cannot open; it is not reopened on IndexedDB, where it would be empty`);
30
+ }
31
+ const existing = await existingDatabaseStore(name, probes);
32
+ let kind;
33
+ if (existing === "opfs" && !await opfsAvailable()) {
34
+ throw new DatabaseStoreUnavailableError("opfs", name, `Database "${name}" lives on the OPFS store, which this context cannot open; it is not reopened on IndexedDB, where it would be empty`);
35
+ } else if (existing !== void 0)
36
+ kind = existing;
37
+ else
38
+ kind = await opfsAvailable() ? "opfs" : "indexeddb";
39
+ const reserved = await reserveChoice(name, kind);
40
+ if (reserved)
41
+ return { kind, reserved: true };
42
+ return resolveAutoStoreKind(name, probes);
43
+ }
44
+ async function openAutoStore(name, open, probes = {}) {
45
+ const { kind, reserved } = await resolveAutoStoreKind(name, probes);
46
+ try {
47
+ return { store: await open(kind), kind };
48
+ } catch (error) {
49
+ if (reserved && await existingDatabaseStore(name, probes).catch(() => kind) !== kind) {
50
+ await forgetStoreChoice(name).catch(() => void 0);
51
+ }
52
+ throw error;
53
+ }
54
+ }
55
+ async function existingDatabaseStore(name, probes) {
56
+ const opfsExists = autoStoreTestHooks.opfsDatabaseExists ?? probes.opfsDatabaseExists;
57
+ if (opfsExists !== void 0 && await opfsExists(name))
58
+ return "opfs";
59
+ if (await indexedDbDatabaseExists(name))
60
+ return "indexeddb";
61
+ return void 0;
62
+ }
63
+ async function indexedDbDatabaseExists(name) {
64
+ const factory = choiceFactory();
65
+ if (factory === void 0)
66
+ return false;
67
+ const list = factory.databases;
68
+ if (typeof list === "function") {
69
+ try {
70
+ return (await list.call(factory)).some((entry) => entry.name === name);
71
+ } catch {
72
+ }
73
+ }
74
+ return new Promise((resolve) => {
75
+ const request = factory.open(name);
76
+ let created = false;
77
+ request.addEventListener("upgradeneeded", () => {
78
+ created = true;
79
+ request.transaction?.abort();
80
+ });
81
+ request.addEventListener("success", () => {
82
+ request.result.close();
83
+ resolve(!created);
84
+ });
85
+ request.addEventListener("error", () => resolve(false));
86
+ request.addEventListener("blocked", () => resolve(true));
87
+ });
88
+ }
89
+ async function forgetStoreChoice(name) {
90
+ const database = await openChoices();
91
+ if (database === void 0)
92
+ return;
93
+ try {
94
+ await complete(database.transaction(CHOICE_STORE, "readwrite").objectStore(CHOICE_STORE).delete(name));
95
+ } finally {
96
+ database.close();
97
+ }
98
+ }
99
+ function choiceFactory() {
100
+ return autoStoreTestHooks.indexedDB ?? globalThis.indexedDB ?? void 0;
101
+ }
102
+ async function openChoices() {
103
+ const factory = choiceFactory();
104
+ if (factory === void 0)
105
+ return void 0;
106
+ const request = factory.open(CHOICE_DATABASE, 1);
107
+ request.addEventListener("upgradeneeded", () => {
108
+ if (!request.result.objectStoreNames.contains(CHOICE_STORE)) {
109
+ request.result.createObjectStore(CHOICE_STORE);
110
+ }
111
+ });
112
+ try {
113
+ return await complete(request);
114
+ } catch {
115
+ return void 0;
116
+ }
117
+ }
118
+ async function readChoice(name) {
119
+ const database = await openChoices();
120
+ if (database === void 0)
121
+ return void 0;
122
+ try {
123
+ const value = await complete(database.transaction(CHOICE_STORE, "readonly").objectStore(CHOICE_STORE).get(name));
124
+ const kind = typeof value === "object" && value !== null ? value.kind : void 0;
125
+ return kind === "opfs" || kind === "indexeddb" ? kind : void 0;
126
+ } finally {
127
+ database.close();
128
+ }
129
+ }
130
+ async function reserveChoice(name, kind) {
131
+ const database = await openChoices();
132
+ if (database === void 0)
133
+ return true;
134
+ try {
135
+ await complete(database.transaction(CHOICE_STORE, "readwrite").objectStore(CHOICE_STORE).add({ kind }, name));
136
+ return true;
137
+ } catch (error) {
138
+ if (error instanceof Error && error.name === "ConstraintError")
139
+ return false;
140
+ throw error;
141
+ } finally {
142
+ database.close();
143
+ }
144
+ }
145
+ function complete(request) {
146
+ return new Promise((resolve, reject) => {
147
+ request.addEventListener("success", () => resolve(request.result));
148
+ request.addEventListener("error", () => reject(request.error ?? new Error("IndexedDB request failed")));
149
+ });
150
+ }
151
+ export {
152
+ autoStoreTestHooks,
153
+ forgetStoreChoice,
154
+ openAutoStore,
155
+ opfsAvailable,
156
+ resolveAutoStoreKind
157
+ };
@@ -6,6 +6,8 @@ export interface BufferedWriterOptions {
6
6
  maxBytes?: number;
7
7
  maxAgeMs?: number;
8
8
  onError?: (error: unknown) => void;
9
+ /** @internal The database's background-error hook, heard when no `onError` is given. */
10
+ onBackgroundError?: (error: unknown, context: string) => void;
9
11
  }
10
12
  export type BufferedFlushResult = InsertBatchResult | UpsertBatchResult;
11
13
  export interface LifecycleFlushRequester {
@@ -9,6 +9,7 @@ class BufferedTableWriter {
9
9
  #maxBytes;
10
10
  #maxAgeMs;
11
11
  #onError;
12
+ #onBackgroundError;
12
13
  #rows = [];
13
14
  #estimatedBytes = 0;
14
15
  #timer;
@@ -25,6 +26,7 @@ class BufferedTableWriter {
25
26
  this.#maxBytes = positiveWholeNumber(options.maxBytes ?? 1024 * 1024, "Buffered byte limit");
26
27
  this.#maxAgeMs = positiveWholeNumber(options.maxAgeMs ?? 1e3, "Buffered age limit");
27
28
  this.#onError = options.onError;
29
+ this.#onBackgroundError = options.onBackgroundError;
28
30
  }
29
31
  get pendingRowCount() {
30
32
  return this.#rows.length;
@@ -79,7 +81,16 @@ class BufferedTableWriter {
79
81
  requestFlush() {
80
82
  if (this.#closed)
81
83
  return;
82
- void this.#flushPending().catch((error) => this.#onError?.(error));
84
+ void this.#flushPending().catch((error) => {
85
+ this.#reportFlushError(error);
86
+ });
87
+ }
88
+ #reportFlushError(error) {
89
+ if (this.#onError !== void 0) {
90
+ this.#onError(error);
91
+ return;
92
+ }
93
+ this.#onBackgroundError?.(error, `buffered writer flush for ${this.tableName}`);
83
94
  }
84
95
  async close() {
85
96
  if (this.#closed)
@@ -132,7 +143,9 @@ class BufferedTableWriter {
132
143
  return;
133
144
  this.#timer = setTimeout(() => {
134
145
  this.#timer = void 0;
135
- void this.#flushPending().catch((error) => this.#onError?.(error));
146
+ void this.#flushPending().catch((error) => {
147
+ this.#reportFlushError(error);
148
+ });
136
149
  }, this.#maxAgeMs);
137
150
  }
138
151
  #clearTimer() {
@@ -0,0 +1,123 @@
1
+ import { MemoryBlockStore } from "../storage/index.js";
2
+ function createBoundary() {
3
+ const clientListeners = /* @__PURE__ */ new Map();
4
+ const workerListeners = /* @__PURE__ */ new Map();
5
+ let chain = Promise.resolve();
6
+ let severed = false;
7
+ const sentByClient = [];
8
+ const sentByWorker = [];
9
+ const deliver = (target, message, transfer) => {
10
+ const data = structuredClone(message, transfer === void 0 ? void 0 : { transfer });
11
+ if (severed)
12
+ return;
13
+ chain = chain.then(() => {
14
+ if (severed)
15
+ return;
16
+ for (const listener of target.get("message") ?? [])
17
+ listener({ data });
18
+ });
19
+ };
20
+ const add = (map, type, listener) => {
21
+ const list = map.get(type) ?? [];
22
+ list.push(listener);
23
+ map.set(type, list);
24
+ };
25
+ const remove = (map, type, listener) => {
26
+ const list = map.get(type) ?? [];
27
+ const index = list.indexOf(listener);
28
+ if (index >= 0)
29
+ list.splice(index, 1);
30
+ };
31
+ const clientSide = {
32
+ postMessage: (message, options) => {
33
+ sentByClient.push(structuredClone(message));
34
+ deliver(workerListeners, message, options?.transfer);
35
+ },
36
+ addEventListener: (type, listener) => {
37
+ add(clientListeners, type, listener);
38
+ },
39
+ removeEventListener: (type, listener) => {
40
+ remove(clientListeners, type, listener);
41
+ },
42
+ terminate: () => {
43
+ severed = true;
44
+ }
45
+ };
46
+ const workerSide = {
47
+ postMessage: (message, options) => {
48
+ sentByWorker.push(structuredClone(message));
49
+ deliver(clientListeners, message, options?.transfer);
50
+ },
51
+ addEventListener: (type, listener) => {
52
+ add(workerListeners, type, listener);
53
+ }
54
+ };
55
+ return {
56
+ sentByClient,
57
+ sentByWorker,
58
+ clientSide,
59
+ workerSide,
60
+ sever: () => {
61
+ severed = true;
62
+ },
63
+ injectToClient: (frame) => {
64
+ for (const listener of clientListeners.get("message") ?? []) {
65
+ listener({ data: frame });
66
+ }
67
+ },
68
+ injectToWorker: (frame) => {
69
+ for (const listener of workerListeners.get("message") ?? []) {
70
+ listener({ data: frame });
71
+ }
72
+ },
73
+ emitWorkerGlobal: (type, event) => {
74
+ for (const listener of workerListeners.get(type) ?? [])
75
+ listener(event);
76
+ },
77
+ emitTransport: (type, event) => {
78
+ for (const listener of clientListeners.get(type) ?? [])
79
+ listener(event);
80
+ },
81
+ flush: async () => {
82
+ await chain;
83
+ await new Promise((resolve) => setTimeout(resolve, 5));
84
+ }
85
+ };
86
+ }
87
+ function settled(ms = 20) {
88
+ return new Promise((resolve) => setTimeout(resolve, ms));
89
+ }
90
+ function faultyStore(fault, options = {}) {
91
+ const inner = new MemoryBlockStore();
92
+ const calls = [];
93
+ const hidden = new Set(options.hide ?? []);
94
+ const store = new Proxy(inner, {
95
+ get(target, property) {
96
+ if (typeof property === "string" && hidden.has(property))
97
+ return void 0;
98
+ const value = target[property];
99
+ if (typeof value !== "function" || typeof property !== "string")
100
+ return value;
101
+ const method = property;
102
+ return (...args) => {
103
+ const run = () => value.apply(inner, args);
104
+ const isAsync = value.constructor?.name === "AsyncFunction";
105
+ if (!isAsync)
106
+ return run();
107
+ calls.push({ method, args });
108
+ return fault(method, args, run);
109
+ };
110
+ },
111
+ has(target, property) {
112
+ if (typeof property === "string" && hidden.has(property))
113
+ return false;
114
+ return Reflect.has(target, property);
115
+ }
116
+ });
117
+ return { store, calls };
118
+ }
119
+ export {
120
+ createBoundary,
121
+ faultyStore,
122
+ settled
123
+ };
@@ -1,5 +1,6 @@
1
1
  import { type LiveQueryPatch, type LiveQueryPatchOptions } from "./live-patch.js";
2
- import { type CompactionJobRecord, type GarbageCollectionJobRecord, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult } from "../storage/types.js";
2
+ import { type CompactionJobRecord, ConnectionLostError, type GarbageCollectionJobRecord, type InterruptedSnapshotImport, type InterruptedSnapshotImportAbortResult, type StorageIntegrityMode, type StorageIntegrityReport, type StorageStats } from "../storage/types.js";
3
+ import { type WorkerErrorKind } from "../worker-protocol/index.js";
3
4
  import { type BatchRow } from "./batch.js";
4
5
  import type { Catalog } from "./catalog.js";
5
6
  import type { BufferPoolStats, StagedWriteResult, StagedUpsertResult, BufferedFlushResult, BufferedWriterOptions, CancelCompactionJobResult, CollectGarbageOptions, CollectGarbageStepOptions, CompactTableOptions, CompactTableResult, CompactTableStepOptions, CompactionJobProgress, CreateTableInput, MigrateOptions, DeleteBatchResult, ExecuteOptions, ExecuteResult, GarbageCollectionProgress, GarbageCollectionResult, MaintenanceStatus, InsertBatchResult, QueryOptions, QueryExecutionStats, QueryCursorOptions, QuerySpillCleanupOptions, QuerySpillCleanupResult, RunStatementOptions, SnapshotExportOptions, SnapshotImportOptions, TableDefinition, UpdateBatchResult, UpsertBatchResult, VisibleSegmentPage, VisibleSegmentPageOptions } from "./database.js";
@@ -7,7 +8,7 @@ import type { LiveQueryDelivery, LiveQueryInput, LiveQueryInvalidation, LiveQuer
7
8
  import type { CompiledQuery, CompiledStatement, QueryResult, QueryValue } from "./query.js";
8
9
  import type { AnySchema, UntypedSchema, AnyTable, BatchColumnName, BatchDeleteInput, BatchInsertInput, BatchInsertRow, BatchKeyValue, BatchReadOptions, BatchReadRow, BatchUpdateChanges, BatchUpdateInput, BatchUpsertOptions, SchemaDefinition, TableName } from "./schema.js";
9
10
  import { type WireMigrationStep } from "./schema-wire.js";
10
- import type { StoreDescriptor, WireDatabaseOptions } from "./worker-host.js";
11
+ import type { OpenedStoreKind, StoreDescriptor, WireDatabaseOptions } from "./worker-host.js";
11
12
  /**
12
13
  * Main-thread async proxy of the full database API, talking to a worker that runs
13
14
  * `@minnowdb/core/worker` (or a custom entry built on
@@ -27,9 +28,14 @@ export interface ClientTransport {
27
28
  transfer: ArrayBuffer[];
28
29
  }): void;
29
30
  addEventListener(type: "message", listener: (event: MessageEvent<unknown>) => void): void;
30
- addEventListener(type: "error" | "messageerror", listener: () => void): void;
31
+ /**
32
+ * The client's listeners accept the Worker's own `ErrorEvent` and `MessageEvent`, and read
33
+ * the message, file, and line off an error event when they are there. A transport that has
34
+ * no such events to give may invoke them with no argument.
35
+ */
36
+ addEventListener(type: "error" | "messageerror", listener: (event?: ErrorEvent | MessageEvent<unknown>) => void): void;
31
37
  removeEventListener?(type: "message", listener: (event: MessageEvent<unknown>) => void): void;
32
- removeEventListener?(type: "error" | "messageerror", listener: () => void): void;
38
+ removeEventListener?(type: "error" | "messageerror", listener: (event?: ErrorEvent | MessageEvent<unknown>) => void): void;
33
39
  terminate?(): void;
34
40
  }
35
41
  export interface MinnowDatabaseClientOptions<TSchema extends AnySchema = UntypedSchema> {
@@ -47,8 +53,35 @@ export interface MinnowDatabaseClientOptions<TSchema extends AnySchema = Untyped
47
53
  store?: StoreDescriptor;
48
54
  /** Cloneable database options applied when the worker constructs the database. */
49
55
  databaseOptions?: WireDatabaseOptions;
50
- /** Maximum response wait, including initialization; defaults to 60 seconds. */
56
+ /**
57
+ * How long a call may go without a word from the worker before the connection is declared
58
+ * dead; defaults to 60 seconds and also covers initialization. The worker reports every few
59
+ * seconds while it works on a call, so a large batch write is bounded by progress, not by wall
60
+ * time — up to ten times this value in all.
61
+ */
51
62
  requestTimeoutMs?: number;
63
+ /**
64
+ * Hears every failure inside the worker that belongs to no call: uncaught exceptions,
65
+ * unhandled rejections, failed background maintenance, and multi-tab coordination errors,
66
+ * plus the transport's own error events. Without a handler each one is written to
67
+ * `console.error`, so nothing that goes wrong in the worker stays in the worker.
68
+ */
69
+ onWorkerError?: (event: DatabaseWorkerErrorEvent) => void;
70
+ /**
71
+ * Called once when the connection is lost — the worker raised an unhandled error, sent an
72
+ * unreadable frame, or fell silent past `requestTimeoutMs`. Every later call on this client
73
+ * fails the same way until `reopen()`; use this hook to reopen and rebuild handles.
74
+ */
75
+ onConnectionLost?: (error: ConnectionLostError) => void;
76
+ }
77
+ /** A worker failure that belongs to no call, delivered to `onWorkerError`. */
78
+ export interface DatabaseWorkerErrorEvent {
79
+ /** `"transport"` is the Worker object's own error or messageerror event. */
80
+ kind: WorkerErrorKind | "transport";
81
+ /** Where in the worker it happened: "opfs election", "auto collection", a file and line. */
82
+ context: string;
83
+ /** Rehydrated like a call failure: `instanceof` works, the stack is the worker's. */
84
+ error: Error;
52
85
  }
53
86
  export interface ClientLiveQueryOptions {
54
87
  /** BroadcastChannel name the worker uses to exchange cross-tab commit hints. */
@@ -86,9 +119,23 @@ interface RpcCallControls {
86
119
  }
87
120
  export declare class MinnowDatabaseClient<TSchema extends AnySchema = UntypedSchema> {
88
121
  #private;
89
- constructor(transport: ClientTransport, options?: MinnowDatabaseClientOptions<TSchema>);
122
+ constructor(transport: ClientTransport | (() => ClientTransport), options?: MinnowDatabaseClientOptions<TSchema>);
123
+ /**
124
+ * Replaces the connection after it was lost: a fresh worker, the same store and options. Pass
125
+ * the new transport, or construct the client with a transport factory
126
+ * (`new MinnowDatabaseClient(() => new Worker(...))`) and call this with no argument. The old
127
+ * transport is terminated when it can be. Every handle from before — write scopes, live
128
+ * sets, cursors, writers — belongs to the old worker and must be recreated; a pending call
129
+ * still in flight fails as a connection loss. Resolves once the new worker is ready.
130
+ */
131
+ reopen(transport?: ClientTransport): Promise<void>;
90
132
  /** Resolves once the worker has opened the store and constructed the database. */
91
133
  ready(): Promise<void>;
134
+ /**
135
+ * The kind of store the worker opened: what `{ kind: "auto" }` resolved to, or the kind the
136
+ * descriptor named. Undefined only when a custom worker entry opened a store without saying.
137
+ */
138
+ storeKind(): Promise<OpenedStoreKind | undefined>;
92
139
  createTable(input: CreateTableInput): Promise<void>;
93
140
  createView(name: string, sql: string, options?: {
94
141
  orReplace?: boolean;
@@ -252,6 +299,8 @@ export declare class ClientBufferedWriter<TRow extends BatchRow = BatchRow> {
252
299
  private readonly client;
253
300
  private readonly handleId;
254
301
  constructor(client: MinnowDatabaseClient, handleId: string, created: Promise<unknown>);
302
+ /** @internal Runs once when the writer closes, however it closes. */
303
+ _onClose(cleanup: () => void): void;
255
304
  add(row: TRow): Promise<BufferedFlushResult | undefined>;
256
305
  flush(): Promise<BufferedFlushResult | undefined>;
257
306
  /** Fire-and-forget: flush failures surface through onError, matching the in-worker contract. */