@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.
Files changed (54) hide show
  1. package/README.md +134 -19
  2. package/dist/backup-scheduler/index.d.ts +18 -3
  3. package/dist/backup-scheduler/index.mjs +2 -2
  4. package/dist/{change-tracker-CFTQ9TSn.d.ts → change-tracker-CbmaMO-N.d.ts} +12 -2
  5. package/dist/chunk-4IGMIJQK.mjs +318 -0
  6. package/dist/chunk-BNUTBHHH.mjs +22 -0
  7. package/dist/chunk-CJLYFDP5.mjs +26 -0
  8. package/dist/chunk-CW6S3WL5.mjs +222 -0
  9. package/dist/chunk-DJLX6CAE.mjs +20 -0
  10. package/dist/chunk-DVWQD3GF.mjs +49 -0
  11. package/dist/chunk-GEZUUIKV.mjs +268 -0
  12. package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
  13. package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
  14. package/dist/chunk-TKGHYWQ6.mjs +35 -0
  15. package/dist/chunk-VLTICJOD.mjs +470 -0
  16. package/dist/{chunk-O7BHI3CF.mjs → chunk-YPYVQJ4C.mjs} +15 -1
  17. package/dist/client/index.d.ts +107 -15
  18. package/dist/client/index.mjs +348 -37
  19. package/dist/core/index.d.ts +70 -13
  20. package/dist/core/index.mjs +635 -192
  21. package/dist/core/writer-worker.d.ts +2 -0
  22. package/dist/core/writer-worker.mjs +107 -0
  23. package/dist/{database-BVY1GqE7.d.ts → database-DuGp0Rtr.d.ts} +51 -19
  24. package/dist/driver/better-sqlite3.d.ts +1 -1
  25. package/dist/driver/better-sqlite3.mjs +44 -6
  26. package/dist/driver/bun.mjs +35 -6
  27. package/dist/driver/expo.mjs +2 -2
  28. package/dist/driver/node.d.ts +1 -1
  29. package/dist/driver/node.mjs +48 -5
  30. package/dist/driver/wa-sqlite.d.ts +109 -0
  31. package/dist/driver/wa-sqlite.mjs +30 -2
  32. package/dist/{errors-C00ed08Q.d.ts → errors-5Nf5ZAEC.d.ts} +19 -1
  33. package/dist/file-migrations/index.d.ts +2 -3
  34. package/dist/file-migrations/index.mjs +1 -1
  35. package/dist/replication/coordinator/etcd.mjs +2 -2
  36. package/dist/replication/index.d.ts +7 -8
  37. package/dist/replication/index.mjs +79 -78
  38. package/dist/server/index.d.ts +111 -28
  39. package/dist/server/index.mjs +1016 -384
  40. package/dist/{sirannon-Cd-lK6T0.d.ts → sirannon-4SspRvP5.d.ts} +3 -3
  41. package/dist/transport/grpc.d.ts +3 -4
  42. package/dist/transport/grpc.mjs +2 -2
  43. package/dist/{types-Lc7ywFx7.d.ts → types-BsjobKbl.d.ts} +2 -2
  44. package/dist/{types-BeozgNPr.d.ts → types-D4p4UyDK.d.ts} +1 -1
  45. package/dist/types-D_hQW1hr.d.ts +494 -0
  46. package/package.json +3 -23
  47. package/dist/chunk-3MCMONVP.mjs +0 -115
  48. package/dist/chunk-74UN4DIE.mjs +0 -14
  49. package/dist/chunk-FB2U2Q3Y.mjs +0 -21
  50. package/dist/chunk-GS7T5YMI.mjs +0 -51
  51. package/dist/chunk-PXKAKK2V.mjs +0 -124
  52. package/dist/index-CLdNrcPz.d.ts +0 -16
  53. package/dist/types-BFSsG77t.d.ts +0 -29
  54. package/dist/types-D-74JiXb.d.ts +0 -265
