@minnowdb/core 0.3.0 → 0.4.0
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/README.md +4 -2
- package/dist/block-format/block.d.ts +32 -13
- package/dist/block-format/block.js +179 -55
- package/dist/block-format/checksum.d.ts +2 -1
- package/dist/block-format/checksum.js +5 -2
- package/dist/block-format/codecs.d.ts +7 -2
- package/dist/block-format/codecs.js +53 -12
- package/dist/block-format/column.d.ts +3 -2
- package/dist/block-format/column.js +96 -30
- package/dist/block-format/index.d.ts +2 -2
- package/dist/block-format/index.js +2 -2
- package/dist/block-format/physical.d.ts +8 -2
- package/dist/block-format/physical.js +14 -6
- package/dist/block-format/types.d.ts +7 -3
- package/dist/block-format/types.js +0 -1
- package/dist/block-format/unicode.d.ts +11 -0
- package/dist/block-format/unicode.js +46 -0
- package/dist/date-value.d.ts +15 -0
- package/dist/date-value.js +64 -0
- package/dist/engine/artifact-cache.d.ts +0 -1
- package/dist/engine/artifact-cache.js +0 -1
- package/dist/engine/batch.d.ts +5 -2
- package/dist/engine/batch.js +0 -1
- package/dist/engine/buffered-writer.d.ts +2 -1
- package/dist/engine/buffered-writer.js +34 -9
- package/dist/engine/cache-limits.d.ts +25 -0
- package/dist/engine/cache-limits.js +25 -0
- package/dist/engine/catalog.d.ts +5 -6
- package/dist/engine/catalog.js +5 -5
- package/dist/engine/client.d.ts +27 -7
- package/dist/engine/client.js +197 -31
- package/dist/engine/database.d.ts +161 -38
- package/dist/engine/database.js +4320 -1523
- package/dist/engine/defaults.d.ts +0 -1
- package/dist/engine/defaults.js +2 -4
- package/dist/engine/errors.d.ts +19 -1
- package/dist/engine/errors.js +32 -2
- package/dist/engine/fts.d.ts +0 -6
- package/dist/engine/fts.js +39 -14
- package/dist/engine/group-index.d.ts +0 -1
- package/dist/engine/group-index.js +6 -11
- package/dist/engine/index.d.ts +16 -11
- package/dist/engine/index.js +12 -10
- package/dist/engine/join-index.d.ts +0 -1
- package/dist/engine/join-index.js +2 -2
- package/dist/engine/keyed-live.d.ts +57 -0
- package/dist/engine/keyed-live.js +226 -0
- package/dist/engine/live-api.d.ts +4 -0
- package/dist/engine/live-api.js +4 -0
- package/dist/engine/live.d.ts +43 -26
- package/dist/engine/live.js +499 -129
- package/dist/engine/memory.d.ts +2 -1
- package/dist/engine/memory.js +3 -2
- package/dist/engine/optimizer.d.ts +0 -1
- package/dist/engine/optimizer.js +4 -4
- package/dist/engine/query-api.d.ts +3 -0
- package/dist/engine/query-api.js +3 -0
- package/dist/engine/query-cache.d.ts +0 -1
- package/dist/engine/query-cache.js +4 -4
- package/dist/engine/query.d.ts +4 -7
- package/dist/engine/query.js +348 -155
- package/dist/engine/result-wire.d.ts +0 -1
- package/dist/engine/result-wire.js +2 -2
- package/dist/engine/schema-wire.d.ts +1 -2
- package/dist/engine/schema-wire.js +0 -1
- package/dist/engine/schema.d.ts +3 -8
- package/dist/engine/schema.js +41 -24
- package/dist/engine/sort-keys.d.ts +0 -1
- package/dist/engine/sort-keys.js +2 -2
- package/dist/engine/sql-domains.d.ts +6 -1
- package/dist/engine/sql-domains.js +209 -45
- package/dist/engine/sql-driver.d.ts +2 -2
- package/dist/engine/sql-driver.js +0 -1
- package/dist/engine/sql-json.d.ts +0 -1
- package/dist/engine/sql-json.js +53 -7
- package/dist/engine/sql-semantics.d.ts +8 -5
- package/dist/engine/sql-semantics.js +427 -65
- package/dist/engine/typed-live.d.ts +67 -0
- package/dist/engine/typed-live.js +349 -0
- package/dist/engine/vector.d.ts +7 -1
- package/dist/engine/vector.js +202 -58
- package/dist/engine/worker-host.d.ts +8 -2
- package/dist/engine/worker-host.js +616 -152
- package/dist/engine/worker.d.ts +0 -1
- package/dist/engine/worker.js +3 -3
- package/dist/engine/write-block-planner.d.ts +18 -0
- package/dist/engine/write-block-planner.js +112 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/plan/index.d.ts +0 -1
- package/dist/plan/index.js +0 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/indexeddb.d.ts +74 -65
- package/dist/storage/indexeddb.js +12301 -2076
- package/dist/storage/memory.d.ts +56 -66
- package/dist/storage/memory.js +1004 -139
- package/dist/storage/opfs/files.d.ts +18 -7
- package/dist/storage/opfs/files.js +115 -28
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +1 -1
- package/dist/storage/opfs/leader.d.ts +403 -91
- package/dist/storage/opfs/leader.js +4306 -602
- package/dist/storage/opfs/rpc.d.ts +15 -3
- package/dist/storage/opfs/rpc.js +223 -2
- package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
- package/dist/storage/opfs/snapshot-ledger.js +281 -0
- package/dist/storage/opfs/store.d.ts +26 -121
- package/dist/storage/opfs/store.js +436 -388
- package/dist/storage/persistence.d.ts +37 -0
- package/dist/storage/persistence.js +78 -0
- package/dist/storage/snapshot-stream.d.ts +24 -0
- package/dist/storage/snapshot-stream.js +897 -0
- package/dist/storage/snapshot.d.ts +9 -99
- package/dist/storage/snapshot.js +33 -309
- package/dist/storage/toolkit/extents.d.ts +41 -7
- package/dist/storage/toolkit/extents.js +402 -39
- package/dist/storage/toolkit/index.d.ts +3 -4
- package/dist/storage/toolkit/index.js +27 -3
- package/dist/storage/toolkit/record-core.d.ts +174 -61
- package/dist/storage/toolkit/record-core.js +5401 -849
- package/dist/storage/toolkit/sync-file.d.ts +10 -1
- package/dist/storage/toolkit/sync-file.js +50 -2
- package/dist/storage/toolkit/wal.d.ts +23 -5
- package/dist/storage/toolkit/wal.js +89 -27
- package/dist/storage/toolkit/wire.d.ts +4 -6
- package/dist/storage/toolkit/wire.js +80 -25
- package/dist/storage/types.d.ts +1271 -218
- package/dist/storage/types.js +1471 -138
- package/dist/testing/block-store-conformance.d.ts +0 -1
- package/dist/testing/block-store-conformance.js +877 -138
- package/dist/testing/index.d.ts +42 -31
- package/dist/testing/index.js +100 -68
- package/dist/testing/opfs-shim.d.ts +13 -3
- package/dist/testing/opfs-shim.js +40 -6
- package/dist/testing/simulator.d.ts +0 -1
- package/dist/testing/simulator.js +43 -12
- package/dist/testing/sqllogictest.d.ts +0 -1
- package/dist/testing/sqllogictest.js +0 -1
- package/dist/transactions/index.d.ts +81 -20
- package/dist/transactions/index.js +1050 -268
- package/dist/worker-protocol/index.d.ts +3 -2
- package/dist/worker-protocol/index.js +2 -5
- package/package.json +49 -1
- package/dist/block-format/block.d.ts.map +0 -1
- package/dist/block-format/block.js.map +0 -1
- package/dist/block-format/checksum.d.ts.map +0 -1
- package/dist/block-format/checksum.js.map +0 -1
- package/dist/block-format/codecs.d.ts.map +0 -1
- package/dist/block-format/codecs.js.map +0 -1
- package/dist/block-format/column.d.ts.map +0 -1
- package/dist/block-format/column.js.map +0 -1
- package/dist/block-format/index.d.ts.map +0 -1
- package/dist/block-format/index.js.map +0 -1
- package/dist/block-format/physical.d.ts.map +0 -1
- package/dist/block-format/physical.js.map +0 -1
- package/dist/block-format/types.d.ts.map +0 -1
- package/dist/block-format/types.js.map +0 -1
- package/dist/engine/artifact-cache.d.ts.map +0 -1
- package/dist/engine/artifact-cache.js.map +0 -1
- package/dist/engine/batch.d.ts.map +0 -1
- package/dist/engine/batch.js.map +0 -1
- package/dist/engine/buffered-writer.d.ts.map +0 -1
- package/dist/engine/buffered-writer.js.map +0 -1
- package/dist/engine/catalog.d.ts.map +0 -1
- package/dist/engine/catalog.js.map +0 -1
- package/dist/engine/client.d.ts.map +0 -1
- package/dist/engine/client.js.map +0 -1
- package/dist/engine/coordinator.d.ts +0 -17
- package/dist/engine/coordinator.d.ts.map +0 -1
- package/dist/engine/coordinator.js +0 -60
- package/dist/engine/coordinator.js.map +0 -1
- package/dist/engine/database.d.ts.map +0 -1
- package/dist/engine/database.js.map +0 -1
- package/dist/engine/defaults.d.ts.map +0 -1
- package/dist/engine/defaults.js.map +0 -1
- package/dist/engine/errors.d.ts.map +0 -1
- package/dist/engine/errors.js.map +0 -1
- package/dist/engine/fts.d.ts.map +0 -1
- package/dist/engine/fts.js.map +0 -1
- package/dist/engine/group-index.d.ts.map +0 -1
- package/dist/engine/group-index.js.map +0 -1
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/join-index.d.ts.map +0 -1
- package/dist/engine/join-index.js.map +0 -1
- package/dist/engine/live.d.ts.map +0 -1
- package/dist/engine/live.js.map +0 -1
- package/dist/engine/memory.d.ts.map +0 -1
- package/dist/engine/memory.js.map +0 -1
- package/dist/engine/optimizer.d.ts.map +0 -1
- package/dist/engine/optimizer.js.map +0 -1
- package/dist/engine/query-cache.d.ts.map +0 -1
- package/dist/engine/query-cache.js.map +0 -1
- package/dist/engine/query.d.ts.map +0 -1
- package/dist/engine/query.js.map +0 -1
- package/dist/engine/result-wire.d.ts.map +0 -1
- package/dist/engine/result-wire.js.map +0 -1
- package/dist/engine/schema-wire.d.ts.map +0 -1
- package/dist/engine/schema-wire.js.map +0 -1
- package/dist/engine/schema.d.ts.map +0 -1
- package/dist/engine/schema.js.map +0 -1
- package/dist/engine/sort-keys.d.ts.map +0 -1
- package/dist/engine/sort-keys.js.map +0 -1
- package/dist/engine/sql-domains.d.ts.map +0 -1
- package/dist/engine/sql-domains.js.map +0 -1
- package/dist/engine/sql-driver.d.ts.map +0 -1
- package/dist/engine/sql-driver.js.map +0 -1
- package/dist/engine/sql-json.d.ts.map +0 -1
- package/dist/engine/sql-json.js.map +0 -1
- package/dist/engine/sql-semantics.d.ts.map +0 -1
- package/dist/engine/sql-semantics.js.map +0 -1
- package/dist/engine/vector.d.ts.map +0 -1
- package/dist/engine/vector.js.map +0 -1
- package/dist/engine/worker-host.d.ts.map +0 -1
- package/dist/engine/worker-host.js.map +0 -1
- package/dist/engine/worker.d.ts.map +0 -1
- package/dist/engine/worker.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/plan/index.d.ts.map +0 -1
- package/dist/plan/index.js.map +0 -1
- package/dist/storage/fixture-shape.d.ts +0 -42
- package/dist/storage/fixture-shape.d.ts.map +0 -1
- package/dist/storage/fixture-shape.js +0 -146
- package/dist/storage/fixture-shape.js.map +0 -1
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/indexeddb.d.ts.map +0 -1
- package/dist/storage/indexeddb.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/opfs/files.d.ts.map +0 -1
- package/dist/storage/opfs/files.js.map +0 -1
- package/dist/storage/opfs/index.d.ts.map +0 -1
- package/dist/storage/opfs/index.js.map +0 -1
- package/dist/storage/opfs/leader.d.ts.map +0 -1
- package/dist/storage/opfs/leader.js.map +0 -1
- package/dist/storage/opfs/rpc.d.ts.map +0 -1
- package/dist/storage/opfs/rpc.js.map +0 -1
- package/dist/storage/opfs/store.d.ts.map +0 -1
- package/dist/storage/opfs/store.js.map +0 -1
- package/dist/storage/snapshot.d.ts.map +0 -1
- package/dist/storage/snapshot.js.map +0 -1
- package/dist/storage/toolkit/extents.d.ts.map +0 -1
- package/dist/storage/toolkit/extents.js.map +0 -1
- package/dist/storage/toolkit/index.d.ts.map +0 -1
- package/dist/storage/toolkit/index.js.map +0 -1
- package/dist/storage/toolkit/record-core.d.ts.map +0 -1
- package/dist/storage/toolkit/record-core.js.map +0 -1
- package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
- package/dist/storage/toolkit/sync-file.js.map +0 -1
- package/dist/storage/toolkit/wal.d.ts.map +0 -1
- package/dist/storage/toolkit/wal.js.map +0 -1
- package/dist/storage/toolkit/wire.d.ts.map +0 -1
- package/dist/storage/toolkit/wire.js.map +0 -1
- package/dist/storage/types.d.ts.map +0 -1
- package/dist/storage/types.js.map +0 -1
- package/dist/testing/block-store-conformance.d.ts.map +0 -1
- package/dist/testing/block-store-conformance.js.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/opfs-shim.d.ts.map +0 -1
- package/dist/testing/opfs-shim.js.map +0 -1
- package/dist/testing/seeds.d.ts +0 -11
- package/dist/testing/seeds.d.ts.map +0 -1
- package/dist/testing/seeds.js +0 -50
- package/dist/testing/seeds.js.map +0 -1
- package/dist/testing/simulator.d.ts.map +0 -1
- package/dist/testing/simulator.js.map +0 -1
- package/dist/testing/sqllogictest.d.ts.map +0 -1
- package/dist/testing/sqllogictest.js.map +0 -1
- package/dist/transactions/index.d.ts.map +0 -1
- package/dist/transactions/index.js.map +0 -1
- package/dist/worker-protocol/index.d.ts.map +0 -1
- package/dist/worker-protocol/index.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
3
|
import { MinnowDatabase, } from "./database.js";
|
|
3
4
|
import { encodeQueryResult, encodeQueryRows } from "./result-wire.js";
|
|
4
5
|
import { deserializeSchema, serializeMigrationSteps } from "./schema-wire.js";
|
|
@@ -22,28 +23,144 @@ class ColumnarResult {
|
|
|
22
23
|
this.encoded = encoded;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
class CursorBatchResult {
|
|
27
|
+
encoded;
|
|
28
|
+
constructor(encoded) {
|
|
29
|
+
this.encoded = encoded;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class SnapshotChunkResult {
|
|
33
|
+
chunk;
|
|
34
|
+
constructor(chunk) {
|
|
35
|
+
this.chunk = chunk;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
25
38
|
class WriteScopeAbortedError extends Error {
|
|
26
39
|
name = "WriteScopeAbortedError";
|
|
27
40
|
constructor() {
|
|
28
41
|
super("Write scope aborted");
|
|
29
42
|
}
|
|
30
43
|
}
|
|
44
|
+
/** One-chunk rendezvous. The sender is acknowledged only when the decoder asks for more. */
|
|
45
|
+
class SnapshotImportChunkQueue {
|
|
46
|
+
#queued;
|
|
47
|
+
#consumed;
|
|
48
|
+
#reader;
|
|
49
|
+
#finished = false;
|
|
50
|
+
#failure;
|
|
51
|
+
[Symbol.asyncIterator]() {
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
push(chunk) {
|
|
55
|
+
if (chunk.byteLength > MAX_SNAPSHOT_STREAM_CHUNK_BYTES) {
|
|
56
|
+
throw new RangeError("Snapshot transfer chunk exceeds the 1 MiB limit");
|
|
57
|
+
}
|
|
58
|
+
if (this.#failure !== undefined)
|
|
59
|
+
return Promise.reject(snapshotQueueError(this.#failure));
|
|
60
|
+
if (this.#finished)
|
|
61
|
+
return Promise.reject(new Error("Snapshot import transfer is closed"));
|
|
62
|
+
if (this.#queued !== undefined) {
|
|
63
|
+
return Promise.reject(new Error("Snapshot import transfer already has a queued chunk"));
|
|
64
|
+
}
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const reader = this.#reader;
|
|
67
|
+
if (reader === undefined) {
|
|
68
|
+
this.#queued = { chunk, resolve, reject };
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.#reader = undefined;
|
|
72
|
+
this.#consumed = { resolve, reject };
|
|
73
|
+
reader.resolve({ done: false, value: chunk });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async next() {
|
|
77
|
+
this.#consumed?.resolve();
|
|
78
|
+
this.#consumed = undefined;
|
|
79
|
+
if (this.#failure !== undefined)
|
|
80
|
+
throw snapshotQueueError(this.#failure);
|
|
81
|
+
const queued = this.#queued;
|
|
82
|
+
if (queued !== undefined) {
|
|
83
|
+
this.#queued = undefined;
|
|
84
|
+
this.#consumed = { resolve: queued.resolve, reject: queued.reject };
|
|
85
|
+
return { done: false, value: queued.chunk };
|
|
86
|
+
}
|
|
87
|
+
if (this.#finished)
|
|
88
|
+
return { done: true, value: undefined };
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
this.#reader = { resolve, reject };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async return() {
|
|
94
|
+
this.finish();
|
|
95
|
+
return { done: true, value: undefined };
|
|
96
|
+
}
|
|
97
|
+
finish() {
|
|
98
|
+
if (this.#finished)
|
|
99
|
+
return;
|
|
100
|
+
this.#finished = true;
|
|
101
|
+
if (this.#queued === undefined) {
|
|
102
|
+
const reader = this.#reader;
|
|
103
|
+
this.#reader = undefined;
|
|
104
|
+
reader?.resolve({ done: true, value: undefined });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
fail(error) {
|
|
108
|
+
if (this.#failure !== undefined)
|
|
109
|
+
return;
|
|
110
|
+
this.#failure = error;
|
|
111
|
+
this.#finished = true;
|
|
112
|
+
this.#queued?.reject(error);
|
|
113
|
+
this.#queued = undefined;
|
|
114
|
+
this.#consumed?.reject(error);
|
|
115
|
+
this.#consumed = undefined;
|
|
116
|
+
this.#reader?.reject(error);
|
|
117
|
+
this.#reader = undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function snapshotQueueError(value) {
|
|
121
|
+
return value instanceof Error ? value : new Error("Snapshot transfer failed", { cause: value });
|
|
122
|
+
}
|
|
123
|
+
/** Hard per-connection ceiling for resident worker resources abandoned by a client. */
|
|
124
|
+
export const MAX_WORKER_HANDLES_PER_CONNECTION = 256;
|
|
125
|
+
const DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS = 30_000;
|
|
126
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
31
127
|
class DatabaseRpcServer {
|
|
32
128
|
database;
|
|
33
129
|
scope;
|
|
34
130
|
options;
|
|
35
131
|
#handles = new Map();
|
|
132
|
+
#reservedHandleIds = new Set();
|
|
133
|
+
#openingHandlePromises = new Set();
|
|
134
|
+
#settlingWritePromises = new Set();
|
|
36
135
|
#disposed = false;
|
|
136
|
+
#inFlightRpcCount = 0;
|
|
137
|
+
#inFlightRpcDrain;
|
|
138
|
+
#resolveInFlightRpcDrain;
|
|
139
|
+
#writeHandleIdleTimeoutMs;
|
|
37
140
|
constructor(database, scope, options) {
|
|
38
141
|
this.database = database;
|
|
39
142
|
this.scope = scope;
|
|
40
143
|
this.options = options;
|
|
144
|
+
this.#writeHandleIdleTimeoutMs =
|
|
145
|
+
options.writeHandleIdleTimeoutMs ?? DEFAULT_WRITE_HANDLE_IDLE_TIMEOUT_MS;
|
|
146
|
+
if (!Number.isSafeInteger(this.#writeHandleIdleTimeoutMs) ||
|
|
147
|
+
this.#writeHandleIdleTimeoutMs <= 0 ||
|
|
148
|
+
this.#writeHandleIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
|
|
149
|
+
throw new RangeError(`Worker write-handle idle timeout must be a positive whole number no greater than ${String(MAX_TIMER_DELAY_MS)}`);
|
|
150
|
+
}
|
|
41
151
|
}
|
|
42
152
|
async handle(request) {
|
|
43
153
|
if (request.kind === "rpc-init") {
|
|
44
154
|
this.scope.postMessage(rpcResult(request.requestId, { ready: true }));
|
|
45
155
|
return;
|
|
46
156
|
}
|
|
157
|
+
const bypassLimit = request.method === "dispose";
|
|
158
|
+
if (!bypassLimit && this.#inFlightRpcCount >= MAX_DATABASE_RPC_IN_FLIGHT) {
|
|
159
|
+
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`)));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!bypassLimit)
|
|
163
|
+
this.#inFlightRpcCount += 1;
|
|
47
164
|
try {
|
|
48
165
|
if (this.#disposed)
|
|
49
166
|
throw new Error("Database connection is disposed");
|
|
@@ -55,6 +172,16 @@ class DatabaseRpcServer {
|
|
|
55
172
|
transfer: result.encoded.transfer,
|
|
56
173
|
});
|
|
57
174
|
}
|
|
175
|
+
else if (result instanceof CursorBatchResult) {
|
|
176
|
+
this.scope.postMessage(rpcResult(request.requestId, result.encoded === undefined
|
|
177
|
+
? { done: true }
|
|
178
|
+
: { done: false, batch: result.encoded.payload }), result.encoded === undefined ? undefined : { transfer: result.encoded.transfer });
|
|
179
|
+
}
|
|
180
|
+
else if (result instanceof SnapshotChunkResult) {
|
|
181
|
+
this.scope.postMessage(rpcResult(request.requestId, { done: false, chunk: result.chunk }), {
|
|
182
|
+
transfer: [result.chunk.buffer],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
58
185
|
else {
|
|
59
186
|
this.scope.postMessage(rpcResult(request.requestId, result));
|
|
60
187
|
}
|
|
@@ -62,6 +189,16 @@ class DatabaseRpcServer {
|
|
|
62
189
|
catch (error) {
|
|
63
190
|
this.scope.postMessage(rpcFailure(request.requestId, error));
|
|
64
191
|
}
|
|
192
|
+
finally {
|
|
193
|
+
if (!bypassLimit)
|
|
194
|
+
this.#inFlightRpcCount -= 1;
|
|
195
|
+
if (this.#inFlightRpcCount === 0 && this.#resolveInFlightRpcDrain !== undefined) {
|
|
196
|
+
const resolve = this.#resolveInFlightRpcDrain;
|
|
197
|
+
this.#inFlightRpcDrain = undefined;
|
|
198
|
+
this.#resolveInFlightRpcDrain = undefined;
|
|
199
|
+
resolve();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
65
202
|
}
|
|
66
203
|
async #callRoot(method, args) {
|
|
67
204
|
const database = this.database;
|
|
@@ -76,10 +213,11 @@ class DatabaseRpcServer {
|
|
|
76
213
|
case "updateBatch":
|
|
77
214
|
case "update":
|
|
78
215
|
case "deleteBatch":
|
|
216
|
+
case "delete":
|
|
79
217
|
case "runStatement":
|
|
80
218
|
case "explain":
|
|
81
219
|
case "execute":
|
|
82
|
-
case "
|
|
220
|
+
case "listVisibleSegmentPage":
|
|
83
221
|
case "cleanupQuerySpill":
|
|
84
222
|
case "compactTable":
|
|
85
223
|
case "compactTableStep":
|
|
@@ -90,12 +228,30 @@ class DatabaseRpcServer {
|
|
|
90
228
|
case "collectGarbageStep":
|
|
91
229
|
case "resumeGarbageCollectionJob":
|
|
92
230
|
case "bufferPoolStats":
|
|
231
|
+
case "maintenanceStatus":
|
|
232
|
+
case "checkIntegrity":
|
|
233
|
+
case "storageStats":
|
|
234
|
+
case "inspectInterruptedImport":
|
|
235
|
+
case "abortInterruptedImport":
|
|
93
236
|
case "listGarbageCollectionJobs":
|
|
94
237
|
return database[method]?.(...args);
|
|
95
238
|
case "query": {
|
|
96
239
|
const [sql, options] = args;
|
|
97
240
|
return new ColumnarResult(encodeQueryResult(await this.database.query(sql, options)));
|
|
98
241
|
}
|
|
242
|
+
case "queryCursorOpen": {
|
|
243
|
+
const handleId = this.#claimHandleId(args[0]);
|
|
244
|
+
const [sql, options] = args.slice(1);
|
|
245
|
+
try {
|
|
246
|
+
const iterator = this.database.queryCursor(sql, options);
|
|
247
|
+
this.#publishHandle(handleId, { type: "query-cursor", iterator });
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
this.#releaseHandleId(handleId);
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
return { handleId };
|
|
254
|
+
}
|
|
99
255
|
case "run": {
|
|
100
256
|
const query = args[0];
|
|
101
257
|
return new ColumnarResult(encodeQueryRows(await this.database.run(query)));
|
|
@@ -118,90 +274,91 @@ class DatabaseRpcServer {
|
|
|
118
274
|
steps: serializeMigrationSteps(result.steps),
|
|
119
275
|
};
|
|
120
276
|
}
|
|
121
|
-
case "writeOpen":
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
finish = (commit) => {
|
|
135
|
-
if (commit)
|
|
136
|
-
resolveCommit();
|
|
137
|
-
else
|
|
138
|
-
rejectAbort(new WriteScopeAbortedError());
|
|
139
|
-
};
|
|
277
|
+
case "writeOpen":
|
|
278
|
+
return this.#trackOpeningHandle(this.#openWriteHandle());
|
|
279
|
+
case "snapshotOpen":
|
|
280
|
+
return this.#trackOpeningHandle(this.#openSnapshotHandle());
|
|
281
|
+
case "exportSnapshotOpen": {
|
|
282
|
+
const handleId = this.#claimGeneratedHandleId();
|
|
283
|
+
const iterator = this.database.exportSnapshotStream()[Symbol.asyncIterator]();
|
|
284
|
+
try {
|
|
285
|
+
this.#publishHandle(handleId, {
|
|
286
|
+
type: "snapshot-export",
|
|
287
|
+
iterator,
|
|
288
|
+
reading: false,
|
|
289
|
+
done: false,
|
|
140
290
|
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
await iterator.return(undefined);
|
|
294
|
+
this.#releaseHandleId(handleId);
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
145
297
|
return { handleId };
|
|
146
298
|
}
|
|
147
|
-
case "snapshotOpen": {
|
|
148
|
-
// The scoped snapshot() holds its lease until the callback settles; the handle keeps
|
|
149
|
-
// the callback pending until the client closes it, so the pin spans the RPC session.
|
|
150
|
-
const handleId = crypto.randomUUID();
|
|
151
|
-
let release;
|
|
152
|
-
const version = await new Promise((resolveVersion, rejectVersion) => {
|
|
153
|
-
void this.database
|
|
154
|
-
.snapshot(async (session) => {
|
|
155
|
-
resolveVersion(session.version);
|
|
156
|
-
await new Promise((resolveRelease) => {
|
|
157
|
-
release = resolveRelease;
|
|
158
|
-
});
|
|
159
|
-
})
|
|
160
|
-
.catch((error) => {
|
|
161
|
-
rejectVersion(error instanceof Error ? error : new Error(String(error)));
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
this.#handles.set(handleId, { type: "snapshot", release });
|
|
165
|
-
return { handleId, version };
|
|
166
|
-
}
|
|
167
|
-
case "exportSnapshotOpen": {
|
|
168
|
-
// The snapshot is encoded here and held by the handle so the client can pull it across
|
|
169
|
-
// in slices. One result frame carrying the whole thing would be a structured clone the
|
|
170
|
-
// size of the database, and the main thread would sit still for all of it.
|
|
171
|
-
const handleId = crypto.randomUUID();
|
|
172
|
-
const bytes = await this.database.exportSnapshot();
|
|
173
|
-
this.#handles.set(handleId, { type: "snapshot-export", bytes });
|
|
174
|
-
return { handleId, byteLength: bytes.byteLength };
|
|
175
|
-
}
|
|
176
299
|
case "importSnapshotOpen": {
|
|
177
300
|
// Named by the client, like the other event-producing handles, so its progress route
|
|
178
301
|
// exists before the first frame the load can emit.
|
|
179
302
|
const handleId = this.#claimHandleId(args[0]);
|
|
180
|
-
|
|
303
|
+
const queue = new SnapshotImportChunkQueue();
|
|
304
|
+
const abort = new AbortController();
|
|
305
|
+
const task = this.database.importSnapshotStream(queue, {
|
|
306
|
+
signal: abort.signal,
|
|
307
|
+
onProgress: (progress) => {
|
|
308
|
+
this.scope.postMessage(rpcEvent(handleId, "progress", progress));
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
void task.catch((error) => queue.fail(error));
|
|
312
|
+
try {
|
|
313
|
+
this.#publishHandle(handleId, { type: "snapshot-import", queue, task, abort });
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
abort.abort(error);
|
|
317
|
+
queue.fail(error);
|
|
318
|
+
await task.catch(() => undefined);
|
|
319
|
+
this.#releaseHandleId(handleId);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
181
322
|
return { handleId };
|
|
182
323
|
}
|
|
183
324
|
case "bufferedWriter": {
|
|
184
325
|
// Event-producing handles are named by the client so its event routes exist before the
|
|
185
326
|
// first frame the handle can emit.
|
|
186
327
|
const handleId = this.#claimHandleId(args[0]);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
328
|
+
let writer;
|
|
329
|
+
try {
|
|
330
|
+
writer = this.database.bufferedWriter(args[1], {
|
|
331
|
+
...args[2],
|
|
332
|
+
onError: (error) => {
|
|
333
|
+
this.scope.postMessage(rpcEvent(handleId, "error", serializeError(error)));
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
this.#publishHandle(handleId, { type: "writer", writer });
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
await writer?.close().catch(() => undefined);
|
|
340
|
+
this.#releaseHandleId(handleId);
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
194
343
|
return { handleId };
|
|
195
344
|
}
|
|
196
345
|
case "liveQueries": {
|
|
197
346
|
const handleId = this.#claimHandleId(args[0]);
|
|
198
347
|
const { pollIntervalMs, channelName } = (args[1] ?? {});
|
|
199
348
|
// The database owns channelName resolution (and closes the channel with the set).
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
349
|
+
let set;
|
|
350
|
+
try {
|
|
351
|
+
set = this.database.liveQueries({
|
|
352
|
+
...(channelName === undefined ? {} : { channelName }),
|
|
353
|
+
...(pollIntervalMs === undefined ? {} : { pollIntervalMs }),
|
|
354
|
+
});
|
|
355
|
+
this.#publishHandle(handleId, { type: "live-set", set, subscriptionIds: new Set() });
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
set?.close();
|
|
359
|
+
this.#releaseHandleId(handleId);
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
205
362
|
return { handleId };
|
|
206
363
|
}
|
|
207
364
|
case "dispose": {
|
|
@@ -216,56 +373,142 @@ class DatabaseRpcServer {
|
|
|
216
373
|
throw new Error(`Unsupported database method: ${method}`);
|
|
217
374
|
}
|
|
218
375
|
}
|
|
376
|
+
async #openWriteHandle() {
|
|
377
|
+
// Same deferred-callback shape as snapshotOpen: the scoped write() stays pending until the
|
|
378
|
+
// client commits or aborts, so its staged transaction spans the RPC session.
|
|
379
|
+
const handleId = this.#claimGeneratedHandleId();
|
|
380
|
+
let sessionResolve;
|
|
381
|
+
let sessionReject;
|
|
382
|
+
const sessionReady = new Promise((resolveSession, rejectSession) => {
|
|
383
|
+
sessionResolve = resolveSession;
|
|
384
|
+
sessionReject = rejectSession;
|
|
385
|
+
});
|
|
386
|
+
let finish;
|
|
387
|
+
const done = this.database
|
|
388
|
+
.write(async (session) => {
|
|
389
|
+
sessionResolve(session);
|
|
390
|
+
await new Promise((resolveCommit, rejectAbort) => {
|
|
391
|
+
finish = (commit) => {
|
|
392
|
+
if (commit)
|
|
393
|
+
resolveCommit();
|
|
394
|
+
else
|
|
395
|
+
rejectAbort(new WriteScopeAbortedError());
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
})
|
|
399
|
+
.then((outcome) => ({ version: outcome.version }));
|
|
400
|
+
// beginDeferred can fail before the callback starts (for example at the durable reader-lease
|
|
401
|
+
// ceiling). Reject the open rendezvous too and observe `done` immediately.
|
|
402
|
+
void done.catch((error) => sessionReject(error));
|
|
403
|
+
try {
|
|
404
|
+
const session = await sessionReady;
|
|
405
|
+
if (finish === undefined)
|
|
406
|
+
throw new Error("Write scope did not install its settlement");
|
|
407
|
+
this.#publishHandle(handleId, {
|
|
408
|
+
type: "write",
|
|
409
|
+
session,
|
|
410
|
+
finish,
|
|
411
|
+
done,
|
|
412
|
+
activeCalls: 0,
|
|
413
|
+
activeCallDone: undefined,
|
|
414
|
+
finishActiveCall: undefined,
|
|
415
|
+
open: true,
|
|
416
|
+
idleTimer: undefined,
|
|
417
|
+
});
|
|
418
|
+
return { handleId };
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
// Publication can lose a race with connection disposal. If the callback started, abort and
|
|
422
|
+
// join it before the opening task releases its lifecycle slot.
|
|
423
|
+
finish?.(false);
|
|
424
|
+
await done.catch(() => undefined);
|
|
425
|
+
this.#releaseHandleId(handleId);
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async #openSnapshotHandle() {
|
|
430
|
+
// The scoped snapshot() holds its lease until the callback settles; the handle keeps the
|
|
431
|
+
// callback pending until close, so the pin spans the RPC session.
|
|
432
|
+
const handleId = this.#claimGeneratedHandleId();
|
|
433
|
+
let release;
|
|
434
|
+
let resolveVersion;
|
|
435
|
+
let rejectVersion;
|
|
436
|
+
const versionReady = new Promise((resolve, reject) => {
|
|
437
|
+
resolveVersion = resolve;
|
|
438
|
+
rejectVersion = reject;
|
|
439
|
+
});
|
|
440
|
+
const done = this.database.snapshot(async (session) => {
|
|
441
|
+
resolveVersion(session.version);
|
|
442
|
+
await new Promise((resolveRelease) => {
|
|
443
|
+
release = resolveRelease;
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
// Lease admission may fail before the callback installs `release`. Preserve that typed error
|
|
447
|
+
// through the rendezvous and keep the task observed from creation.
|
|
448
|
+
void done.catch((error) => rejectVersion(error));
|
|
449
|
+
try {
|
|
450
|
+
const version = await versionReady;
|
|
451
|
+
if (release === undefined)
|
|
452
|
+
throw new Error("Snapshot scope did not install its release");
|
|
453
|
+
this.#publishHandle(handleId, { type: "snapshot", release, done });
|
|
454
|
+
return { handleId, version };
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
release?.();
|
|
458
|
+
await done.catch(() => undefined);
|
|
459
|
+
this.#releaseHandleId(handleId);
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
#trackOpeningHandle(opening) {
|
|
464
|
+
this.#openingHandlePromises.add(opening);
|
|
465
|
+
void opening.then(() => this.#openingHandlePromises.delete(opening), () => this.#openingHandlePromises.delete(opening));
|
|
466
|
+
return opening;
|
|
467
|
+
}
|
|
219
468
|
async #callHandle(handleId, method, args) {
|
|
220
469
|
const handle = this.#handles.get(handleId);
|
|
221
470
|
if (handle === undefined)
|
|
222
471
|
throw new Error(`Unknown handle: ${handleId}`);
|
|
472
|
+
if (handle.type === "write") {
|
|
473
|
+
this.#beginWriteHandleCall(handle);
|
|
474
|
+
try {
|
|
475
|
+
return await this.#callWriteHandle(handleId, handle, method, args);
|
|
476
|
+
}
|
|
477
|
+
finally {
|
|
478
|
+
this.#endWriteHandleCall(handleId, handle);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
223
481
|
switch (handle.type) {
|
|
224
482
|
case "snapshot": {
|
|
225
483
|
if (method !== "close")
|
|
226
484
|
throw new Error(`Unsupported snapshot method: ${method}`);
|
|
227
485
|
handle.release();
|
|
228
486
|
this.#handles.delete(handleId);
|
|
487
|
+
await handle.done;
|
|
229
488
|
return undefined;
|
|
230
489
|
}
|
|
231
|
-
case "write": {
|
|
232
|
-
if (method === "query") {
|
|
233
|
-
const [sql, options] = args;
|
|
234
|
-
return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, options)));
|
|
235
|
-
}
|
|
236
|
-
if (method === "execute") {
|
|
237
|
-
const [sql, params] = args;
|
|
238
|
-
return handle.session.execute(sql, params);
|
|
239
|
-
}
|
|
240
|
-
if (method === "stage") {
|
|
241
|
-
const [op, tableName, input] = args;
|
|
242
|
-
if (!isStageOp(op)) {
|
|
243
|
-
throw new Error(`Unsupported write stage operation: ${String(op)}`);
|
|
244
|
-
}
|
|
245
|
-
return handle.session[op](tableName, input);
|
|
246
|
-
}
|
|
247
|
-
if (method === "commit") {
|
|
248
|
-
handle.finish(true);
|
|
249
|
-
this.#handles.delete(handleId);
|
|
250
|
-
return handle.done;
|
|
251
|
-
}
|
|
252
|
-
if (method === "abort") {
|
|
253
|
-
handle.finish(false);
|
|
254
|
-
this.#handles.delete(handleId);
|
|
255
|
-
return handle.done.catch((error) => {
|
|
256
|
-
if (error instanceof WriteScopeAbortedError)
|
|
257
|
-
return undefined;
|
|
258
|
-
throw error;
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
throw new Error(`Unsupported write method: ${method}`);
|
|
262
|
-
}
|
|
263
490
|
case "snapshot-export":
|
|
264
491
|
return this.#callSnapshotExport(handleId, handle, method, args);
|
|
265
492
|
case "snapshot-import":
|
|
266
493
|
return this.#callSnapshotImport(handleId, handle, method, args);
|
|
267
494
|
case "writer":
|
|
268
495
|
return this.#callWriter(handleId, handle.writer, method, args);
|
|
496
|
+
case "query-cursor": {
|
|
497
|
+
if (method === "next") {
|
|
498
|
+
const next = await handle.iterator.next();
|
|
499
|
+
if (next.done) {
|
|
500
|
+
this.#handles.delete(handleId);
|
|
501
|
+
return new CursorBatchResult(undefined);
|
|
502
|
+
}
|
|
503
|
+
return new CursorBatchResult(encodeQueryResult(next.value));
|
|
504
|
+
}
|
|
505
|
+
if (method === "close") {
|
|
506
|
+
await handle.iterator.return?.();
|
|
507
|
+
this.#handles.delete(handleId);
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
throw new Error(`Unsupported query cursor method: ${method}`);
|
|
511
|
+
}
|
|
269
512
|
case "live-set":
|
|
270
513
|
return this.#callLiveSet(handleId, handle, method, args);
|
|
271
514
|
case "live-subscription": {
|
|
@@ -280,25 +523,61 @@ class DatabaseRpcServer {
|
|
|
280
523
|
}
|
|
281
524
|
}
|
|
282
525
|
}
|
|
526
|
+
async #callWriteHandle(handleId, handle, method, args) {
|
|
527
|
+
if (method === "query") {
|
|
528
|
+
const [sql, options] = args;
|
|
529
|
+
return new ColumnarResult(encodeQueryResult(await handle.session.query(sql, options)));
|
|
530
|
+
}
|
|
531
|
+
if (method === "execute") {
|
|
532
|
+
const [sql, params] = args;
|
|
533
|
+
return handle.session.execute(sql, params);
|
|
534
|
+
}
|
|
535
|
+
if (method === "stage") {
|
|
536
|
+
const [op, tableName, input] = args;
|
|
537
|
+
if (!isStageOp(op)) {
|
|
538
|
+
throw new Error(`Unsupported write stage operation: ${String(op)}`);
|
|
539
|
+
}
|
|
540
|
+
return handle.session[op](tableName, input);
|
|
541
|
+
}
|
|
542
|
+
if (method === "commit")
|
|
543
|
+
return this.#settleWriteHandle(handleId, handle, true);
|
|
544
|
+
if (method === "abort")
|
|
545
|
+
return this.#settleWriteHandle(handleId, handle, false);
|
|
546
|
+
throw new Error(`Unsupported write method: ${method}`);
|
|
547
|
+
}
|
|
283
548
|
/**
|
|
284
549
|
* Hands one slice of an encoded snapshot back. The slice is copied rather than viewed: a
|
|
285
550
|
* subarray shares the whole buffer, and structured clone copies the buffer behind a view, not
|
|
286
551
|
* the window into it — so every read would post the entire snapshot again.
|
|
287
552
|
*/
|
|
288
|
-
#callSnapshotExport(handleId, handle, method, args) {
|
|
553
|
+
async #callSnapshotExport(handleId, handle, method, args) {
|
|
289
554
|
switch (method) {
|
|
290
555
|
case "read": {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
556
|
+
if (args.length !== 0)
|
|
557
|
+
throw new TypeError("Snapshot stream read takes no arguments");
|
|
558
|
+
if (handle.reading)
|
|
559
|
+
throw new Error("Snapshot stream already has a read in flight");
|
|
560
|
+
if (handle.done)
|
|
561
|
+
return { done: true };
|
|
562
|
+
handle.reading = true;
|
|
563
|
+
try {
|
|
564
|
+
const next = await handle.iterator.next();
|
|
565
|
+
if (next.done === true) {
|
|
566
|
+
handle.done = true;
|
|
567
|
+
return { done: true };
|
|
568
|
+
}
|
|
569
|
+
// The iterator may yield a view into a 64 MiB block. Transfer only this bounded slice;
|
|
570
|
+
// detaching the larger backing buffer would corrupt subsequent slices.
|
|
571
|
+
const chunk = new Uint8Array(next.value.byteLength);
|
|
572
|
+
chunk.set(next.value);
|
|
573
|
+
return new SnapshotChunkResult(chunk);
|
|
294
574
|
}
|
|
295
|
-
|
|
296
|
-
|
|
575
|
+
finally {
|
|
576
|
+
handle.reading = false;
|
|
297
577
|
}
|
|
298
|
-
const start = offset;
|
|
299
|
-
return handle.bytes.slice(start, start + length);
|
|
300
578
|
}
|
|
301
579
|
case "close": {
|
|
580
|
+
await handle.iterator.return?.();
|
|
302
581
|
this.#handles.delete(handleId);
|
|
303
582
|
return undefined;
|
|
304
583
|
}
|
|
@@ -314,26 +593,13 @@ class DatabaseRpcServer {
|
|
|
314
593
|
if (!(chunk instanceof Uint8Array)) {
|
|
315
594
|
throw new TypeError("Snapshot chunk must be a Uint8Array");
|
|
316
595
|
}
|
|
317
|
-
handle.
|
|
318
|
-
handle.byteLength += chunk.byteLength;
|
|
596
|
+
await handle.queue.push(chunk);
|
|
319
597
|
return undefined;
|
|
320
598
|
}
|
|
321
599
|
case "finish": {
|
|
322
|
-
const bytes = new Uint8Array(handle.byteLength);
|
|
323
|
-
let offset = 0;
|
|
324
|
-
for (const chunk of handle.chunks) {
|
|
325
|
-
bytes.set(chunk, offset);
|
|
326
|
-
offset += chunk.byteLength;
|
|
327
|
-
}
|
|
328
|
-
// Dropped before the load starts: the decoded snapshot is another copy of these bytes,
|
|
329
|
-
// and holding the chunks too would make three of them.
|
|
330
|
-
handle.chunks.length = 0;
|
|
331
600
|
try {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
this.scope.postMessage(rpcEvent(handleId, "progress", progress));
|
|
335
|
-
},
|
|
336
|
-
});
|
|
601
|
+
handle.queue.finish();
|
|
602
|
+
await handle.task;
|
|
337
603
|
}
|
|
338
604
|
finally {
|
|
339
605
|
this.#handles.delete(handleId);
|
|
@@ -341,6 +607,15 @@ class DatabaseRpcServer {
|
|
|
341
607
|
return undefined;
|
|
342
608
|
}
|
|
343
609
|
case "close": {
|
|
610
|
+
handle.queue.fail(new Error("Snapshot import transfer was cancelled"));
|
|
611
|
+
await handle.task.catch(() => undefined);
|
|
612
|
+
this.#handles.delete(handleId);
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
case "cancel": {
|
|
616
|
+
handle.abort.abort(new Error("Snapshot import was cancelled"));
|
|
617
|
+
handle.queue.fail(handle.abort.signal.reason);
|
|
618
|
+
await handle.task.catch(() => undefined);
|
|
344
619
|
this.#handles.delete(handleId);
|
|
345
620
|
return undefined;
|
|
346
621
|
}
|
|
@@ -377,29 +652,71 @@ class DatabaseRpcServer {
|
|
|
377
652
|
// posted before this call's own result frame.
|
|
378
653
|
const subscriptionId = this.#claimHandleId(args[0]);
|
|
379
654
|
const query = args[1];
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
parent.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
655
|
+
let subscription;
|
|
656
|
+
try {
|
|
657
|
+
subscription = await handle.set.subscribe(query, {
|
|
658
|
+
onChange: (result) => {
|
|
659
|
+
const encoded = encodeQueryResult(result);
|
|
660
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "change", encoded.payload), {
|
|
661
|
+
transfer: encoded.transfer,
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
onError: (error) => {
|
|
665
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
|
|
666
|
+
},
|
|
667
|
+
onComplete: () => {
|
|
668
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
|
|
669
|
+
this.#handles.delete(subscriptionId);
|
|
670
|
+
const parent = this.#handles.get(handleId);
|
|
671
|
+
if (parent?.type === "live-set")
|
|
672
|
+
parent.subscriptionIds.delete(subscriptionId);
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
this.#publishHandle(subscriptionId, {
|
|
676
|
+
type: "live-subscription",
|
|
677
|
+
subscription,
|
|
678
|
+
setId: handleId,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
subscription?.close();
|
|
683
|
+
this.#releaseHandleId(subscriptionId);
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
handle.subscriptionIds.add(subscriptionId);
|
|
687
|
+
return { dependencyTableIds: subscription.dependencyTableIds };
|
|
688
|
+
}
|
|
689
|
+
case "observe": {
|
|
690
|
+
const subscriptionId = this.#claimHandleId(args[0]);
|
|
691
|
+
const query = args[1];
|
|
692
|
+
let subscription;
|
|
693
|
+
try {
|
|
694
|
+
subscription = await handle.set.observe(query, {
|
|
695
|
+
onInvalidate: (invalidation) => {
|
|
696
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "invalidate", invalidation));
|
|
697
|
+
},
|
|
698
|
+
onError: (error) => {
|
|
699
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "error", serializeError(error)));
|
|
700
|
+
},
|
|
701
|
+
onComplete: () => {
|
|
702
|
+
this.scope.postMessage(rpcEvent(subscriptionId, "complete", null));
|
|
703
|
+
this.#handles.delete(subscriptionId);
|
|
704
|
+
const parent = this.#handles.get(handleId);
|
|
705
|
+
if (parent?.type === "live-set")
|
|
706
|
+
parent.subscriptionIds.delete(subscriptionId);
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
this.#publishHandle(subscriptionId, {
|
|
710
|
+
type: "live-subscription",
|
|
711
|
+
subscription,
|
|
712
|
+
setId: handleId,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
subscription?.close();
|
|
717
|
+
this.#releaseHandleId(subscriptionId);
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
403
720
|
handle.subscriptionIds.add(subscriptionId);
|
|
404
721
|
return { dependencyTableIds: subscription.dependencyTableIds };
|
|
405
722
|
}
|
|
@@ -421,10 +738,96 @@ class DatabaseRpcServer {
|
|
|
421
738
|
if (typeof value !== "string" || value.length === 0) {
|
|
422
739
|
throw new TypeError("Handle ID must be a non-empty string");
|
|
423
740
|
}
|
|
424
|
-
if (this.#handles.has(value))
|
|
741
|
+
if (this.#handles.has(value) || this.#reservedHandleIds.has(value)) {
|
|
425
742
|
throw new Error(`Handle ID already exists: ${value}`);
|
|
743
|
+
}
|
|
744
|
+
if (this.#handles.size + this.#reservedHandleIds.size + this.#settlingWritePromises.size >=
|
|
745
|
+
MAX_WORKER_HANDLES_PER_CONNECTION) {
|
|
746
|
+
throw new Error(`Database connection cannot hold more than ${String(MAX_WORKER_HANDLES_PER_CONNECTION)} open handles (active or settling)`);
|
|
747
|
+
}
|
|
748
|
+
this.#reservedHandleIds.add(value);
|
|
426
749
|
return value;
|
|
427
750
|
}
|
|
751
|
+
#claimGeneratedHandleId() {
|
|
752
|
+
for (;;) {
|
|
753
|
+
const id = crypto.randomUUID();
|
|
754
|
+
if (!this.#handles.has(id) && !this.#reservedHandleIds.has(id))
|
|
755
|
+
return this.#claimHandleId(id);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
#publishHandle(id, handle) {
|
|
759
|
+
if (this.#disposed)
|
|
760
|
+
throw new Error("Database connection is disposed");
|
|
761
|
+
if (!this.#reservedHandleIds.delete(id))
|
|
762
|
+
throw new Error(`Handle ID was not reserved: ${id}`);
|
|
763
|
+
this.#handles.set(id, handle);
|
|
764
|
+
if (handle.type === "write")
|
|
765
|
+
this.#armWriteHandleIdleTimer(id, handle);
|
|
766
|
+
}
|
|
767
|
+
#releaseHandleId(id) {
|
|
768
|
+
this.#reservedHandleIds.delete(id);
|
|
769
|
+
}
|
|
770
|
+
#beginWriteHandleCall(handle) {
|
|
771
|
+
if (!handle.open)
|
|
772
|
+
throw new Error("Write handle is closed");
|
|
773
|
+
if (handle.activeCalls !== 0) {
|
|
774
|
+
throw new Error("Write handle already has a call in flight");
|
|
775
|
+
}
|
|
776
|
+
handle.activeCalls = 1;
|
|
777
|
+
handle.activeCallDone = new Promise((resolve) => {
|
|
778
|
+
handle.finishActiveCall = resolve;
|
|
779
|
+
});
|
|
780
|
+
if (handle.idleTimer !== undefined) {
|
|
781
|
+
clearTimeout(handle.idleTimer);
|
|
782
|
+
handle.idleTimer = undefined;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
#endWriteHandleCall(handleId, handle) {
|
|
786
|
+
handle.activeCalls -= 1;
|
|
787
|
+
if (handle.activeCalls < 0)
|
|
788
|
+
throw new Error("Write handle activity count underflow");
|
|
789
|
+
const finishActiveCall = handle.finishActiveCall;
|
|
790
|
+
handle.activeCallDone = undefined;
|
|
791
|
+
handle.finishActiveCall = undefined;
|
|
792
|
+
finishActiveCall?.();
|
|
793
|
+
if (handle.open && handle.activeCalls === 0)
|
|
794
|
+
this.#armWriteHandleIdleTimer(handleId, handle);
|
|
795
|
+
}
|
|
796
|
+
#armWriteHandleIdleTimer(handleId, handle) {
|
|
797
|
+
if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
|
|
798
|
+
return;
|
|
799
|
+
if (handle.idleTimer !== undefined)
|
|
800
|
+
clearTimeout(handle.idleTimer);
|
|
801
|
+
handle.idleTimer = setTimeout(() => {
|
|
802
|
+
handle.idleTimer = undefined;
|
|
803
|
+
if (!handle.open || handle.activeCalls !== 0 || this.#handles.get(handleId) !== handle)
|
|
804
|
+
return;
|
|
805
|
+
// Delete first: a late commit or stage frame must fail rather than revive an expired scope
|
|
806
|
+
// while its asynchronous durable rollback is still joining.
|
|
807
|
+
void this.#settleWriteHandle(handleId, handle, false).catch(() => undefined);
|
|
808
|
+
}, this.#writeHandleIdleTimeoutMs);
|
|
809
|
+
unrefTimer(handle.idleTimer);
|
|
810
|
+
}
|
|
811
|
+
async #settleWriteHandle(handleId, handle, commit) {
|
|
812
|
+
if (!handle.open)
|
|
813
|
+
throw new Error("Write handle is closed");
|
|
814
|
+
handle.open = false;
|
|
815
|
+
if (handle.idleTimer !== undefined) {
|
|
816
|
+
clearTimeout(handle.idleTimer);
|
|
817
|
+
handle.idleTimer = undefined;
|
|
818
|
+
}
|
|
819
|
+
if (this.#handles.get(handleId) === handle)
|
|
820
|
+
this.#handles.delete(handleId);
|
|
821
|
+
handle.finish(commit);
|
|
822
|
+
const settlement = handle.done.catch((error) => {
|
|
823
|
+
if (!commit && error instanceof WriteScopeAbortedError)
|
|
824
|
+
return undefined;
|
|
825
|
+
throw error;
|
|
826
|
+
});
|
|
827
|
+
this.#settlingWritePromises.add(settlement);
|
|
828
|
+
void settlement.then(() => this.#settlingWritePromises.delete(settlement), () => this.#settlingWritePromises.delete(settlement));
|
|
829
|
+
return settlement;
|
|
830
|
+
}
|
|
428
831
|
#closeLiveSet(handleId, handle) {
|
|
429
832
|
handle.set.close();
|
|
430
833
|
for (const subscriptionId of handle.subscriptionIds)
|
|
@@ -433,18 +836,51 @@ class DatabaseRpcServer {
|
|
|
433
836
|
}
|
|
434
837
|
async #dispose() {
|
|
435
838
|
this.#disposed = true;
|
|
839
|
+
this.#reservedHandleIds.clear();
|
|
436
840
|
const handles = [...this.#handles.entries()];
|
|
437
841
|
this.#handles.clear();
|
|
842
|
+
const handleCleanup = [];
|
|
843
|
+
// Stop every abandoned-write deadline before waiting on active calls. The cleared handle map
|
|
844
|
+
// also prevents their completions from rearming one while disposal is in progress.
|
|
845
|
+
for (const [, handle] of handles) {
|
|
846
|
+
if (handle.type === "write" && handle.idleTimer !== undefined) {
|
|
847
|
+
clearTimeout(handle.idleTimer);
|
|
848
|
+
handle.idleTimer = undefined;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// Signal every handle before awaiting any cleanup. Imports and iterators can have an admitted
|
|
852
|
+
// RPC blocked inside them; closing sequentially could wait on the first while never reaching
|
|
853
|
+
// the signal that lets a later one finish.
|
|
438
854
|
for (const [handleId, handle] of handles) {
|
|
439
855
|
try {
|
|
440
|
-
if (handle.type === "snapshot")
|
|
856
|
+
if (handle.type === "snapshot") {
|
|
441
857
|
handle.release();
|
|
858
|
+
handleCleanup.push(handle.done.catch(() => undefined));
|
|
859
|
+
}
|
|
442
860
|
else if (handle.type === "write") {
|
|
443
|
-
|
|
444
|
-
|
|
861
|
+
// A session operation owns the transaction revision until it returns. Aborting sooner
|
|
862
|
+
// can race its final journal update and leave an active owner until TTL. Once it exits,
|
|
863
|
+
// abort only if commit/abort/idle expiry has not already chosen a terminal outcome.
|
|
864
|
+
handleCleanup.push((async () => {
|
|
865
|
+
await handle.activeCallDone;
|
|
866
|
+
if (handle.open) {
|
|
867
|
+
await this.#settleWriteHandle(handleId, handle, false).catch(() => undefined);
|
|
868
|
+
}
|
|
869
|
+
})());
|
|
445
870
|
}
|
|
446
871
|
else if (handle.type === "writer")
|
|
447
|
-
|
|
872
|
+
handleCleanup.push(handle.writer.close());
|
|
873
|
+
else if (handle.type === "query-cursor") {
|
|
874
|
+
handleCleanup.push(Promise.resolve(handle.iterator.return?.()));
|
|
875
|
+
}
|
|
876
|
+
else if (handle.type === "snapshot-export") {
|
|
877
|
+
handleCleanup.push(Promise.resolve(handle.iterator.return?.()));
|
|
878
|
+
}
|
|
879
|
+
else if (handle.type === "snapshot-import") {
|
|
880
|
+
handle.abort.abort(new Error("Database connection was disposed"));
|
|
881
|
+
handle.queue.fail(new Error("Database connection was disposed"));
|
|
882
|
+
handleCleanup.push(handle.task.catch(() => undefined));
|
|
883
|
+
}
|
|
448
884
|
else if (handle.type === "live-set")
|
|
449
885
|
this.#closeLiveSet(handleId, handle);
|
|
450
886
|
}
|
|
@@ -452,9 +888,30 @@ class DatabaseRpcServer {
|
|
|
452
888
|
// Dispose every handle even when one close fails; the client is already gone.
|
|
453
889
|
}
|
|
454
890
|
}
|
|
891
|
+
await Promise.allSettled(handleCleanup);
|
|
892
|
+
// Commit/abort/expiry removes its handle before durable settlement completes so late RPCs
|
|
893
|
+
// fail closed. Join those detached operations before closing the database or injected store.
|
|
894
|
+
while (this.#settlingWritePromises.size > 0) {
|
|
895
|
+
await Promise.allSettled([...this.#settlingWritePromises]);
|
|
896
|
+
}
|
|
897
|
+
// A handle whose lease admission was already in flight when disposal began is not yet in the
|
|
898
|
+
// handle map. Its opener observes the disposed publication, releases any admitted lease, and
|
|
899
|
+
// settles only after that cleanup is complete.
|
|
900
|
+
while (this.#openingHandlePromises.size > 0) {
|
|
901
|
+
await Promise.allSettled([...this.#openingHandlePromises]);
|
|
902
|
+
}
|
|
903
|
+
await this.#waitForInFlightRpcs();
|
|
455
904
|
await this.database.close();
|
|
456
905
|
await this.options.onDispose?.();
|
|
457
906
|
}
|
|
907
|
+
#waitForInFlightRpcs() {
|
|
908
|
+
if (this.#inFlightRpcCount === 0)
|
|
909
|
+
return Promise.resolve();
|
|
910
|
+
this.#inFlightRpcDrain ??= new Promise((resolve) => {
|
|
911
|
+
this.#resolveInFlightRpcDrain = resolve;
|
|
912
|
+
});
|
|
913
|
+
return this.#inFlightRpcDrain;
|
|
914
|
+
}
|
|
458
915
|
}
|
|
459
916
|
/**
|
|
460
917
|
* Answers client RPC frames against a database you constructed yourself. Use this in a custom
|
|
@@ -535,6 +992,9 @@ async function createServer(scope, payload, options) {
|
|
|
535
992
|
const store = await createStore(payload.store, options);
|
|
536
993
|
const database = new MinnowDatabase(store, payload.options ?? {});
|
|
537
994
|
return new DatabaseRpcServer(database, scope, {
|
|
995
|
+
...(payload.options?.transactionIdleTimeoutMs === undefined
|
|
996
|
+
? {}
|
|
997
|
+
: { writeHandleIdleTimeoutMs: payload.options.transactionIdleTimeoutMs }),
|
|
538
998
|
onDispose: () => store.close(),
|
|
539
999
|
onVisibility: (visible) => {
|
|
540
1000
|
store.setForeground?.(visible);
|
|
@@ -576,4 +1036,8 @@ function requestIdOf(value) {
|
|
|
576
1036
|
}
|
|
577
1037
|
return undefined;
|
|
578
1038
|
}
|
|
579
|
-
|
|
1039
|
+
/** A worker cleanup deadline must not keep a Node process alive by itself. */
|
|
1040
|
+
function unrefTimer(timer) {
|
|
1041
|
+
const candidate = timer;
|
|
1042
|
+
candidate.unref?.();
|
|
1043
|
+
}
|