@minnowdb/core 0.6.9 → 0.7.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 (103) hide show
  1. package/dist/block-format/block.js +254 -325
  2. package/dist/block-format/checksum.js +30 -52
  3. package/dist/block-format/codecs.js +112 -125
  4. package/dist/block-format/column.js +187 -205
  5. package/dist/block-format/index.js +5 -1
  6. package/dist/block-format/physical.js +371 -387
  7. package/dist/block-format/types.js +4 -1
  8. package/dist/block-format/unicode.js +30 -39
  9. package/dist/date-value.d.ts +5 -0
  10. package/dist/date-value.js +71 -41
  11. package/dist/engine/artifact-cache.js +106 -116
  12. package/dist/engine/batch.js +36 -44
  13. package/dist/engine/buffered-writer.js +156 -158
  14. package/dist/engine/byte-estimates.js +20 -22
  15. package/dist/engine/cache-limits.js +24 -25
  16. package/dist/engine/cancellation.js +5 -3
  17. package/dist/engine/catalog.js +64 -73
  18. package/dist/engine/client.js +715 -780
  19. package/dist/engine/database.js +15952 -18465
  20. package/dist/engine/defaults.js +104 -111
  21. package/dist/engine/errors.js +113 -128
  22. package/dist/engine/fts.js +233 -280
  23. package/dist/engine/group-index.js +287 -321
  24. package/dist/engine/index.js +23 -12
  25. package/dist/engine/join-index.js +177 -186
  26. package/dist/engine/keyed-live.js +168 -173
  27. package/dist/engine/live-api.js +0 -1
  28. package/dist/engine/live-equal.js +32 -37
  29. package/dist/engine/live.js +603 -647
  30. package/dist/engine/memory.js +109 -111
  31. package/dist/engine/optimizer.d.ts +19 -1
  32. package/dist/engine/optimizer.js +2642 -2515
  33. package/dist/engine/point-read.d.ts +4 -2
  34. package/dist/engine/point-read.js +121 -171
  35. package/dist/engine/query-api.js +12 -3
  36. package/dist/engine/query-cache.js +61 -69
  37. package/dist/engine/query.d.ts +45 -0
  38. package/dist/engine/query.js +8506 -9375
  39. package/dist/engine/result-wire.js +221 -252
  40. package/dist/engine/schema-wire.js +97 -112
  41. package/dist/engine/schema.js +1099 -1254
  42. package/dist/engine/sort-keys.js +315 -389
  43. package/dist/engine/sql-domains.d.ts +7 -1
  44. package/dist/engine/sql-domains.js +648 -734
  45. package/dist/engine/sql-driver.js +0 -1
  46. package/dist/engine/sql-functions.d.ts +11 -0
  47. package/dist/engine/sql-functions.js +1092 -0
  48. package/dist/engine/sql-json.js +199 -218
  49. package/dist/engine/sql-semantics.d.ts +25 -4
  50. package/dist/engine/sql-semantics.js +579 -513
  51. package/dist/engine/typed-live.js +271 -293
  52. package/dist/engine/vector.d.ts +7 -1
  53. package/dist/engine/vector.js +4513 -4580
  54. package/dist/engine/worker-host.d.ts +8 -63
  55. package/dist/engine/worker-host.js +28 -1130
  56. package/dist/engine/worker-indexeddb.d.ts +1 -0
  57. package/dist/engine/worker-indexeddb.js +3 -0
  58. package/dist/engine/worker-memory.d.ts +1 -0
  59. package/dist/engine/worker-memory.js +3 -0
  60. package/dist/engine/worker-opfs.d.ts +1 -0
  61. package/dist/engine/worker-opfs.js +3 -0
  62. package/dist/engine/worker-server.d.ts +93 -0
  63. package/dist/engine/worker-server.js +999 -0
  64. package/dist/engine/worker-store-indexeddb.d.ts +2 -0
  65. package/dist/engine/worker-store-indexeddb.js +11 -0
  66. package/dist/engine/worker-store-memory.d.ts +2 -0
  67. package/dist/engine/worker-store-memory.js +6 -0
  68. package/dist/engine/worker-store-opfs.d.ts +2 -0
  69. package/dist/engine/worker-store-opfs.js +9 -0
  70. package/dist/engine/worker.js +0 -12
  71. package/dist/engine/write-block-planner.js +93 -100
  72. package/dist/plan/index.js +15 -16
  73. package/dist/plan/model.d.ts +9 -1
  74. package/dist/plan/model.js +20 -27
  75. package/dist/storage/index.js +0 -12
  76. package/dist/storage/indexeddb.js +11776 -13171
  77. package/dist/storage/memory.js +1198 -1241
  78. package/dist/storage/opfs/files.js +238 -295
  79. package/dist/storage/opfs/index.js +9 -3
  80. package/dist/storage/opfs/leader.js +4386 -4856
  81. package/dist/storage/opfs/rpc.js +223 -259
  82. package/dist/storage/opfs/snapshot-ledger.js +219 -260
  83. package/dist/storage/opfs/store.js +884 -1003
  84. package/dist/storage/persistence.js +59 -74
  85. package/dist/storage/snapshot-stream.js +788 -826
  86. package/dist/storage/snapshot.js +36 -37
  87. package/dist/storage/toolkit/extents.js +444 -520
  88. package/dist/storage/toolkit/index.js +28 -29
  89. package/dist/storage/toolkit/record-core.js +5792 -6377
  90. package/dist/storage/toolkit/sync-file.js +34 -42
  91. package/dist/storage/toolkit/wal.js +92 -127
  92. package/dist/storage/toolkit/wire.js +232 -256
  93. package/dist/storage/types.js +2952 -3228
  94. package/dist/testing/block-store-conformance.js +1556 -1629
  95. package/dist/testing/index.js +310 -291
  96. package/dist/testing/opfs-shim.js +280 -312
  97. package/dist/testing/simulator.js +496 -534
  98. package/dist/testing/sqllogictest.js +425 -365
  99. package/dist/transactions/index.js +1464 -1712
  100. package/dist/worker-protocol/index.js +70 -69
  101. package/package.json +20 -2
  102. package/postgres-feature-profile.json +15 -5
  103. package/sql-feature-matrix.json +252 -2
@@ -1,811 +1,746 @@
1
- import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, PostingBuildConflictError, SnapshotManifestMissingError, SnapshotImportConflictError, SchemaConflictError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, StorageCorruptionError, StorageFormatVersionError, OpfsUncertainOutcomeError, } from "../storage/types.js";
1
+ import { BlockReadBatchTooLargeError, CompactionBacklogError, CompactionJobConflictError, GarbageCollectionJobConflictError, IndexedDbSchemaUpgradeBlockedError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, PostingBuildConflictError, SnapshotManifestMissingError, SnapshotImportConflictError, SchemaConflictError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyBuildConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, StorageCorruptionError, StorageFormatVersionError, OpfsUncertainOutcomeError } from "../storage/types.js";
2
2
  import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
3
- import { parseRpcResponse, MAX_DATABASE_RPC_IN_FLIGHT, protocolVersion, } from "../worker-protocol/index.js";
3
+ import { parseRpcResponse, MAX_DATABASE_RPC_IN_FLIGHT, protocolVersion } from "../worker-protocol/index.js";
4
4
  import { toColumnarBatch } from "./batch.js";
5
- import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, DatabaseReadBacklogError, LiveQueryLimitError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError, } from "./errors.js";
5
+ import { CompactionJobCancelledError, CompactionMemoryBudgetError, CompactionWriteAmplificationError, MaintenanceBacklogError, DatabaseReadBacklogError, LiveQueryLimitError, MissingKeyError, SqlCompileError, UnknownTableError, UniqueConstraintError, VisibleSegmentCursorStaleError } from "./errors.js";
6
6
  import { QueryMemoryBudgetError } from "./memory.js";
7
7
  import { decodeQueryResult } from "./result-wire.js";
8
8
  import { serializeSchema } from "./schema-wire.js";
9
- /**
10
- * Bytes per slice when a snapshot crosses the channel. Large enough that a hundred-megabyte
11
- * database is tens of round trips, small enough that no single structured clone is long enough
12
- * to be felt as a dropped frame.
13
- */
14
9
  const SNAPSHOT_CHUNK_BYTES = MAX_SNAPSHOT_STREAM_CHUNK_BYTES;
15
- const MAX_SNAPSHOT_BYTE_ARRAY_LENGTH = 0xffffffff;
10
+ const MAX_SNAPSHOT_BYTE_ARRAY_LENGTH = 4294967295;
16
11
  async function* clientSnapshotChunks(bytes) {
17
- for (let offset = 0; offset < bytes.byteLength; offset += SNAPSHOT_CHUNK_BYTES) {
18
- yield bytes.subarray(offset, offset + SNAPSHOT_CHUNK_BYTES);
19
- }
12
+ for (let offset = 0; offset < bytes.byteLength; offset += SNAPSHOT_CHUNK_BYTES) {
13
+ yield bytes.subarray(offset, offset + SNAPSHOT_CHUNK_BYTES);
14
+ }
20
15
  }
