@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
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
import { MAX_SNAPSHOT_STREAM_CHUNK_BYTES } from "../storage/snapshot.js";
|
|
2
|
+
import { parseRpcRequest, MAX_DATABASE_RPC_IN_FLIGHT, rpcEvent, rpcFailure, rpcResult, serializeError } from "../worker-protocol/index.js";
|
|
3
|
+
import { MinnowDatabase } from "./database.js";
|
|
4
|
+
import { encodeQueryResult, encodeQueryRows } from "./result-wire.js";
|
|
5
|
+
import { deserializeSchema, serializeMigrationSteps } from "./schema-wire.js";
|
|
6
|
+
const stageOps = ["insertBatch", "upsertBatch", "updateBatch", "deleteBatch"];
|
|
7
|
+
function isStageOp(value) {
|
|
8
|
+
return typeof value === "string" && stageOps.includes(value);
|
|
9
|
+
}
|
|
10
|
+
const directRootMethods = [
|
|
11
|
+
"createTable",
|
|
12
|
+
"createView",
|
|
13
|
+
"dropView",
|
|
14
|
+
"dropColumn",
|
|
15
|
+
"dropTable",
|
|
16
|
+
"createIndex",
|
|
17
|
+
"dropIndex",
|
|
18
|
+
"buildFtsIndex",
|
|
19
|
+
"introspect",
|
|
20
|
+
"listTables",
|
|
21
|
+
"insertBatch",
|
|
22
|
+
"insert",
|
|
23
|
+
"upsertBatch",
|
|
24
|
+
"upsert",
|
|
25
|
+
"updateBatch",
|
|
26
|
+
"update",
|
|
27
|
+
"deleteBatch",
|
|
28
|
+
"delete",
|
|
29
|
+
"runStatement",
|
|
30
|
+
"explain",
|
|
31
|
+
"listVisibleSegmentPage",
|
|
32
|
+
"cleanupQuerySpill",
|
|
33
|
+
"compactTable",
|
|
34
|
+
"compactTableStep",
|
|
35
|
+
"resumeCompactionJob",
|
|
36
|
+
"listCompactionJobs",
|
|
37
|
+
"cancelCompactionJob",
|
|
38
|
+
"collectGarbage",
|
|
39
|
+
"collectGarbageStep",
|
|
40
|
+
"resumeGarbageCollectionJob",
|
|
41
|
+
"bufferPoolStats",
|
|
42
|
+
"maintenanceStatus",
|
|
43
|
+
"checkIntegrity",
|
|
44
|
+
"storageStats",
|
|
45
|
+
"inspectInterruptedImport",
|
|
46
|
+
"abortInterruptedImport",
|
|
47
|
+
"listGarbageCollectionJobs"
|
|
48
|
+
];
|
|
49
|
+
function isDirectRootMethod(value) {
|
|
50
|
+
return directRootMethods.includes(value);
|
|
51
|
+
}
|
|
52
|
+
class ColumnarResult {
|
|
53
|
+
encoded;
|
|
54
|
+
constructor(encoded) {
|
|
55
|
+
this.encoded = encoded;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
class CursorBatchResult {
|
|
59
|
+
encoded;
|
|
60
|
+
constructor(encoded) {
|
|
61
|
+
this.encoded = encoded;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
class SnapshotChunkResult {
|
|
65
|
+
chunk;
|
|
66
|
+
constructor(chunk) {
|
|
67
|
+
this.chunk = chunk;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
class WriteScopeAbortedError extends Error {
|
|
71
|
+
name = "WriteScopeAbortedError";
|
|
72
|
+
constructor() {
|
|
73
|
+
super("Write scope aborted");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
class SnapshotImportChunkQueue {
|
|
77
|
+
#queued;
|
|
78
|
+
#consumed;
|
|
79
|
+
#reader;
|
|
80
|
+
#finished = false;
|
|
81
|
+
#failure;
|
|
82
|
+
[Symbol.asyncIterator]() {
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
push(chunk) {
|
|
86
|
+
if (chunk.byteLength > MAX_SNAPSHOT_STREAM_CHUNK_BYTES) {
|
|
87
|
+
throw new RangeError("Snapshot transfer chunk exceeds the 1 MiB limit");
|
|
88
|
+
}
|
|
89
|
+
if (this.#failure !== void 0)
|
|
90
|
+
return Promise.reject(snapshotQueueError(this.#failure));
|
|
91
|
+
if (this.#finished)
|
|
92
|
+
return Promise.reject(new Error("Snapshot import transfer is closed"));
|
|
93
|
+
if (this.#queued !== void 0) {
|
|
94
|
+
return Promise.reject(new Error("Snapshot import transfer already has a queued chunk"));
|
|
95
|
+
}
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const reader = this.#reader;
|
|
98
|
+
if (reader === void 0) {
|
|
99
|
+
this.#queued = { chunk, resolve, reject };
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.#reader = void 0;
|
|
103
|
+
this.#consumed = { resolve, reject };
|
|
104
|
+
reader.resolve({ done: false, value: chunk });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async next() {
|
|
108
|
+
this.#consumed?.resolve();
|
|
109
|
+
this.#consumed = void 0;
|
|
110
|
+
if (this.#failure !== void 0)
|
|
111
|
+
throw snapshotQueueError(this.#failure);
|
|
112
|
+
const queued = this.#queued;
|
|
113
|
+
if (queued !== void 0) {
|
|
114
|
+
this.#queued = void 0;
|
|
115
|
+
this.#consumed = { resolve: queued.resolve, reject: queued.reject };
|
|
116
|
+
return { done: false, value: queued.chunk };
|
|
117
|
+
}
|
|
118
|
+
if (this.#finished)
|
|
119
|
+
return { done: true, value: void 0 };
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
this.#reader = { resolve, reject };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async return() {
|
|
125
|
+
this.finish();
|
|
126
|
+
return { done: true, value: void 0 };
|
|
127
|
+
}
|
|
128
|
+
finish() {
|
|
129
|
+
if (this.#finished)
|
|
130
|
+
return;
|
|
131
|
+
this.#finished = true;
|
|
132
|
+
if (this.#queued === void 0) {
|
|
133
|
+
const reader = this.#reader;
|
|
134
|
+
this.#reader = void 0;
|
|
135
|
+
reader?.resolve({ done: true, value: void 0 });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
fail(error) {
|
|
139
|
+
if (this.#failure !== void 0)
|
|
140
|
+
return;
|
|
141
|
+
this.#failure = error;
|
|
142
|
+
this.#finished = true;
|
|
143
|
+
this.#queued?.reject(error);
|
|
144
|
+
this.#queued = void 0;
|
|
145
|
+
this.#consumed?.reject(error);
|
|
146
|
+
this.#consumed = void 0;
|
|
147
|
+
this.#reader?.reject(error);
|
|
148
|
+
this.#reader = void 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function snapshotQueueError(value) {
|
|
152
|
+
return value instanceof Error ? value : new Error("Snapshot transfer failed", { cause: value });
|
|
153
|
+
}
|
|
154
|
+
function abortError(message) {
|
|
155
|
+
const error = new Error(message);
|
|
156
|
+
error.name = "AbortError";
|
|
157
|
+
return error;
|
|
158
|
+
}
|
|
159
|
+
const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
|
|
160
|
+
const DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS = 3e4;
|
|
161
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
162
|
+
const passiveRpcSignal = new AbortController().signal;
|
|
163
|
+
class DatabaseRpcServer {
|
|
164
|
+
database;
|
|
165
|
+
scope;
|
|
166
|
+
options;
|
|
167
|
+
#handles = /* @__PURE__ */ new Map();
|
|
168
|
+
#reservedHandleIds = /* @__PURE__ */ new Set();
|
|
169
|
+
#openingHandlePromises = /* @__PURE__ */ new Set();
|
|
170
|
+
#settlingWritePromises = /* @__PURE__ */ new Set();
|
|
171
|
+
#requestAborts = /* @__PURE__ */ new Map();
|
|
172
|
+
#disposed = false;
|
|
173
|
+
#inFlightRpcCount = 0;
|
|
174
|
+
#inFlightRpcDrain;
|
|
175
|
+
#resolveInFlightRpcDrain;
|
|
176
|
+
#writeHandleIdleTimeoutMs;
|
|
177
|
+
constructor(database, scope, options) {
|
|
178
|
+
this.database = database;
|
|
179
|
+
this.scope = scope;
|
|
180
|
+
this.options = options;
|
|
181
|
+
this.#writeHandleIdleTimeoutMs = options.writeHandleIdleTimeoutMs ?? DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS;
|
|
182
|
+
if (!Number.isSafeInteger(this.#writeHandleIdleTimeoutMs) || this.#writeHandleIdleTimeoutMs <= 0 || this.#writeHandleIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
|
|
183
|
+
throw new RangeError(`Worker write-handle idle timeout must be a positive whole number no greater than ${String(MAX_TIMER_DELAY_MS)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async handle(request) {
|
|
187
|
+
if (request.kind === "rpc-init") {
|
|
188
|
+
this.scope.postMessage(rpcResult(request.requestId, { ready: true }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (request.kind === "rpc-cancel") {
|
|
192
|
+
this.#requestAborts.get(request.requestId)?.abort(abortError("Database request was cancelled"));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const bypassLimit = request.method === "dispose";
|
|
196
|
+
if (!bypassLimit && this.#inFlightRpcCount >= MAX_DATABASE_RPC_IN_FLIGHT) {
|
|
197
|
+
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`)));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!bypassLimit)
|
|
201
|
+
this.#inFlightRpcCount += 1;
|
|
202
|
+
const abort = request.method === "query" || request.method === "execute" ? new AbortController() : void 0;
|
|
203
|
+
if (abort !== void 0)
|
|
204
|
+
this.#requestAborts.set(request.requestId, abort);
|
|
205
|
+
const context = {
|
|
206
|
+
requestId: request.requestId,
|
|
207
|
+
signal: abort?.signal ?? passiveRpcSignal
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
if (this.#disposed)
|
|
211
|
+
throw new Error("Database connection is disposed");
|
|
212
|
+
const result = request.handleId === null ? await this.#callRoot(request.method, request.args, context) : await this.#callHandle(request.handleId, request.method, request.args, context);
|
|
213
|
+
if (result instanceof ColumnarResult) {
|
|
214
|
+
this.scope.postMessage(rpcResult(request.requestId, result.encoded.payload), {
|
|
215
|
+
transfer: result.encoded.transfer
|
|
216
|
+
});
|
|
217
|
+
} else if (result instanceof CursorBatchResult) {
|
|
218
|
+
this.scope.postMessage(rpcResult(request.requestId, result.encoded === void 0 ? { done: true } : { done: false, batch: result.encoded.payload }), result.encoded === void 0 ? void 0 : { transfer: result.encoded.transfer });
|
|
219
|
+
} else if (result instanceof SnapshotChunkResult) {
|
|
220
|
+
this.scope.postMessage(rpcResult(request.requestId, { done: false, chunk: result.chunk }), {
|
|
221
|
+
transfer: [result.chunk.buffer]
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
this.scope.postMessage(rpcResult(request.requestId, result));
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.scope.postMessage(rpcFailure(request.requestId, error));
|
|
228
|
+
} finally {
|
|
229
|
+
if (abort !== void 0)
|
|
230
|
+
this.#requestAborts.delete(request.requestId);
|
|
231
|
+
if (!bypassLimit)
|
|
232
|
+
this.#inFlightRpcCount -= 1;
|
|
233
|
+
if (this.#inFlightRpcCount === 0 && this.#resolveInFlightRpcDrain !== void 0) {
|
|
234
|
+
const resolve = this.#resolveInFlightRpcDrain;
|
|
235
|
+
this.#inFlightRpcDrain = void 0;
|
|
236
|
+
this.#resolveInFlightRpcDrain = void 0;
|
|
237
|
+
resolve();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async #callRoot(method, args, context) {
|
|
242
|
+
const database = this.database;
|
|
243
|
+
if (isDirectRootMethod(method)) {
|
|
244
|
+
const operation = database[method];
|
|
245
|
+
if (operation === void 0) {
|
|
246
|
+
throw new Error(`Database root method is unavailable: ${method}`);
|
|
247
|
+
}
|
|
248
|
+
return operation.apply(this.database, args);
|
|
249
|
+
}
|
|
250
|
+
switch (method) {
|
|
251
|
+
case "query": {
|
|
252
|
+
const [sql, options, reportStats = false] = args;
|
|
253
|
+
return new ColumnarResult(encodeQueryResult(await this.database.query(sql, {
|
|
254
|
+
...options,
|
|
255
|
+
signal: context.signal,
|
|
256
|
+
...reportStats ? {
|
|
257
|
+
onStats: (stats) => {
|
|
258
|
+
this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
|
|
259
|
+
}
|
|
260
|
+
} : {}
|
|
261
|
+
})));
|
|
262
|
+
}
|
|
263
|
+
case "execute": {
|
|
264
|
+
const [sql, params, options, reportStats = false] = args;
|
|
265
|
+
return this.database.execute(sql, params, {
|
|
266
|
+
...options,
|
|
267
|
+
signal: context.signal,
|
|
268
|
+
...reportStats ? {
|
|
269
|
+
onStats: (stats) => {
|
|
270
|
+
this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
|
|
271
|
+
}
|
|
272
|
+
} : {}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
case "queryCursorOpen": {
|
|
276
|
+
const handleId = this.#claimHandleId(args[0]);
|
|
277
|
+
const [sql, options, reportStats = false] = args.slice(1);
|
|
278
|
+
try {
|
|
279
|
+
const iterator = this.database.queryCursor(sql, {
|
|
280
|
+
...options,
|
|
281
|
+
...reportStats ? {
|
|
282
|
+
onStats: (stats) => {
|
|
283
|
+
this.scope.postMessage(rpcEvent(handleId, "stats", stats));
|
|
284
|
+
}
|
|
285
|
+
} : {}
|
|
286
|
+
});
|
|
287
|
+
this.#publishHandle(handleId, { type: "query-cursor", iterator });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
this.#releaseHandleId(handleId);
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
return { handleId };
|
|
293
|
+
}
|
|
294
|
+
case "run": {
|
|
295
|
+
const query = args[0];
|
|
296
|
+
return new ColumnarResult(encodeQueryRows(await this.database.run(query)));
|
|
297
|
+
}
|
|
298
|
+
case "readTable": {
|
|
299
|
+
const [tableName, versionOrOptions] = args;
|
|
300
|
+
const rows = await this.database.readTable(tableName, versionOrOptions);
|
|
301
|
+
return new ColumnarResult(encodeQueryRows(rows));
|
|
302
|
+
}
|
|
303
|
+
case "migrate": {
|
|
304
|
+
const result = await this.database.migrate(deserializeSchema(args[0]), args[1] ?? {});
|
|
305
|
+
return {
|
|
306
|
+
createdTables: result.createdTables,
|
|
307
|
+
alteredTables: result.alteredTables,
|
|
308
|
+
droppedTables: result.droppedTables,
|
|
309
|
+
replacedViews: result.replacedViews,
|
|
310
|
+
droppedViews: result.droppedViews,
|
|
311
|
+
steps: serializeMigrationSteps(result.steps)
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
case "writeOpen":
|
|
315
|
+
return this.#trackOpeningHandle(this.#openWriteHandle());
|
|
316
|
+
case "snapshotOpen":
|
|
317
|
+
return this.#trackOpeningHandle(this.#openSnapshotHandle());
|
|
318
|
+
case "exportSnapshotOpen": {
|
|
319
|
+
const handleId = this.#claimGeneratedHandleId();
|
|
320
|
+
const iterator = this.database.exportSnapshotStream()[Symbol.asyncIterator]();
|
|
321
|
+
try {
|
|
322
|
+
this.#publishHandle(handleId, {
|
|
323
|
+
type: "snapshot-export",
|
|
324
|
+
iterator,
|
|
325
|
+
reading: false,
|
|
326
|
+
done: false
|
|
327
|
+
});
|
|
328
|
+
} catch (error) {
|
|
329
|
+
await iterator.return(void 0);
|
|
330
|
+
this.#releaseHandleId(handleId);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
return { handleId };
|
|
334
|
+
}
|
|
335
|
+
case "importSnapshotOpen": {
|
|
336
|
+
const handleId = this.#claimHandleId(args[0]);
|
|
337
|
+
const queue = new SnapshotImportChunkQueue();
|
|
338
|
+
const abort = new AbortController();
|
|
339
|
+
const task = this.database.importSnapshotStream(queue, {
|
|
340
|
+
signal: abort.signal,
|
|
341
|
+
onProgress: (progress) => {
|
|
342
|
+
this.scope.postMessage(rpcEvent(handleId, "progress", progress));
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
void task.catch((error) => queue.fail(error));
|
|
346
|
+
try {
|
|
347
|
+
this.#publishHandle(handleId, { type: "snapshot-import", queue, task, abort });
|
|
348
|
+
} catch (error) {
|
|
349
|
+
abort.abort(error);
|
|
350
|
+
queue.fail(error);
|
|
351
|
+
await task.catch(() => void 0);
|
|
352
|
+
this.#releaseHandleId(handleId);
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
return { handleId };
|
|
356
|
+
}
|
|
357
|
+
case "bufferedWriter": {
|
|
358
|
+
const handleId = this.#claimHandleId(args[0]);
|
|
359
|
+
let writer;
|
|
360
|
+
try {
|
|
361
|
+
writer = this.database.bufferedWriter(args[1], {
|
|
362
|
+
...args[2],
|
|
363
|
+
onError: (error) => {
|
|
364
|
+
this.scope.postMessage(rpcEvent(handleId, "error", serializeError(error)));
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
this.#publishHandle(handleId, { type: "writer", writer });
|
|
368
|
+
} catch (error) {
|
|
369
|
+
await writer?.close().catch(() => void 0);
|
|
370
|
+
this.#releaseHandleId(handleId);
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
return { handleId };
|
|
374
|
+
}
|
|
375
|
+
case "liveQueries": {
|
|
376
|
+
const handleId = this.#claimHandleId(args[0]);
|
|
377
|
+
const { pollIntervalMs, channelName } = args[1] ?? {};
|
|
378
|
+
let set;
|
|
379
|
+
try {
|
|
380
|
+
set = this.database.liveQueries({
|
|
381
|
+
...channelName === void 0 ? {} : { channelName },
|
|
382
|
+
...pollIntervalMs === void 0 ? {} : { pollIntervalMs }
|
|
383
|
+
});
|
|
384
|
+
this.#publishHandle(handleId, { type: "live-set", set, subscriptionIds: /* @__PURE__ */ new Set() });
|
|
385
|
+
} catch (error) {
|
|
386
|
+
set?.close();
|
|
387
|
+
this.#releaseHandleId(handleId);
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
return { handleId };
|
|
391
|
+
}
|
|
392
|
+
case "dispose": {
|
|
393
|
+
await this.#dispose();
|
|
394
|
+
return { disposed: true };
|
|
395
|
+
}
|
|
396
|
+
case "setVisibility": {
|
|
397
|
+
this.options.onVisibility?.(args[0] === true);
|
|
398
|
+
return { acknowledged: true };
|
|
399
|
+
}
|
|
400
|
+
default:
|
|
401
|
+
throw new Error(`Unsupported database method: ${method}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async #openWriteHandle() {
|
|
405
|
+
const handleId = this.#claimGeneratedHandleId();
|
|
406
|
+
let sessionResolve;
|
|
407
|
+
let sessionReject;
|
|
408
|
+
const sessionReady = new Promise((resolveSession, rejectSession) => {
|
|
409
|
+
sessionResolve = resolveSession;
|
|
410
|
+
sessionReject = rejectSession;
|
|
411
|
+
});
|
|
412
|
+
let finish;
|
|
413
|
+
const done = this.database.write(async (session) => {
|
|
414
|
+
sessionResolve(session);
|
|
415
|
+
await new Promise((resolveCommit, rejectAbort) => {
|
|
416
|
+
finish = (commit) => {
|
|
417
|
+
if (commit)
|
|
418
|
+
resolveCommit();
|
|
419
|
+
else
|
|
420
|
+
rejectAbort(new WriteScopeAbortedError());
|
|
421
|
+
};
|
|
422
|
+
});
|
|
423
|
+
}).then((outcome) => ({ version: outcome.version }));
|
|
424
|
+
void done.catch((error) => sessionReject(error));
|
|
425
|
+
try {
|
|
426
|
+
const session = await sessionReady;
|
|
427
|
+
if (finish === void 0)
|
|
428
|
+
throw new Error("Write scope did not install its settlement");
|
|
429
|
+
this.#publishHandle(handleId, {
|
|
430
|
+
type: "write",
|
|
431
|
+
session,
|
|
432
|
+
finish,
|
|
433
|
+
done,
|
|
434
|
+
activeCalls: 0,
|
|
435
|
+
activeCallDone: void 0,
|
|
436
|
+
finishActiveCall: void 0,
|
|
437
|
+
open: true,
|
|
438
|
+
idleTimer: void 0
|
|
439
|
+
});
|
|
440
|
+
return { handleId };
|
|
441
|
+
} catch (error) {
|
|
442
|
+
finish?.(false);
|
|
443
|
+
await done.catch(() => void 0);
|
|
444
|
+
this.#releaseHandleId(handleId);
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async #openSnapshotHandle() {
|
|
449
|
+
const handleId = this.#claimGeneratedHandleId();
|
|
450
|
+
let release;
|
|
451
|
+
let resolveVersion;
|
|
452
|
+
let rejectVersion;
|
|
453
|
+
const versionReady = new Promise((resolve, reject) => {
|
|
454
|
+
resolveVersion = resolve;
|
|
455
|
+
rejectVersion = reject;
|
|
456
|
+
});
|
|
457
|
+
const done = this.database.snapshot(async (session) => {
|
|
458
|
+
resolveVersion(session.version);
|
|
459
|
+
await new Promise((resolveRelease) => {
|
|
460
|
+
release = resolveRelease;
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
void done.catch((error) => rejectVersion(error));
|
|
464
|
+
try {
|
|
465
|
+
const version = await versionReady;
|
|
466
|
+
if (release === void 0)
|
|
467
|
+
throw new Error("Snapshot scope did not install its release");
|
|
468
|
+
this.#publishHandle(handleId, { type: "snapshot", release, done });
|
|
469
|
+
return { handleId, version };
|
|
470
|
+
} catch (error) {
|
|
471
|
+
release?.();
|
|
472
|
+
await done.catch(() => void 0);
|
|
473
|
+
this.#releaseHandleId(handleId);
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
#trackOpeningHandle(opening) {
|
|
478
|
+
this.#openingHandlePromises.add(opening);
|
|
479
|
+
void opening.then(() => this.#openingHandlePromises.delete(opening), () => this.#openingHandlePromises.delete(opening));
|
|
480
|
+
return opening;
|
|
481
|
+
}
|
|
482
|
+
async #callHandle(handleId, method, args, context) {
|
|
483
|
+
const handle = this.#handles.get(handleId);
|
|
484
|
+
if (handle === void 0)
|
|
485
|
+
throw new Error(`Unknown handle: ${handleId}`);
|
|
486
|
+
if (handle.type === "write") {
|
|
487
|
+
this.#beginWriteHandleCall(handle);
|
|
488
|
+
try {
|
|
489
|
+
return await this.#callWriteHandle(handleId, handle, method, args, context);
|
|
490
|
+
} finally {
|
|
491
|
+
this.#endWriteHandleCall(handleId, handle);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
switch (handle.type) {
|
|
495
|
+
case "snapshot": {
|
|
496
|
+
if (method !== "close")
|
|
497
|
+
throw new Error(`Unsupported snapshot method: ${method}`);
|
|
498
|
+
handle.release();
|
|
499
|
+
this.#handles.delete(handleId);
|
|
500
|
+
await handle.done;
|
|
501
|
+
return void 0;
|
|
502
|
+
}
|
|
503
|
+
case "snapshot-export":
|
|
504
|
+
return this.#callSnapshotExport(handleId, handle, method, args);
|
|
505
|
+
case "snapshot-import":
|
|
506
|
+
return this.#callSnapshotImport(handleId, handle, method, args);
|
|
507
|
+
case "writer":
|
|
508
|
+
return this.#callWriter(handleId, handle.writer, method, args);
|
|
509
|
+
case "query-cursor": {
|
|
510
|
+
if (method === "next") {
|
|
511
|
+
const next = await handle.iterator.next();
|
|
512
|
+
if (next.done) {
|
|
513
|
+
this.#handles.delete(handleId);
|
|
514
|
+
return new CursorBatchResult(void 0);
|
|
515
|
+
}
|
|
516
|
+
return new CursorBatchResult(encodeQueryResult(next.value));
|
|
517
|
+
}
|
|
518
|
+
if (method === "close") {
|
|
519
|
+
await handle.iterator.return?.();
|
|
520
|
+
this.#handles.delete(handleId);
|
|
521
|
+
return void 0;
|
|
522
|
+
}
|
|
523
|
+
throw new Error(`Unsupported query cursor method: ${method}`);
|
|
524
|
+
}
|
|
525
|
+
case "live-set":
|
|
526
|
+
return this.#callLiveSet(handleId, handle, method, args);
|
|
527
|
+
case "live-subscription": {
|
|
528
|
+
if (method !== "close")
|
|
529
|
+
throw new Error(`Unsupported subscription method: ${method}`);
|
|
530
|
+
handle.subscription.close();
|
|
531
|
+
this.#handles.delete(handleId);
|
|
532
|
+
const parent = this.#handles.get(handle.setId);
|
|
533
|
+
if (parent?.type === "live-set")
|
|
534
|
+
parent.subscriptionIds.delete(handleId);
|
|
535
|
+
return void 0;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async #callWriteHandle(handleId, handle, method, args, context) {
|
|
540
|
+
if (method === "query") {
|
|
541
|
+
const [sql, options, reportStats = false] = args;
|
|
542
|
+
return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, {
|
|
543
|
+
...options,
|
|
544
|
+
signal: context.signal,
|
|
545
|
+
...reportStats ? {
|
|
546
|
+
onStats: (stats) => {
|
|
547
|
+
this.scope.postMessage(rpcEvent(context.requestId, "stats", stats));
|
|
548
|
+
}
|
|
549
|
+
} : {}
|
|
550
|
+
})));
|
|
551
|
+
}
|
|
552
|
+
if (method === "execute") {
|
|
553
|
+
const [sql, params] = args;
|
|
554
|
+
return handle.session.execute(sql, params);
|
|
555
|
+
}
|
|
556
|
+
if (method === "stage") {
|
|
557
|
+
const [op, tableName, input] = args;
|
|
558
|
+
if (!isStageOp(op)) {
|
|
559
|
+
throw new Error(`Unsupported write stage operation: ${String(op)}`);
|
|
560
|
+
}
|
|
561
|
+
return handle.session[op](tableName, input);
|
|
562
|
+
}
|
|
563
|
+
if (method === "commit")
|
|
564
|
+
return this.#settleWriteHandle(handleId, handle, true);
|
|
565
|
+
if (method === "abort")
|
|
566
|
+
return this.#settleWriteHandle(handleId, handle, false);
|
|
567
|
+
throw new Error(`Unsupported write method: ${method}`);
|
|
568
|
+
}
|
|
569
|
+
async #callSnapshotExport(handleId, handle, method, args) {
|
|
570
|
+
switch (method) {
|
|
571
|
+
case "read": {
|
|
572
|
+
if (args.length !== 0)
|
|
573
|
+
throw new TypeError("Snapshot stream read takes no arguments");
|
|
574
|
+
if (handle.reading)
|
|
575
|
+
throw new Error("Snapshot stream already has a read in flight");
|
|
576
|
+
if (handle.done)
|
|
577
|
+
return { done: true };
|
|
578
|
+
handle.reading = true;
|
|
579
|
+
try {
|
|
580
|
+
const next = await handle.iterator.next();
|
|
581
|
+
if (next.done === true) {
|
|
582
|
+
handle.done = true;
|
|
583
|
+
return { done: true };
|
|
584
|
+
}
|
|
585
|
+
const chunk = new Uint8Array(next.value.byteLength);
|
|
586
|
+
chunk.set(next.value);
|
|
587
|
+
return new SnapshotChunkResult(chunk);
|
|
588
|
+
} finally {
|
|
589
|
+
handle.reading = false;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
case "close": {
|
|
593
|
+
await handle.iterator.return?.();
|
|
594
|
+
this.#handles.delete(handleId);
|
|
595
|
+
return void 0;
|
|
596
|
+
}
|
|
597
|
+
default:
|
|
598
|
+
throw new Error(`Unsupported snapshot export method: ${method}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async #callSnapshotImport(handleId, handle, method, args) {
|
|
602
|
+
switch (method) {
|
|
603
|
+
case "write": {
|
|
604
|
+
const chunk = args[0];
|
|
605
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
606
|
+
throw new TypeError("Snapshot chunk must be a Uint8Array");
|
|
607
|
+
}
|
|
608
|
+
await handle.queue.push(chunk);
|
|
609
|
+
return void 0;
|
|
610
|
+
}
|
|
611
|
+
case "finish": {
|
|
612
|
+
try {
|
|
613
|
+
handle.queue.finish();
|
|
614
|
+
await handle.task;
|
|
615
|
+
} finally {
|
|
616
|
+
this.#handles.delete(handleId);
|
|
617
|
+
}
|
|
618
|
+
return void 0;
|
|
619
|
+
}
|
|
620
|
+
case "close": {
|
|
621
|
+
handle.queue.fail(new Error("Snapshot import transfer was cancelled"));
|
|
622
|
+
await handle.task.catch(() => void 0);
|
|
623
|
+
this.#handles.delete(handleId);
|
|
624
|
+
return void 0;
|
|
625
|
+
}
|
|
626
|
+
case "cancel": {
|
|
627
|
+
handle.abort.abort(new Error("Snapshot import was cancelled"));
|
|
628
|
+
handle.queue.fail(handle.abort.signal.reason);
|
|
629
|
+
await handle.task.catch(() => void 0);
|
|
630
|
+
this.#handles.delete(handleId);
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
default:
|
|
634
|
+
throw new Error(`Unsupported snapshot import method: ${method}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async #callWriter(handleId, writer, method, args) {
|
|
638
|
+
switch (method) {
|
|
639
|
+
case "add":
|
|
640
|
+
return writer.add(args[0]);
|
|
641
|
+
case "flush":
|
|
642
|
+
return writer.flush();
|
|
643
|
+
case "requestFlush":
|
|
644
|
+
writer.requestFlush();
|
|
645
|
+
return void 0;
|
|
646
|
+
case "discard":
|
|
647
|
+
return writer.discard();
|
|
648
|
+
case "stats":
|
|
649
|
+
return { pendingRowCount: writer.pendingRowCount, estimatedBytes: writer.estimatedBytes };
|
|
650
|
+
case "close": {
|
|
651
|
+
const result = await writer.close();
|
|
652
|
+
this.#handles.delete(handleId);
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
default:
|
|
656
|
+
throw new Error(`Unsupported buffered writer method: ${method}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async #callLiveSet(handleId, handle, method, args) {
|
|
660
|
+
switch (method) {
|
|
661
|
+
case "subscribe": {
|
|
662
|
+
const subscriptionId = this.#claimHandleId(args[0]);
|
|
663
|
+
const query = args[1];
|
|
664
|
+
let subscription;
|
|
665
|
+
try {
|
|
666
|
+
subscription = await handle.set.subscribe(query, {
|
|
667
|
+
onChange: (result) => {
|
|
668
|
+
const encoded = encodeQueryResult(result);
|
|
669
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "change", encoded.payload), {
|
|
670
|
+
transfer: encoded.transfer
|
|
671
|
+
});
|
|
672
|
+
},
|
|
673
|
+
onError: (error) => {
|
|
674
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
|
|
675
|
+
},
|
|
676
|
+
onComplete: () => {
|
|
677
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
|
|
678
|
+
this.#handles.delete(subscriptionId);
|
|
679
|
+
const parent = this.#handles.get(handleId);
|
|
680
|
+
if (parent?.type === "live-set")
|
|
681
|
+
parent.subscriptionIds.delete(subscriptionId);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
this.#publishHandle(subscriptionId, {
|
|
685
|
+
type: "live-subscription",
|
|
686
|
+
subscription,
|
|
687
|
+
setId: handleId
|
|
688
|
+
});
|
|
689
|
+
} catch (error) {
|
|
690
|
+
subscription?.close();
|
|
691
|
+
this.#releaseHandleId(subscriptionId);
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
handle.subscriptionIds.add(subscriptionId);
|
|
695
|
+
return { dependencyTableIds: subscription.dependencyTableIds };
|
|
696
|
+
}
|
|
697
|
+
case "observe": {
|
|
698
|
+
const subscriptionId = this.#claimHandleId(args[0]);
|
|
699
|
+
const query = args[1];
|
|
700
|
+
let subscription;
|
|
701
|
+
try {
|
|
702
|
+
subscription = await handle.set.observe(query, {
|
|
703
|
+
onInvalidate: (invalidation) => {
|
|
704
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "invalidate", invalidation));
|
|
705
|
+
},
|
|
706
|
+
onError: (error) => {
|
|
707
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
|
|
708
|
+
},
|
|
709
|
+
onComplete: () => {
|
|
710
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
|
|
711
|
+
this.#handles.delete(subscriptionId);
|
|
712
|
+
const parent = this.#handles.get(handleId);
|
|
713
|
+
if (parent?.type === "live-set")
|
|
714
|
+
parent.subscriptionIds.delete(subscriptionId);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
this.#publishHandle(subscriptionId, {
|
|
718
|
+
type: "live-subscription",
|
|
719
|
+
subscription,
|
|
720
|
+
setId: handleId
|
|
721
|
+
});
|
|
722
|
+
} catch (error) {
|
|
723
|
+
subscription?.close();
|
|
724
|
+
this.#releaseHandleId(subscriptionId);
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
handle.subscriptionIds.add(subscriptionId);
|
|
728
|
+
return { dependencyTableIds: subscription.dependencyTableIds };
|
|
729
|
+
}
|
|
730
|
+
case "stats":
|
|
731
|
+
return handle.set.stats;
|
|
732
|
+
case "notifyLocalCommit":
|
|
733
|
+
handle.set.notifyLocalCommit();
|
|
734
|
+
return void 0;
|
|
735
|
+
case "refresh":
|
|
736
|
+
return handle.set.refresh();
|
|
737
|
+
case "close":
|
|
738
|
+
this.#closeLiveSet(handleId, handle);
|
|
739
|
+
return void 0;
|
|
740
|
+
default:
|
|
741
|
+
throw new Error(`Unsupported live query method: ${method}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
#claimHandleId(value) {
|
|
745
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
746
|
+
throw new TypeError("Handle ID must be a non-empty string");
|
|
747
|
+
}
|
|
748
|
+
if (this.#handles.has(value) || this.#reservedHandleIds.has(value)) {
|
|
749
|
+
throw new Error(`Handle ID already exists: ${value}`);
|
|
750
|
+
}
|
|
751
|
+
if (this.#handles.size + this.#reservedHandleIds.size + this.#settlingWritePromises.size >= MAX_WORKER_HANDLES_PER_CONNECTION) {
|
|
752
|
+
throw new Error(`Database connection cannot hold more than ${String(MAX_WORKER_HANDLES_PER_CONNECTION)} open handles (active or settling)`);
|
|
753
|
+
}
|
|
754
|
+
this.#reservedHandleIds.add(value);
|
|
755
|
+
return value;
|
|
756
|
+
}
|
|
757
|
+
#claimGeneratedHandleId() {
|
|
758
|
+
for (; ; ) {
|
|
759
|
+
const id = crypto.randomUUID();
|
|
760
|
+
if (!this.#handles.has(id) && !this.#reservedHandleIds.has(id))
|
|
761
|
+
return this.#claimHandleId(id);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
#publishHandle(id, handle) {
|
|
765
|
+
if (this.#disposed)
|
|
766
|
+
throw new Error("Database connection is disposed");
|
|
767
|
+
if (!this.#reservedHandleIds.delete(id))
|
|
768
|
+
throw new Error(`Handle ID was not reserved: ${id}`);
|
|
769
|
+
this.#handles.set(id, handle);
|
|
770
|
+
if (handle.type === "write")
|
|
771
|
+
this.#armWriteHandleIdleTimer(id, handle);
|
|
772
|
+
}
|
|
773
|
+
#releaseHandleId(id) {
|
|
774
|
+
this.#reservedHandleIds.delete(id);
|
|
775
|
+
}
|
|
776
|
+
#beginWriteHandleCall(handle) {
|
|
777
|
+
if (!handle.open)
|
|
778
|
+
throw new Error("Write handle is closed");
|
|
779
|
+
if (handle.activeCalls !== 0) {
|
|
780
|
+
throw new Error("Write handle already has a call in flight");
|
|
781
|
+
}
|
|
782
|
+
handle.activeCalls = 1;
|
|
783
|
+
handle.activeCallDone = new Promise((resolve) => {
|
|
784
|
+
handle.finishActiveCall = resolve;
|
|
785
|
+
});
|
|
786
|
+
if (handle.idleTimer !== void 0) {
|
|
787
|
+
clearTimeout(handle.idleTimer);
|
|
788
|
+
handle.idleTimer = void 0;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
#endWriteHandleCall(handleId, handle) {
|
|
792
|
+
handle.activeCalls -= 1;
|
|
793
|
+
if (handle.activeCalls < 0)
|
|
794
|
+
throw new Error("Write handle activity count underflow");
|
|
795
|
+
const finishActiveCall = handle.finishActiveCall;
|
|
796
|
+
handle.activeCallDone = void 0;
|
|
797
|
+
handle.finishActiveCall = void 0;
|
|
798
|
+
finishActiveCall?.();
|
|
799
|
+
if (handle.open && handle.activeCalls === 0)
|
|
800
|
+
this.#armWriteHandleIdleTimer(handleId, handle);
|
|
801
|
+
}
|
|
802
|
+
#armWriteHandleIdleTimer(handleId, handle) {
|
|
803
|
+
if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
|
|
804
|
+
return;
|
|
805
|
+
if (handle.idleTimer !== void 0)
|
|
806
|
+
clearTimeout(handle.idleTimer);
|
|
807
|
+
handle.idleTimer = setTimeout(() => {
|
|
808
|
+
handle.idleTimer = void 0;
|
|
809
|
+
if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
|
|
810
|
+
return;
|
|
811
|
+
void this.#settleWriteHandle(handleId, handle, false).catch(() => void 0);
|
|
812
|
+
}, this.#writeHandleIdleTimeoutMs);
|
|
813
|
+
unrefTimer(handle.idleTimer);
|
|
814
|
+
}
|
|
815
|
+
async #settleWriteHandle(handleId, handle, commit) {
|
|
816
|
+
if (!handle.open)
|
|
817
|
+
throw new Error("Write handle is closed");
|
|
818
|
+
handle.open = false;
|
|
819
|
+
if (handle.idleTimer !== void 0) {
|
|
820
|
+
clearTimeout(handle.idleTimer);
|
|
821
|
+
handle.idleTimer = void 0;
|
|
822
|
+
}
|
|
823
|
+
if (this.#handles.get(handleId) === handle)
|
|
824
|
+
this.#handles.delete(handleId);
|
|
825
|
+
handle.finish(commit);
|
|
826
|
+
const settlement = handle.done.catch((error) => {
|
|
827
|
+
if (!commit && error instanceof WriteScopeAbortedError)
|
|
828
|
+
return void 0;
|
|
829
|
+
throw error;
|
|
830
|
+
});
|
|
831
|
+
this.#settlingWritePromises.add(settlement);
|
|
832
|
+
void settlement.then(() => this.#settlingWritePromises.delete(settlement), () => this.#settlingWritePromises.delete(settlement));
|
|
833
|
+
return settlement;
|
|
834
|
+
}
|
|
835
|
+
#closeLiveSet(handleId, handle) {
|
|
836
|
+
handle.set.close();
|
|
837
|
+
for (const subscriptionId of handle.subscriptionIds)
|
|
838
|
+
this.#handles.delete(subscriptionId);
|
|
839
|
+
this.#handles.delete(handleId);
|
|
840
|
+
}
|
|
841
|
+
async #dispose() {
|
|
842
|
+
this.#disposed = true;
|
|
843
|
+
this.#reservedHandleIds.clear();
|
|
844
|
+
const handles = [...this.#handles.entries()];
|
|
845
|
+
this.#handles.clear();
|
|
846
|
+
const handleCleanup = [];
|
|
847
|
+
for (const [, handle] of handles) {
|
|
848
|
+
if (handle.type === "write" && handle.idleTimer !== void 0) {
|
|
849
|
+
clearTimeout(handle.idleTimer);
|
|
850
|
+
handle.idleTimer = void 0;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
for (const [handleId, handle] of handles) {
|
|
854
|
+
try {
|
|
855
|
+
if (handle.type === "snapshot") {
|
|
856
|
+
handle.release();
|
|
857
|
+
handleCleanup.push(handle.done.catch(() => void 0));
|
|
858
|
+
} else if (handle.type === "write") {
|
|
859
|
+
handleCleanup.push((async () => {
|
|
860
|
+
await handle.activeCallDone;
|
|
861
|
+
if (handle.open) {
|
|
862
|
+
await this.#settleWriteHandle(handleId, handle, false).catch(() => void 0);
|
|
863
|
+
}
|
|
864
|
+
})());
|
|
865
|
+
} else if (handle.type === "writer")
|
|
866
|
+
handleCleanup.push(handle.writer.close());
|
|
867
|
+
else if (handle.type === "query-cursor") {
|
|
868
|
+
handleCleanup.push(Promise.resolve(handle.iterator.return?.()));
|
|
869
|
+
} else if (handle.type === "snapshot-export") {
|
|
870
|
+
handleCleanup.push(Promise.resolve(handle.iterator.return?.()));
|
|
871
|
+
} else if (handle.type === "snapshot-import") {
|
|
872
|
+
handle.abort.abort(new Error("Database connection was disposed"));
|
|
873
|
+
handle.queue.fail(new Error("Database connection was disposed"));
|
|
874
|
+
handleCleanup.push(handle.task.catch(() => void 0));
|
|
875
|
+
} else if (handle.type === "live-set")
|
|
876
|
+
this.#closeLiveSet(handleId, handle);
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
await Promise.allSettled(handleCleanup);
|
|
881
|
+
while (this.#settlingWritePromises.size > 0) {
|
|
882
|
+
await Promise.allSettled([...this.#settlingWritePromises]);
|
|
883
|
+
}
|
|
884
|
+
while (this.#openingHandlePromises.size > 0) {
|
|
885
|
+
await Promise.allSettled([...this.#openingHandlePromises]);
|
|
886
|
+
}
|
|
887
|
+
await this.#waitForInFlightRpcs();
|
|
888
|
+
await this.database.close();
|
|
889
|
+
await this.options.onDispose?.();
|
|
890
|
+
}
|
|
891
|
+
#waitForInFlightRpcs() {
|
|
892
|
+
if (this.#inFlightRpcCount === 0)
|
|
893
|
+
return Promise.resolve();
|
|
894
|
+
this.#inFlightRpcDrain ??= new Promise((resolve) => {
|
|
895
|
+
this.#resolveInFlightRpcDrain = resolve;
|
|
896
|
+
});
|
|
897
|
+
return this.#inFlightRpcDrain;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function exposeDatabase(database, scope, options = {}) {
|
|
901
|
+
const server = new DatabaseRpcServer(database, scope, options);
|
|
902
|
+
scope.addEventListener("message", (event) => {
|
|
903
|
+
let request;
|
|
904
|
+
try {
|
|
905
|
+
request = parseRpcRequest(event.data);
|
|
906
|
+
} catch (error) {
|
|
907
|
+
const requestId = requestIdOf(event.data);
|
|
908
|
+
if (requestId !== void 0)
|
|
909
|
+
scope.postMessage(rpcFailure(requestId, error));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (request !== null)
|
|
913
|
+
void server.handle(request);
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
function attachWorkerHost(scope, createStore, options = {}) {
|
|
917
|
+
let initialized;
|
|
918
|
+
let initFailure;
|
|
919
|
+
scope.addEventListener("message", (event) => {
|
|
920
|
+
let request;
|
|
921
|
+
try {
|
|
922
|
+
request = parseRpcRequest(event.data);
|
|
923
|
+
} catch (error) {
|
|
924
|
+
const requestId = requestIdOf(event.data);
|
|
925
|
+
if (requestId !== void 0)
|
|
926
|
+
scope.postMessage(rpcFailure(requestId, error));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (request === null)
|
|
930
|
+
return;
|
|
931
|
+
if (request.kind === "rpc-init" && initialized === void 0) {
|
|
932
|
+
initFailure = void 0;
|
|
933
|
+
const attempt = createServer(scope, request.payload, createStore, options);
|
|
934
|
+
initialized = attempt;
|
|
935
|
+
attempt.catch((error) => {
|
|
936
|
+
if (initialized === attempt) {
|
|
937
|
+
initialized = void 0;
|
|
938
|
+
initFailure = error;
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const pending = initialized;
|
|
943
|
+
if (pending === void 0) {
|
|
944
|
+
const failure = initFailure === void 0 ? new Error("Database is not initialized: send init first") : new Error(`Database initialization failed: ${initFailure instanceof Error ? `${initFailure.name}: ${initFailure.message}` : typeof initFailure === "string" ? initFailure : "unknown reason"}`, { cause: initFailure });
|
|
945
|
+
scope.postMessage(rpcFailure(request.requestId, failure));
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
void pending.then((server) => server.handle(request)).catch((error) => {
|
|
949
|
+
scope.postMessage(rpcFailure(request.requestId, error));
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
async function createServer(scope, payload, createStore, options) {
|
|
954
|
+
const store = await createStore(payload.store, options);
|
|
955
|
+
const database = new MinnowDatabase(store, payload.options ?? {});
|
|
956
|
+
return new DatabaseRpcServer(database, scope, {
|
|
957
|
+
...payload.options?.transactionIdleTimeoutMs === void 0 ? {} : { writeHandleIdleTimeoutMs: payload.options.transactionIdleTimeoutMs },
|
|
958
|
+
onDispose: () => store.close(),
|
|
959
|
+
onVisibility: (visible) => {
|
|
960
|
+
store.setForeground?.(visible);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const storeKindLabels = {
|
|
965
|
+
indexeddb: "IndexedDB",
|
|
966
|
+
opfs: "OPFS",
|
|
967
|
+
memory: "memory"
|
|
968
|
+
};
|
|
969
|
+
function singleStoreFactory(kind, open) {
|
|
970
|
+
return (descriptor, options) => {
|
|
971
|
+
if (descriptor.kind !== kind) {
|
|
972
|
+
throw new Error(unsupportedStoreKindMessage(kind, descriptor.kind));
|
|
973
|
+
}
|
|
974
|
+
return open(descriptor, options);
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function unsupportedStoreKindMessage(bundled, requested) {
|
|
978
|
+
const requestedLabel = Object.hasOwn(storeKindLabels, requested) ? storeKindLabels[requested] : void 0;
|
|
979
|
+
const requestedEntry = requestedLabel === void 0 ? "" : ` or "@minnowdb/core/worker/${requested}" for the ${requestedLabel} store`;
|
|
980
|
+
return `This worker bundles only the ${storeKindLabels[bundled]} store and cannot open ${requestedLabel === void 0 ? `store kind "${requested}"` : `the ${requestedLabel} store`}; import "@minnowdb/core/worker" for every adapter${requestedEntry}.`;
|
|
981
|
+
}
|
|
982
|
+
function requestIdOf(value) {
|
|
983
|
+
if (typeof value === "object" && value !== null && "requestId" in value) {
|
|
984
|
+
const requestId = value.requestId;
|
|
985
|
+
if (typeof requestId === "string")
|
|
986
|
+
return requestId;
|
|
987
|
+
}
|
|
988
|
+
return void 0;
|
|
989
|
+
}
|
|
990
|
+
function unrefTimer(timer) {
|
|
991
|
+
const candidate = timer;
|
|
992
|
+
candidate.unref?.();
|
|
993
|
+
}
|
|
994
|
+
export {
|
|
995
|
+
MAX_WORKER_HANDLES_PER_CONNECTION,
|
|
996
|
+
attachWorkerHost,
|
|
997
|
+
exposeDatabase,
|
|
998
|
+
singleStoreFactory
|
|
999
|
+
};
|