@delali/sirannon-db 0.1.6 → 0.1.7
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 +134 -19
- package/dist/backup-scheduler/index.d.ts +18 -3
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/{change-tracker-CFTQ9TSn.d.ts → change-tracker-CbmaMO-N.d.ts} +12 -2
- package/dist/chunk-4IGMIJQK.mjs +318 -0
- package/dist/chunk-BNUTBHHH.mjs +22 -0
- package/dist/chunk-CJLYFDP5.mjs +26 -0
- package/dist/chunk-CW6S3WL5.mjs +222 -0
- package/dist/chunk-DJLX6CAE.mjs +20 -0
- package/dist/chunk-DVWQD3GF.mjs +49 -0
- package/dist/chunk-GEZUUIKV.mjs +268 -0
- package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
- package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
- package/dist/chunk-TKGHYWQ6.mjs +35 -0
- package/dist/chunk-VLTICJOD.mjs +470 -0
- package/dist/{chunk-O7BHI3CF.mjs → chunk-YPYVQJ4C.mjs} +15 -1
- package/dist/client/index.d.ts +107 -15
- package/dist/client/index.mjs +348 -37
- package/dist/core/index.d.ts +70 -13
- package/dist/core/index.mjs +635 -192
- package/dist/core/writer-worker.d.ts +2 -0
- package/dist/core/writer-worker.mjs +107 -0
- package/dist/{database-BVY1GqE7.d.ts → database-DuGp0Rtr.d.ts} +51 -19
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +44 -6
- package/dist/driver/bun.mjs +35 -6
- package/dist/driver/expo.mjs +2 -2
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +48 -5
- package/dist/driver/wa-sqlite.d.ts +109 -0
- package/dist/driver/wa-sqlite.mjs +30 -2
- package/dist/{errors-C00ed08Q.d.ts → errors-5Nf5ZAEC.d.ts} +19 -1
- package/dist/file-migrations/index.d.ts +2 -3
- package/dist/file-migrations/index.mjs +1 -1
- package/dist/replication/coordinator/etcd.mjs +2 -2
- package/dist/replication/index.d.ts +7 -8
- package/dist/replication/index.mjs +79 -78
- package/dist/server/index.d.ts +111 -28
- package/dist/server/index.mjs +1016 -384
- package/dist/{sirannon-Cd-lK6T0.d.ts → sirannon-4SspRvP5.d.ts} +3 -3
- package/dist/transport/grpc.d.ts +3 -4
- package/dist/transport/grpc.mjs +2 -2
- package/dist/{types-Lc7ywFx7.d.ts → types-BsjobKbl.d.ts} +2 -2
- package/dist/{types-BeozgNPr.d.ts → types-D4p4UyDK.d.ts} +1 -1
- package/dist/types-D_hQW1hr.d.ts +494 -0
- package/package.json +3 -23
- package/dist/chunk-3MCMONVP.mjs +0 -115
- package/dist/chunk-74UN4DIE.mjs +0 -14
- package/dist/chunk-FB2U2Q3Y.mjs +0 -21
- package/dist/chunk-GS7T5YMI.mjs +0 -51
- package/dist/chunk-PXKAKK2V.mjs +0 -124
- package/dist/index-CLdNrcPz.d.ts +0 -16
- package/dist/types-BFSsG77t.d.ts +0 -29
- package/dist/types-D-74JiXb.d.ts +0 -265
package/dist/server/index.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ChangeTracker, SubscriptionManager } from '../chunk-
|
|
2
|
-
import '../chunk-
|
|
3
|
-
import {
|
|
1
|
+
import { ChangeTracker, SubscriptionManager, changeMatchesFilter, isBulkLoadDurability } from '../chunk-HHRMRFFR.mjs';
|
|
2
|
+
import '../chunk-CJLYFDP5.mjs';
|
|
3
|
+
import { encodeTaggedValues, decodeTaggedValues, META_TABLE } from '../chunk-GEZUUIKV.mjs';
|
|
4
|
+
import { SirannonError, CDCError, WriteOverloadError } from '../chunk-YPYVQJ4C.mjs';
|
|
4
5
|
import uWS from 'uWebSockets.js';
|
|
6
|
+
import { randomBytes } from 'crypto';
|
|
5
7
|
|
|
6
8
|
// src/server/protocol.ts
|
|
7
9
|
function toExecuteResponse(result) {
|
|
@@ -10,6 +12,147 @@ function toExecuteResponse(result) {
|
|
|
10
12
|
lastInsertRowId: typeof result.lastInsertRowId === "bigint" ? result.lastInsertRowId.toString() : result.lastInsertRowId
|
|
11
13
|
};
|
|
12
14
|
}
|
|
15
|
+
function loadDurabilityValidationError(value) {
|
|
16
|
+
if (value === void 0) return null;
|
|
17
|
+
if (!isBulkLoadDurability(value)) {
|
|
18
|
+
return `Field "durability" must be 'off' or 'normal' when provided`;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function loadCheckpointValidationError(value) {
|
|
23
|
+
if (value === void 0) return null;
|
|
24
|
+
if (typeof value !== "boolean") {
|
|
25
|
+
return 'Field "checkpoint" must be a boolean when provided';
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function toBulkLoadOptions(source) {
|
|
30
|
+
if (source.durability === void 0 && source.checkpoint === void 0) return void 0;
|
|
31
|
+
const options = {};
|
|
32
|
+
if (source.durability !== void 0) options.durability = source.durability;
|
|
33
|
+
if (source.checkpoint !== void 0) options.checkpoint = source.checkpoint;
|
|
34
|
+
return options;
|
|
35
|
+
}
|
|
36
|
+
function decodeBoundParams(value, field) {
|
|
37
|
+
if (value === void 0 || value === null) return { ok: true, value: void 0 };
|
|
38
|
+
try {
|
|
39
|
+
return { ok: true, value: decodeTaggedValues(value) };
|
|
40
|
+
} catch {
|
|
41
|
+
return { ok: false, message: `Field "${field}" contains an invalid tagged value` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function validateReadConcern(value) {
|
|
45
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
46
|
+
if (!isPlainRecord(value)) {
|
|
47
|
+
return { ok: false, message: 'Field "readConcern" must be an object when provided' };
|
|
48
|
+
}
|
|
49
|
+
const keys = Object.keys(value);
|
|
50
|
+
if (keys.length !== 1 || !keys.includes("level")) {
|
|
51
|
+
return { ok: false, message: 'Field "readConcern" must contain only "level"' };
|
|
52
|
+
}
|
|
53
|
+
if (!isReadConcernLevel(value.level)) {
|
|
54
|
+
return { ok: false, message: 'Field "readConcern.level" is invalid' };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, value: { level: value.level } };
|
|
57
|
+
}
|
|
58
|
+
function validateWriteConcern(value) {
|
|
59
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
60
|
+
if (!isPlainRecord(value)) {
|
|
61
|
+
return { ok: false, message: 'Field "writeConcern" must be an object when provided' };
|
|
62
|
+
}
|
|
63
|
+
const allowedKeys = /* @__PURE__ */ new Set(["level", "timeoutMs"]);
|
|
64
|
+
if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
|
|
65
|
+
return { ok: false, message: 'Field "writeConcern" contains unsupported keys' };
|
|
66
|
+
}
|
|
67
|
+
if (!isWriteConcernLevel(value.level)) {
|
|
68
|
+
return { ok: false, message: 'Field "writeConcern.level" is invalid' };
|
|
69
|
+
}
|
|
70
|
+
const timeoutMs = value.timeoutMs;
|
|
71
|
+
if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)) {
|
|
72
|
+
return { ok: false, message: 'Field "writeConcern.timeoutMs" must be a positive safe integer' };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
value: timeoutMs === void 0 ? { level: value.level } : { level: value.level, timeoutMs }
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function isPlainRecord(value) {
|
|
80
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
81
|
+
}
|
|
82
|
+
function isReadConcernLevel(value) {
|
|
83
|
+
return value === "local" || value === "majority" || value === "linearizable";
|
|
84
|
+
}
|
|
85
|
+
function isWriteConcernLevel(value) {
|
|
86
|
+
return value === "local" || value === "majority" || value === "all";
|
|
87
|
+
}
|
|
88
|
+
function transactionStatementsValidationError(value) {
|
|
89
|
+
if (!Array.isArray(value)) {
|
|
90
|
+
return 'Field "statements" is required and must be an array';
|
|
91
|
+
}
|
|
92
|
+
if (value.length === 0) {
|
|
93
|
+
return "Transaction requires at least one statement";
|
|
94
|
+
}
|
|
95
|
+
for (let i = 0; i < value.length; i++) {
|
|
96
|
+
const stmt = value[i];
|
|
97
|
+
if (typeof stmt !== "object" || stmt === null) {
|
|
98
|
+
return `Statement at index ${i} is missing a valid "sql" field`;
|
|
99
|
+
}
|
|
100
|
+
const sql = stmt.sql;
|
|
101
|
+
if (typeof sql !== "string" || sql.length === 0) {
|
|
102
|
+
return `Statement at index ${i} is missing a valid "sql" field`;
|
|
103
|
+
}
|
|
104
|
+
const params = stmt.params;
|
|
105
|
+
if (params !== void 0 && params !== null && typeof params !== "object") {
|
|
106
|
+
return `Statement at index ${i} has invalid "params"`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function paramsBatchValidationError(value) {
|
|
112
|
+
if (!Array.isArray(value)) {
|
|
113
|
+
return 'Field "paramsBatch" is required and must be an array';
|
|
114
|
+
}
|
|
115
|
+
if (value.length === 0) {
|
|
116
|
+
return 'Field "paramsBatch" requires at least one parameter set';
|
|
117
|
+
}
|
|
118
|
+
for (let i = 0; i < value.length; i++) {
|
|
119
|
+
const entry = value[i];
|
|
120
|
+
if (typeof entry !== "object" || entry === null) {
|
|
121
|
+
return `Parameter set at index ${i} must be an object or array`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/server/cors.ts
|
|
128
|
+
function resolveCors(cors) {
|
|
129
|
+
if (!cors) return null;
|
|
130
|
+
if (cors === true) {
|
|
131
|
+
return {
|
|
132
|
+
origin: "*",
|
|
133
|
+
methods: "GET, POST, OPTIONS",
|
|
134
|
+
headers: "Content-Type, Authorization"
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
origin: cors.origin ?? "*",
|
|
139
|
+
methods: cors.methods?.join(", ") ?? "GET, POST, OPTIONS",
|
|
140
|
+
headers: cors.headers?.join(", ") ?? "Content-Type, Authorization"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function matchOrigin(cors, requestOrigin) {
|
|
144
|
+
if (typeof cors.origin === "string") return cors.origin;
|
|
145
|
+
if (cors.origin.includes(requestOrigin)) return requestOrigin;
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function writeCorsOrigin(res, cors, requestOrigin) {
|
|
149
|
+
const allowed = matchOrigin(cors, requestOrigin);
|
|
150
|
+
if (!allowed) return;
|
|
151
|
+
res.writeHeader("Access-Control-Allow-Origin", allowed);
|
|
152
|
+
if (allowed !== "*") {
|
|
153
|
+
res.writeHeader("Vary", "Origin");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
13
156
|
|
|
14
157
|
// src/server/health.ts
|
|
15
158
|
function handleLiveness() {
|
|
@@ -76,10 +219,11 @@ function readinessStatusForReplication(replication, current) {
|
|
|
76
219
|
return current;
|
|
77
220
|
}
|
|
78
221
|
|
|
79
|
-
// src/server/http-
|
|
222
|
+
// src/server/http-common.ts
|
|
80
223
|
function initAbortHandler(res) {
|
|
81
224
|
const listeners = [];
|
|
82
225
|
let aborted = false;
|
|
226
|
+
let claimed = false;
|
|
83
227
|
res.onAborted(() => {
|
|
84
228
|
aborted = true;
|
|
85
229
|
for (const fn of listeners) fn();
|
|
@@ -94,6 +238,11 @@ function initAbortHandler(res) {
|
|
|
94
238
|
} else {
|
|
95
239
|
listeners.push(fn);
|
|
96
240
|
}
|
|
241
|
+
},
|
|
242
|
+
claim() {
|
|
243
|
+
if (aborted || claimed) return false;
|
|
244
|
+
claimed = true;
|
|
245
|
+
return true;
|
|
97
246
|
}
|
|
98
247
|
};
|
|
99
248
|
}
|
|
@@ -116,13 +265,16 @@ function readBody(res, maxBytes, abort) {
|
|
|
116
265
|
totalLength += chunk.byteLength;
|
|
117
266
|
if (totalLength > maxBytes) {
|
|
118
267
|
done = true;
|
|
119
|
-
if (
|
|
268
|
+
if (abort.claim()) {
|
|
120
269
|
sendError(res, 413, "PAYLOAD_TOO_LARGE", "Request body exceeds size limit");
|
|
121
270
|
}
|
|
122
271
|
reject(new Error("Payload too large"));
|
|
123
272
|
return;
|
|
124
273
|
}
|
|
125
|
-
|
|
274
|
+
const bytes = new Uint8Array(chunk);
|
|
275
|
+
const copy = Buffer.allocUnsafe(bytes.byteLength);
|
|
276
|
+
copy.set(bytes);
|
|
277
|
+
chunks.push(copy);
|
|
126
278
|
if (isLast) {
|
|
127
279
|
done = true;
|
|
128
280
|
resolve(Buffer.concat(chunks));
|
|
@@ -148,11 +300,15 @@ function sendJson(res, data) {
|
|
|
148
300
|
res.writeStatus("200 OK").writeHeader("Content-Type", "application/json").end(payload);
|
|
149
301
|
});
|
|
150
302
|
}
|
|
151
|
-
function sendError(res, status, code, message, details) {
|
|
303
|
+
function sendError(res, status, code, message, details, headers) {
|
|
152
304
|
const body = { error: details ? { code, message, details } : { code, message } };
|
|
153
305
|
const payload = JSON.stringify(body);
|
|
154
306
|
res.cork(() => {
|
|
155
|
-
res.writeStatus(`${status}`)
|
|
307
|
+
res.writeStatus(`${status}`);
|
|
308
|
+
if (headers) {
|
|
309
|
+
for (const [name, value] of Object.entries(headers)) res.writeHeader(name, value);
|
|
310
|
+
}
|
|
311
|
+
res.writeHeader("Content-Type", "application/json").end(payload);
|
|
156
312
|
});
|
|
157
313
|
}
|
|
158
314
|
function httpStatusForError(err) {
|
|
@@ -160,9 +316,12 @@ function httpStatusForError(err) {
|
|
|
160
316
|
case "DATABASE_NOT_FOUND":
|
|
161
317
|
return 404;
|
|
162
318
|
case "READ_ONLY":
|
|
319
|
+
case "FORBIDDEN_SQL":
|
|
163
320
|
return 403;
|
|
164
321
|
case "QUERY_ERROR":
|
|
165
322
|
case "TRANSACTION_ERROR":
|
|
323
|
+
case "INVALID_DURABILITY":
|
|
324
|
+
case "INVALID_SYNCHRONOUS":
|
|
166
325
|
return 400;
|
|
167
326
|
case "STALE_PRIMARY":
|
|
168
327
|
case "PROTOCOL_VERSION_MISMATCH":
|
|
@@ -178,16 +337,31 @@ function httpStatusForError(err) {
|
|
|
178
337
|
case "NODE_NOT_IN_SYNC":
|
|
179
338
|
case "NODE_DRAINING":
|
|
180
339
|
case "UNSAFE_RECOVERY_REQUIRED":
|
|
340
|
+
case "WRITE_OVERLOADED":
|
|
181
341
|
return 503;
|
|
182
342
|
default:
|
|
183
343
|
return 500;
|
|
184
344
|
}
|
|
185
345
|
}
|
|
186
|
-
|
|
346
|
+
function errorDetails(err) {
|
|
347
|
+
const details = err.details;
|
|
348
|
+
return details && Object.keys(details).length > 0 ? details : void 0;
|
|
349
|
+
}
|
|
350
|
+
function sendCaughtError(res, abort, err) {
|
|
351
|
+
if (abort.aborted) return;
|
|
352
|
+
if (err instanceof SirannonError) {
|
|
353
|
+
const headers = err instanceof WriteOverloadError ? { "Retry-After": `${Math.ceil(err.retryAfterMs / 1e3)}` } : void 0;
|
|
354
|
+
sendError(res, httpStatusForError(err), err.code, err.message, errorDetails(err), headers);
|
|
355
|
+
} else {
|
|
356
|
+
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function resolveExecutionTarget(res, abort, sirannon, id, resolver) {
|
|
187
360
|
let target;
|
|
188
361
|
try {
|
|
189
362
|
target = resolver ? await resolver(id) : await sirannon.resolve(id);
|
|
190
363
|
} catch (err) {
|
|
364
|
+
if (abort.aborted) return null;
|
|
191
365
|
if (err instanceof SirannonError) {
|
|
192
366
|
sendError(res, httpStatusForError(err), err.code, err.message);
|
|
193
367
|
} else {
|
|
@@ -195,12 +369,51 @@ async function resolveExecutionTarget(res, sirannon, id, resolver) {
|
|
|
195
369
|
}
|
|
196
370
|
return null;
|
|
197
371
|
}
|
|
372
|
+
if (abort.aborted) return null;
|
|
198
373
|
if (!target) {
|
|
199
374
|
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${id}' not found`);
|
|
200
375
|
return null;
|
|
201
376
|
}
|
|
202
377
|
return target;
|
|
203
378
|
}
|
|
379
|
+
function parseReadConcern(res, value) {
|
|
380
|
+
const validation = validateReadConcern(value);
|
|
381
|
+
if (!validation.ok) {
|
|
382
|
+
sendError(res, 400, "INVALID_REQUEST", validation.message);
|
|
383
|
+
return { ok: false };
|
|
384
|
+
}
|
|
385
|
+
return validation;
|
|
386
|
+
}
|
|
387
|
+
function parseWriteConcern(res, value) {
|
|
388
|
+
const validation = validateWriteConcern(value);
|
|
389
|
+
if (!validation.ok) {
|
|
390
|
+
sendError(res, 400, "INVALID_REQUEST", validation.message);
|
|
391
|
+
return { ok: false };
|
|
392
|
+
}
|
|
393
|
+
return validation;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/server/wire-rows.ts
|
|
397
|
+
async function queryWireRows(target, sql, params, options) {
|
|
398
|
+
if (target.queryForWire) {
|
|
399
|
+
return target.queryForWire(sql, params, options);
|
|
400
|
+
}
|
|
401
|
+
return encodeTaggedValues(await target.query(sql, params, options));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/server/http-handler.ts
|
|
405
|
+
function decodeBatchParams(res, raw) {
|
|
406
|
+
const decoded = [];
|
|
407
|
+
for (const entry of raw) {
|
|
408
|
+
const result = decodeBoundParams(entry, "paramsBatch");
|
|
409
|
+
if (!result.ok) {
|
|
410
|
+
sendError(res, 400, "INVALID_REQUEST", result.message);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
decoded.push(result.value);
|
|
414
|
+
}
|
|
415
|
+
return decoded;
|
|
416
|
+
}
|
|
204
417
|
function handleQuery(sirannon, resolveTarget) {
|
|
205
418
|
return async (res, dbId, rawBody, abort) => {
|
|
206
419
|
const body = parseBody(res, rawBody);
|
|
@@ -211,23 +424,24 @@ function handleQuery(sirannon, resolveTarget) {
|
|
|
211
424
|
}
|
|
212
425
|
const readConcern = parseReadConcern(res, body.readConcern);
|
|
213
426
|
if (!readConcern.ok) return;
|
|
214
|
-
const
|
|
427
|
+
const params = decodeBoundParams(body.params, "params");
|
|
428
|
+
if (!params.ok) {
|
|
429
|
+
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
215
433
|
if (!target) return;
|
|
216
434
|
try {
|
|
217
|
-
const rows = await
|
|
435
|
+
const rows = await queryWireRows(
|
|
436
|
+
target,
|
|
218
437
|
body.sql,
|
|
219
|
-
|
|
438
|
+
params.value,
|
|
220
439
|
readConcern.value ? { readConcern: readConcern.value } : void 0
|
|
221
440
|
);
|
|
222
441
|
if (abort.aborted) return;
|
|
223
442
|
sendJson(res, { rows });
|
|
224
443
|
} catch (err) {
|
|
225
|
-
|
|
226
|
-
if (err instanceof SirannonError) {
|
|
227
|
-
sendError(res, httpStatusForError(err), err.code, err.message, errorDetails(err));
|
|
228
|
-
} else {
|
|
229
|
-
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
230
|
-
}
|
|
444
|
+
sendCaughtError(res, abort, err);
|
|
231
445
|
}
|
|
232
446
|
};
|
|
233
447
|
}
|
|
@@ -241,23 +455,23 @@ function handleExecute(sirannon, resolveTarget) {
|
|
|
241
455
|
}
|
|
242
456
|
const writeConcern = parseWriteConcern(res, body.writeConcern);
|
|
243
457
|
if (!writeConcern.ok) return;
|
|
244
|
-
const
|
|
458
|
+
const params = decodeBoundParams(body.params, "params");
|
|
459
|
+
if (!params.ok) {
|
|
460
|
+
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
245
464
|
if (!target) return;
|
|
246
465
|
try {
|
|
247
466
|
const result = await target.execute(
|
|
248
467
|
body.sql,
|
|
249
|
-
|
|
468
|
+
params.value,
|
|
250
469
|
writeConcern.value ? { writeConcern: writeConcern.value } : void 0
|
|
251
470
|
);
|
|
252
471
|
if (abort.aborted) return;
|
|
253
472
|
sendJson(res, toExecuteResponse(result));
|
|
254
473
|
} catch (err) {
|
|
255
|
-
|
|
256
|
-
if (err instanceof SirannonError) {
|
|
257
|
-
sendError(res, httpStatusForError(err), err.code, err.message, errorDetails(err));
|
|
258
|
-
} else {
|
|
259
|
-
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
260
|
-
}
|
|
474
|
+
sendCaughtError(res, abort, err);
|
|
261
475
|
}
|
|
262
476
|
};
|
|
263
477
|
}
|
|
@@ -265,35 +479,64 @@ function handleTransaction(sirannon, resolveTarget) {
|
|
|
265
479
|
return async (res, dbId, rawBody, abort) => {
|
|
266
480
|
const body = parseBody(res, rawBody);
|
|
267
481
|
if (!body) return;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
if (body.statements.length === 0) {
|
|
273
|
-
sendError(res, 400, "INVALID_REQUEST", "Transaction requires at least one statement");
|
|
482
|
+
const statementsError = transactionStatementsValidationError(body.statements);
|
|
483
|
+
if (statementsError !== null) {
|
|
484
|
+
sendError(res, 400, "INVALID_REQUEST", statementsError);
|
|
274
485
|
return;
|
|
275
486
|
}
|
|
276
487
|
const writeConcern = parseWriteConcern(res, body.writeConcern);
|
|
277
488
|
if (!writeConcern.ok) return;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
489
|
+
const statements = [];
|
|
490
|
+
for (const stmt of body.statements) {
|
|
491
|
+
const params = decodeBoundParams(stmt.params, "params");
|
|
492
|
+
if (!params.ok) {
|
|
493
|
+
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
282
494
|
return;
|
|
283
495
|
}
|
|
496
|
+
statements.push({ sql: stmt.sql, params: params.value });
|
|
284
497
|
}
|
|
285
|
-
const target = await resolveExecutionTarget(res, sirannon, dbId, resolveTarget);
|
|
498
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
499
|
+
if (!target) return;
|
|
500
|
+
const txOptions = writeConcern.value ? { writeConcern: writeConcern.value } : void 0;
|
|
501
|
+
try {
|
|
502
|
+
const results = target.executeTransaction ? await target.executeTransaction(statements, txOptions) : await target.transaction(async (tx) => {
|
|
503
|
+
const txResults = [];
|
|
504
|
+
for (const stmt of statements) {
|
|
505
|
+
txResults.push(await tx.execute(stmt.sql, stmt.params));
|
|
506
|
+
}
|
|
507
|
+
return txResults;
|
|
508
|
+
}, txOptions);
|
|
509
|
+
if (abort.aborted) return;
|
|
510
|
+
sendJson(res, {
|
|
511
|
+
results: results.map(toExecuteResponse)
|
|
512
|
+
});
|
|
513
|
+
} catch (err) {
|
|
514
|
+
sendCaughtError(res, abort, err);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function handleBatch(sirannon, resolveTarget) {
|
|
519
|
+
return async (res, dbId, rawBody, abort) => {
|
|
520
|
+
const body = parseBody(res, rawBody);
|
|
521
|
+
if (!body) return;
|
|
522
|
+
if (!body.sql || typeof body.sql !== "string") {
|
|
523
|
+
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const paramsBatchError = paramsBatchValidationError(body.paramsBatch);
|
|
527
|
+
if (paramsBatchError !== null) {
|
|
528
|
+
sendError(res, 400, "INVALID_REQUEST", paramsBatchError);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const writeConcern = parseWriteConcern(res, body.writeConcern);
|
|
532
|
+
if (!writeConcern.ok) return;
|
|
533
|
+
const paramsBatch = decodeBatchParams(res, body.paramsBatch);
|
|
534
|
+
if (paramsBatch === null) return;
|
|
535
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
286
536
|
if (!target) return;
|
|
287
537
|
try {
|
|
288
538
|
const results = await target.transaction(
|
|
289
|
-
async (tx) =>
|
|
290
|
-
const txResults = [];
|
|
291
|
-
for (const stmt of body.statements) {
|
|
292
|
-
if (abort.aborted) throw new Error("Request aborted");
|
|
293
|
-
txResults.push(await tx.execute(stmt.sql, stmt.params));
|
|
294
|
-
}
|
|
295
|
-
return txResults;
|
|
296
|
-
},
|
|
539
|
+
async (tx) => tx.executeBatch(body.sql, paramsBatch),
|
|
297
540
|
writeConcern.value ? { writeConcern: writeConcern.value } : void 0
|
|
298
541
|
);
|
|
299
542
|
if (abort.aborted) return;
|
|
@@ -301,66 +544,51 @@ function handleTransaction(sirannon, resolveTarget) {
|
|
|
301
544
|
results: results.map(toExecuteResponse)
|
|
302
545
|
});
|
|
303
546
|
} catch (err) {
|
|
304
|
-
|
|
305
|
-
if (err instanceof SirannonError) {
|
|
306
|
-
sendError(res, httpStatusForError(err), err.code, err.message, errorDetails(err));
|
|
307
|
-
} else {
|
|
308
|
-
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
309
|
-
}
|
|
547
|
+
sendCaughtError(res, abort, err);
|
|
310
548
|
}
|
|
311
549
|
};
|
|
312
550
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
value: timeoutMs === void 0 ? { level: value.level } : { level: value.level, timeoutMs }
|
|
551
|
+
function handleLoad(sirannon, resolveTarget) {
|
|
552
|
+
return async (res, dbId, rawBody, abort) => {
|
|
553
|
+
const body = parseBody(res, rawBody);
|
|
554
|
+
if (!body) return;
|
|
555
|
+
if (!body.sql || typeof body.sql !== "string") {
|
|
556
|
+
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const paramsBatchError = paramsBatchValidationError(body.paramsBatch);
|
|
560
|
+
if (paramsBatchError !== null) {
|
|
561
|
+
sendError(res, 400, "INVALID_REQUEST", paramsBatchError);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const durabilityError = loadDurabilityValidationError(body.durability);
|
|
565
|
+
if (durabilityError !== null) {
|
|
566
|
+
sendError(res, 400, "INVALID_REQUEST", durabilityError);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const checkpointError = loadCheckpointValidationError(body.checkpoint);
|
|
570
|
+
if (checkpointError !== null) {
|
|
571
|
+
sendError(res, 400, "INVALID_REQUEST", checkpointError);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const paramsBatch = decodeBatchParams(res, body.paramsBatch);
|
|
575
|
+
if (paramsBatch === null) return;
|
|
576
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
577
|
+
if (!target) return;
|
|
578
|
+
const bulkLoad = target.bulkLoad;
|
|
579
|
+
if (typeof bulkLoad !== "function") {
|
|
580
|
+
sendError(res, 501, "BULK_LOAD_UNSUPPORTED", "The execution target for this database does not support bulk load");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const summary = await bulkLoad.call(target, body.sql, paramsBatch, toBulkLoadOptions(body));
|
|
585
|
+
if (abort.aborted) return;
|
|
586
|
+
sendJson(res, summary);
|
|
587
|
+
} catch (err) {
|
|
588
|
+
sendCaughtError(res, abort, err);
|
|
589
|
+
}
|
|
353
590
|
};
|
|
354
591
|
}
|
|
355
|
-
function isPlainRecord(value) {
|
|
356
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
357
|
-
}
|
|
358
|
-
function isReadConcernLevel(value) {
|
|
359
|
-
return value === "local" || value === "majority" || value === "linearizable";
|
|
360
|
-
}
|
|
361
|
-
function isWriteConcernLevel(value) {
|
|
362
|
-
return value === "local" || value === "majority" || value === "all";
|
|
363
|
-
}
|
|
364
592
|
function handleClusterStatus(getClusterStatus) {
|
|
365
593
|
return (res, dbId) => {
|
|
366
594
|
if (!getClusterStatus) {
|
|
@@ -386,26 +614,421 @@ function toClusterStatusResponse(status) {
|
|
|
386
614
|
primaryTerm: status.primaryTerm?.toString()
|
|
387
615
|
};
|
|
388
616
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
617
|
+
|
|
618
|
+
// src/server/request-hook.ts
|
|
619
|
+
function decodeRemoteAddress(res) {
|
|
620
|
+
return Buffer.from(res.getRemoteAddressAsText()).toString();
|
|
621
|
+
}
|
|
622
|
+
function isRequestDenial(value) {
|
|
623
|
+
return typeof value === "object" && value !== null && "status" in value;
|
|
624
|
+
}
|
|
625
|
+
async function runOnRequest(res, abort, ctx, hook) {
|
|
626
|
+
try {
|
|
627
|
+
const result = await hook(ctx);
|
|
628
|
+
if (isRequestDenial(result)) {
|
|
629
|
+
if (abort.claim()) sendError(res, result.status, result.code, result.message);
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return true;
|
|
633
|
+
} catch {
|
|
634
|
+
if (abort.claim()) sendError(res, 500, "HOOK_ERROR", "onRequest hook threw an error");
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
var EPOCH_KEY = "cdc_epoch";
|
|
639
|
+
async function ensureCdcEpoch(conn) {
|
|
640
|
+
await conn.exec(`CREATE TABLE IF NOT EXISTS "${META_TABLE}" (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);
|
|
641
|
+
const insert = await conn.prepare(`INSERT OR IGNORE INTO "${META_TABLE}" (key, value) VALUES (?, ?)`);
|
|
642
|
+
await insert.run(EPOCH_KEY, randomBytes(16).toString("hex"));
|
|
643
|
+
const select = await conn.prepare(`SELECT value FROM "${META_TABLE}" WHERE key = ?`);
|
|
644
|
+
const row = await select.get(EPOCH_KEY);
|
|
645
|
+
const value = row?.value;
|
|
646
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
647
|
+
throw new CDCError("Failed to read the CDC epoch identifier");
|
|
648
|
+
}
|
|
649
|
+
return value;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/server/ws-cdc.ts
|
|
653
|
+
var DEFAULT_POLL_INTERVAL_MS = 50;
|
|
654
|
+
var CLEANUP_INTERVAL_TICKS = 100;
|
|
655
|
+
var CdcContextRegistry = class {
|
|
656
|
+
sirannon;
|
|
657
|
+
retentionMs;
|
|
658
|
+
contexts = /* @__PURE__ */ new Map();
|
|
659
|
+
pending = /* @__PURE__ */ new Map();
|
|
660
|
+
closed = false;
|
|
661
|
+
constructor(sirannon, retentionMs) {
|
|
662
|
+
this.sirannon = sirannon;
|
|
663
|
+
this.retentionMs = retentionMs;
|
|
664
|
+
}
|
|
665
|
+
async ensure(databaseId, database) {
|
|
666
|
+
const existing = this.contexts.get(databaseId);
|
|
667
|
+
if (existing) return existing;
|
|
668
|
+
const pending = this.pending.get(databaseId);
|
|
669
|
+
if (pending) return pending;
|
|
670
|
+
const promise = this.createContext(database);
|
|
671
|
+
this.pending.set(databaseId, promise);
|
|
672
|
+
try {
|
|
673
|
+
const ctx = await promise;
|
|
674
|
+
if (this.closed) {
|
|
675
|
+
ctx.stopPolling();
|
|
676
|
+
await ctx.cdcConn.close().catch(() => {
|
|
677
|
+
});
|
|
678
|
+
throw new SirannonError("WebSocket handler is shut down", "HANDLER_CLOSED");
|
|
679
|
+
}
|
|
680
|
+
this.contexts.set(databaseId, ctx);
|
|
681
|
+
return ctx;
|
|
682
|
+
} finally {
|
|
683
|
+
this.pending.delete(databaseId);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
get(databaseId) {
|
|
687
|
+
return this.contexts.get(databaseId);
|
|
688
|
+
}
|
|
689
|
+
maybeCleanup(databaseId) {
|
|
690
|
+
const ctx = this.contexts.get(databaseId);
|
|
691
|
+
if (!ctx || ctx.manager.size > 0) return;
|
|
692
|
+
ctx.stopPolling();
|
|
693
|
+
ctx.cdcConn.close().catch(() => {
|
|
694
|
+
});
|
|
695
|
+
this.contexts.delete(databaseId);
|
|
696
|
+
}
|
|
697
|
+
async closeAll() {
|
|
698
|
+
if (this.closed) return;
|
|
699
|
+
this.closed = true;
|
|
700
|
+
for (const ctx of this.contexts.values()) {
|
|
701
|
+
ctx.stopPolling();
|
|
702
|
+
try {
|
|
703
|
+
await ctx.cdcConn.close();
|
|
704
|
+
} catch {
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
this.contexts.clear();
|
|
708
|
+
}
|
|
709
|
+
async createContext(database) {
|
|
710
|
+
const cdcConn = await this.sirannon.driver.open(database.path, { walMode: true });
|
|
711
|
+
const tracker = new ChangeTracker(this.retentionMs === void 0 ? void 0 : { retention: this.retentionMs });
|
|
712
|
+
const manager = new SubscriptionManager();
|
|
713
|
+
let epoch = "";
|
|
714
|
+
try {
|
|
715
|
+
await tracker.advanceToLatest(cdcConn);
|
|
716
|
+
await database.runCdcMaintenance(async (writer) => {
|
|
717
|
+
epoch = await ensureCdcEpoch(writer);
|
|
718
|
+
});
|
|
719
|
+
} catch (err) {
|
|
720
|
+
await cdcConn.close().catch(() => {
|
|
721
|
+
});
|
|
722
|
+
throw err;
|
|
723
|
+
}
|
|
724
|
+
if (epoch === "") {
|
|
725
|
+
await cdcConn.close().catch(() => {
|
|
726
|
+
});
|
|
727
|
+
throw new SirannonError(`Database '${database.id}' is closed`, "DATABASE_CLOSED");
|
|
728
|
+
}
|
|
729
|
+
let polling = false;
|
|
730
|
+
let consecutiveErrors = 0;
|
|
731
|
+
let tickCount = 0;
|
|
732
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
733
|
+
const stopPolling = () => {
|
|
734
|
+
clearInterval(interval);
|
|
735
|
+
};
|
|
736
|
+
const tick = async () => {
|
|
737
|
+
if (manager.size === 0) return;
|
|
738
|
+
if (polling) return;
|
|
739
|
+
polling = true;
|
|
740
|
+
try {
|
|
741
|
+
const events = await tracker.poll(cdcConn);
|
|
742
|
+
if (events.length > 0) {
|
|
743
|
+
manager.dispatch(events);
|
|
744
|
+
}
|
|
745
|
+
consecutiveErrors = 0;
|
|
746
|
+
tickCount++;
|
|
747
|
+
if (tickCount >= CLEANUP_INTERVAL_TICKS) {
|
|
748
|
+
tickCount = 0;
|
|
749
|
+
await database.runCdcMaintenance((writer) => tracker.cleanup(writer)).catch(() => {
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
} catch {
|
|
753
|
+
consecutiveErrors++;
|
|
754
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
755
|
+
stopPolling();
|
|
756
|
+
}
|
|
757
|
+
} finally {
|
|
758
|
+
polling = false;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
const interval = setInterval(tick, DEFAULT_POLL_INTERVAL_MS);
|
|
762
|
+
interval.unref?.();
|
|
763
|
+
return { cdcConn, tracker, manager, stopPolling, epoch };
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// src/server/ws-cdc-resume.ts
|
|
768
|
+
var REPLAY_BATCH_SIZE = 1e3;
|
|
769
|
+
var DEFAULT_PRIME_BUFFER_LIMIT = 1e4;
|
|
770
|
+
var DEFAULT_PRIME_BUFFER_BYTES = 16 * 1048576;
|
|
771
|
+
function approxEventBytes(event) {
|
|
772
|
+
let bytes = 0;
|
|
773
|
+
if (event.row) bytes += JSON.stringify(encodeTaggedValues(event.row)).length;
|
|
774
|
+
if (event.oldRow) bytes += JSON.stringify(encodeTaggedValues(event.oldRow)).length;
|
|
775
|
+
return bytes;
|
|
776
|
+
}
|
|
777
|
+
function needsResync(sinceSeq, minSeq, boundary) {
|
|
778
|
+
if (sinceSeq >= boundary) return false;
|
|
779
|
+
if (minSeq === null) return true;
|
|
780
|
+
return minSeq > sinceSeq + 1n;
|
|
781
|
+
}
|
|
782
|
+
var PrimedSubscription = class {
|
|
783
|
+
constructor(sinceSeq, deliver, onOverload, bufferLimit = DEFAULT_PRIME_BUFFER_LIMIT, bufferByteLimit = DEFAULT_PRIME_BUFFER_BYTES) {
|
|
784
|
+
this.sinceSeq = sinceSeq;
|
|
785
|
+
this.deliver = deliver;
|
|
786
|
+
this.onOverload = onOverload;
|
|
787
|
+
this.bufferLimit = bufferLimit;
|
|
788
|
+
this.bufferByteLimit = bufferByteLimit;
|
|
789
|
+
}
|
|
790
|
+
mode = "priming";
|
|
791
|
+
buffer = [];
|
|
792
|
+
bufferBytes = 0;
|
|
793
|
+
bufferLimit;
|
|
794
|
+
bufferByteLimit;
|
|
795
|
+
onLiveEvent(event) {
|
|
796
|
+
if (this.mode === "stopped") return;
|
|
797
|
+
if (this.mode === "live") {
|
|
798
|
+
this.emit(event);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const nextBytes = this.bufferBytes + approxEventBytes(event);
|
|
802
|
+
if (this.buffer.length >= this.bufferLimit || nextBytes > this.bufferByteLimit) {
|
|
803
|
+
this.stop();
|
|
804
|
+
this.onOverload();
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
this.buffer.push(event);
|
|
808
|
+
this.bufferBytes = nextBytes;
|
|
809
|
+
}
|
|
810
|
+
async replay(tracker, conn, table, filter, boundary) {
|
|
811
|
+
let cursor = this.sinceSeq;
|
|
812
|
+
while (this.mode === "priming" && cursor < boundary) {
|
|
813
|
+
const events = await tracker.readSince(conn, table, cursor, boundary, REPLAY_BATCH_SIZE);
|
|
814
|
+
if (events.length === 0) break;
|
|
815
|
+
for (const event of events) {
|
|
816
|
+
if (filter && !changeMatchesFilter(event, filter)) continue;
|
|
817
|
+
if (!this.emit(event)) return;
|
|
818
|
+
}
|
|
819
|
+
const lastEvent = events[events.length - 1];
|
|
820
|
+
if (lastEvent === void 0) break;
|
|
821
|
+
cursor = lastEvent.seq;
|
|
822
|
+
if (events.length < REPLAY_BATCH_SIZE) break;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
goLive() {
|
|
826
|
+
if (this.mode !== "priming") return;
|
|
827
|
+
const pending = this.buffer;
|
|
828
|
+
this.buffer = [];
|
|
829
|
+
for (const event of pending) {
|
|
830
|
+
if (!this.emit(event)) return;
|
|
831
|
+
}
|
|
832
|
+
if (this.mode === "priming") {
|
|
833
|
+
this.mode = "live";
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
emit(event) {
|
|
837
|
+
if (this.mode === "stopped") return false;
|
|
838
|
+
if (event.seq <= this.sinceSeq) return true;
|
|
839
|
+
const outcome = this.deliver(event);
|
|
840
|
+
if (outcome === "dropped") {
|
|
841
|
+
this.stop();
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
stop() {
|
|
847
|
+
this.mode = "stopped";
|
|
848
|
+
this.buffer = [];
|
|
849
|
+
this.bufferBytes = 0;
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// src/server/ws-connection.ts
|
|
854
|
+
var WS_CLOSE_OVERLOADED = 4290;
|
|
855
|
+
|
|
856
|
+
// src/server/ws-operations.ts
|
|
857
|
+
function isValidParams(params) {
|
|
858
|
+
if (params === void 0 || params === null) return true;
|
|
859
|
+
return typeof params === "object";
|
|
860
|
+
}
|
|
861
|
+
function decodeBatchParams2(ctx, id, raw) {
|
|
862
|
+
const decoded = [];
|
|
863
|
+
for (const entry of raw) {
|
|
864
|
+
const result = decodeBoundParams(entry, "paramsBatch");
|
|
865
|
+
if (!result.ok) {
|
|
866
|
+
ctx.sendError(id, "INVALID_MESSAGE", result.message);
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
decoded.push(result.value);
|
|
870
|
+
}
|
|
871
|
+
return decoded;
|
|
872
|
+
}
|
|
873
|
+
async function handleQueryMessage(ctx, msg, id) {
|
|
874
|
+
if (typeof msg.sql !== "string") {
|
|
875
|
+
ctx.sendError(id, "INVALID_MESSAGE", 'Query message requires a "sql" string field');
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (!isValidParams(msg.params)) {
|
|
879
|
+
ctx.sendError(id, "INVALID_MESSAGE", '"params" must be an object or array');
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const params = decodeBoundParams(msg.params, "params");
|
|
883
|
+
if (!params.ok) {
|
|
884
|
+
ctx.sendError(id, "INVALID_MESSAGE", params.message);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
const rows = await queryWireRows(ctx.target, msg.sql, params.value);
|
|
889
|
+
ctx.sendResult(id, { rows });
|
|
890
|
+
} catch (err) {
|
|
891
|
+
ctx.sendCaughtError(id, err);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function handleExecuteMessage(ctx, msg, id) {
|
|
895
|
+
if (typeof msg.sql !== "string") {
|
|
896
|
+
ctx.sendError(id, "INVALID_MESSAGE", 'Execute message requires a "sql" string field');
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (!isValidParams(msg.params)) {
|
|
900
|
+
ctx.sendError(id, "INVALID_MESSAGE", '"params" must be an object or array');
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const params = decodeBoundParams(msg.params, "params");
|
|
904
|
+
if (!params.ok) {
|
|
905
|
+
ctx.sendError(id, "INVALID_MESSAGE", params.message);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const result = await ctx.target.execute(msg.sql, params.value);
|
|
910
|
+
ctx.sendResult(id, toExecuteResponse(result));
|
|
911
|
+
} catch (err) {
|
|
912
|
+
ctx.sendCaughtError(id, err);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
async function handleTransactionMessage(ctx, msg, id) {
|
|
916
|
+
const statementsError = transactionStatementsValidationError(msg.statements);
|
|
917
|
+
if (statementsError !== null) {
|
|
918
|
+
ctx.sendError(id, "INVALID_MESSAGE", statementsError);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const writeConcern = validateWriteConcern(msg.writeConcern);
|
|
922
|
+
if (!writeConcern.ok) {
|
|
923
|
+
ctx.sendError(id, "INVALID_MESSAGE", writeConcern.message);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const validStatements = msg.statements;
|
|
927
|
+
const statements = [];
|
|
928
|
+
for (const stmt of validStatements) {
|
|
929
|
+
const params = decodeBoundParams(stmt.params, "params");
|
|
930
|
+
if (!params.ok) {
|
|
931
|
+
ctx.sendError(id, "INVALID_MESSAGE", params.message);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
statements.push({ sql: stmt.sql, params: params.value });
|
|
935
|
+
}
|
|
936
|
+
const txOptions = writeConcern.value ? { writeConcern: writeConcern.value } : void 0;
|
|
937
|
+
try {
|
|
938
|
+
const results = ctx.target.executeTransaction ? await ctx.target.executeTransaction(statements, txOptions) : await ctx.target.transaction(async (tx) => {
|
|
939
|
+
const txResults = [];
|
|
940
|
+
for (const stmt of statements) {
|
|
941
|
+
txResults.push(await tx.execute(stmt.sql, stmt.params));
|
|
942
|
+
}
|
|
943
|
+
return txResults;
|
|
944
|
+
}, txOptions);
|
|
945
|
+
ctx.sendResult(id, { results: results.map(toExecuteResponse) });
|
|
946
|
+
} catch (err) {
|
|
947
|
+
ctx.sendCaughtError(id, err);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
async function handleBatchMessage(ctx, msg, id) {
|
|
951
|
+
if (typeof msg.sql !== "string") {
|
|
952
|
+
ctx.sendError(id, "INVALID_MESSAGE", 'Batch message requires a "sql" string field');
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const paramsBatchError = paramsBatchValidationError(msg.paramsBatch);
|
|
956
|
+
if (paramsBatchError !== null) {
|
|
957
|
+
ctx.sendError(id, "INVALID_MESSAGE", paramsBatchError);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const writeConcern = validateWriteConcern(msg.writeConcern);
|
|
961
|
+
if (!writeConcern.ok) {
|
|
962
|
+
ctx.sendError(id, "INVALID_MESSAGE", writeConcern.message);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const sql = msg.sql;
|
|
966
|
+
const paramsBatch = decodeBatchParams2(ctx, id, msg.paramsBatch);
|
|
967
|
+
if (paramsBatch === null) return;
|
|
968
|
+
try {
|
|
969
|
+
const results = await ctx.target.transaction(
|
|
970
|
+
async (tx) => tx.executeBatch(sql, paramsBatch),
|
|
971
|
+
writeConcern.value ? { writeConcern: writeConcern.value } : void 0
|
|
972
|
+
);
|
|
973
|
+
ctx.sendResult(id, { results: results.map(toExecuteResponse) });
|
|
974
|
+
} catch (err) {
|
|
975
|
+
ctx.sendCaughtError(id, err);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async function handleLoadMessage(ctx, msg, id) {
|
|
979
|
+
if (typeof msg.sql !== "string") {
|
|
980
|
+
ctx.sendError(id, "INVALID_MESSAGE", 'Load message requires a "sql" string field');
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const paramsBatchError = paramsBatchValidationError(msg.paramsBatch);
|
|
984
|
+
if (paramsBatchError !== null) {
|
|
985
|
+
ctx.sendError(id, "INVALID_MESSAGE", paramsBatchError);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const durabilityError = loadDurabilityValidationError(msg.durability);
|
|
989
|
+
if (durabilityError !== null) {
|
|
990
|
+
ctx.sendError(id, "INVALID_MESSAGE", durabilityError);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const checkpointError = loadCheckpointValidationError(msg.checkpoint);
|
|
994
|
+
if (checkpointError !== null) {
|
|
995
|
+
ctx.sendError(id, "INVALID_MESSAGE", checkpointError);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const bulkLoad = ctx.target.bulkLoad;
|
|
999
|
+
if (typeof bulkLoad !== "function") {
|
|
1000
|
+
ctx.sendError(id, "BULK_LOAD_UNSUPPORTED", "The execution target for this database does not support bulk load");
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const sql = msg.sql;
|
|
1004
|
+
const paramsBatch = decodeBatchParams2(ctx, id, msg.paramsBatch);
|
|
1005
|
+
if (paramsBatch === null) return;
|
|
1006
|
+
const options = toBulkLoadOptions({
|
|
1007
|
+
durability: msg.durability,
|
|
1008
|
+
checkpoint: msg.checkpoint
|
|
1009
|
+
});
|
|
1010
|
+
try {
|
|
1011
|
+
const summary = await bulkLoad.call(ctx.target, sql, paramsBatch, options);
|
|
1012
|
+
ctx.sendResult(id, summary);
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
ctx.sendCaughtError(id, err);
|
|
1015
|
+
}
|
|
392
1016
|
}
|
|
393
1017
|
|
|
394
1018
|
// src/server/ws-handler.ts
|
|
395
|
-
var DEFAULT_POLL_INTERVAL_MS = 50;
|
|
396
1019
|
var DEFAULT_MAX_PAYLOAD_LENGTH = 1048576;
|
|
397
1020
|
var WSHandler = class {
|
|
398
1021
|
sirannon;
|
|
399
1022
|
maxPayloadLength;
|
|
400
1023
|
resolveExecutionTarget;
|
|
401
1024
|
connections = /* @__PURE__ */ new Map();
|
|
402
|
-
|
|
403
|
-
cdcPending = /* @__PURE__ */ new Map();
|
|
1025
|
+
cdc;
|
|
404
1026
|
closed = false;
|
|
405
1027
|
constructor(sirannon, options) {
|
|
406
1028
|
this.sirannon = sirannon;
|
|
407
1029
|
this.maxPayloadLength = options?.maxPayloadLength ?? DEFAULT_MAX_PAYLOAD_LENGTH;
|
|
408
1030
|
this.resolveExecutionTarget = options?.resolveExecutionTarget;
|
|
1031
|
+
this.cdc = new CdcContextRegistry(sirannon, options?.cdcRetentionMs);
|
|
409
1032
|
}
|
|
410
1033
|
async handleOpen(conn, databaseId) {
|
|
411
1034
|
if (this.closed) {
|
|
@@ -441,13 +1064,25 @@ var WSHandler = class {
|
|
|
441
1064
|
databaseId,
|
|
442
1065
|
database,
|
|
443
1066
|
executionTarget,
|
|
444
|
-
subscriptions: /* @__PURE__ */ new Map()
|
|
1067
|
+
subscriptions: /* @__PURE__ */ new Map(),
|
|
1068
|
+
overloaded: false
|
|
445
1069
|
});
|
|
446
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Tears down a connection whose outbound buffer overflowed. Closing lets the
|
|
1073
|
+
* client reject in-flight requests and reconnect; guarding on `overloaded`
|
|
1074
|
+
* keeps a burst of dropped frames from repeatedly re-closing the socket.
|
|
1075
|
+
*/
|
|
1076
|
+
handleOverload(conn) {
|
|
1077
|
+
const state = this.connections.get(conn);
|
|
1078
|
+
if (!state || state.overloaded) return;
|
|
1079
|
+
state.overloaded = true;
|
|
1080
|
+
conn.close(WS_CLOSE_OVERLOADED, "Connection overloaded: backpressure limit exceeded");
|
|
1081
|
+
}
|
|
447
1082
|
handleMessage(conn, data) {
|
|
448
1083
|
const state = this.connections.get(conn);
|
|
449
1084
|
if (!state) return;
|
|
450
|
-
if (data
|
|
1085
|
+
if (Buffer.byteLength(data) > this.maxPayloadLength) {
|
|
451
1086
|
this.sendError(conn, "", "PAYLOAD_TOO_LARGE", "Message exceeds maximum payload length");
|
|
452
1087
|
return;
|
|
453
1088
|
}
|
|
@@ -467,16 +1102,26 @@ var WSHandler = class {
|
|
|
467
1102
|
return;
|
|
468
1103
|
}
|
|
469
1104
|
if (typeof msg.id !== "string") {
|
|
470
|
-
|
|
1105
|
+
const echoId = typeof msg.id === "number" || typeof msg.id === "bigint" ? String(msg.id) : "";
|
|
1106
|
+
this.sendError(conn, echoId, "INVALID_MESSAGE", 'Message must have a string "id" field');
|
|
471
1107
|
return;
|
|
472
1108
|
}
|
|
473
1109
|
const id = msg.id;
|
|
474
1110
|
switch (msg.type) {
|
|
475
1111
|
case "query":
|
|
476
|
-
this.
|
|
1112
|
+
handleQueryMessage(this.operationContext(conn, state), msg, id);
|
|
477
1113
|
break;
|
|
478
1114
|
case "execute":
|
|
479
|
-
this.
|
|
1115
|
+
handleExecuteMessage(this.operationContext(conn, state), msg, id);
|
|
1116
|
+
break;
|
|
1117
|
+
case "transaction":
|
|
1118
|
+
handleTransactionMessage(this.operationContext(conn, state), msg, id);
|
|
1119
|
+
break;
|
|
1120
|
+
case "batch":
|
|
1121
|
+
handleBatchMessage(this.operationContext(conn, state), msg, id);
|
|
1122
|
+
break;
|
|
1123
|
+
case "load":
|
|
1124
|
+
handleLoadMessage(this.operationContext(conn, state), msg, id);
|
|
480
1125
|
break;
|
|
481
1126
|
case "subscribe":
|
|
482
1127
|
this.handleSubscribe(conn, state, msg, id);
|
|
@@ -495,7 +1140,7 @@ var WSHandler = class {
|
|
|
495
1140
|
sub.unsubscribe();
|
|
496
1141
|
}
|
|
497
1142
|
state.subscriptions.clear();
|
|
498
|
-
this.
|
|
1143
|
+
this.cdc.maybeCleanup(state.databaseId);
|
|
499
1144
|
this.connections.delete(conn);
|
|
500
1145
|
}
|
|
501
1146
|
get connectionCount() {
|
|
@@ -512,52 +1157,15 @@ var WSHandler = class {
|
|
|
512
1157
|
conn.close(1001, "Handler shutting down");
|
|
513
1158
|
}
|
|
514
1159
|
this.connections.clear();
|
|
515
|
-
|
|
516
|
-
ctx.stopPolling();
|
|
517
|
-
try {
|
|
518
|
-
await ctx.cdcConn.close();
|
|
519
|
-
} catch {
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
this.cdcContexts.clear();
|
|
523
|
-
}
|
|
524
|
-
async handleQuery(conn, state, msg, id) {
|
|
525
|
-
if (typeof msg.sql !== "string") {
|
|
526
|
-
this.sendError(conn, id, "INVALID_MESSAGE", 'Query message requires a "sql" string field');
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
if (!this.isValidParams(msg.params)) {
|
|
530
|
-
this.sendError(conn, id, "INVALID_MESSAGE", '"params" must be an object or array');
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
try {
|
|
534
|
-
const params = msg.params ?? void 0;
|
|
535
|
-
const rows = await state.executionTarget.query(msg.sql, params);
|
|
536
|
-
this.send(conn, { type: "result", id, data: { rows } });
|
|
537
|
-
} catch (err) {
|
|
538
|
-
this.sendSirannonError(conn, id, err);
|
|
539
|
-
}
|
|
1160
|
+
await this.cdc.closeAll();
|
|
540
1161
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
try {
|
|
551
|
-
const params = msg.params ?? void 0;
|
|
552
|
-
const result = await state.executionTarget.execute(msg.sql, params);
|
|
553
|
-
this.send(conn, {
|
|
554
|
-
type: "result",
|
|
555
|
-
id,
|
|
556
|
-
data: toExecuteResponse(result)
|
|
557
|
-
});
|
|
558
|
-
} catch (err) {
|
|
559
|
-
this.sendSirannonError(conn, id, err);
|
|
560
|
-
}
|
|
1162
|
+
operationContext(conn, state) {
|
|
1163
|
+
return {
|
|
1164
|
+
target: state.executionTarget,
|
|
1165
|
+
sendResult: (id, data) => this.send(conn, { type: "result", id, data }),
|
|
1166
|
+
sendError: (id, code, message) => this.sendError(conn, id, code, message),
|
|
1167
|
+
sendCaughtError: (id, err) => this.sendSirannonError(conn, id, err)
|
|
1168
|
+
};
|
|
561
1169
|
}
|
|
562
1170
|
async handleSubscribe(conn, state, msg, id) {
|
|
563
1171
|
if (typeof msg.table !== "string") {
|
|
@@ -580,22 +1188,88 @@ var WSHandler = class {
|
|
|
580
1188
|
this.sendError(conn, id, "INVALID_MESSAGE", '"filter" must be a plain object');
|
|
581
1189
|
return;
|
|
582
1190
|
}
|
|
583
|
-
const
|
|
584
|
-
|
|
1191
|
+
const decodedFilter = decodeBoundParams(msg.filter, "filter");
|
|
1192
|
+
if (!decodedFilter.ok) {
|
|
1193
|
+
this.sendError(conn, id, "INVALID_MESSAGE", decodedFilter.message);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const filter = decodedFilter.value;
|
|
1197
|
+
let sinceSeq;
|
|
1198
|
+
if (msg.sinceSeq !== void 0) {
|
|
1199
|
+
if (typeof msg.sinceSeq !== "string" || !/^\d+$/.test(msg.sinceSeq)) {
|
|
1200
|
+
this.sendError(conn, id, "INVALID_MESSAGE", '"sinceSeq" must be a non-negative integer string');
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
sinceSeq = BigInt(msg.sinceSeq);
|
|
1204
|
+
}
|
|
1205
|
+
let clientEpoch;
|
|
1206
|
+
if (msg.epoch !== void 0) {
|
|
1207
|
+
if (typeof msg.epoch !== "string") {
|
|
1208
|
+
this.sendError(conn, id, "INVALID_MESSAGE", '"epoch" must be a string');
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
clientEpoch = msg.epoch;
|
|
1212
|
+
}
|
|
1213
|
+
if (sinceSeq === void 0) {
|
|
1214
|
+
await this.subscribeLive(conn, state, id, msg.table, filter);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
await this.subscribeResuming(conn, state, id, msg.table, filter, sinceSeq, clientEpoch);
|
|
1218
|
+
}
|
|
1219
|
+
async subscribeLive(conn, state, id, table, filter) {
|
|
585
1220
|
try {
|
|
586
|
-
ctx = await this.
|
|
587
|
-
await ctx.tracker.watch(ctx.cdcConn,
|
|
588
|
-
const
|
|
1221
|
+
const ctx = await this.cdc.ensure(state.databaseId, state.database);
|
|
1222
|
+
await ctx.tracker.watch(ctx.cdcConn, table);
|
|
1223
|
+
const boundary = ctx.tracker.cursor;
|
|
1224
|
+
const sub = ctx.manager.subscribe(table, filter, (event) => {
|
|
589
1225
|
this.sendChange(conn, id, event);
|
|
590
1226
|
});
|
|
591
1227
|
state.subscriptions.set(id, sub);
|
|
592
|
-
this.send(conn, { type: "subscribed", id });
|
|
1228
|
+
this.send(conn, { type: "subscribed", id, seq: boundary.toString(), epoch: ctx.epoch });
|
|
593
1229
|
} catch (err) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1230
|
+
this.cdc.maybeCleanup(state.databaseId);
|
|
1231
|
+
this.sendSirannonError(conn, id, err);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
async subscribeResuming(conn, state, id, table, filter, sinceSeq, clientEpoch) {
|
|
1235
|
+
let ctx;
|
|
1236
|
+
let primed;
|
|
1237
|
+
let boundary;
|
|
1238
|
+
let resync;
|
|
1239
|
+
try {
|
|
1240
|
+
ctx = await this.cdc.ensure(state.databaseId, state.database);
|
|
1241
|
+
await ctx.tracker.watch(ctx.cdcConn, table);
|
|
1242
|
+
boundary = ctx.tracker.cursor;
|
|
1243
|
+
primed = new PrimedSubscription(
|
|
1244
|
+
sinceSeq,
|
|
1245
|
+
(event) => this.sendChange(conn, id, event),
|
|
1246
|
+
() => this.handleOverload(conn)
|
|
1247
|
+
);
|
|
1248
|
+
const sub = ctx.manager.subscribe(table, filter, (event) => primed.onLiveEvent(event));
|
|
1249
|
+
state.subscriptions.set(id, sub);
|
|
1250
|
+
const minSeq = await ctx.tracker.getMinSeq(ctx.cdcConn);
|
|
1251
|
+
const foreignEpoch = clientEpoch !== void 0 && clientEpoch !== ctx.epoch;
|
|
1252
|
+
resync = foreignEpoch || needsResync(sinceSeq, minSeq, boundary);
|
|
1253
|
+
this.send(conn, {
|
|
1254
|
+
type: "subscribed",
|
|
1255
|
+
id,
|
|
1256
|
+
seq: boundary.toString(),
|
|
1257
|
+
epoch: ctx.epoch,
|
|
1258
|
+
...resync ? { resync: true } : {}
|
|
1259
|
+
});
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
this.cdc.maybeCleanup(state.databaseId);
|
|
597
1262
|
this.sendSirannonError(conn, id, err);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (!resync) {
|
|
1266
|
+
try {
|
|
1267
|
+
await primed.replay(ctx.tracker, ctx.cdcConn, table, filter, boundary);
|
|
1268
|
+
} catch {
|
|
1269
|
+
this.send(conn, { type: "subscribed", id, seq: boundary.toString(), epoch: ctx.epoch, resync: true });
|
|
1270
|
+
}
|
|
598
1271
|
}
|
|
1272
|
+
primed.goLive();
|
|
599
1273
|
}
|
|
600
1274
|
handleUnsubscribe(conn, state, id) {
|
|
601
1275
|
const sub = state.subscriptions.get(id);
|
|
@@ -606,80 +1280,7 @@ var WSHandler = class {
|
|
|
606
1280
|
sub.unsubscribe();
|
|
607
1281
|
state.subscriptions.delete(id);
|
|
608
1282
|
this.send(conn, { type: "unsubscribed", id });
|
|
609
|
-
this.
|
|
610
|
-
}
|
|
611
|
-
async ensureCDC(databaseId, database) {
|
|
612
|
-
const existing = this.cdcContexts.get(databaseId);
|
|
613
|
-
if (existing) return existing;
|
|
614
|
-
const pending = this.cdcPending.get(databaseId);
|
|
615
|
-
if (pending) return pending;
|
|
616
|
-
const promise = this.createCDCContext(database);
|
|
617
|
-
this.cdcPending.set(databaseId, promise);
|
|
618
|
-
try {
|
|
619
|
-
const ctx = await promise;
|
|
620
|
-
if (this.closed) {
|
|
621
|
-
ctx.stopPolling();
|
|
622
|
-
await ctx.cdcConn.close().catch(() => {
|
|
623
|
-
});
|
|
624
|
-
throw new SirannonError("WebSocket handler is shut down", "HANDLER_CLOSED");
|
|
625
|
-
}
|
|
626
|
-
this.cdcContexts.set(databaseId, ctx);
|
|
627
|
-
return ctx;
|
|
628
|
-
} finally {
|
|
629
|
-
this.cdcPending.delete(databaseId);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
async createCDCContext(database) {
|
|
633
|
-
const cdcConn = await this.sirannon.driver.open(database.path, { walMode: true });
|
|
634
|
-
const tracker = new ChangeTracker();
|
|
635
|
-
const manager = new SubscriptionManager();
|
|
636
|
-
try {
|
|
637
|
-
await tracker.advanceToLatest(cdcConn);
|
|
638
|
-
} catch (err) {
|
|
639
|
-
await cdcConn.close().catch(() => {
|
|
640
|
-
});
|
|
641
|
-
throw err;
|
|
642
|
-
}
|
|
643
|
-
let polling = false;
|
|
644
|
-
let consecutiveErrors = 0;
|
|
645
|
-
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
646
|
-
const stopPolling = () => {
|
|
647
|
-
clearInterval(interval);
|
|
648
|
-
};
|
|
649
|
-
const tick = async () => {
|
|
650
|
-
if (manager.size === 0) return;
|
|
651
|
-
if (polling) return;
|
|
652
|
-
polling = true;
|
|
653
|
-
try {
|
|
654
|
-
const events = await tracker.poll(cdcConn);
|
|
655
|
-
if (events.length > 0) {
|
|
656
|
-
manager.dispatch(events);
|
|
657
|
-
}
|
|
658
|
-
consecutiveErrors = 0;
|
|
659
|
-
} catch {
|
|
660
|
-
consecutiveErrors++;
|
|
661
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
662
|
-
stopPolling();
|
|
663
|
-
}
|
|
664
|
-
} finally {
|
|
665
|
-
polling = false;
|
|
666
|
-
}
|
|
667
|
-
};
|
|
668
|
-
const interval = setInterval(tick, DEFAULT_POLL_INTERVAL_MS);
|
|
669
|
-
interval.unref?.();
|
|
670
|
-
return { cdcConn, tracker, manager, stopPolling };
|
|
671
|
-
}
|
|
672
|
-
maybeCleanupCDC(databaseId) {
|
|
673
|
-
const ctx = this.cdcContexts.get(databaseId);
|
|
674
|
-
if (!ctx || ctx.manager.size > 0) return;
|
|
675
|
-
ctx.stopPolling();
|
|
676
|
-
ctx.cdcConn.close().catch(() => {
|
|
677
|
-
});
|
|
678
|
-
this.cdcContexts.delete(databaseId);
|
|
679
|
-
}
|
|
680
|
-
isValidParams(params) {
|
|
681
|
-
if (params === void 0 || params === null) return true;
|
|
682
|
-
return typeof params === "object";
|
|
1283
|
+
this.cdc.maybeCleanup(state.databaseId);
|
|
683
1284
|
}
|
|
684
1285
|
async resolveTarget(databaseId) {
|
|
685
1286
|
if (!this.resolveExecutionTarget) {
|
|
@@ -688,10 +1289,18 @@ var WSHandler = class {
|
|
|
688
1289
|
return await this.resolveExecutionTarget(databaseId) ?? null;
|
|
689
1290
|
}
|
|
690
1291
|
send(conn, msg) {
|
|
1292
|
+
let data;
|
|
691
1293
|
try {
|
|
692
|
-
|
|
1294
|
+
data = JSON.stringify(msg);
|
|
693
1295
|
} catch {
|
|
1296
|
+
this.handleOverload(conn);
|
|
1297
|
+
return "dropped";
|
|
1298
|
+
}
|
|
1299
|
+
const outcome = conn.send(data);
|
|
1300
|
+
if (outcome === "dropped") {
|
|
1301
|
+
this.handleOverload(conn);
|
|
694
1302
|
}
|
|
1303
|
+
return outcome;
|
|
695
1304
|
}
|
|
696
1305
|
sendError(conn, id, code, message) {
|
|
697
1306
|
this.send(conn, { type: "error", id, error: { code, message } });
|
|
@@ -702,14 +1311,14 @@ var WSHandler = class {
|
|
|
702
1311
|
this.sendError(conn, id, code, message);
|
|
703
1312
|
}
|
|
704
1313
|
sendChange(conn, subscriptionId, event) {
|
|
705
|
-
this.send(conn, {
|
|
1314
|
+
return this.send(conn, {
|
|
706
1315
|
type: "change",
|
|
707
1316
|
id: subscriptionId,
|
|
708
1317
|
event: {
|
|
709
1318
|
type: event.type,
|
|
710
1319
|
table: event.table,
|
|
711
|
-
row: event.row,
|
|
712
|
-
oldRow: event.oldRow,
|
|
1320
|
+
row: encodeTaggedValues(event.row),
|
|
1321
|
+
oldRow: event.oldRow === void 0 ? void 0 : encodeTaggedValues(event.oldRow),
|
|
713
1322
|
seq: event.seq.toString(),
|
|
714
1323
|
timestamp: event.timestamp
|
|
715
1324
|
}
|
|
@@ -720,58 +1329,151 @@ function createWSHandler(sirannon, options) {
|
|
|
720
1329
|
return new WSHandler(sirannon, options);
|
|
721
1330
|
}
|
|
722
1331
|
|
|
723
|
-
// src/server/
|
|
724
|
-
function
|
|
725
|
-
if (
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
origin: "*",
|
|
729
|
-
methods: "GET, POST, OPTIONS",
|
|
730
|
-
headers: "Content-Type, Authorization"
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
return {
|
|
734
|
-
origin: cors.origin ?? "*",
|
|
735
|
-
methods: cors.methods?.join(", ") ?? "GET, POST, OPTIONS",
|
|
736
|
-
headers: cors.headers?.join(", ") ?? "Content-Type, Authorization"
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
function matchOrigin(cors, requestOrigin) {
|
|
740
|
-
if (cors.origin === "*") return "*";
|
|
741
|
-
if (typeof cors.origin === "string") return cors.origin;
|
|
742
|
-
if (cors.origin.includes(requestOrigin)) return requestOrigin;
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
745
|
-
function writeCorsOrigin(res, cors, requestOrigin) {
|
|
746
|
-
const allowed = matchOrigin(cors, requestOrigin);
|
|
747
|
-
if (!allowed) return;
|
|
748
|
-
res.writeHeader("Access-Control-Allow-Origin", allowed);
|
|
749
|
-
if (allowed !== "*") {
|
|
750
|
-
res.writeHeader("Vary", "Origin");
|
|
751
|
-
}
|
|
1332
|
+
// src/server/ws-route.ts
|
|
1333
|
+
function toSendOutcome(result) {
|
|
1334
|
+
if (result === 2) return "dropped";
|
|
1335
|
+
if (result === 0) return "buffered";
|
|
1336
|
+
return "sent";
|
|
752
1337
|
}
|
|
753
1338
|
function selectWebSocketProtocol(header) {
|
|
754
1339
|
const [firstProtocol] = header.split(",");
|
|
755
1340
|
return firstProtocol?.trim() ?? "";
|
|
756
1341
|
}
|
|
757
|
-
function
|
|
758
|
-
|
|
1342
|
+
function registerWebSocketRoute(options) {
|
|
1343
|
+
const { app, wsHandler, onRequestHook } = options;
|
|
1344
|
+
app.ws("/db/:id", {
|
|
1345
|
+
maxPayloadLength: options.maxBodyBytes,
|
|
1346
|
+
maxBackpressure: options.maxBackpressureBytes,
|
|
1347
|
+
idleTimeout: 120,
|
|
1348
|
+
sendPingsAutomatically: true,
|
|
1349
|
+
upgrade: (res, req, context) => {
|
|
1350
|
+
const dbId = req.getParameter(0) ?? "";
|
|
1351
|
+
const url = req.getUrl();
|
|
1352
|
+
const method = req.getMethod();
|
|
1353
|
+
const secWebSocketKey = req.getHeader("sec-websocket-key");
|
|
1354
|
+
const secWebSocketProtocol = req.getHeader("sec-websocket-protocol");
|
|
1355
|
+
const selectedWebSocketProtocol = selectWebSocketProtocol(secWebSocketProtocol);
|
|
1356
|
+
const secWebSocketExtensions = req.getHeader("sec-websocket-extensions");
|
|
1357
|
+
const headers = {};
|
|
1358
|
+
req.forEach((key, value) => {
|
|
1359
|
+
headers[key] = value;
|
|
1360
|
+
});
|
|
1361
|
+
const remoteAddress = decodeRemoteAddress(res);
|
|
1362
|
+
const abort = initAbortHandler(res);
|
|
1363
|
+
if (!onRequestHook) {
|
|
1364
|
+
if (abort.claim()) {
|
|
1365
|
+
res.upgrade(
|
|
1366
|
+
{ databaseId: dbId },
|
|
1367
|
+
secWebSocketKey,
|
|
1368
|
+
selectedWebSocketProtocol,
|
|
1369
|
+
secWebSocketExtensions,
|
|
1370
|
+
context
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const ctx = {
|
|
1376
|
+
headers,
|
|
1377
|
+
method,
|
|
1378
|
+
path: url,
|
|
1379
|
+
databaseId: dbId,
|
|
1380
|
+
remoteAddress
|
|
1381
|
+
};
|
|
1382
|
+
runOnRequest(res, abort, ctx, onRequestHook).then((allowed) => {
|
|
1383
|
+
if (!allowed || !abort.claim()) return;
|
|
1384
|
+
res.upgrade(
|
|
1385
|
+
{ databaseId: dbId },
|
|
1386
|
+
secWebSocketKey,
|
|
1387
|
+
selectedWebSocketProtocol,
|
|
1388
|
+
secWebSocketExtensions,
|
|
1389
|
+
context
|
|
1390
|
+
);
|
|
1391
|
+
}).catch(() => {
|
|
1392
|
+
});
|
|
1393
|
+
},
|
|
1394
|
+
open: (ws) => {
|
|
1395
|
+
const userData = ws.getUserData();
|
|
1396
|
+
const conn = {
|
|
1397
|
+
send(data) {
|
|
1398
|
+
try {
|
|
1399
|
+
return toSendOutcome(ws.send(data, false));
|
|
1400
|
+
} catch {
|
|
1401
|
+
return "dropped";
|
|
1402
|
+
}
|
|
1403
|
+
},
|
|
1404
|
+
close(code, reason) {
|
|
1405
|
+
try {
|
|
1406
|
+
ws.end(code, reason);
|
|
1407
|
+
} catch {
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
userData.conn = conn;
|
|
1412
|
+
wsHandler.handleOpen(conn, userData.databaseId).catch(() => {
|
|
1413
|
+
});
|
|
1414
|
+
},
|
|
1415
|
+
message: (ws, message) => {
|
|
1416
|
+
const userData = ws.getUserData();
|
|
1417
|
+
if (!userData.conn) return;
|
|
1418
|
+
const text = Buffer.from(message).toString("utf-8");
|
|
1419
|
+
wsHandler.handleMessage(userData.conn, text);
|
|
1420
|
+
},
|
|
1421
|
+
dropped: (ws) => {
|
|
1422
|
+
const userData = ws.getUserData();
|
|
1423
|
+
if (userData.conn) {
|
|
1424
|
+
wsHandler.handleOverload(userData.conn);
|
|
1425
|
+
}
|
|
1426
|
+
},
|
|
1427
|
+
close: (ws) => {
|
|
1428
|
+
const userData = ws.getUserData();
|
|
1429
|
+
if (!userData.conn) return;
|
|
1430
|
+
wsHandler.handleClose(userData.conn);
|
|
1431
|
+
userData.conn = void 0;
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
759
1434
|
}
|
|
760
|
-
|
|
761
|
-
|
|
1435
|
+
|
|
1436
|
+
// src/server/server.ts
|
|
1437
|
+
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
1438
|
+
var DEFAULT_WS_BACKPRESSURE_BYTES = 16 * 1048576;
|
|
1439
|
+
var UWS_MAX_LIMIT_BYTES = 4294967295;
|
|
1440
|
+
function resolveMaxBodyBytes(value) {
|
|
1441
|
+
if (value === void 0) return DEFAULT_MAX_BODY_BYTES;
|
|
1442
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
1443
|
+
throw new SirannonError(
|
|
1444
|
+
"ServerOptions.maxBodyBytes must be a positive integer number of bytes",
|
|
1445
|
+
"INVALID_MAX_BODY_BYTES"
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
if (value > UWS_MAX_LIMIT_BYTES) {
|
|
1449
|
+
throw new SirannonError(
|
|
1450
|
+
`ServerOptions.maxBodyBytes must be at most ${UWS_MAX_LIMIT_BYTES} bytes; uWebSockets.js stores the limit as an unsigned 32-bit integer and would silently wrap a larger value modulo 2^32`,
|
|
1451
|
+
"INVALID_MAX_BODY_BYTES"
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
return value;
|
|
762
1455
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1456
|
+
function resolveWsBackpressure(value, maxBodyBytes) {
|
|
1457
|
+
const resolved = value ?? Math.max(DEFAULT_WS_BACKPRESSURE_BYTES, maxBodyBytes);
|
|
1458
|
+
if (typeof resolved !== "number" || !Number.isInteger(resolved) || resolved <= 0) {
|
|
1459
|
+
throw new SirannonError(
|
|
1460
|
+
"ServerOptions.maxWebSocketBackpressureBytes must be a positive integer number of bytes",
|
|
1461
|
+
"INVALID_WS_BACKPRESSURE"
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
if (resolved > UWS_MAX_LIMIT_BYTES) {
|
|
1465
|
+
throw new SirannonError(
|
|
1466
|
+
`ServerOptions.maxWebSocketBackpressureBytes must be at most ${UWS_MAX_LIMIT_BYTES} bytes; uWebSockets.js stores the limit as an unsigned 32-bit integer and would silently wrap a larger value modulo 2^32`,
|
|
1467
|
+
"INVALID_WS_BACKPRESSURE"
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
if (resolved < maxBodyBytes) {
|
|
1471
|
+
throw new SirannonError(
|
|
1472
|
+
"ServerOptions.maxWebSocketBackpressureBytes must be at least maxBodyBytes so a single frame fits",
|
|
1473
|
+
"INVALID_WS_BACKPRESSURE"
|
|
1474
|
+
);
|
|
774
1475
|
}
|
|
1476
|
+
return resolved;
|
|
775
1477
|
}
|
|
776
1478
|
var SirannonServer = class {
|
|
777
1479
|
app;
|
|
@@ -785,6 +1487,8 @@ var SirannonServer = class {
|
|
|
785
1487
|
getClusterStatus;
|
|
786
1488
|
sirannon;
|
|
787
1489
|
wsHandler;
|
|
1490
|
+
maxBodyBytes;
|
|
1491
|
+
maxWsBackpressureBytes;
|
|
788
1492
|
constructor(sirannon, options) {
|
|
789
1493
|
this.sirannon = sirannon;
|
|
790
1494
|
this.host = options?.host ?? "127.0.0.1";
|
|
@@ -794,7 +1498,13 @@ var SirannonServer = class {
|
|
|
794
1498
|
this.resolveExecutionTarget = options?.resolveExecutionTarget;
|
|
795
1499
|
this.getReplicationStatus = options?.getReplicationStatus;
|
|
796
1500
|
this.getClusterStatus = options?.getClusterStatus;
|
|
797
|
-
this.
|
|
1501
|
+
this.maxBodyBytes = resolveMaxBodyBytes(options?.maxBodyBytes);
|
|
1502
|
+
this.maxWsBackpressureBytes = resolveWsBackpressure(options?.maxWebSocketBackpressureBytes, this.maxBodyBytes);
|
|
1503
|
+
this.wsHandler = new WSHandler(sirannon, {
|
|
1504
|
+
resolveExecutionTarget: this.resolveExecutionTarget,
|
|
1505
|
+
maxPayloadLength: this.maxBodyBytes,
|
|
1506
|
+
cdcRetentionMs: options?.cdcRetentionMs
|
|
1507
|
+
});
|
|
798
1508
|
this.app = uWS.App();
|
|
799
1509
|
this.registerRoutes();
|
|
800
1510
|
}
|
|
@@ -845,98 +1555,20 @@ var SirannonServer = class {
|
|
|
845
1555
|
"/db/:id/transaction",
|
|
846
1556
|
this.wrapDbRoute(handleTransaction(this.sirannon, this.resolveExecutionTarget))
|
|
847
1557
|
);
|
|
1558
|
+
this.app.post("/db/:id/batch", this.wrapDbRoute(handleBatch(this.sirannon, this.resolveExecutionTarget)));
|
|
1559
|
+
this.app.post("/db/:id/load", this.wrapDbRoute(handleLoad(this.sirannon, this.resolveExecutionTarget)));
|
|
848
1560
|
this.registerWebSocketRoute();
|
|
849
1561
|
this.app.any("/*", (res) => {
|
|
850
1562
|
sendError(res, 404, "NOT_FOUND", "Route not found");
|
|
851
1563
|
});
|
|
852
1564
|
}
|
|
853
1565
|
registerWebSocketRoute() {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
upgrade: (res, req, context) => {
|
|
861
|
-
const dbId = req.getParameter(0) ?? "";
|
|
862
|
-
const url = req.getUrl();
|
|
863
|
-
const method = req.getMethod();
|
|
864
|
-
const secWebSocketKey = req.getHeader("sec-websocket-key");
|
|
865
|
-
const secWebSocketProtocol = req.getHeader("sec-websocket-protocol");
|
|
866
|
-
const selectedWebSocketProtocol = selectWebSocketProtocol(secWebSocketProtocol);
|
|
867
|
-
const secWebSocketExtensions = req.getHeader("sec-websocket-extensions");
|
|
868
|
-
const headers = {};
|
|
869
|
-
req.forEach((key, value) => {
|
|
870
|
-
headers[key] = value;
|
|
871
|
-
});
|
|
872
|
-
const remoteAddress = decodeRemoteAddress(res);
|
|
873
|
-
let aborted = false;
|
|
874
|
-
res.onAborted(() => {
|
|
875
|
-
aborted = true;
|
|
876
|
-
});
|
|
877
|
-
if (!onRequestHook) {
|
|
878
|
-
if (!aborted) {
|
|
879
|
-
res.upgrade(
|
|
880
|
-
{ databaseId: dbId },
|
|
881
|
-
secWebSocketKey,
|
|
882
|
-
selectedWebSocketProtocol,
|
|
883
|
-
secWebSocketExtensions,
|
|
884
|
-
context
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
const ctx = {
|
|
890
|
-
headers,
|
|
891
|
-
method,
|
|
892
|
-
path: url,
|
|
893
|
-
databaseId: dbId,
|
|
894
|
-
remoteAddress
|
|
895
|
-
};
|
|
896
|
-
runOnRequest(res, ctx, onRequestHook).then((allowed) => {
|
|
897
|
-
if (aborted || !allowed) return;
|
|
898
|
-
res.upgrade(
|
|
899
|
-
{ databaseId: dbId },
|
|
900
|
-
secWebSocketKey,
|
|
901
|
-
selectedWebSocketProtocol,
|
|
902
|
-
secWebSocketExtensions,
|
|
903
|
-
context
|
|
904
|
-
);
|
|
905
|
-
}).catch(() => {
|
|
906
|
-
});
|
|
907
|
-
},
|
|
908
|
-
open: (ws) => {
|
|
909
|
-
const userData = ws.getUserData();
|
|
910
|
-
const conn = {
|
|
911
|
-
send(data) {
|
|
912
|
-
try {
|
|
913
|
-
ws.send(data, false);
|
|
914
|
-
} catch {
|
|
915
|
-
}
|
|
916
|
-
},
|
|
917
|
-
close(code, reason) {
|
|
918
|
-
try {
|
|
919
|
-
ws.end(code, reason);
|
|
920
|
-
} catch {
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
};
|
|
924
|
-
userData.conn = conn;
|
|
925
|
-
wsHandler.handleOpen(conn, userData.databaseId).catch(() => {
|
|
926
|
-
});
|
|
927
|
-
},
|
|
928
|
-
message: (ws, message) => {
|
|
929
|
-
const userData = ws.getUserData();
|
|
930
|
-
if (!userData.conn) return;
|
|
931
|
-
const text = Buffer.from(message).toString("utf-8");
|
|
932
|
-
wsHandler.handleMessage(userData.conn, text);
|
|
933
|
-
},
|
|
934
|
-
close: (ws) => {
|
|
935
|
-
const userData = ws.getUserData();
|
|
936
|
-
if (!userData.conn) return;
|
|
937
|
-
wsHandler.handleClose(userData.conn);
|
|
938
|
-
userData.conn = void 0;
|
|
939
|
-
}
|
|
1566
|
+
registerWebSocketRoute({
|
|
1567
|
+
app: this.app,
|
|
1568
|
+
wsHandler: this.wsHandler,
|
|
1569
|
+
onRequestHook: this.onRequestHook,
|
|
1570
|
+
maxBodyBytes: this.maxBodyBytes,
|
|
1571
|
+
maxBackpressureBytes: this.maxWsBackpressureBytes
|
|
940
1572
|
});
|
|
941
1573
|
}
|
|
942
1574
|
withCors(handler) {
|
|
@@ -950,7 +1582,7 @@ var SirannonServer = class {
|
|
|
950
1582
|
wrapDbRoute(handler) {
|
|
951
1583
|
const onRequestHook = this.onRequestHook;
|
|
952
1584
|
const corsHeaders = this.cors;
|
|
953
|
-
const
|
|
1585
|
+
const maxBody = this.maxBodyBytes;
|
|
954
1586
|
return (res, req) => {
|
|
955
1587
|
const dbId = req.getParameter(0) ?? "";
|
|
956
1588
|
const method = req.getMethod();
|
|
@@ -959,10 +1591,10 @@ var SirannonServer = class {
|
|
|
959
1591
|
writeCorsOrigin(res, corsHeaders, req.getHeader("origin"));
|
|
960
1592
|
}
|
|
961
1593
|
const abort = initAbortHandler(res);
|
|
962
|
-
const bodyPromise = readBody(res,
|
|
1594
|
+
const bodyPromise = readBody(res, maxBody, abort);
|
|
963
1595
|
if (!onRequestHook) {
|
|
964
1596
|
bodyPromise.then(async (rawBody) => {
|
|
965
|
-
if (abort.
|
|
1597
|
+
if (!abort.claim()) return;
|
|
966
1598
|
try {
|
|
967
1599
|
await handler(res, dbId, rawBody, abort);
|
|
968
1600
|
} catch {
|
|
@@ -986,9 +1618,9 @@ var SirannonServer = class {
|
|
|
986
1618
|
databaseId: dbId,
|
|
987
1619
|
remoteAddress
|
|
988
1620
|
};
|
|
989
|
-
const hookPromise = runOnRequest(res, ctx, onRequestHook);
|
|
1621
|
+
const hookPromise = runOnRequest(res, abort, ctx, onRequestHook);
|
|
990
1622
|
Promise.all([bodyPromise, hookPromise]).then(async ([rawBody, allowed]) => {
|
|
991
|
-
if (
|
|
1623
|
+
if (!allowed || !abort.claim()) return;
|
|
992
1624
|
try {
|
|
993
1625
|
await handler(res, dbId, rawBody, abort);
|
|
994
1626
|
} catch {
|
|
@@ -1025,10 +1657,10 @@ var SirannonServer = class {
|
|
|
1025
1657
|
databaseId: dbId,
|
|
1026
1658
|
remoteAddress: decodeRemoteAddress(res)
|
|
1027
1659
|
};
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1660
|
+
const abort = initAbortHandler(res);
|
|
1661
|
+
runOnRequest(res, abort, ctx, onRequestHook).then((allowed) => {
|
|
1662
|
+
if (!allowed || !abort.claim()) return;
|
|
1663
|
+
handler(res, dbId);
|
|
1032
1664
|
}).catch(() => {
|
|
1033
1665
|
});
|
|
1034
1666
|
};
|