21
16
  function throwIfClientSnapshotAborted(signal) {
22
- if (signal?.aborted !== true)
23
- return;
24
- if (signal.reason instanceof Error)
25
- throw signal.reason;
26
- const error = new Error("Snapshot operation was aborted", { cause: signal.reason });
27
- error.name = "AbortError";
28
- throw error;
17
+ if (signal?.aborted !== true)
18
+ return;
19
+ if (signal.reason instanceof Error)
20
+ throw signal.reason;
21
+ const error = new Error("Snapshot operation was aborted", { cause: signal.reason });
22
+ error.name = "AbortError";
23
+ throw error;
29
24
  }
30
25
  const errorRegistry = new Map([
31
- UniqueConstraintError,
32
- MissingKeyError,
33
- UnknownTableError,
34
- CompactionBacklogError,
35
- CompactionMemoryBudgetError,
36
- CompactionWriteAmplificationError,
37
- CompactionJobCancelledError,
38
- MaintenanceBacklogError,
39
- DatabaseReadBacklogError,
40
- LiveQueryLimitError,
41
- SqlCompileError,
42
- QueryMemoryBudgetError,
43
- VisibleSegmentCursorStaleError,
44
- BlockReadBatchTooLargeError,
45
- WriteConflictError,
46
- SchemaConflictError,
47
- UniqueKeyConflictError,
48
- UniqueIndexCoverageError,
49
- TableRecordConflictError,
50
- CompactionJobConflictError,
51
- GarbageCollectionJobConflictError,
52
- IndexedDbSchemaUpgradeBlockedError,
53
- SnapshotManifestMissingError,
54
- SnapshotImportConflictError,
55
- TableInUseError,
56
- TransactionRecordConflictError,
57
- LeaseConflictError,
58
- LeaseExpiredError,
59
- LeaseOwnerConflictError,
60
- StorageResourceLimitError,
61
- TempOwnerConflictError,
62
- UniqueKeyBuildConflictError,
63
- PostingBuildConflictError,
64
- StorageCorruptionError,
65
- StorageFormatVersionError,
66
- OpfsUncertainOutcomeError,
26
+ UniqueConstraintError,
27
+ MissingKeyError,
28
+ UnknownTableError,
29
+ CompactionBacklogError,
30
+ CompactionMemoryBudgetError,
31
+ CompactionWriteAmplificationError,
32
+ CompactionJobCancelledError,
33
+ MaintenanceBacklogError,
34
+ DatabaseReadBacklogError,
35
+ LiveQueryLimitError,
36
+ SqlCompileError,
37
+ QueryMemoryBudgetError,
38
+ VisibleSegmentCursorStaleError,
39
+ BlockReadBatchTooLargeError,
40
+ WriteConflictError,
41
+ SchemaConflictError,
42
+ UniqueKeyConflictError,
43
+ UniqueIndexCoverageError,
44
+ TableRecordConflictError,
45
+ CompactionJobConflictError,
46
+ GarbageCollectionJobConflictError,
47
+ IndexedDbSchemaUpgradeBlockedError,
48
+ SnapshotManifestMissingError,
49
+ SnapshotImportConflictError,
50
+ TableInUseError,
51
+ TransactionRecordConflictError,
52
+ LeaseConflictError,
53
+ LeaseExpiredError,
54
+ LeaseOwnerConflictError,
55
+ StorageResourceLimitError,
56
+ TempOwnerConflictError,
57
+ UniqueKeyBuildConflictError,
58
+ PostingBuildConflictError,
59
+ StorageCorruptionError,
60
+ StorageFormatVersionError,
61
+ OpfsUncertainOutcomeError
67
62
  ].map((constructor) => [constructor.name, constructor]));
