@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.
- package/dist/block-format/block.js +254 -325
- package/dist/block-format/checksum.js +30 -52
- package/dist/block-format/codecs.js +112 -125
- package/dist/block-format/column.js +187 -205
- package/dist/block-format/index.js +5 -1
- package/dist/block-format/physical.js +371 -387
- package/dist/block-format/types.js +4 -1
- package/dist/block-format/unicode.js +30 -39
- package/dist/date-value.d.ts +5 -0
- package/dist/date-value.js +71 -41
- package/dist/engine/artifact-cache.js +106 -116
- package/dist/engine/batch.js +36 -44
- package/dist/engine/buffered-writer.js +156 -158
- package/dist/engine/byte-estimates.js +20 -22
- package/dist/engine/cache-limits.js +24 -25
- package/dist/engine/cancellation.js +5 -3
- package/dist/engine/catalog.js +64 -73
- package/dist/engine/client.js +715 -780
- package/dist/engine/database.js +15952 -18465
- package/dist/engine/defaults.js +104 -111
- package/dist/engine/errors.js +113 -128
- package/dist/engine/fts.js +233 -280
- package/dist/engine/group-index.js +287 -321
- package/dist/engine/index.js +23 -12
- package/dist/engine/join-index.js +177 -186
- package/dist/engine/keyed-live.js +168 -173
- package/dist/engine/live-api.js +0 -1
- package/dist/engine/live-equal.js +32 -37
- package/dist/engine/live.js +603 -647
- package/dist/engine/memory.js +109 -111
- package/dist/engine/optimizer.d.ts +19 -1
- package/dist/engine/optimizer.js +2642 -2515
- package/dist/engine/point-read.d.ts +4 -2
- package/dist/engine/point-read.js +121 -171
- package/dist/engine/query-api.js +12 -3
- package/dist/engine/query-cache.js +61 -69
- package/dist/engine/query.d.ts +45 -0
- package/dist/engine/query.js +8506 -9375
- package/dist/engine/result-wire.js +221 -252
- package/dist/engine/schema-wire.js +97 -112
- package/dist/engine/schema.js +1099 -1254
- package/dist/engine/sort-keys.js +315 -389
- package/dist/engine/sql-domains.d.ts +7 -1
- package/dist/engine/sql-domains.js +648 -734
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-functions.d.ts +11 -0
- package/dist/engine/sql-functions.js +1092 -0
- package/dist/engine/sql-json.js +199 -218
- package/dist/engine/sql-semantics.d.ts +25 -4
- package/dist/engine/sql-semantics.js +579 -513
- package/dist/engine/typed-live.js +271 -293
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +4513 -4580
- package/dist/engine/worker-host.d.ts +8 -63
- package/dist/engine/worker-host.js +28 -1130
- package/dist/engine/worker-indexeddb.d.ts +1 -0
- package/dist/engine/worker-indexeddb.js +3 -0
- package/dist/engine/worker-memory.d.ts +1 -0
- package/dist/engine/worker-memory.js +3 -0
- package/dist/engine/worker-opfs.d.ts +1 -0
- package/dist/engine/worker-opfs.js +3 -0
- package/dist/engine/worker-server.d.ts +93 -0
- package/dist/engine/worker-server.js +999 -0
- package/dist/engine/worker-store-indexeddb.d.ts +2 -0
- package/dist/engine/worker-store-indexeddb.js +11 -0
- package/dist/engine/worker-store-memory.d.ts +2 -0
- package/dist/engine/worker-store-memory.js +6 -0
- package/dist/engine/worker-store-opfs.d.ts +2 -0
- package/dist/engine/worker-store-opfs.js +9 -0
- package/dist/engine/worker.js +0 -12
- package/dist/engine/write-block-planner.js +93 -100
- package/dist/plan/index.js +15 -16
- package/dist/plan/model.d.ts +9 -1
- package/dist/plan/model.js +20 -27
- package/dist/storage/index.js +0 -12
- package/dist/storage/indexeddb.js +11776 -13171
- package/dist/storage/memory.js +1198 -1241
- package/dist/storage/opfs/files.js +238 -295
- package/dist/storage/opfs/index.js +9 -3
- package/dist/storage/opfs/leader.js +4386 -4856
- package/dist/storage/opfs/rpc.js +223 -259
- package/dist/storage/opfs/snapshot-ledger.js +219 -260
- package/dist/storage/opfs/store.js +884 -1003
- package/dist/storage/persistence.js +59 -74
- package/dist/storage/snapshot-stream.js +788 -826
- package/dist/storage/snapshot.js +36 -37
- package/dist/storage/toolkit/extents.js +444 -520
- package/dist/storage/toolkit/index.js +28 -29
- package/dist/storage/toolkit/record-core.js +5792 -6377
- package/dist/storage/toolkit/sync-file.js +34 -42
- package/dist/storage/toolkit/wal.js +92 -127
- package/dist/storage/toolkit/wire.js +232 -256
- package/dist/storage/types.js +2952 -3228
- package/dist/testing/block-store-conformance.js +1556 -1629
- package/dist/testing/index.js +310 -291
- package/dist/testing/opfs-shim.js +280 -312
- package/dist/testing/simulator.js +496 -534
- package/dist/testing/sqllogictest.js +425 -365
- package/dist/transactions/index.js +1464 -1712
- package/dist/worker-protocol/index.js +70 -69
- package/package.json +20 -2
- package/postgres-feature-profile.json +15 -5
- package/sql-feature-matrix.json +252 -2
|
@@ -1,1133 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { deserializeSchema, serializeMigrationSteps } from "./schema-wire.js";
|
|
6
|
-
/**
|
|
7
|
-
* The staged-mutation ops a client may name inside a "stage" call. The op arrives as wire data,
|
|
8
|
-
* so it must be checked against this list before indexing into the session — otherwise any
|
|
9
|
-
* property name (including Object.prototype members like "constructor") would be reachable.
|
|
10
|
-
*/
|
|
11
|
-
const stageOps = ["insertBatch", "upsertBatch", "updateBatch", "deleteBatch"];
|
|
12
|
-
function isStageOp(value) {
|
|
13
|
-
return typeof value === "string" && stageOps.includes(value);
|
|
1
|
+
import { attachWorkerHost } from "./worker-server.js";
|
|
2
|
+
import { exposeDatabase, singleStoreFactory, MAX_WORKER_HANDLES_PER_CONNECTION } from "./worker-server.js";
|
|
3
|
+
function attachDatabaseWorker(scope, options = {}) {
|
|
4
|
+
attachWorkerHost(scope, options.createStore ?? createStore, options);
|
|
14
5
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Root methods whose arguments and results are already structured-clone-safe. `satisfies` makes
|
|
17
|
-
* a removed or renamed database method a compile error, while the runtime guard below fails
|
|
18
|
-
* closed if a caller supplies a malformed database object through the manual host seam.
|
|
19
|
-
*/
|
|
20
|
-
const directRootMethods = [
|
|
21
|
-
"createTable",
|
|
22
|
-
"createView",
|
|
23
|
-
"dropView",
|
|
24
|
-
"dropColumn",
|
|
25
|
-
"dropTable",
|
|
26
|
-
"createIndex",
|
|
27
|
-
"dropIndex",
|
|
28
|
-
"buildFtsIndex",
|
|
29
|
-
"introspect",
|
|
30
|
-
"listTables",
|
|
31
|
-
"insertBatch",
|
|
32
|
-
"insert",
|
|
33
|
-
"upsertBatch",
|
|
34
|
-
"upsert",
|
|
35
|
-
"updateBatch",
|
|
36
|
-
"update",
|
|
37
|
-
"deleteBatch",
|
|
38
|
-
"delete",
|
|
39
|
-
"runStatement",
|
|
40
|
-
"explain",
|
|
41
|
-
"listVisibleSegmentPage",
|
|
42
|
-
"cleanupQuerySpill",
|
|
43
|
-
"compactTable",
|
|
44
|
-
"compactTableStep",
|
|
45
|
-
"resumeCompactionJob",
|
|
46
|
-
"listCompactionJobs",
|
|
47
|
-
"cancelCompactionJob",
|
|
48
|
-
"collectGarbage",
|
|
49
|
-
"collectGarbageStep",
|
|
50
|
-
"resumeGarbageCollectionJob",
|
|
51
|
-
"bufferPoolStats",
|
|
52
|
-
"maintenanceStatus",
|
|
53
|
-
"checkIntegrity",
|
|
54
|
-
"storageStats",
|
|
55
|
-
"inspectInterruptedImport",
|
|
56
|
-
"abortInterruptedImport",
|
|
57
|
-
"listGarbageCollectionJobs",
|
|
58
|
-
];
|
|
59
|
-
function isDirectRootMethod(value) {
|
|
60
|
-
return directRootMethods.includes(value);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* A call result that travels as a columnar frame with its buffers transferred. Only query
|
|
64
|
-
* results take this path: rows of objects are what structured clone is slowest at, so they are
|
|
65
|
-
* pivoted into per-column arrays here and rebuilt by the client.
|
|
66
|
-
*/
|
|
67
|
-
class ColumnarResult {
|
|
68
|
-
encoded;
|
|
69
|
-
constructor(encoded) {
|
|
70
|
-
this.encoded = encoded;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
class CursorBatchResult {
|
|
74
|
-
encoded;
|
|
75
|
-
constructor(encoded) {
|
|
76
|
-
this.encoded = encoded;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
class SnapshotChunkResult {
|
|
80
|
-
chunk;
|
|
81
|
-
constructor(chunk) {
|
|
82
|
-
this.chunk = chunk;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
class WriteScopeAbortedError extends Error {
|
|
86
|
-
name = "WriteScopeAbortedError";
|
|
87
|
-
constructor() {
|
|
88
|
-
super("Write scope aborted");
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/** One-chunk rendezvous. The sender is acknowledged only when the decoder asks for more. */
|
|
92
|
-
class SnapshotImportChunkQueue {
|
|
93
|
-
#queued;
|
|
94
|
-
#consumed;
|
|
95
|
-
#reader;
|
|
96
|
-
#finished = false;
|
|
97
|
-
#failure;
|
|
98
|
-
[Symbol.asyncIterator]() {
|
|
99
|
-
return this;
|
|
100
|
-
}
|
|
101
|
-
push(chunk) {
|
|
102
|
-
if (chunk.byteLength > MAX_SNAPSHOT_STREAM_CHUNK_BYTES) {
|
|
103
|
-
throw new RangeError("Snapshot transfer chunk exceeds the 1 MiB limit");
|
|
104
|
-
}
|
|
105
|
-
if (this.#failure !== undefined)
|
|
106
|
-
return Promise.reject(snapshotQueueError(this.#failure));
|
|
107
|
-
if (this.#finished)
|
|
108
|
-
return Promise.reject(new Error("Snapshot import transfer is closed"));
|
|
109
|
-
if (this.#queued !== undefined) {
|
|
110
|
-
return Promise.reject(new Error("Snapshot import transfer already has a queued chunk"));
|
|
111
|
-
}
|
|
112
|
-
return new Promise((resolve, reject) => {
|
|
113
|
-
const reader = this.#reader;
|
|
114
|
-
if (reader === undefined) {
|
|
115
|
-
this.#queued = { chunk, resolve, reject };
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
this.#reader = undefined;
|
|
119
|
-
this.#consumed = { resolve, reject };
|
|
120
|
-
reader.resolve({ done: false, value: chunk });
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
async next() {
|
|
124
|
-
this.#consumed?.resolve();
|
|
125
|
-
this.#consumed = undefined;
|
|
126
|
-
if (this.#failure !== undefined)
|
|
127
|
-
throw snapshotQueueError(this.#failure);
|
|
128
|
-
const queued = this.#queued;
|
|
129
|
-
if (queued !== undefined) {
|
|
130
|
-
this.#queued = undefined;
|
|
131
|
-
this.#consumed = { resolve: queued.resolve, reject: queued.reject };
|
|
132
|
-
return { done: false, value: queued.chunk };
|
|
133
|
-
}
|
|
134
|
-
if (this.#finished)
|
|
135
|
-
return { done: true, value: undefined };
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
this.#reader = { resolve, reject };
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
async return() {
|
|
141
|
-
this.finish();
|
|
142
|
-
return { done: true, value: undefined };
|
|
143
|
-
}
|
|
144
|
-
finish() {
|
|
145
|
-
if (this.#finished)
|
|
146
|
-
return;
|
|
147
|
-
this.#finished = true;
|
|
148
|
-
if (this.#queued === undefined) {
|
|
149
|
-
const reader = this.#reader;
|
|
150
|
-
this.#reader = undefined;
|
|
151
|
-
reader?.resolve({ done: true, value: undefined });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
fail(error) {
|
|
155
|
-
if (this.#failure !== undefined)
|
|
156
|
-
return;
|
|
157
|
-
this.#failure = error;
|
|
158
|
-
this.#finished = true;
|
|
159
|
-
this.#queued?.reject(error);
|
|
160
|
-
this.#queued = undefined;
|
|
161
|
-
this.#consumed?.reject(error);
|
|
162
|
-
this.#consumed = undefined;
|
|
163
|
-
this.#reader?.reject(error);
|
|
164
|
-
this.#reader = undefined;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
function snapshotQueueError(value) {
|
|
168
|
-
return value instanceof Error ? value : new Error("Snapshot transfer failed", { cause: value });
|
|
169
|
-
}
|
|
170
|
-
function abortError(message) {
|
|
171
|
-
const error = new Error(message);
|
|
172
|
-
error.name = "AbortError";
|
|
173
|
-
return error;
|
|
174
|
-
}
|
|
175
|
-
/** Hard per-connection ceiling for resident worker resources abandoned by a client. */
|
|
176
|
-
export const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
|
|
177
|
-
const DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS = 30_000;
|
|
178
|
-
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
179
|
-
/** Shared by RPC methods that do not expose cancellation, avoiding one controller per call. */
|
|
180
|
-
const passiveRpcSignal = new AbortController().signal;
|
|
181
|
-
class DatabaseRpcServer {
|
|
182
|
-
database;
|
|
183
|
-
scope;
|
|
184
|
-
options;
|
|
185
|
-
#handles = new Map();
|
|
186
|
-
#reservedHandleIds = new Set();
|
|
187
|
-
#openingHandlePromises = new Set();
|
|
188
|
-
#settlingWritePromises = new Set();
|
|
189
|
-
#requestAborts = new Map();
|
|
190
|
-
#disposed = false;
|
|
191
|
-
#inFlightRpcCount = 0;
|
|
192
|
-
#inFlightRpcDrain;
|
|
193
|
-
#resolveInFlightRpcDrain;
|
|
194
|
-
#writeHandleIdleTimeoutMs;
|
|
195
|
-
constructor(database, scope, options) {
|
|
196
|
-
this.database = database;
|
|
197
|
-
this.scope = scope;
|
|
198
|
-
this.options = options;
|
|
199
|
-
this.#writeHandleIdleTimeoutMs =
|
|
200
|
-
options.writeHandleIdleTimeoutMs ?? DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS;
|
|
201
|
-
if (!Number.isSafeInteger(this.#writeHandleIdleTimeoutMs) ||
|
|
202
|
-
this.#writeHandleIdleTimeoutMs <= 0 ||
|
|
203
|
-
this.#writeHandleIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
|
|
204
|
-
throw new RangeError(`Worker write-handle idle timeout must be a positive whole number no greater than ${String(MAX_TIMER_DELAY_MS)}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
async handle(request) {
|
|
208
|
-
if (request.kind === "rpc-init") {
|
|
209
|
-
this.scope.postMessage(rpcResult(request.requestId, { ready: true }));
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (request.kind === "rpc-cancel") {
|
|
213
|
-
this.#requestAborts
|
|
214
|
-
.get(request.requestId)
|
|
215
|
-
?.abort(abortError("Database request was cancelled"));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const bypassLimit = request.method === "dispose";
|
|
219
|
-
if (!bypassLimit && this.#inFlightRpcCount >= MAX_DATABASE_RPC_IN_FLIGHT) {
|
|
220
|
-
this.scope.postMessage(rpcFailure(request.requestId, new RangeError(`A database worker connection cannot hold more than ${String(MAX_DATABASE_RPC_IN_FLIGHT)} in-flight requests`)));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (!bypassLimit)
|
|
224
|
-
this.#inFlightRpcCount += 1;
|
|
225
|
-
const abort = request.method === "query" || request.method === "execute"
|
|
226
|
-
? new AbortController()
|
|
227
|
-
: undefined;
|
|
228
|
-
if (abort !== undefined)
|
|
229
|
-
this.#requestAborts.set(request.requestId, abort);
|
|
230
|
-
const context = {
|
|
231
|
-
requestId: request.requestId,
|
|
232
|
-
signal: abort?.signal ?? passiveRpcSignal,
|
|
233
|
-
};
|
|
234
|
-
try {
|
|
235
|
-
if (this.#disposed)
|
|
236
|
-
throw new Error("Database connection is disposed");
|
|
237
|
-
const result = request.handleId === null
|
|
238
|
-
? await this.#callRoot(request.method, request.args, context)
|
|
239
|
-
: await this.#callHandle(request.handleId, request.method, request.args, context);
|
|
240
|
-
if (result instanceof ColumnarResult) {
|
|
241
|
-
this.scope.postMessage(rpcResult(request.requestId, result.encoded.payload), {
|
|
242
|
-
transfer: result.encoded.transfer,
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
else if (result instanceof CursorBatchResult) {
|
|
246
|
-
this.scope.postMessage(rpcResult(request.requestId, result.encoded === undefined
|
|
247
|
-
? { done: true }
|
|
248
|
-
: { done: false, batch: result.encoded.payload }), result.encoded === undefined ? undefined : { transfer: result.encoded.transfer });
|
|
249
|
-
}
|
|
250
|
-
else if (result instanceof SnapshotChunkResult) {
|
|
251
|
-
this.scope.postMessage(rpcResult(request.requestId, { done: false, chunk: result.chunk }), {
|
|
252
|
-
transfer: [result.chunk.buffer],
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
this.scope.postMessage(rpcResult(request.requestId, result));
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
this.scope.postMessage(rpcFailure(request.requestId, error));
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
if (abort !== undefined)
|
|
264
|
-
this.#requestAborts.delete(request.requestId);
|
|
265
|
-
if (!bypassLimit)
|
|
266
|
-
this.#inFlightRpcCount -= 1;
|
|
267
|
-
if (this.#inFlightRpcCount === 0 && this.#resolveInFlightRpcDrain !== undefined) {
|
|
268
|
-
const resolve = this.#resolveInFlightRpcDrain;
|
|
269
|
-
this.#inFlightRpcDrain = undefined;
|
|
270
|
-
this.#resolveInFlightRpcDrain = undefined;
|
|
271
|
-
resolve();
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
async #callRoot(method, args, context) {
|
|
276
|
-
const database = this.database;
|
|
277
|
-
if (isDirectRootMethod(method)) {
|
|
278
|
-
const operation = database[method];
|
|
279
|
-
if (operation === undefined) {
|
|
280
|
-
throw new Error(`Database root method is unavailable: ${method}`);
|
|
281
|
-
}
|
|
282
|
-
return operation.apply(this.database, args);
|
|
283
|
-
}
|
|
284
|
-
switch (method) {
|
|
285
|
-
case "query": {
|
|
286
|
-
const [sql, options, reportStats = false] = args;
|
|
287
|
-
return new ColumnarResult(encodeQueryResult(await this.database.query(sql, {
|
|
288
|
-
...options,
|
|
289
|
-
signal: context.signal,
|
|
290
|
-
...(reportStats
|
|
291
|
-
? {
|
|
292
|
-
onStats: (stats) => {
|
|
293
|
-
this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
|
|
294
|
-
},
|
|
295
|
-
}
|
|
296
|
-
: {}),
|
|
297
|
-
})));
|
|
298
|
-
}
|
|
299
|
-
case "execute": {
|
|
300
|
-
const [sql, params, options, reportStats = false] = args;
|
|
301
|
-
return this.database.execute(sql, params, {
|
|
302
|
-
...options,
|
|
303
|
-
signal: context.signal,
|
|
304
|
-
...(reportStats
|
|
305
|
-
? {
|
|
306
|
-
onStats: (stats) => {
|
|
307
|
-
this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
|
|
308
|
-
},
|
|
309
|
-
}
|
|
310
|
-
: {}),
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
case "queryCursorOpen": {
|
|
314
|
-
const handleId = this.#claimHandleId(args[0]);
|
|
315
|
-
const [sql, options, reportStats = false] = args.slice(1);
|
|
316
|
-
try {
|
|
317
|
-
const iterator = this.database.queryCursor(sql, {
|
|
318
|
-
...options,
|
|
319
|
-
...(reportStats
|
|
320
|
-
? {
|
|
321
|
-
onStats: (stats) => {
|
|
322
|
-
this.scope.postMessage(rpcEvent(handleId, "stats", stats));
|
|
323
|
-
},
|
|
324
|
-
}
|
|
325
|
-
: {}),
|
|
326
|
-
});
|
|
327
|
-
this.#publishHandle(handleId, { type: "query-cursor", iterator });
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
this.#releaseHandleId(handleId);
|
|
331
|
-
throw error;
|
|
332
|
-
}
|
|
333
|
-
return { handleId };
|
|
334
|
-
}
|
|
335
|
-
case "run": {
|
|
336
|
-
const query = args[0];
|
|
337
|
-
return new ColumnarResult(encodeQueryRows(await this.database.run(query)));
|
|
338
|
-
}
|
|
339
|
-
case "readTable": {
|
|
340
|
-
// A table's rows are uniform — every row carries every read column — so they take the
|
|
341
|
-
// same columnar frame a query result does.
|
|
342
|
-
const [tableName, versionOrOptions] = args;
|
|
343
|
-
const rows = await this.database.readTable(tableName, versionOrOptions);
|
|
344
|
-
return new ColumnarResult(encodeQueryRows(rows));
|
|
345
|
-
}
|
|
346
|
-
case "migrate": {
|
|
347
|
-
const result = await this.database.migrate(deserializeSchema(args[0]), args[1] ?? {});
|
|
348
|
-
return {
|
|
349
|
-
createdTables: result.createdTables,
|
|
350
|
-
alteredTables: result.alteredTables,
|
|
351
|
-
droppedTables: result.droppedTables,
|
|
352
|
-
replacedViews: result.replacedViews,
|
|
353
|
-
droppedViews: result.droppedViews,
|
|
354
|
-
steps: serializeMigrationSteps(result.steps),
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
case "writeOpen":
|
|
358
|
-
return this.#trackOpeningHandle(this.#openWriteHandle());
|
|
359
|
-
case "snapshotOpen":
|
|
360
|
-
return this.#trackOpeningHandle(this.#openSnapshotHandle());
|
|
361
|
-
case "exportSnapshotOpen": {
|
|
362
|
-
const handleId = this.#claimGeneratedHandleId();
|
|
363
|
-
const iterator = this.database.exportSnapshotStream()[Symbol.asyncIterator]();
|
|
364
|
-
try {
|
|
365
|
-
this.#publishHandle(handleId, {
|
|
366
|
-
type: "snapshot-export",
|
|
367
|
-
iterator,
|
|
368
|
-
reading: false,
|
|
369
|
-
done: false,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
catch (error) {
|
|
373
|
-
await iterator.return(undefined);
|
|
374
|
-
this.#releaseHandleId(handleId);
|
|
375
|
-
throw error;
|
|
376
|
-
}
|
|
377
|
-
return { handleId };
|
|
378
|
-
}
|
|
379
|
-
case "importSnapshotOpen": {
|
|
380
|
-
// Named by the client, like the other event-producing handles, so its progress route
|
|
381
|
-
// exists before the first frame the load can emit.
|
|
382
|
-
const handleId = this.#claimHandleId(args[0]);
|
|
383
|
-
const queue = new SnapshotImportChunkQueue();
|
|
384
|
-
const abort = new AbortController();
|
|
385
|
-
const task = this.database.importSnapshotStream(queue, {
|
|
386
|
-
signal: abort.signal,
|
|
387
|
-
onProgress: (progress) => {
|
|
388
|
-
this.scope.postMessage(rpcEvent(handleId, "progress", progress));
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
void task.catch((error) => queue.fail(error));
|
|
392
|
-
try {
|
|
393
|
-
this.#publishHandle(handleId, { type: "snapshot-import", queue, task, abort });
|
|
394
|
-
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
abort.abort(error);
|
|
397
|
-
queue.fail(error);
|
|
398
|
-
await task.catch(() => undefined);
|
|
399
|
-
this.#releaseHandleId(handleId);
|
|
400
|
-
throw error;
|
|
401
|
-
}
|
|
402
|
-
return { handleId };
|
|
403
|
-
}
|
|
404
|
-
case "bufferedWriter": {
|
|
405
|
-
// Event-producing handles are named by the client so its event routes exist before the
|
|
406
|
-
// first frame the handle can emit.
|
|
407
|
-
const handleId = this.#claimHandleId(args[0]);
|
|
408
|
-
let writer;
|
|
409
|
-
try {
|
|
410
|
-
writer = this.database.bufferedWriter(args[1], {
|
|
411
|
-
...args[2],
|
|
412
|
-
onError: (error) => {
|
|
413
|
-
this.scope.postMessage(rpcEvent(handleId, "error", serializeError(error)));
|
|
414
|
-
},
|
|
415
|
-
});
|
|
416
|
-
this.#publishHandle(handleId, { type: "writer", writer });
|
|
417
|
-
}
|
|
418
|
-
catch (error) {
|
|
419
|
-
await writer?.close().catch(() => undefined);
|
|
420
|
-
this.#releaseHandleId(handleId);
|
|
421
|
-
throw error;
|
|
422
|
-
}
|
|
423
|
-
return { handleId };
|
|
424
|
-
}
|
|
425
|
-
case "liveQueries": {
|
|
426
|
-
const handleId = this.#claimHandleId(args[0]);
|
|
427
|
-
const { pollIntervalMs, channelName } = (args[1] ?? {});
|
|
428
|
-
// The database owns channelName resolution (and closes the channel with the set).
|
|
429
|
-
let set;
|
|
430
|
-
try {
|
|
431
|
-
set = this.database.liveQueries({
|
|
432
|
-
...(channelName === undefined ? {} : { channelName }),
|
|
433
|
-
...(pollIntervalMs === undefined ? {} : { pollIntervalMs }),
|
|
434
|
-
});
|
|
435
|
-
this.#publishHandle(handleId, { type: "live-set", set, subscriptionIds: new Set() });
|
|
436
|
-
}
|
|
437
|
-
catch (error) {
|
|
438
|
-
set?.close();
|
|
439
|
-
this.#releaseHandleId(handleId);
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
442
|
-
return { handleId };
|
|
443
|
-
}
|
|
444
|
-
case "dispose": {
|
|
445
|
-
await this.#dispose();
|
|
446
|
-
return { disposed: true };
|
|
447
|
-
}
|
|
448
|
-
case "setVisibility": {
|
|
449
|
-
this.options.onVisibility?.(args[0] === true);
|
|
450
|
-
return { acknowledged: true };
|
|
451
|
-
}
|
|
452
|
-
default:
|
|
453
|
-
throw new Error(`Unsupported database method: ${method}`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
async #openWriteHandle() {
|
|
457
|
-
// Same deferred-callback shape as snapshotOpen: the scoped write() stays pending until the
|
|
458
|
-
// client commits or aborts, so its staged transaction spans the RPC session.
|
|
459
|
-
const handleId = this.#claimGeneratedHandleId();
|
|
460
|
-
let sessionResolve;
|
|
461
|
-
let sessionReject;
|
|
462
|
-
const sessionReady = new Promise((resolveSession, rejectSession) => {
|
|
463
|
-
sessionResolve = resolveSession;
|
|
464
|
-
sessionReject = rejectSession;
|
|
465
|
-
});
|
|
466
|
-
let finish;
|
|
467
|
-
const done = this.database
|
|
468
|
-
.write(async (session) => {
|
|
469
|
-
sessionResolve(session);
|
|
470
|
-
await new Promise((resolveCommit, rejectAbort) => {
|
|
471
|
-
finish = (commit) => {
|
|
472
|
-
if (commit)
|
|
473
|
-
resolveCommit();
|
|
474
|
-
else
|
|
475
|
-
rejectAbort(new WriteScopeAbortedError());
|
|
476
|
-
};
|
|
477
|
-
});
|
|
478
|
-
})
|
|
479
|
-
.then((outcome) => ({ version: outcome.version }));
|
|
480
|
-
// beginDeferred can fail before the callback starts (for example at the durable reader-lease
|
|
481
|
-
// ceiling). Reject the open rendezvous too and observe `done` immediately.
|
|
482
|
-
void done.catch((error) => sessionReject(error));
|
|
483
|
-
try {
|
|
484
|
-
const session = await sessionReady;
|
|
485
|
-
if (finish === undefined)
|
|
486
|
-
throw new Error("Write scope did not install its settlement");
|
|
487
|
-
this.#publishHandle(handleId, {
|
|
488
|
-
type: "write",
|
|
489
|
-
session,
|
|
490
|
-
finish,
|
|
491
|
-
done,
|
|
492
|
-
activeCalls: 0,
|
|
493
|
-
activeCallDone: undefined,
|
|
494
|
-
finishActiveCall: undefined,
|
|
495
|
-
open: true,
|
|
496
|
-
idleTimer: undefined,
|
|
497
|
-
});
|
|
498
|
-
return { handleId };
|
|
499
|
-
}
|
|
500
|
-
catch (error) {
|
|
501
|
-
// Publication can lose a race with connection disposal. If the callback started, abort and
|
|
502
|
-
// join it before the opening task releases its lifecycle slot.
|
|
503
|
-
finish?.(false);
|
|
504
|
-
await done.catch(() => undefined);
|
|
505
|
-
this.#releaseHandleId(handleId);
|
|
506
|
-
throw error;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
async #openSnapshotHandle() {
|
|
510
|
-
// The scoped snapshot() holds its lease until the callback settles; the handle keeps the
|
|
511
|
-
// callback pending until close, so the pin spans the RPC session.
|
|
512
|
-
const handleId = this.#claimGeneratedHandleId();
|
|
513
|
-
let release;
|
|
514
|
-
let resolveVersion;
|
|
515
|
-
let rejectVersion;
|
|
516
|
-
const versionReady = new Promise((resolve, reject) => {
|
|
517
|
-
resolveVersion = resolve;
|
|
518
|
-
rejectVersion = reject;
|
|
519
|
-
});
|
|
520
|
-
const done = this.database.snapshot(async (session) => {
|
|
521
|
-
resolveVersion(session.version);
|
|
522
|
-
await new Promise((resolveRelease) => {
|
|
523
|
-
release = resolveRelease;
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
// Lease admission may fail before the callback installs `release`. Preserve that typed error
|
|
527
|
-
// through the rendezvous and keep the task observed from creation.
|
|
528
|
-
void done.catch((error) => rejectVersion(error));
|
|
529
|
-
try {
|
|
530
|
-
const version = await versionReady;
|
|
531
|
-
if (release === undefined)
|
|
532
|
-
throw new Error("Snapshot scope did not install its release");
|
|
533
|
-
this.#publishHandle(handleId, { type: "snapshot", release, done });
|
|
534
|
-
return { handleId, version };
|
|
535
|
-
}
|
|
536
|
-
catch (error) {
|
|
537
|
-
release?.();
|
|
538
|
-
await done.catch(() => undefined);
|
|
539
|
-
this.#releaseHandleId(handleId);
|
|
540
|
-
throw error;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
#trackOpeningHandle(opening) {
|
|
544
|
-
this.#openingHandlePromises.add(opening);
|
|
545
|
-
void opening.then(() => this.#openingHandlePromises.delete(opening), () => this.#openingHandlePromises.delete(opening));
|
|
546
|
-
return opening;
|
|
547
|
-
}
|
|
548
|
-
async #callHandle(handleId, method, args, context) {
|
|
549
|
-
const handle = this.#handles.get(handleId);
|
|
550
|
-
if (handle === undefined)
|
|
551
|
-
throw new Error(`Unknown handle: ${handleId}`);
|
|
552
|
-
if (handle.type === "write") {
|
|
553
|
-
this.#beginWriteHandleCall(handle);
|
|
554
|
-
try {
|
|
555
|
-
return await this.#callWriteHandle(handleId, handle, method, args, context);
|
|
556
|
-
}
|
|
557
|
-
finally {
|
|
558
|
-
this.#endWriteHandleCall(handleId, handle);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
switch (handle.type) {
|
|
562
|
-
case "snapshot": {
|
|
563
|
-
if (method !== "close")
|
|
564
|
-
throw new Error(`Unsupported snapshot method: ${method}`);
|
|
565
|
-
handle.release();
|
|
566
|
-
this.#handles.delete(handleId);
|
|
567
|
-
await handle.done;
|
|
568
|
-
return undefined;
|
|
569
|
-
}
|
|
570
|
-
case "snapshot-export":
|
|
571
|
-
return this.#callSnapshotExport(handleId, handle, method, args);
|
|
572
|
-
case "snapshot-import":
|
|
573
|
-
return this.#callSnapshotImport(handleId, handle, method, args);
|
|
574
|
-
case "writer":
|
|
575
|
-
return this.#callWriter(handleId, handle.writer, method, args);
|
|
576
|
-
case "query-cursor": {
|
|
577
|
-
if (method === "next") {
|
|
578
|
-
const next = await handle.iterator.next();
|
|
579
|
-
if (next.done) {
|
|
580
|
-
this.#handles.delete(handleId);
|
|
581
|
-
return new CursorBatchResult(undefined);
|
|
582
|
-
}
|
|
583
|
-
return new CursorBatchResult(encodeQueryResult(next.value));
|
|
584
|
-
}
|
|
585
|
-
if (method === "close") {
|
|
586
|
-
await handle.iterator.return?.();
|
|
587
|
-
this.#handles.delete(handleId);
|
|
588
|
-
return undefined;
|
|
589
|
-
}
|
|
590
|
-
throw new Error(`Unsupported query cursor method: ${method}`);
|
|
591
|
-
}
|
|
592
|
-
case "live-set":
|
|
593
|
-
return this.#callLiveSet(handleId, handle, method, args);
|
|
594
|
-
case "live-subscription": {
|
|
595
|
-
if (method !== "close")
|
|
596
|
-
throw new Error(`Unsupported subscription method: ${method}`);
|
|
597
|
-
handle.subscription.close();
|
|
598
|
-
this.#handles.delete(handleId);
|
|
599
|
-
const parent = this.#handles.get(handle.setId);
|
|
600
|
-
if (parent?.type === "live-set")
|
|
601
|
-
parent.subscriptionIds.delete(handleId);
|
|
602
|
-
return undefined;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
async #callWriteHandle(handleId, handle, method, args, context) {
|
|
607
|
-
if (method === "query") {
|
|
608
|
-
const [sql, options, reportStats = false] = args;
|
|
609
|
-
return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, {
|
|
610
|
-
...options,
|
|
611
|
-
signal: context.signal,
|
|
612
|
-
...(reportStats
|
|
613
|
-
? {
|
|
614
|
-
onStats: (stats) => {
|
|
615
|
-
this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
|
|
616
|
-
},
|
|
617
|
-
}
|
|
618
|
-
: {}),
|
|
619
|
-
})));
|
|
620
|
-
}
|
|
621
|
-
if (method === "execute") {
|
|
622
|
-
const [sql, params] = args;
|
|
623
|
-
return handle.session.execute(sql, params);
|
|
624
|
-
}
|
|
625
|
-
if (method === "stage") {
|
|
626
|
-
const [op, tableName, input] = args;
|
|
627
|
-
if (!isStageOp(op)) {
|
|
628
|
-
throw new Error(`Unsupported write stage operation: ${String(op)}`);
|
|
629
|
-
}
|
|
630
|
-
return handle.session[op](tableName, input);
|
|
631
|
-
}
|
|
632
|
-
if (method === "commit")
|
|
633
|
-
return this.#settleWriteHandle(handleId, handle, true);
|
|
634
|
-
if (method === "abort")
|
|
635
|
-
return this.#settleWriteHandle(handleId, handle, false);
|
|
636
|
-
throw new Error(`Unsupported write method: ${method}`);
|
|
637
|
-
}
|
|
638
|
-
/**
|
|
639
|
-
* Hands one slice of an encoded snapshot back. The slice is copied rather than viewed: a
|
|
640
|
-
* subarray shares the whole buffer, and structured clone copies the buffer behind a view, not
|
|
641
|
-
* the window into it — so every read would post the entire snapshot again.
|
|
642
|
-
*/
|
|
643
|
-
async #callSnapshotExport(handleId, handle, method, args) {
|
|
644
|
-
switch (method) {
|
|
645
|
-
case "read": {
|
|
646
|
-
if (args.length !== 0)
|
|
647
|
-
throw new TypeError("Snapshot stream read takes no arguments");
|
|
648
|
-
if (handle.reading)
|
|
649
|
-
throw new Error("Snapshot stream already has a read in flight");
|
|
650
|
-
if (handle.done)
|
|
651
|
-
return { done: true };
|
|
652
|
-
handle.reading = true;
|
|
653
|
-
try {
|
|
654
|
-
const next = await handle.iterator.next();
|
|
655
|
-
if (next.done === true) {
|
|
656
|
-
handle.done = true;
|
|
657
|
-
return { done: true };
|
|
658
|
-
}
|
|
659
|
-
// The iterator may yield a view into a 64 MiB block. Transfer only this bounded slice;
|
|
660
|
-
// detaching the larger backing buffer would corrupt subsequent slices.
|
|
661
|
-
const chunk = new Uint8Array(next.value.byteLength);
|
|
662
|
-
chunk.set(next.value);
|
|
663
|
-
return new SnapshotChunkResult(chunk);
|
|
664
|
-
}
|
|
665
|
-
finally {
|
|
666
|
-
handle.reading = false;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
case "close": {
|
|
670
|
-
await handle.iterator.return?.();
|
|
671
|
-
this.#handles.delete(handleId);
|
|
672
|
-
return undefined;
|
|
673
|
-
}
|
|
674
|
-
default:
|
|
675
|
-
throw new Error(`Unsupported snapshot export method: ${method}`);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
/** Collects an uploaded snapshot chunk by chunk, then loads it, reporting progress as events. */
|
|
679
|
-
async #callSnapshotImport(handleId, handle, method, args) {
|
|
680
|
-
switch (method) {
|
|
681
|
-
case "write": {
|
|
682
|
-
const chunk = args[0];
|
|
683
|
-
if (!(chunk instanceof Uint8Array)) {
|
|
684
|
-
throw new TypeError("Snapshot chunk must be a Uint8Array");
|
|
685
|
-
}
|
|
686
|
-
await handle.queue.push(chunk);
|
|
687
|
-
return undefined;
|
|
688
|
-
}
|
|
689
|
-
case "finish": {
|
|
690
|
-
try {
|
|
691
|
-
handle.queue.finish();
|
|
692
|
-
await handle.task;
|
|
693
|
-
}
|
|
694
|
-
finally {
|
|
695
|
-
this.#handles.delete(handleId);
|
|
696
|
-
}
|
|
697
|
-
return undefined;
|
|
698
|
-
}
|
|
699
|
-
case "close": {
|
|
700
|
-
handle.queue.fail(new Error("Snapshot import transfer was cancelled"));
|
|
701
|
-
await handle.task.catch(() => undefined);
|
|
702
|
-
this.#handles.delete(handleId);
|
|
703
|
-
return undefined;
|
|
704
|
-
}
|
|
705
|
-
case "cancel": {
|
|
706
|
-
handle.abort.abort(new Error("Snapshot import was cancelled"));
|
|
707
|
-
handle.queue.fail(handle.abort.signal.reason);
|
|
708
|
-
await handle.task.catch(() => undefined);
|
|
709
|
-
this.#handles.delete(handleId);
|
|
710
|
-
return undefined;
|
|
711
|
-
}
|
|
712
|
-
default:
|
|
713
|
-
throw new Error(`Unsupported snapshot import method: ${method}`);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
async #callWriter(handleId, writer, method, args) {
|
|
717
|
-
switch (method) {
|
|
718
|
-
case "add":
|
|
719
|
-
return writer.add(args[0]);
|
|
720
|
-
case "flush":
|
|
721
|
-
return writer.flush();
|
|
722
|
-
case "requestFlush":
|
|
723
|
-
writer.requestFlush();
|
|
724
|
-
return undefined;
|
|
725
|
-
case "discard":
|
|
726
|
-
return writer.discard();
|
|
727
|
-
case "stats":
|
|
728
|
-
return { pendingRowCount: writer.pendingRowCount, estimatedBytes: writer.estimatedBytes };
|
|
729
|
-
case "close": {
|
|
730
|
-
const result = await writer.close();
|
|
731
|
-
this.#handles.delete(handleId);
|
|
732
|
-
return result;
|
|
733
|
-
}
|
|
734
|
-
default:
|
|
735
|
-
throw new Error(`Unsupported buffered writer method: ${method}`);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
async #callLiveSet(handleId, handle, method, args) {
|
|
739
|
-
switch (method) {
|
|
740
|
-
case "subscribe": {
|
|
741
|
-
// The client names the subscription so it can route the initial change event, which is
|
|
742
|
-
// posted before this call's own result frame.
|
|
743
|
-
const subscriptionId = this.#claimHandleId(args[0]);
|
|
744
|
-
const query = args[1];
|
|
745
|
-
let subscription;
|
|
746
|
-
try {
|
|
747
|
-
subscription = await handle.set.subscribe(query, {
|
|
748
|
-
onChange: (result) => {
|
|
749
|
-
const encoded = encodeQueryResult(result);
|
|
750
|
-
this.scope.postMessage(rpcEvent(subscriptionId, "change", encoded.payload), {
|
|
751
|
-
transfer: encoded.transfer,
|
|
752
|
-
});
|
|
753
|
-
},
|
|
754
|
-
onError: (error) => {
|
|
755
|
-
this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
|
|
756
|
-
},
|
|
757
|
-
onComplete: () => {
|
|
758
|
-
this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
|
|
759
|
-
this.#handles.delete(subscriptionId);
|
|
760
|
-
const parent = this.#handles.get(handleId);
|
|
761
|
-
if (parent?.type === "live-set")
|
|
762
|
-
parent.subscriptionIds.delete(subscriptionId);
|
|
763
|
-
},
|
|
764
|
-
});
|
|
765
|
-
this.#publishHandle(subscriptionId, {
|
|
766
|
-
type: "live-subscription",
|
|
767
|
-
subscription,
|
|
768
|
-
setId: handleId,
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
catch (error) {
|
|
772
|
-
subscription?.close();
|
|
773
|
-
this.#releaseHandleId(subscriptionId);
|
|
774
|
-
throw error;
|
|
775
|
-
}
|
|
776
|
-
handle.subscriptionIds.add(subscriptionId);
|
|
777
|
-
return { dependencyTableIds: subscription.dependencyTableIds };
|
|
778
|
-
}
|
|
779
|
-
case "observe": {
|
|
780
|
-
const subscriptionId = this.#claimHandleId(args[0]);
|
|
781
|
-
const query = args[1];
|
|
782
|
-
let subscription;
|
|
783
|
-
try {
|
|
784
|
-
subscription = await handle.set.observe(query, {
|
|
785
|
-
onInvalidate: (invalidation) => {
|
|
786
|
-
this.scope.postMessage(rpcEvent(subscriptionId, "invalidate", invalidation));
|
|
787
|
-
},
|
|
788
|
-
onError: (error) => {
|
|
789
|
-
this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
|
|
790
|
-
},
|
|
791
|
-
onComplete: () => {
|
|
792
|
-
this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
|
|
793
|
-
this.#handles.delete(subscriptionId);
|
|
794
|
-
const parent = this.#handles.get(handleId);
|
|
795
|
-
if (parent?.type === "live-set")
|
|
796
|
-
parent.subscriptionIds.delete(subscriptionId);
|
|
797
|
-
},
|
|
798
|
-
});
|
|
799
|
-
this.#publishHandle(subscriptionId, {
|
|
800
|
-
type: "live-subscription",
|
|
801
|
-
subscription,
|
|
802
|
-
setId: handleId,
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
catch (error) {
|
|
806
|
-
subscription?.close();
|
|
807
|
-
this.#releaseHandleId(subscriptionId);
|
|
808
|
-
throw error;
|
|
809
|
-
}
|
|
810
|
-
handle.subscriptionIds.add(subscriptionId);
|
|
811
|
-
return { dependencyTableIds: subscription.dependencyTableIds };
|
|
812
|
-
}
|
|
813
|
-
case "stats":
|
|
814
|
-
return handle.set.stats;
|
|
815
|
-
case "notifyLocalCommit":
|
|
816
|
-
handle.set.notifyLocalCommit();
|
|
817
|
-
return undefined;
|
|
818
|
-
case "refresh":
|
|
819
|
-
return handle.set.refresh();
|
|
820
|
-
case "close":
|
|
821
|
-
this.#closeLiveSet(handleId, handle);
|
|
822
|
-
return undefined;
|
|
823
|
-
default:
|
|
824
|
-
throw new Error(`Unsupported live query method: ${method}`);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
#claimHandleId(value) {
|
|
828
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
829
|
-
throw new TypeError("Handle ID must be a non-empty string");
|
|
830
|
-
}
|
|
831
|
-
if (this.#handles.has(value) || this.#reservedHandleIds.has(value)) {
|
|
832
|
-
throw new Error(`Handle ID already exists: ${value}`);
|
|
833
|
-
}
|
|
834
|
-
if (this.#handles.size + this.#reservedHandleIds.size + this.#settlingWritePromises.size >=
|
|
835
|
-
MAX_WORKER_HANDLES_PER_CONNECTION) {
|
|
836
|
-
throw new Error(`Database connection cannot hold more than ${String(MAX_WORKER_HANDLES_PER_CONNECTION)} open handles (active or settling)`);
|
|
837
|
-
}
|
|
838
|
-
this.#reservedHandleIds.add(value);
|
|
839
|
-
return value;
|
|
840
|
-
}
|
|
841
|
-
#claimGeneratedHandleId() {
|
|
842
|
-
for (;;) {
|
|
843
|
-
const id = crypto.randomUUID();
|
|
844
|
-
if (!this.#handles.has(id) && !this.#reservedHandleIds.has(id))
|
|
845
|
-
return this.#claimHandleId(id);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
#publishHandle(id, handle) {
|
|
849
|
-
if (this.#disposed)
|
|
850
|
-
throw new Error("Database connection is disposed");
|
|
851
|
-
if (!this.#reservedHandleIds.delete(id))
|
|
852
|
-
throw new Error(`Handle ID was not reserved: ${id}`);
|
|
853
|
-
this.#handles.set(id, handle);
|
|
854
|
-
if (handle.type === "write")
|
|
855
|
-
this.#armWriteHandleIdleTimer(id, handle);
|
|
856
|
-
}
|
|
857
|
-
#releaseHandleId(id) {
|
|
858
|
-
this.#reservedHandleIds.delete(id);
|
|
859
|
-
}
|
|
860
|
-
#beginWriteHandleCall(handle) {
|
|
861
|
-
if (!handle.open)
|
|
862
|
-
throw new Error("Write handle is closed");
|
|
863
|
-
if (handle.activeCalls !== 0) {
|
|
864
|
-
throw new Error("Write handle already has a call in flight");
|
|
865
|
-
}
|
|
866
|
-
handle.activeCalls = 1;
|
|
867
|
-
handle.activeCallDone = new Promise((resolve) => {
|
|
868
|
-
handle.finishActiveCall = resolve;
|
|
869
|
-
});
|
|
870
|
-
if (handle.idleTimer !== undefined) {
|
|
871
|
-
clearTimeout(handle.idleTimer);
|
|
872
|
-
handle.idleTimer = undefined;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
#endWriteHandleCall(handleId, handle) {
|
|
876
|
-
handle.activeCalls -= 1;
|
|
877
|
-
if (handle.activeCalls < 0)
|
|
878
|
-
throw new Error("Write handle activity count underflow");
|
|
879
|
-
const finishActiveCall = handle.finishActiveCall;
|
|
880
|
-
handle.activeCallDone = undefined;
|
|
881
|
-
handle.finishActiveCall = undefined;
|
|
882
|
-
finishActiveCall?.();
|
|
883
|
-
if (handle.open && handle.activeCalls === 0)
|
|
884
|
-
this.#armWriteHandleIdleTimer(handleId, handle);
|
|
885
|
-
}
|
|
886
|
-
#armWriteHandleIdleTimer(handleId, handle) {
|
|
887
|
-
if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
|
|
888
|
-
return;
|
|
889
|
-
if (handle.idleTimer !== undefined)
|
|
890
|
-
clearTimeout(handle.idleTimer);
|
|
891
|
-
handle.idleTimer = setTimeout(() => {
|
|
892
|
-
handle.idleTimer = undefined;
|
|
893
|
-
if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
|
|
894
|
-
return;
|
|
895
|
-
// Delete first: a late commit or stage frame must fail rather than revive an expired scope
|
|
896
|
-
// while its asynchronous durable rollback is still joining.
|
|
897
|
-
void this.#settleWriteHandle(handleId, handle, false).catch(() => undefined);
|
|
898
|
-
}, this.#writeHandleIdleTimeoutMs);
|
|
899
|
-
unrefTimer(handle.idleTimer);
|
|
900
|
-
}
|
|
901
|
-
async #settleWriteHandle(handleId, handle, commit) {
|
|
902
|
-
if (!handle.open)
|
|
903
|
-
throw new Error("Write handle is closed");
|
|
904
|
-
handle.open = false;
|
|
905
|
-
if (handle.idleTimer !== undefined) {
|
|
906
|
-
clearTimeout(handle.idleTimer);
|
|
907
|
-
handle.idleTimer = undefined;
|
|
908
|
-
}
|
|
909
|
-
if (this.#handles.get(handleId) === handle)
|
|
910
|
-
this.#handles.delete(handleId);
|
|
911
|
-
handle.finish(commit);
|
|
912
|
-
const settlement = handle.done.catch((error) => {
|
|
913
|
-
if (!commit && error instanceof WriteScopeAbortedError)
|
|
914
|
-
return undefined;
|
|
915
|
-
throw error;
|
|
916
|
-
});
|
|
917
|
-
this.#settlingWritePromises.add(settlement);
|
|
918
|
-
void settlement.then(() => this.#settlingWritePromises.delete(settlement), () => this.#settlingWritePromises.delete(settlement));
|
|
919
|
-
return settlement;
|
|
920
|
-
}
|
|
921
|
-
#closeLiveSet(handleId, handle) {
|
|
922
|
-
handle.set.close();
|
|
923
|
-
for (const subscriptionId of handle.subscriptionIds)
|
|
924
|
-
this.#handles.delete(subscriptionId);
|
|
925
|
-
this.#handles.delete(handleId);
|
|
926
|
-
}
|
|
927
|
-
async #dispose() {
|
|
928
|
-
this.#disposed = true;
|
|
929
|
-
this.#reservedHandleIds.clear();
|
|
930
|
-
const handles = [...this.#handles.entries()];
|
|
931
|
-
this.#handles.clear();
|
|
932
|
-
const handleCleanup = [];
|
|
933
|
-
// Stop every abandoned-write deadline before waiting on active calls. The cleared handle map
|
|
934
|
-
// also prevents their completions from rearming one while disposal is in progress.
|
|
935
|
-
for (const [, handle] of handles) {
|
|
936
|
-
if (handle.type === "write" && handle.idleTimer !== undefined) {
|
|
937
|
-
clearTimeout(handle.idleTimer);
|
|
938
|
-
handle.idleTimer = undefined;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
// Signal every handle before awaiting any cleanup. Imports and iterators can have an admitted
|
|
942
|
-
// RPC blocked inside them; closing sequentially could wait on the first while never reaching
|
|
943
|
-
// the signal that lets a later one finish.
|
|
944
|
-
for (const [handleId, handle] of handles) {
|
|
945
|
-
try {
|
|
946
|
-
if (handle.type === "snapshot") {
|
|
947
|
-
handle.release();
|
|
948
|
-
handleCleanup.push(handle.done.catch(() => undefined));
|
|
949
|
-
}
|
|
950
|
-
else if (handle.type === "write") {
|
|
951
|
-
// A session operation owns the transaction revision until it returns. Aborting sooner
|
|
952
|
-
// can race its final journal update and leave an active owner until TTL. Once it exits,
|
|
953
|
-
// abort only if commit/abort/idle expiry has not already chosen a terminal outcome.
|
|
954
|
-
handleCleanup.push((async () => {
|
|
955
|
-
await handle.activeCallDone;
|
|
956
|
-
if (handle.open) {
|
|
957
|
-
await this.#settleWriteHandle(handleId, handle, false).catch(() => undefined);
|
|
958
|
-
}
|
|
959
|
-
})());
|
|
960
|
-
}
|
|
961
|
-
else if (handle.type === "writer")
|
|
962
|
-
handleCleanup.push(handle.writer.close());
|
|
963
|
-
else if (handle.type === "query-cursor") {
|
|
964
|
-
handleCleanup.push(Promise.resolve(handle.iterator.return?.()));
|
|
965
|
-
}
|
|
966
|
-
else if (handle.type === "snapshot-export") {
|
|
967
|
-
handleCleanup.push(Promise.resolve(handle.iterator.return?.()));
|
|
968
|
-
}
|
|
969
|
-
else if (handle.type === "snapshot-import") {
|
|
970
|
-
handle.abort.abort(new Error("Database connection was disposed"));
|
|
971
|
-
handle.queue.fail(new Error("Database connection was disposed"));
|
|
972
|
-
handleCleanup.push(handle.task.catch(() => undefined));
|
|
973
|
-
}
|
|
974
|
-
else if (handle.type === "live-set")
|
|
975
|
-
this.#closeLiveSet(handleId, handle);
|
|
976
|
-
}
|
|
977
|
-
catch {
|
|
978
|
-
// Dispose every handle even when one close fails; the client is already gone.
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
await Promise.allSettled(handleCleanup);
|
|
982
|
-
// Commit/abort/expiry removes its handle before durable settlement completes so late RPCs
|
|
983
|
-
// fail closed. Join those detached operations before closing the database or injected store.
|
|
984
|
-
while (this.#settlingWritePromises.size > 0) {
|
|
985
|
-
await Promise.allSettled([...this.#settlingWritePromises]);
|
|
986
|
-
}
|
|
987
|
-
// A handle whose lease admission was already in flight when disposal began is not yet in the
|
|
988
|
-
// handle map. Its opener observes the disposed publication, releases any admitted lease, and
|
|
989
|
-
// settles only after that cleanup is complete.
|
|
990
|
-
while (this.#openingHandlePromises.size > 0) {
|
|
991
|
-
await Promise.allSettled([...this.#openingHandlePromises]);
|
|
992
|
-
}
|
|
993
|
-
await this.#waitForInFlightRpcs();
|
|
994
|
-
await this.database.close();
|
|
995
|
-
await this.options.onDispose?.();
|
|
996
|
-
}
|
|
997
|
-
#waitForInFlightRpcs() {
|
|
998
|
-
if (this.#inFlightRpcCount === 0)
|
|
999
|
-
return Promise.resolve();
|
|
1000
|
-
this.#inFlightRpcDrain ??= new Promise((resolve) => {
|
|
1001
|
-
this.#resolveInFlightRpcDrain = resolve;
|
|
1002
|
-
});
|
|
1003
|
-
return this.#inFlightRpcDrain;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
/**
|
|
1007
|
-
* Answers client RPC frames against a database you constructed yourself. Use this in a custom
|
|
1008
|
-
* worker entry when you need non-cloneable construction options (a custom store, `now`,
|
|
1009
|
-
* `createId`) — the client's init frame is acknowledged without reconstructing anything.
|
|
1010
|
-
*/
|
|
1011
|
-
export function exposeDatabase(database, scope, options = {}) {
|
|
1012
|
-
const server = new DatabaseRpcServer(database, scope, options);
|
|
1013
|
-
scope.addEventListener("message", (event) => {
|
|
1014
|
-
let request;
|
|
1015
|
-
try {
|
|
1016
|
-
request = parseRpcRequest(event.data);
|
|
1017
|
-
}
|
|
1018
|
-
catch (error) {
|
|
1019
|
-
const requestId = requestIdOf(event.data);
|
|
1020
|
-
if (requestId !== undefined)
|
|
1021
|
-
scope.postMessage(rpcFailure(requestId, error));
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
if (request !== null)
|
|
1025
|
-
void server.handle(request);
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Full worker-side wiring: waits for the client's init frame, builds the store it describes,
|
|
1030
|
-
* constructs the database, and exposes it. The store is closed when the client disposes.
|
|
1031
|
-
*/
|
|
1032
|
-
export function attachDatabaseWorker(scope, options = {}) {
|
|
1033
|
-
let initialized;
|
|
1034
|
-
let initFailure;
|
|
1035
|
-
scope.addEventListener("message", (event) => {
|
|
1036
|
-
let request;
|
|
1037
|
-
try {
|
|
1038
|
-
request = parseRpcRequest(event.data);
|
|
1039
|
-
}
|
|
1040
|
-
catch (error) {
|
|
1041
|
-
const requestId = requestIdOf(event.data);
|
|
1042
|
-
if (requestId !== undefined)
|
|
1043
|
-
scope.postMessage(rpcFailure(requestId, error));
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
if (request === null)
|
|
1047
|
-
return;
|
|
1048
|
-
if (request.kind === "rpc-init" && initialized === undefined) {
|
|
1049
|
-
initFailure = undefined;
|
|
1050
|
-
const attempt = createServer(scope, request.payload, options);
|
|
1051
|
-
initialized = attempt;
|
|
1052
|
-
attempt.catch((error) => {
|
|
1053
|
-
// Leave the worker reusable: a failed store open (quota, blocked upgrade) may be
|
|
1054
|
-
// retried. Remember why it failed — the client pipelines calls behind init, so the
|
|
1055
|
-
// ones that land after this reset must carry the real reason, not a generic refusal.
|
|
1056
|
-
if (initialized === attempt) {
|
|
1057
|
-
initialized = undefined;
|
|
1058
|
-
initFailure = error;
|
|
1059
|
-
}
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
const pending = initialized;
|
|
1063
|
-
if (pending === undefined) {
|
|
1064
|
-
const failure = initFailure === undefined
|
|
1065
|
-
? new Error("Database is not initialized: send init first")
|
|
1066
|
-
: new Error(`Database initialization failed: ${initFailure instanceof Error
|
|
1067
|
-
? `${initFailure.name}: ${initFailure.message}`
|
|
1068
|
-
: typeof initFailure === "string"
|
|
1069
|
-
? initFailure
|
|
1070
|
-
: "unknown reason"}`, { cause: initFailure });
|
|
1071
|
-
scope.postMessage(rpcFailure(request.requestId, failure));
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
void pending
|
|
1075
|
-
.then((server) => server.handle(request))
|
|
1076
|
-
.catch((error) => {
|
|
1077
|
-
scope.postMessage(rpcFailure(request.requestId, error));
|
|
1078
|
-
});
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
async function createServer(scope, payload, options) {
|
|
1082
|
-
const store = await createStore(payload.store, options);
|
|
1083
|
-
const database = new MinnowDatabase(store, payload.options ?? {});
|
|
1084
|
-
return new DatabaseRpcServer(database, scope, {
|
|
1085
|
-
...(payload.options?.transactionIdleTimeoutMs === undefined
|
|
1086
|
-
? {}
|
|
1087
|
-
: { writeHandleIdleTimeoutMs: payload.options.transactionIdleTimeoutMs }),
|
|
1088
|
-
onDispose: () => store.close(),
|
|
1089
|
-
onVisibility: (visible) => {
|
|
1090
|
-
store.setForeground?.(visible);
|
|
1091
|
-
},
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* The composition root: the one place adapters are named. Each loads only when its descriptor
|
|
1096
|
-
* asks for it — a bundler splits the adapters into their own chunks, so an application's worker
|
|
1097
|
-
* downloads the store it opens and none of the others.
|
|
1098
|
-
*/
|
|
1099
6
|
async function createStore(descriptor, options) {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
const { IndexedDbBlockStore } = await import("../storage/indexeddb.js");
|
|
1112
|
-
return IndexedDbBlockStore.open({
|
|
1113
|
-
name: descriptor.name,
|
|
1114
|
-
...(descriptor.durability === undefined ? {} : { durability: descriptor.durability }),
|
|
1115
|
-
...(descriptor.uniqueKeyCacheBytes === undefined
|
|
1116
|
-
? {}
|
|
1117
|
-
: { uniqueKeyCacheBytes: descriptor.uniqueKeyCacheBytes }),
|
|
1118
|
-
...(options.indexedDB === undefined ? {} : { indexedDB: options.indexedDB }),
|
|
7
|
+
if (descriptor.kind === "memory") {
|
|
8
|
+
const { MemoryBlockStore } = await import("../storage/memory.js");
|
|
9
|
+
return new MemoryBlockStore();
|
|
10
|
+
}
|
|
11
|
+
if (descriptor.kind === "opfs") {
|
|
12
|
+
const { OpfsBlockStore } = await import("../storage/opfs/index.js");
|
|
13
|
+
return OpfsBlockStore.open({
|
|
14
|
+
name: descriptor.name,
|
|
15
|
+
...descriptor.durability === void 0 ? {} : { durability: descriptor.durability }
|
|
1119
16
|
});
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
17
|
+
}
|
|
18
|
+
const { IndexedDbBlockStore } = await import("../storage/indexeddb.js");
|
|
19
|
+
return IndexedDbBlockStore.open({
|
|
20
|
+
name: descriptor.name,
|
|
21
|
+
...descriptor.durability === void 0 ? {} : { durability: descriptor.durability },
|
|
22
|
+
...descriptor.uniqueKeyCacheBytes === void 0 ? {} : { uniqueKeyCacheBytes: descriptor.uniqueKeyCacheBytes },
|
|
23
|
+
...options.indexedDB === void 0 ? {} : { indexedDB: options.indexedDB }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
MAX_WORKER_HANDLES_PER_CONNECTION,
|
|
28
|
+
attachDatabaseWorker,
|
|
29
|
+
exposeDatabase,
|
|
30
|
+
singleStoreFactory
|
|
31
|
+
};
|