@@ -1,7 +1,9 @@
1
- import { ChangeTracker, SubscriptionManager } from '../chunk-UTO3ZAFS.mjs';
2
- import '../chunk-GS7T5YMI.mjs';
3
- import { SirannonError } from '../chunk-O7BHI3CF.mjs';
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-handler.ts
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 (!abort.aborted) {
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
- chunks.push(Buffer.from(chunk));
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}`).writeHeader("Content-Type", "application/json").end(payload);
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
- async function resolveExecutionTarget(res, sirannon, id, resolver) {
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 target = await resolveExecutionTarget(res, sirannon, dbId, resolveTarget);
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 target.query(
435
+ const rows = await queryWireRows(
436
+ target,
218
437
  body.sql,
219
- body.params,
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
- if (abort.aborted) return;
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 target = await resolveExecutionTarget(res, sirannon, dbId, resolveTarget);
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
- body.params,
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
- if (abort.aborted) return;
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
- if (!Array.isArray(body.statements)) {
269
- sendError(res, 400, "INVALID_REQUEST", 'Field "statements" is required and must be an array');
270
- return;
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
- for (let i = 0; i < body.statements.length; i++) {
279
- const stmt = body.statements[i];
280
- if (!stmt.sql || typeof stmt.sql !== "string") {
281
- sendError(res, 400, "INVALID_REQUEST", `Statement at index ${i} is missing a valid "sql" field`);
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
- if (abort.aborted) return;
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 parseReadConcern(res, value) {
314
- if (value === void 0) return { ok: true, value: void 0 };
315
- if (!isPlainRecord(value)) {
316
- sendError(res, 400, "INVALID_REQUEST", 'Field "readConcern" must be an object when provided');
317
- return { ok: false };
318
- }
319
- const keys = Object.keys(value);
320
- if (keys.length !== 1 || !keys.includes("level")) {
321
- sendError(res, 400, "INVALID_REQUEST", 'Field "readConcern" must contain only "level"');
322
- return { ok: false };
323
- }
324
- if (!isReadConcernLevel(value.level)) {
325
- sendError(res, 400, "INVALID_REQUEST", 'Field "readConcern.level" is invalid');
326
- return { ok: false };
327
- }
328
- return { ok: true, value: { level: value.level } };
329
- }
330
- function parseWriteConcern(res, value) {
331
- if (value === void 0) return { ok: true, value: void 0 };
332
- if (!isPlainRecord(value)) {
333
- sendError(res, 400, "INVALID_REQUEST", 'Field "writeConcern" must be an object when provided');
334
- return { ok: false };
335
- }
336
- const allowedKeys = /* @__PURE__ */ new Set(["level", "timeoutMs"]);
337
- if (!Object.keys(value).every((key) => allowedKeys.has(key))) {
338
- sendError(res, 400, "INVALID_REQUEST", 'Field "writeConcern" contains unsupported keys');
339
- return { ok: false };
340
- }
341
- if (!isWriteConcernLevel(value.level)) {
342
- sendError(res, 400, "INVALID_REQUEST", 'Field "writeConcern.level" is invalid');
343
- return { ok: false };
344
- }
345
- const timeoutMs = value.timeoutMs;
346
- if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)) {
347
- sendError(res, 400, "INVALID_REQUEST", 'Field "writeConcern.timeoutMs" must be a positive safe integer');
348
- return { ok: false };
349
- }
350
- return {
351
- ok: true,
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
- function errorDetails(err) {
390
- const details = err.details;
391
- return details && Object.keys(details).length > 0 ? details : void 0;
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
- cdcContexts = /* @__PURE__ */ new Map();
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.length > this.maxPayloadLength) {
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
- this.sendError(conn, "", "INVALID_MESSAGE", 'Message must have a string "id" field');
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.handleQuery(conn, state, msg, id);
1112
+ handleQueryMessage(this.operationContext(conn, state), msg, id);
477
1113
  break;
478
1114
  case "execute":
479
- this.handleExecute(conn, state, msg, id);
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.maybeCleanupCDC(state.databaseId);
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
- for (const ctx of this.cdcContexts.values()) {
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
- async handleExecute(conn, state, msg, id) {
542
- if (typeof msg.sql !== "string") {
543
- this.sendError(conn, id, "INVALID_MESSAGE", 'Execute message requires a "sql" string field');
544
- return;
545
- }
546
- if (!this.isValidParams(msg.params)) {
547
- this.sendError(conn, id, "INVALID_MESSAGE", '"params" must be an object or array');
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 filter = msg.filter ?? void 0;
584
- let ctx = null;
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.ensureCDC(state.databaseId, state.database);
587
- await ctx.tracker.watch(ctx.cdcConn, msg.table);
588
- const sub = ctx.manager.subscribe(msg.table, filter, (event) => {
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
- if (ctx?.manager.size === 0) {
595
- this.maybeCleanupCDC(state.databaseId);
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.maybeCleanupCDC(state.databaseId);
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
- conn.send(JSON.stringify(msg));
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/server.ts
724
- function resolveCors(cors) {
725
- if (!cors) return null;
726
- if (cors === true) {
727
- return {
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 decodeRemoteAddress(res) {
758
- return Buffer.from(res.getRemoteAddressAsText()).toString();
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
- function isRequestDenial(value) {
761
- return typeof value === "object" && value !== null && "status" in value;
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
- async function runOnRequest(res, ctx, hook) {
764
- try {
765
- const result = await hook(ctx);
766
- if (isRequestDenial(result)) {
767
- sendError(res, result.status, result.code, result.message);
768
- return false;
769
- }
770
- return true;
771
- } catch {
772
- sendError(res, 500, "HOOK_ERROR", "onRequest hook threw an error");
773
- return false;
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.wsHandler = new WSHandler(sirannon, { resolveExecutionTarget: this.resolveExecutionTarget });
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
- const wsHandler = this.wsHandler;
855
- const onRequestHook = this.onRequestHook;
856
- this.app.ws("/db/:id", {
857
- maxPayloadLength: 1048576,
858
- idleTimeout: 120,
859
- sendPingsAutomatically: true,
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 MAX_BODY = 1048576;
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, MAX_BODY, abort);
1594
+ const bodyPromise = readBody(res, maxBody, abort);
963
1595
  if (!onRequestHook) {
964
1596
  bodyPromise.then(async (rawBody) => {
965
- if (abort.aborted) return;
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 (abort.aborted || !allowed) return;
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
- runOnRequest(res, ctx, onRequestHook).then((allowed) => {
1029
- if (allowed) {
1030
- handler(res, dbId);
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
  };