68
63
  function rehydrateError(serialized) {
69
- const constructor = errorRegistry.get(serialized.name);
70
- const error = constructor === undefined
71
- ? new Error(serialized.message)
72
- : Object.create(constructor.prototype);
73
- Object.defineProperty(error, "message", {
74
- value: serialized.message,
75
- writable: true,
76
- configurable: true,
77
- });
78
- Object.defineProperty(error, "name", {
79
- value: serialized.name,
80
- writable: true,
81
- configurable: true,
64
+ const constructor = errorRegistry.get(serialized.name);
65
+ const error = constructor === void 0 ? new Error(serialized.message) : Object.create(constructor.prototype);
66
+ Object.defineProperty(error, "message", {
67
+ value: serialized.message,
68
+ writable: true,
69
+ configurable: true
70
+ });
71
+ Object.defineProperty(error, "name", {
72
+ value: serialized.name,
73
+ writable: true,
74
+ configurable: true
75
+ });
76
+ if (serialized.stack !== void 0) {
77
+ Object.defineProperty(error, "stack", {
78
+ value: serialized.stack,
79
+ writable: true,
80
+ configurable: true
82
81
  });
83
- if (serialized.stack !== undefined) {
84
- Object.defineProperty(error, "stack", {
85
- value: serialized.stack,
86
- writable: true,
87
- configurable: true,
88
- });
89
- }
90
- if (serialized.props !== undefined)
91
- Object.assign(error, serialized.props);
92
- return error;
82
+ }
83
+ if (serialized.props !== void 0)
84
+ Object.assign(error, serialized.props);
85
+ return error;
93
86
  }
94
- export class MinnowDatabaseClient {
95
- #transport;
96
- #pending = new Map();
97
- #events = new Map();
98
- #ready;
99
- #fatal;
100
- #closed = false;
101
- #onVisibilityChange;
102
- #onMessage = (event) => {
103
- this.#receive(event.data);
104
- };
105
- #onError = () => {
106
- this.#fail(new Error("The database worker failed; see the worker's own error output"));
107
- };
108
- #onMessageError = () => {
109
- this.#fail(new Error("A database worker message could not be deserialized"));
87
+ function rehydrateResponseError(payload) {
88
+ const candidate = payload;
89
+ if (typeof candidate !== "object" || candidate === null || typeof candidate.name !== "string" || typeof candidate.message !== "string") {
90
+ return new Error("The database worker reported a failure without a readable error payload");
91
+ }
92
+ return rehydrateError(candidate);
93
+ }
94
+ class MinnowDatabaseClient {
95
+ #transport;
96
+ #pending = /* @__PURE__ */ new Map();
97
+ #events = /* @__PURE__ */ new Map();
98
+ #ready;
99
+ #fatal;
100
+ #closed = false;
101
+ #onVisibilityChange;
102
+ #onMessage = (event) => {
103
+ this.#receive(event.data);
104
+ };
105
+ #onError = () => {
106
+ this.#fail(new Error("The database worker failed; see the worker's own error output"));
107
+ };
108
+ #onMessageError = () => {
109
+ this.#fail(new Error("A database worker message could not be deserialized"));
110
+ };
111
+ constructor(transport, options = {}) {
112
+ this.#transport = transport;
113
+ transport.addEventListener("message", this.#onMessage);
114
+ transport.addEventListener("error", this.#onError);
115
+ transport.addEventListener("messageerror", this.#onMessageError);
116
+ const payload = {
117
+ store: options.store ?? { kind: "indexeddb", name: "minnow" },
118
+ ...options.databaseOptions === void 0 ? {} : { options: options.databaseOptions }
110
119
  };
111
- constructor(transport, options = {}) {
112
- this.#transport = transport;
113
- transport.addEventListener("message", this.#onMessage);
114
- transport.addEventListener("error", this.#onError);
115
- transport.addEventListener("messageerror", this.#onMessageError);
116
- const payload = {
117
- store: options.store ?? { kind: "indexeddb", name: "minnow" },
118
- ...(options.databaseOptions === undefined ? {} : { options: options.databaseOptions }),
119
- };
120
- this.#ready = this.#post("rpc-init", null, "init", [payload]).then(() => undefined);
121
- // Callers may rely on call ordering instead of awaiting ready(); keep its rejection observed.
122
- this.#ready.catch(() => undefined);
123
- // Report page visibility so a store that cares (the OPFS store's leadership preference)
124
- // can follow the tab the user is looking at. Best-effort: a worker-side host that predates
125
- // the frame rejects it, and nothing here depends on the answer.
126
- if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
127
- const report = () => {
128
- if (this.#closed)
129
- return;
130
- this.#post("rpc-call", null, "setVisibility", [
131
- document.visibilityState === "visible",
132
- ]).catch(() => undefined);
133
- };
134
- this.#onVisibilityChange = report;
135
- document.addEventListener("visibilitychange", report);
136
- report();
120
+ this.#ready = this.#post("rpc-init", null, "init", [payload]).then(() => void 0);
121
+ this.#ready.catch(() => void 0);
122
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
123
+ const report = () => {
124
+ if (this.#closed)
125
+ return;
126
+ this.#post("rpc-call", null, "setVisibility", [
127
+ document.visibilityState === "visible"
128
+ ]).catch(() => void 0);
129
+ };
130
+ this.#onVisibilityChange = report;
131
+ document.addEventListener("visibilitychange", report);
132
+ report();
133
+ }
134
+ }
135
+ async ready() {
136
+ return this.#ready;
137
+ }
138
+ async createTable(input) {
139
+ await this.#call("createTable", [input]);
140
+ }
141
+ async createView(name, sql, options = {}) {
142
+ await this.#call("createView", [name, sql, options]);
143
+ }
144
+ async dropView(name, options = {}) {
145
+ return await this.#call("dropView", [name, options]);
146
+ }
147
+ async dropColumn(tableName, columnName, options = {}) {
148
+ return await this.#call("dropColumn", [tableName, columnName, options]);
149
+ }
150
+ async dropTable(tableName, options = {}) {
151
+ return await this.#call("dropTable", [tableName, options]);
152
+ }
153
+ async createIndex(indexName, tableName, requestedColumns, options = {}) {
154
+ await this.#call("createIndex", [indexName, tableName, requestedColumns, options]);
155
+ }
156
+ async dropIndex(indexName, options = {}) {
157
+ return await this.#call("dropIndex", [indexName, options]);
158
+ }
159
+ async buildFtsIndex(tableName, columnName) {
160
+ await this.#call("buildFtsIndex", [tableName, columnName]);
161
+ }
162
+ async introspect() {
163
+ return await this.#call("introspect", []);
164
+ }
165
+ async listTables() {
166
+ return await this.#call("listTables", []);
167
+ }
168
+ async migrate(definition, options = {}) {
169
+ return await this.#call("migrate", [
170
+ serializeSchema(definition),
171
+ options
172
+ ]);
173
+ }
174
+ async insertBatch(tableName, input) {
175
+ const batch = toColumnarBatch(input);
176
+ return await this.#call("insertBatch", [tableName, batch]);
177
+ }
178
+ async insert(tableName, row) {
179
+ return await this.#call("insert", [tableName, row]);
180
+ }
181
+ async upsertBatch(tableName, input, options = {}) {
182
+ const batch = toColumnarBatch(input);
183
+ return await this.#call("upsertBatch", [tableName, batch, options]);
184
+ }
185
+ async upsert(tableName, row, options = {}) {
186
+ return await this.#call("upsert", [tableName, row, options]);
187
+ }
188
+ async updateBatch(tableName, input) {
189
+ return await this.#call("updateBatch", [tableName, input]);
190
+ }
191
+ async update(tableName, key, changes) {
192
+ return await this.#call("update", [tableName, key, changes]);
193
+ }
194
+ async deleteBatch(tableName, input) {
195
+ return await this.#call("deleteBatch", [tableName, input]);
196
+ }
197
+ async delete(tableName, key) {
198
+ return await this.#call("delete", [tableName, key]);
199
+ }
200
+ bufferedWriter(tableName, options = {}) {
201
+ const handleId = crypto.randomUUID();
202
+ const { onError, ...wireOptions } = options;
203
+ this.#events.set(handleId, {
204
+ ...onError === void 0 ? {} : { onError }
205
+ });
206
+ const created = this.#call("bufferedWriter", [handleId, tableName, wireOptions]);
207
+ return new ClientBufferedWriter(this, handleId, created);
208
+ }
209
+ async readTable(tableName, versionOrOptions) {
210
+ return decodeQueryResult(await this.#call("readTable", versionOrOptions === void 0 ? [tableName] : [tableName, versionOrOptions])).rows;
211
+ }
212
+ async query(sql, options) {
213
+ const { signal, onStats, ...wireOptions } = options ?? {};
214
+ return decodeQueryResult(await this.#call("query", [sql, wireOptions, onStats !== void 0], { signal, onStats }));
215
+ }
216
+ queryCursor(sql, options = {}) {
217
+ const call = this.#call.bind(this);
218
+ const invoke = this._invoke.bind(this);
219
+ const routeEvents = this._routeEvents.bind(this);
220
+ const unrouteEvents = this._unrouteEvents.bind(this);
221
+ const handleId = crypto.randomUUID();
222
+ const { signal, onStats, ...wireOptions } = options;
223
+ async function* batches() {
224
+ let opened = false;
225
+ let closing;
226
+ const close = () => {
227
+ if (!opened)
228
+ return Promise.resolve();
229
+ closing ??= invoke(handleId, "close", []).catch(() => void 0);
230
+ return closing;
231
+ };
232
+ const onAbort = () => {
233
+ void close();
234
+ };
235
+ signal?.addEventListener("abort", onAbort, { once: true });
236
+ if (onStats !== void 0)
237
+ routeEvents(handleId, { onStats });
238
+ try {
239
+ signal?.throwIfAborted();
240
+ await call("queryCursorOpen", [handleId, sql, wireOptions, onStats !== void 0]);
241
+ opened = true;
242
+ if (signal?.aborted === true) {
243
+ await close();
244
+ signal.throwIfAborted();
137
245
  }
138
- }
139
- /** Resolves once the worker has opened the store and constructed the database. */
140
- async ready() {
141
- return this.#ready;
142
- }
143
- // --- Catalog and writes -----------------------------------------------------------------------
144
- async createTable(input) {
145
- await this.#call("createTable", [input]);
146
- }
147
- async createView(name, sql, options = {}) {
148
- await this.#call("createView", [name, sql, options]);
149
- }
150
- async dropView(name, options = {}) {
151
- return (await this.#call("dropView", [name, options]));
152
- }
153
- async dropColumn(tableName, columnName, options = {}) {
154
- return (await this.#call("dropColumn", [tableName, columnName, options]));
155
- }
156
- async dropTable(tableName, options = {}) {
157
- return (await this.#call("dropTable", [tableName, options]));
158
- }
159
- async createIndex(indexName, tableName, requestedColumns, options = {}) {
160
- await this.#call("createIndex", [indexName, tableName, requestedColumns, options]);
161
- }
162
- async dropIndex(indexName, options = {}) {
163
- return (await this.#call("dropIndex", [indexName, options]));
164
- }
165
- async buildFtsIndex(tableName, columnName) {
166
- await this.#call("buildFtsIndex", [tableName, columnName]);
167
- }
168
- /** The published catalog; see `MinnowDatabase.introspect()`. */
169
- async introspect() {
170
- return (await this.#call("introspect", []));
171
- }
172
- async listTables() {
173
- return (await this.#call("listTables", []));
174
- }
175
- async migrate(definition, options = {}) {
176
- return (await this.#call("migrate", [
177
- serializeSchema(definition),
178
- options,
179
- ]));
180
- }
181
- async insertBatch(tableName, input) {
182
- // Pivoted here rather than in the worker: the columnar form is the cheaper structured clone,
183
- // and it keeps the worker protocol's payload shape the same whichever form the caller used.
184
- const batch = toColumnarBatch(input);
185
- return (await this.#call("insertBatch", [tableName, batch]));
186
- }
187
- async insert(tableName, row) {
188
- return (await this.#call("insert", [tableName, row]));
189
- }
190
- async upsertBatch(tableName, input, options = {}) {
191
- const batch = toColumnarBatch(input);
192
- return (await this.#call("upsertBatch", [tableName, batch, options]));
193
- }
194
- async upsert(tableName, row, options = {}) {
195
- return (await this.#call("upsert", [tableName, row, options]));
196
- }
197
- async updateBatch(tableName, input) {
198
- return (await this.#call("updateBatch", [tableName, input]));
199
- }
200
- async update(tableName, key, changes) {
201
- return (await this.#call("update", [tableName, key, changes]));
202
- }
203
- async deleteBatch(tableName, input) {
204
- return (await this.#call("deleteBatch", [tableName, input]));
205
- }
206
- async delete(tableName, key) {
207
- return (await this.#call("delete", [tableName, key]));
208
- }
209
- bufferedWriter(tableName, options = {}) {
210
- const handleId = crypto.randomUUID();
211
- const { onError, ...wireOptions } = options;
212
- this.#events.set(handleId, {
213
- ...(onError === undefined ? {} : { onError }),
214
- });
215
- const created = this.#call("bufferedWriter", [handleId, tableName, wireOptions]);
216
- return new ClientBufferedWriter(this, handleId, created);
217
- }
218
- // --- Reads ------------------------------------------------------------------------------------
219
- async readTable(tableName, versionOrOptions) {
220
- return decodeQueryResult(await this.#call("readTable", versionOrOptions === undefined ? [tableName] : [tableName, versionOrOptions])).rows;
221
- }
222
- /**
223
- * Results cross the channel as one array per column (typed arrays for numbers, booleans, and
224
- * datetimes) and are rebuilt into row objects here; see `result-wire.ts`.
225
- */
226
- async query(sql, options) {
227
- const { signal, onStats, ...wireOptions } = options ?? {};
228
- return decodeQueryResult(await this.#call("query", [sql, wireOptions, onStats !== undefined], { signal, onStats }));
229
- }
230
- /** Pulls one transferred columnar page at a time, preserving worker backpressure. */
231
- queryCursor(sql, options = {}) {
232
- const call = this.#call.bind(this);
233
- const invoke = this._invoke.bind(this);
234
- const routeEvents = this._routeEvents.bind(this);
235
- const unrouteEvents = this._unrouteEvents.bind(this);
236
- const handleId = crypto.randomUUID();
237
- const { signal, onStats, ...wireOptions } = options;
238
- async function* batches() {
239
- let opened = false;
240
- let closing;
241
- const close = () => {
242
- if (!opened)
243
- return Promise.resolve();
244
- closing ??= invoke(handleId, "close", []).catch(() => undefined);
245
- return closing;
246
- };
247
- const onAbort = () => {
248
- void close();
249
- };
250
- signal?.addEventListener("abort", onAbort, { once: true });
251
- if (onStats !== undefined)
252
- routeEvents(handleId, { onStats });
253
- try {
254
- signal?.throwIfAborted();
255
- await call("queryCursorOpen", [handleId, sql, wireOptions, onStats !== undefined]);
256
- opened = true;
257
- if (signal?.aborted === true) {
258
- await close();
259
- signal.throwIfAborted();
260
- }
261
- for (;;) {
262
- const next = (await invoke(handleId, "next", []));
263
- if (next.done)
264
- return undefined;
265
- yield decodeQueryResult(next.batch);
266
- }
267
- }
268
- finally {
269
- signal?.removeEventListener("abort", onAbort);
270
- await close();
271
- unrouteEvents(handleId);
272
- }
246
+ for (; ; ) {
247
+ const next = await invoke(handleId, "next", []);
248
+ if (next.done)
249
+ return void 0;
250
+ yield decodeQueryResult(next.batch);
273
251
  }
274
- return batches();
275
- }
276
- async run(query) {
277
- return decodeQueryResult(await this.#call("run", [query])).rows;
278
- }
279
- async explain(sql) {
280
- return (await this.#call("explain", [sql]));
281
- }
282
- async execute(sql, params, options = {}) {
252
+ } finally {
253
+ signal?.removeEventListener("abort", onAbort);
254
+ await close();
255
+ unrouteEvents(handleId);
256
+ }
257
+ }
258
+ return batches();
259
+ }
260
+ async run(query) {
261
+ return decodeQueryResult(await this.#call("run", [query])).rows;
262
+ }
263
+ async explain(sql) {
264
+ return await this.#call("explain", [sql]);
265
+ }
266
+ async execute(sql, params, options = {}) {
267
+ const { signal, onStats, ...wireOptions } = options;
268
+ return await this.#call("execute", [sql, params, wireOptions, onStats !== void 0], {
269
+ signal,
270
+ onStats
271
+ });
272
+ }
273
+ async runStatement(statement, options) {
274
+ return await this.#call("runStatement", options === void 0 ? [statement] : [statement, options]);
275
+ }
276
+ async snapshot(action) {
277
+ const opened = await this.#call("snapshotOpen", []);
278
+ try {
279
+ const session = {
280
+ version: opened.version,
281
+ query: (sql, options = {}) => this.query(sql, {
282
+ ...options,
283
+ ...opened.version === null ? {} : { version: opened.version }
284
+ })
285
+ };
286
+ return await action(session);
287
+ } finally {
288
+ await this._invoke(opened.handleId, "close", []);
289
+ }
290
+ }
291
+ async write(action) {
292
+ const opened = await this.#call("writeOpen", []);
293
+ const stage = (op, tableName, input) => this._invoke(opened.handleId, "stage", [op, tableName, input]);
294
+ const session = {
295
+ query: async (sql, options = {}) => {
283
296
  const { signal, onStats, ...wireOptions } = options;
284
- return (await this.#call("execute", [sql, params, wireOptions, onStats !== undefined], {
285
- signal,
286
- onStats,
287
- }));
288
- }
289
- /** Executes a compiled statement from the typed mutation builders in the worker. */
290
- async runStatement(statement, options) {
291
- return (await this.#call("runStatement", options === undefined ? [statement] : [statement, options]));
292
- }
293
- /**
294
- * Runs the callback against one pinned manifest version, exactly like the in-worker
295
- * `MinnowDatabase.snapshot()`: the worker holds the version's reader lease until the
296
- * callback settles, and every session query crosses the channel pinned to that version.
297
- */
298
- async snapshot(action) {
299
- const opened = (await this.#call("snapshotOpen", []));
300
- try {
301
- const session = {
302
- version: opened.version,
303
- query: (sql, options = {}) => this.query(sql, {
304
- ...options,
305
- ...(opened.version === null ? {} : { version: opened.version }),
306
- }),
307
- };
308
- return await action(session);
309
- }
310
- finally {
311
- await this._invoke(opened.handleId, "close", []);
312
- }
313
- }
314
- /**
315
- * Runs the callback against one worker-side write transaction, exactly like the in-worker
316
- * `MinnowDatabase.write()`: every staged mutation crosses the channel into the shared
317
- * transaction and publishes as one atomic commit when the callback returns; an error
318
- * aborts the scope with nothing published.
319
- */
320
- async write(action) {
321
- const opened = (await this.#call("writeOpen", []));
322
- const stage = (op, tableName, input) => this._invoke(opened.handleId, "stage", [op, tableName, input]);
323
- const session = {
324
- query: async (sql, options = {}) => {
325
- const { signal, onStats, ...wireOptions } = options;
326
- return decodeQueryResult(await this._invokeControlled(opened.handleId, "query", [sql, wireOptions, onStats !== undefined], { signal, onStats }));
327
- },
328
- execute: (sql, params) => this._invoke(opened.handleId, "execute", params === undefined ? [sql] : [sql, params]),
329
- insertBatch: (tableName, input) => stage("insertBatch", tableName, input),
330
- upsertBatch: (tableName, input) => stage("upsertBatch", tableName, input),
331
- updateBatch: (tableName, input) => stage("updateBatch", tableName, input),
332
- deleteBatch: (tableName, input) => stage("deleteBatch", tableName, input),
333
- };
334
- try {
335
- const result = await action(session);
336
- const committed = (await this._invoke(opened.handleId, "commit", []));
337
- return { result, version: committed.version };
338
- }
339
- catch (error) {
340
- await this._invoke(opened.handleId, "abort", []).catch(() => undefined);
341
- throw error;
342
- }
343
- }
344
- /**
345
- * Copies the database out as a portable snapshot file, exactly like the in-worker
346
- * `MinnowDatabase.exportSnapshot()`. The worker encodes it and hands it back in slices, so the
347
- * main thread copies a few megabytes at a time instead of stalling on one clone of the whole
348
- * database; `onProgress` reports each slice as it lands.
349
- */
350
- async exportSnapshot(options = {}) {
351
- const chunks = [];
352
- let byteLength = 0;
353
- for await (const chunk of this.exportSnapshotStream(options)) {
354
- byteLength += chunk.byteLength;
355
- if (!Number.isSafeInteger(byteLength) || byteLength > MAX_SNAPSHOT_BYTE_ARRAY_LENGTH) {
356
- throw new RangeError("Snapshot exceeds the byte-array API's 4 GiB limit");
357
- }
358
- chunks.push(chunk);
359
- }
360
- const bytes = new Uint8Array(byteLength);
361
- let offset = 0;
362
- for (const chunk of chunks) {
363
- bytes.set(chunk, offset);
364
- offset += chunk.byteLength;
365
- }
366
- return bytes;
367
- }
368
- /** Pulls at most one bounded worker chunk at a time; closing releases the worker-side pin. */
369
- async *exportSnapshotStream(options = {}) {
297
+ return decodeQueryResult(await this._invokeControlled(opened.handleId, "query", [sql, wireOptions, onStats !== void 0], { signal, onStats }));
298
+ },
299
+ execute: (sql, params) => this._invoke(opened.handleId, "execute", params === void 0 ? [sql] : [sql, params]),
300
+ insertBatch: (tableName, input) => stage("insertBatch", tableName, input),
301
+ upsertBatch: (tableName, input) => stage("upsertBatch", tableName, input),
302
+ updateBatch: (tableName, input) => stage("updateBatch", tableName, input),
303
+ deleteBatch: (tableName, input) => stage("deleteBatch", tableName, input)
304
+ };
305
+ try {
306
+ const result = await action(session);
307
+ const committed = await this._invoke(opened.handleId, "commit", []);
308
+ return { result, version: committed.version };
309
+ } catch (error) {
310
+ await this._invoke(opened.handleId, "abort", []).catch(() => void 0);
311
+ throw error;
312
+ }
313
+ }
314
+ async exportSnapshot(options = {}) {
315
+ const chunks = [];
316
+ let byteLength = 0;
317
+ for await (const chunk of this.exportSnapshotStream(options)) {
318
+ byteLength += chunk.byteLength;
319
+ if (!Number.isSafeInteger(byteLength) || byteLength > MAX_SNAPSHOT_BYTE_ARRAY_LENGTH) {
320
+ throw new RangeError("Snapshot exceeds the byte-array API's 4 GiB limit");
321
+ }
322
+ chunks.push(chunk);
323
+ }
324
+ const bytes = new Uint8Array(byteLength);
325
+ let offset = 0;
326
+ for (const chunk of chunks) {
327
+ bytes.set(chunk, offset);
328
+ offset += chunk.byteLength;
329
+ }
330
+ return bytes;
331
+ }
332
+ async *exportSnapshotStream(options = {}) {
333
+ throwIfClientSnapshotAborted(options.signal);
334
+ options.onProgress?.({ phase: "reading", transferredBytes: 0, totalBytes: 0 });
335
+ const opened = await this.#call("exportSnapshotOpen", []);
336
+ let transferredBytes = 0;
337
+ let succeeded = false;
338
+ try {
339
+ for (; ; ) {
370
340
  throwIfClientSnapshotAborted(options.signal);
371
- options.onProgress?.({ phase: "reading", transferredBytes: 0, totalBytes: 0 });
372
- const opened = (await this.#call("exportSnapshotOpen", []));
373
- let transferredBytes = 0;
374
- let succeeded = false;
375
- try {
376
- for (;;) {
377
- throwIfClientSnapshotAborted(options.signal);
378
- const next = (await this._invoke(opened.handleId, "read", []));
379
- if (next.done)
380
- break;
381
- throwIfClientSnapshotAborted(options.signal);
382
- if (!(next.chunk instanceof Uint8Array) || next.chunk.byteLength === 0) {
383
- throw new Error("Snapshot transfer returned an invalid chunk");
384
- }
385
- yield next.chunk;
386
- transferredBytes += next.chunk.byteLength;
387
- options.onProgress?.({ phase: "transfer", transferredBytes, totalBytes: 0 });
388
- }
389
- succeeded = true;
390
- }
391
- finally {
392
- await this._invoke(opened.handleId, "close", []).catch(() => undefined);
393
- if (succeeded) {
394
- options.onProgress?.({
395
- phase: "done",
396
- transferredBytes,
397
- totalBytes: transferredBytes,
398
- });
399
- }
341
+ const next = await this._invoke(opened.handleId, "read", []);
342
+ if (next.done)
343
+ break;
344
+ throwIfClientSnapshotAborted(options.signal);
345
+ if (!(next.chunk instanceof Uint8Array) || next.chunk.byteLength === 0) {
346
+ throw new Error("Snapshot transfer returned an invalid chunk");
400
347
  }
401
- }
402
- /**
403
- * Loads a snapshot file into the worker's store, which must be empty. The bytes go over in the
404
- * same slices the export comes back in, and the worker's load progress arrives as events, so a
405
- * multi-megabyte restore can be shown moving rather than as a frozen tab.
406
- */
407
- async importSnapshot(bytes, options = {}) {
408
- await this.importSnapshotStream(clientSnapshotChunks(bytes), options);
409
- }
410
- /** Uploads with one acknowledged worker chunk in flight; cancellation leaves resumable state. */
411
- async importSnapshotStream(source, options = {}) {
412
- const handleId = crypto.randomUUID();
413
- const { onProgress } = options;
414
- this.#events.set(handleId, {
415
- ...(onProgress === undefined
416
- ? {}
417
- : {
418
- onProgress: (progress) => {
419
- onProgress(progress);
420
- },
421
- }),
348
+ yield next.chunk;
349
+ transferredBytes += next.chunk.byteLength;
350
+ options.onProgress?.({ phase: "transfer", transferredBytes, totalBytes: 0 });
351
+ }
352
+ succeeded = true;
353
+ } finally {
354
+ await this._invoke(opened.handleId, "close", []).catch(() => void 0);
355
+ if (succeeded) {
356
+ options.onProgress?.({
357
+ phase: "done",
358
+ transferredBytes,
359
+ totalBytes: transferredBytes
422
360
  });
423
- try {
424
- throwIfClientSnapshotAborted(options.signal);
425
- await this.#call("importSnapshotOpen", [handleId]);
426
- for await (const input of source) {
427
- throwIfClientSnapshotAborted(options.signal);
428
- if (!(input instanceof Uint8Array)) {
429
- throw new TypeError("Snapshot stream chunks must be Uint8Array values");
430
- }
431
- for (let offset = 0; offset < input.byteLength; offset += SNAPSHOT_CHUNK_BYTES) {
432
- // Copy the bounded window: cloning a subarray would otherwise transfer its whole backing
433
- // file on every request.
434
- const chunk = input.slice(offset, offset + SNAPSHOT_CHUNK_BYTES);
435
- await this.#post("rpc-call", handleId, "write", [chunk], [chunk.buffer]);
436
- throwIfClientSnapshotAborted(options.signal);
437
- }
438
- }
439
- await this._invoke(handleId, "finish", []);
440
- }
441
- catch (error) {
442
- await this._invoke(handleId, options.signal?.aborted === true ? "cancel" : "close", []).catch(() => undefined);
443
- throw error;
361
+ }
362
+ }
363
+ }
364
+ async importSnapshot(bytes, options = {}) {
365
+ await this.importSnapshotStream(clientSnapshotChunks(bytes), options);
366
+ }
367
+ async importSnapshotStream(source, options = {}) {
368
+ const handleId = crypto.randomUUID();
369
+ const { onProgress } = options;
370
+ this.#events.set(handleId, {
371
+ ...onProgress === void 0 ? {} : {
372
+ onProgress: (progress) => {
373
+ onProgress(progress);
444
374
  }
445
- finally {
446
- this.#events.delete(handleId);
375
+ }
376
+ });
377
+ try {
378
+ throwIfClientSnapshotAborted(options.signal);
379
+ await this.#call("importSnapshotOpen", [handleId]);
380
+ for await (const input of source) {
381
+ throwIfClientSnapshotAborted(options.signal);
382
+ if (!(input instanceof Uint8Array)) {
383
+ throw new TypeError("Snapshot stream chunks must be Uint8Array values");
447
384
  }
448
- }
449
- /** The worker-side buffer pool's byte budget, residency, and lifetime counters. */
450
- async bufferPoolStats() {
451
- return (await this.#call("bufferPoolStats", []));
452
- }
453
- async maintenanceStatus() {
454
- return (await this.#call("maintenanceStatus", []));
455
- }
456
- async checkIntegrity(options = {}) {
457
- return (await this.#call("checkIntegrity", [options]));
458
- }
459
- async storageStats() {
460
- return (await this.#call("storageStats", []));
461
- }
462
- async inspectInterruptedImport() {
463
- return (await this.#call("inspectInterruptedImport", []));
464
- }
465
- async abortInterruptedImport(identity) {
466
- return (await this.#call("abortInterruptedImport", [
467
- identity,
468
- ]));
469
- }
470
- liveQueries(options = {}) {
471
- const handleId = crypto.randomUUID();
472
- const created = this.#call("liveQueries", [handleId, options]);
473
- return new ClientLiveQuerySet(this, handleId, created);
474
- }
475
- // --- Maintenance ------------------------------------------------------------------------------
476
- async listVisibleSegmentPage(tableName, options) {
477
- return (await this.#call("listVisibleSegmentPage", options === undefined ? [tableName] : [tableName, options]));
478
- }
479
- async cleanupQuerySpill(options) {
480
- return (await this.#call("cleanupQuerySpill", options === undefined ? [] : [options]));
481
- }
482
- async compactTable(tableName, options) {
483
- return (await this.#call("compactTable", options === undefined ? [tableName] : [tableName, options]));
484
- }
485
- async compactTableStep(tableName, options) {
486
- return (await this.#call("compactTableStep", options === undefined ? [tableName] : [tableName, options]));
487
- }
488
- async resumeCompactionJob(jobId, options) {
489
- return (await this.#call("resumeCompactionJob", options === undefined ? [jobId] : [jobId, options]));
490
- }
491
- async listCompactionJobs(tableName) {
492
- return (await this.#call("listCompactionJobs", tableName === undefined ? [] : [tableName]));
493
- }
494
- async cancelCompactionJob(jobId) {
495
- return (await this.#call("cancelCompactionJob", [jobId]));
496
- }
497
- async collectGarbage(options) {
498
- return (await this.#call("collectGarbage", options === undefined ? [] : [options]));
499
- }
500
- async collectGarbageStep(options) {
501
- return (await this.#call("collectGarbageStep", options === undefined ? [] : [options]));
502
- }
503
- async resumeGarbageCollectionJob(jobId, options) {
504
- return (await this.#call("resumeGarbageCollectionJob", options === undefined ? [jobId] : [jobId, options]));
505
- }
506
- async listGarbageCollectionJobs() {
507
- return (await this.#call("listGarbageCollectionJobs", []));
508
- }
509
- // --- Lifecycle --------------------------------------------------------------------------------
510
- /** Disposes every worker-side handle, closes the store, and optionally terminates the worker. */
511
- async close(options = {}) {
512
- if (this.#closed)
513
- return;
514
- this.#closed = true;
515
- if (this.#onVisibilityChange !== undefined && typeof document !== "undefined") {
516
- document.removeEventListener("visibilitychange", this.#onVisibilityChange);
385
+ for (let offset = 0; offset < input.byteLength; offset += SNAPSHOT_CHUNK_BYTES) {
386
+ const chunk = input.slice(offset, offset + SNAPSHOT_CHUNK_BYTES);
387
+ await this.#post("rpc-call", handleId, "write", [chunk], [chunk.buffer]);
388
+ throwIfClientSnapshotAborted(options.signal);
517
389
  }
390
+ }
391
+ await this._invoke(handleId, "finish", []);
392
+ } catch (error) {
393
+ await this._invoke(handleId, options.signal?.aborted === true ? "cancel" : "close", []).catch(() => void 0);
394
+ throw error;
395
+ } finally {
396
+ this.#events.delete(handleId);
397
+ }
398
+ }
399
+ async bufferPoolStats() {
400
+ return await this.#call("bufferPoolStats", []);
401
+ }
402
+ async maintenanceStatus() {
403
+ return await this.#call("maintenanceStatus", []);
404
+ }
405
+ async checkIntegrity(options = {}) {
406
+ return await this.#call("checkIntegrity", [options]);
407
+ }
408
+ async storageStats() {
409
+ return await this.#call("storageStats", []);
410
+ }
411
+ async inspectInterruptedImport() {
412
+ return await this.#call("inspectInterruptedImport", []);
413
+ }
414
+ async abortInterruptedImport(identity) {
415
+ return await this.#call("abortInterruptedImport", [
416
+ identity
417
+ ]);
418
+ }
419
+ liveQueries(options = {}) {
420
+ const handleId = crypto.randomUUID();
421
+ const created = this.#call("liveQueries", [handleId, options]);
422
+ return new ClientLiveQuerySet(this, handleId, created);
423
+ }
424
+ async listVisibleSegmentPage(tableName, options) {
425
+ return await this.#call("listVisibleSegmentPage", options === void 0 ? [tableName] : [tableName, options]);
426
+ }
427
+ async cleanupQuerySpill(options) {
428
+ return await this.#call("cleanupQuerySpill", options === void 0 ? [] : [options]);
429
+ }
430
+ async compactTable(tableName, options) {
431
+ return await this.#call("compactTable", options === void 0 ? [tableName] : [tableName, options]);
432
+ }
433
+ async compactTableStep(tableName, options) {
434
+ return await this.#call("compactTableStep", options === void 0 ? [tableName] : [tableName, options]);
435
+ }
436
+ async resumeCompactionJob(jobId, options) {
437
+ return await this.#call("resumeCompactionJob", options === void 0 ? [jobId] : [jobId, options]);
438
+ }
439
+ async listCompactionJobs(tableName) {
440
+ return await this.#call("listCompactionJobs", tableName === void 0 ? [] : [tableName]);
441
+ }
442
+ async cancelCompactionJob(jobId) {
443
+ return await this.#call("cancelCompactionJob", [jobId]);
444
+ }
445
+ async collectGarbage(options) {
446
+ return await this.#call("collectGarbage", options === void 0 ? [] : [options]);
447
+ }
448
+ async collectGarbageStep(options) {
449
+ return await this.#call("collectGarbageStep", options === void 0 ? [] : [options]);
450
+ }
451
+ async resumeGarbageCollectionJob(jobId, options) {
452
+ return await this.#call("resumeGarbageCollectionJob", options === void 0 ? [jobId] : [jobId, options]);
453
+ }
454
+ async listGarbageCollectionJobs() {
455
+ return await this.#call("listGarbageCollectionJobs", []);
456
+ }
457
+ async close(options = {}) {
458
+ if (this.#closed)
459
+ return;
460
+ this.#closed = true;
461
+ if (this.#onVisibilityChange !== void 0 && typeof document !== "undefined") {
462
+ document.removeEventListener("visibilitychange", this.#onVisibilityChange);
463
+ }
464
+ try {
465
+ await this.#post("rpc-call", null, "dispose", []);
466
+ } finally {
467
+ this.#events.clear();
468
+ this.#transport.removeEventListener?.("message", this.#onMessage);
469
+ this.#transport.removeEventListener?.("error", this.#onError);
470
+ this.#transport.removeEventListener?.("messageerror", this.#onMessageError);
471
+ if (options.terminateWorker === true)
472
+ this.#transport.terminate?.();
473
+ }
474
+ }
475
+ async _invoke(handleId, method, args) {
476
+ if (this.#closed)
477
+ throw new Error("Database client is closed");
478
+ return this.#post("rpc-call", handleId, method, args);
479
+ }
480
+ async _invokeControlled(handleId, method, args, controls) {
481
+ if (this.#closed)
482
+ throw new Error("Database client is closed");
483
+ return this.#post("rpc-call", handleId, method, args, void 0, false, controls);
484
+ }
485
+ _routeEvents(handleId, route) {
486
+ this.#events.set(handleId, route);
487
+ }
488
+ _unrouteEvents(handleId) {
489
+ this.#events.delete(handleId);
490
+ }
491
+ async #call(method, args, controls = {}) {
492
+ if (this.#closed)
493
+ throw new Error("Database client is closed");
494
+ return this.#post("rpc-call", null, method, args, void 0, false, controls);
495
+ }
496
+ async #post(kind, handleId, method, args, transfer, bypassLimit = kind === "rpc-init" || method === "dispose", controls = {}) {
497
+ if (this.#fatal !== void 0)
498
+ throw this.#fatal;
499
+ controls.signal?.throwIfAborted();
500
+ if (!bypassLimit && this.#pending.size >= MAX_DATABASE_RPC_IN_FLIGHT) {
501
+ throw new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`);
502
+ }
503
+ const requestId = crypto.randomUUID();
504
+ return new Promise((resolve, reject) => {
505
+ const onAbort = () => {
518
506
  try {
519
- await this.#post("rpc-call", null, "dispose", []);
507
+ this.#transport.postMessage({ version: protocolVersion, requestId, kind: "rpc-cancel" });
508
+ } catch {
520
509
  }
521
- finally {
522
- this.#events.clear();
523
- this.#transport.removeEventListener?.("message", this.#onMessage);
524
- this.#transport.removeEventListener?.("error", this.#onError);
525
- this.#transport.removeEventListener?.("messageerror", this.#onMessageError);
526
- if (options.terminateWorker === true)
527
- this.#transport.terminate?.();
528
- }
529
- }
530
- // --- Channel internals ------------------------------------------------------------------------
531
- /** @internal */
532
- async _invoke(handleId, method, args) {
533
- if (this.#closed)
534
- throw new Error("Database client is closed");
535
- return this.#post("rpc-call", handleId, method, args);
536
- }
537
- /** @internal */
538
- async _invokeControlled(handleId, method, args, controls) {
539
- if (this.#closed)
540
- throw new Error("Database client is closed");
541
- return this.#post("rpc-call", handleId, method, args, undefined, false, controls);
542
- }
543
- /** @internal */
544
- _routeEvents(handleId, route) {
545
- this.#events.set(handleId, route);
546
- }
547
- /** @internal */
548
- _unrouteEvents(handleId) {
549
- this.#events.delete(handleId);
550
- }
551
- async #call(method, args, controls = {}) {
552
- if (this.#closed)
553
- throw new Error("Database client is closed");
554
- return this.#post("rpc-call", null, method, args, undefined, false, controls);
555
- }
556
- async #post(kind, handleId, method, args, transfer, bypassLimit = kind === "rpc-init" || method === "dispose", controls = {}) {
557
- if (this.#fatal !== undefined)
558
- throw this.#fatal;
559
- controls.signal?.throwIfAborted();
560
- if (!bypassLimit && this.#pending.size >= MAX_DATABASE_RPC_IN_FLIGHT) {
561
- throw new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`);
562
- }
563
- const requestId = crypto.randomUUID();
564
- return new Promise((resolve, reject) => {
565
- const onAbort = () => {
566
- try {
567
- this.#transport.postMessage({ version: protocolVersion, requestId, kind: "rpc-cancel" });
568
- }
569
- catch {
570
- // The original RPC still owns completion. A transport failure also reaches #fail via
571
- // its error event; throwing from an AbortSignal listener would only be unhandled noise.
572
- }
573
- };
574
- const cleanup = () => {
575
- controls.signal?.removeEventListener("abort", onAbort);
576
- if (controls.onStats !== undefined)
577
- this.#events.delete(requestId);
578
- };
579
- controls.signal?.addEventListener("abort", onAbort, { once: true });
580
- if (controls.onStats !== undefined) {
581
- this.#events.set(requestId, { onStats: controls.onStats });
582
- }
583
- this.#pending.set(requestId, { resolve, reject, cleanup });
584
- try {
585
- this.#transport.postMessage(kind === "rpc-init"
586
- ? { version: protocolVersion, requestId, kind, payload: args[0] }
587
- : { version: protocolVersion, requestId, kind, handleId, method, args }, transfer === undefined ? undefined : { transfer });
588
- }
589
- catch (error) {
590
- this.#pending.delete(requestId);
591
- cleanup();
592
- reject(error instanceof Error ? error : new Error("Database request failed", { cause: error }));
593
- }
594
- });
595
- }
596
- #receive(message) {
597
- const response = parseRpcResponse(message);
598
- if (response === null)
599
- return;
600
- if (response.kind === "rpc-event") {
601
- const route = this.#events.get(response.handleId);
602
- if (route === undefined)
603
- return;
604
- if (response.event === "change")
605
- route.onChange?.(decodeQueryResult(response.payload));
606
- else if (response.event === "invalidate") {
607
- route.onInvalidate?.(response.payload);
608
- }
609
- else if (response.event === "error") {
610
- route.onError?.(rehydrateError(response.payload));
611
- }
612
- else if (response.event === "progress")
613
- route.onProgress?.(response.payload);
614
- else if (response.event === "stats") {
615
- route.onStats?.(response.payload);
616
- }
617
- else if (response.event === "complete") {
618
- this.#events.delete(response.handleId);
619
- route.onComplete?.();
620
- }
621
- return;
622
- }
623
- const pending = this.#pending.get(response.requestId);
624
- if (pending === undefined)
625
- return;
626
- this.#pending.delete(response.requestId);
627
- pending.cleanup?.();
628
- if (response.kind === "rpc-result")
629
- pending.resolve(response.result);
630
- else
631
- pending.reject(rehydrateError(response.error));
632
- }
633
- #fail(error) {
634
- this.#fatal = error;
635
- const pending = [...this.#pending.values()];
636
- this.#pending.clear();
637
- this.#events.clear();
638
- for (const call of pending) {
639
- call.cleanup?.();
640
- call.reject(error);
641
- }
642
- }
510
+ };
511
+ const cleanup = () => {
512
+ controls.signal?.removeEventListener("abort", onAbort);
513
+ if (controls.onStats !== void 0)
514
+ this.#events.delete(requestId);
515
+ };
516
+ controls.signal?.addEventListener("abort", onAbort, { once: true });
517
+ if (controls.onStats !== void 0) {
518
+ this.#events.set(requestId, { onStats: controls.onStats });
519
+ }
520
+ this.#pending.set(requestId, { resolve, reject, cleanup });
521
+ try {
522
+ 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 });
523
+ } catch (error) {
524
+ this.#pending.delete(requestId);
525
+ cleanup();
526
+ reject(error instanceof Error ? error : new Error("Database request failed", { cause: error }));
527
+ }
528
+ });
529
+ }
530
+ #receive(message) {
531
+ let response;
532
+ try {
533
+ response = parseRpcResponse(message);
534
+ } catch (cause) {
535
+ this.#rejectUnreadable(message, cause);
536
+ return;
537
+ }
538
+ if (response === null)
539
+ return;
540
+ if (response.kind === "rpc-event") {
541
+ const route = this.#events.get(response.handleId);
542
+ if (route === void 0)
543
+ return;
544
+ if (response.event === "change")
545
+ route.onChange?.(decodeQueryResult(response.payload));
546
+ else if (response.event === "invalidate") {
547
+ route.onInvalidate?.(response.payload);
548
+ } else if (response.event === "error") {
549
+ route.onError?.(rehydrateResponseError(response.payload));
550
+ } else if (response.event === "progress")
551
+ route.onProgress?.(response.payload);
552
+ else if (response.event === "stats") {
553
+ route.onStats?.(response.payload);
554
+ } else if (response.event === "complete") {
555
+ this.#events.delete(response.handleId);
556
+ route.onComplete?.();
557
+ }
558
+ return;
559
+ }
560
+ const pending = this.#pending.get(response.requestId);
561
+ if (pending === void 0)
562
+ return;
563
+ this.#pending.delete(response.requestId);
564
+ pending.cleanup?.();
565
+ if (response.kind === "rpc-result")
566
+ pending.resolve(response.result);
567
+ else
568
+ pending.reject(rehydrateResponseError(response.error));
569
+ }
570
+ #rejectUnreadable(message, cause) {
571
+ const reason = cause instanceof Error ? cause.message : String(cause);
572
+ const error = new Error(`The database worker sent a frame this client cannot read: ${reason}`, {
573
+ cause
574
+ });
575
+ const requestId = message.requestId;
576
+ const pending = typeof requestId === "string" ? this.#pending.get(requestId) : void 0;
577
+ if (pending === void 0 || typeof requestId !== "string") {
578
+ this.#fail(error);
579
+ return;
580
+ }
581
+ this.#pending.delete(requestId);
582
+ pending.cleanup?.();
583
+ pending.reject(error);
584
+ }
585
+ #fail(error) {
586
+ this.#fatal = error;
587
+ const pending = [...this.#pending.values()];
588
+ this.#pending.clear();
589
+ this.#events.clear();
590
+ for (const call of pending) {
591
+ call.cleanup?.();
592
+ call.reject(error);
593
+ }
594
+ }
643
595
  }
644
- /**
645
- * Proxy of a worker-side BufferedTableWriter. The age timer runs on the worker's clock, and
646
- * onError fires for background flush failures exactly as in-worker — delivered as an event.
647
- */
648
- export class ClientBufferedWriter {
649
- client;
650
- handleId;
651
- constructor(client, handleId, created) {
652
- this.client = client;
653
- this.handleId = handleId;
654
- this.#created = created;
655
- // The creation result also arrives through the first chained call; keep it observed.
656
- created.catch(() => undefined);
657
- }
658
- #created;
659
- async add(row) {
660
- await this.#created;
661
- return (await this.client._invoke(this.handleId, "add", [row]));
662
- }
663
- async flush() {
664
- await this.#created;
665
- return (await this.client._invoke(this.handleId, "flush", []));
666
- }
667
- /** Fire-and-forget: flush failures surface through onError, matching the in-worker contract. */
668
- requestFlush() {
669
- void this.#created
670
- .then(() => this.client._invoke(this.handleId, "requestFlush", []))
671
- .catch(() => undefined);
672
- }
673
- async stats() {
674
- await this.#created;
675
- return (await this.client._invoke(this.handleId, "stats", []));
676
- }
677
- async discard() {
678
- await this.#created;
679
- return (await this.client._invoke(this.handleId, "discard", []));
680
- }
681
- async close() {
682
- await this.#created;
683
- try {
684
- return (await this.client._invoke(this.handleId, "close", []));
685
- }
686
- finally {
687
- this.client._unrouteEvents(this.handleId);
688
- }
689
- }
596
+ class ClientBufferedWriter {
597
+ client;
598
+ handleId;
599
+ constructor(client, handleId, created) {
600
+ this.client = client;
601
+ this.handleId = handleId;
602
+ this.#created = created;
603
+ created.catch(() => void 0);
604
+ }
605
+ #created;
606
+ async add(row) {
607
+ await this.#created;
608
+ return await this.client._invoke(this.handleId, "add", [row]);
609
+ }
610
+ async flush() {
611
+ await this.#created;
612
+ return await this.client._invoke(this.handleId, "flush", []);
613
+ }
614
+ requestFlush() {
615
+ void this.#created.then(() => this.client._invoke(this.handleId, "requestFlush", [])).catch(() => void 0);
616
+ }
617
+ async stats() {
618
+ await this.#created;
619
+ return await this.client._invoke(this.handleId, "stats", []);
620
+ }
621
+ async discard() {
622
+ await this.#created;
623
+ return await this.client._invoke(this.handleId, "discard", []);
624
+ }
625
+ async close() {
626
+ await this.#created;
627
+ try {
628
+ return await this.client._invoke(this.handleId, "close", []);
629
+ } finally {
630
+ this.client._unrouteEvents(this.handleId);
631
+ }
632
+ }
690
633
  }
691
- /** Proxy of a worker-side LiveQuerySet; onChange/onError arrive as events from the worker. */
692
- export class ClientLiveQuerySet {
693
- client;
694
- handleId;
695
- #subscriptionIds = new Set();
696
- constructor(client, handleId, created) {
697
- this.client = client;
698
- this.handleId = handleId;
699
- this.#created = created;
700
- created.catch(() => undefined);
701
- }
702
- #created;
703
- /** Registers a query (SQL or a compiled-plan envelope) and re-runs it on relevant changes. */
704
- async subscribe(query, options) {
705
- await this.#created;
706
- const subscriptionId = crypto.randomUUID();
707
- // The worker tears down its handle when the subscription completes (set closed there);
708
- // closing the client wrapper afterwards must not call into the vanished handle.
709
- const state = { completed: false };
710
- this.client._routeEvents(subscriptionId, {
711
- onChange: options.onChange.bind(options),
712
- ...(options.onError === undefined ? {} : { onError: options.onError.bind(options) }),
713
- onComplete: () => {
714
- state.completed = true;
715
- this.#subscriptionIds.delete(subscriptionId);
716
- options.onComplete?.();
717
- },
718
- });
719
- try {
720
- const created = (await this.client._invoke(this.handleId, "subscribe", [
721
- subscriptionId,
722
- query,
723
- ]));
724
- this.#subscriptionIds.add(subscriptionId);
725
- return new ClientLiveSubscription(this.client, subscriptionId, created.dependencyTableIds, () => this.#subscriptionIds.delete(subscriptionId), state);
726
- }
727
- catch (error) {
728
- this.client._unrouteEvents(subscriptionId);
729
- throw error;
730
- }
731
- }
732
- /** Registers dependency observation while leaving execution/result mapping to an adapter. */
733
- async observe(query, options) {
734
- await this.#created;
735
- const subscriptionId = crypto.randomUUID();
736
- const state = { completed: false };
737
- this.client._routeEvents(subscriptionId, {
738
- onInvalidate: options.onInvalidate.bind(options),
739
- ...(options.onError === undefined ? {} : { onError: options.onError.bind(options) }),
740
- onComplete: () => {
741
- state.completed = true;
742
- this.#subscriptionIds.delete(subscriptionId);
743
- options.onComplete?.();
744
- },
745
- });
746
- try {
747
- const created = (await this.client._invoke(this.handleId, "observe", [
748
- subscriptionId,
749
- query,
750
- ]));
751
- this.#subscriptionIds.add(subscriptionId);
752
- return new ClientLiveSubscription(this.client, subscriptionId, created.dependencyTableIds, () => this.#subscriptionIds.delete(subscriptionId), state);
753
- }
754
- catch (error) {
755
- this.client._unrouteEvents(subscriptionId);
756
- throw error;
757
- }
758
- }
759
- async stats() {
760
- await this.#created;
761
- return (await this.client._invoke(this.handleId, "stats", []));
762
- }
763
- /** Hints the worker that this tab committed elsewhere (e.g. through another client). */
764
- notifyLocalCommit() {
765
- void this.#created
766
- .then(() => this.client._invoke(this.handleId, "notifyLocalCommit", []))
767
- .catch(() => undefined);
768
- }
769
- /** Runs one authoritative version check and selective re-execution sweep. */
770
- async refresh() {
771
- await this.#created;
772
- await this.client._invoke(this.handleId, "refresh", []);
773
- }
774
- async close() {
775
- await this.#created;
776
- try {
777
- await this.client._invoke(this.handleId, "close", []);
778
- }
779
- finally {
780
- for (const subscriptionId of this.#subscriptionIds) {
781
- this.client._unrouteEvents(subscriptionId);
782
- }
783
- this.#subscriptionIds.clear();
784
- }
785
- }
634
+ class ClientLiveQuerySet {
635
+ client;
636
+ handleId;
637
+ #subscriptionIds = /* @__PURE__ */ new Set();
638
+ constructor(client, handleId, created) {
639
+ this.client = client;
640
+ this.handleId = handleId;
641
+ this.#created = created;
642
+ created.catch(() => void 0);
643
+ }
644
+ #created;
645
+ async subscribe(query, options) {
646
+ await this.#created;
647
+ const subscriptionId = crypto.randomUUID();
648
+ const state = { completed: false };
649
+ this.client._routeEvents(subscriptionId, {
650
+ onChange: options.onChange.bind(options),
651
+ ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
652
+ onComplete: () => {
653
+ state.completed = true;
654
+ this.#subscriptionIds.delete(subscriptionId);
655
+ options.onComplete?.();
656
+ }
657
+ });
658
+ try {
659
+ const created = await this.client._invoke(this.handleId, "subscribe", [
660
+ subscriptionId,
661
+ query
662
+ ]);
663
+ this.#subscriptionIds.add(subscriptionId);
664
+ return new ClientLiveSubscription(this.client, subscriptionId, created.dependencyTableIds, () => this.#subscriptionIds.delete(subscriptionId), state);
665
+ } catch (error) {
666
+ this.client._unrouteEvents(subscriptionId);
667
+ throw error;
668
+ }
669
+ }
670
+ async observe(query, options) {
671
+ await this.#created;
672
+ const subscriptionId = crypto.randomUUID();
673
+ const state = { completed: false };
674
+ this.client._routeEvents(subscriptionId, {
675
+ onInvalidate: options.onInvalidate.bind(options),
676
+ ...options.onError === void 0 ? {} : { onError: options.onError.bind(options) },
677
+ onComplete: () => {
678
+ state.completed = true;
679
+ this.#subscriptionIds.delete(subscriptionId);
680
+ options.onComplete?.();
681
+ }
682
+ });
683
+ try {
684
+ const created = await this.client._invoke(this.handleId, "observe", [
685
+ subscriptionId,
686
+ query
687
+ ]);
688
+ this.#subscriptionIds.add(subscriptionId);
689
+ return new ClientLiveSubscription(this.client, subscriptionId, created.dependencyTableIds, () => this.#subscriptionIds.delete(subscriptionId), state);
690
+ } catch (error) {
691
+ this.client._unrouteEvents(subscriptionId);
692
+ throw error;
693
+ }
694
+ }
695
+ async stats() {
696
+ await this.#created;
697
+ return await this.client._invoke(this.handleId, "stats", []);
698
+ }
699
+ notifyLocalCommit() {
700
+ void this.#created.then(() => this.client._invoke(this.handleId, "notifyLocalCommit", [])).catch(() => void 0);
701
+ }
702
+ async refresh() {
703
+ await this.#created;
704
+ await this.client._invoke(this.handleId, "refresh", []);
705
+ }
706
+ async close() {
707
+ await this.#created;
708
+ try {
709
+ await this.client._invoke(this.handleId, "close", []);
710
+ } finally {
711
+ for (const subscriptionId of this.#subscriptionIds) {
712
+ this.client._unrouteEvents(subscriptionId);
713
+ }
714
+ this.#subscriptionIds.clear();
715
+ }
716
+ }
786
717
  }
787
- export class ClientLiveSubscription {
788
- client;
789
- handleId;
790
- dependencyTableIds;
791
- onClosed;
792
- state;
793
- constructor(client, handleId, dependencyTableIds, onClosed, state = { completed: false }) {
794
- this.client = client;
795
- this.handleId = handleId;
796
- this.dependencyTableIds = dependencyTableIds;
797
- this.onClosed = onClosed;
798
- this.state = state;
799
- }
800
- async close() {
801
- try {
802
- // A completed subscription no longer exists worker-side; only clean up locally.
803
- if (!this.state.completed)
804
- await this.client._invoke(this.handleId, "close", []);
805
- }
806
- finally {
807
- this.client._unrouteEvents(this.handleId);
808
- this.onClosed();
809
- }
810
- }
718
+ class ClientLiveSubscription {
719
+ client;
720
+ handleId;
721
+ dependencyTableIds;
722
+ onClosed;
723
+ state;
724
+ constructor(client, handleId, dependencyTableIds, onClosed, state = { completed: false }) {
725
+ this.client = client;
726
+ this.handleId = handleId;
727
+ this.dependencyTableIds = dependencyTableIds;
728
+ this.onClosed = onClosed;
729
+ this.state = state;
730
+ }
731
+ async close() {
732
+ try {
733
+ if (!this.state.completed)
734
+ await this.client._invoke(this.handleId, "close", []);
735
+ } finally {
736
+ this.client._unrouteEvents(this.handleId);
737
+ this.onClosed();
738
+ }
739
+ }
811
740
  }
741
+ export {
742
+ ClientBufferedWriter,
743
+ ClientLiveQuerySet,
744
+ ClientLiveSubscription,
745
+ MinnowDatabaseClient
746
+ };