@delali/sirannon-db 0.3.4-next.62 → 0.3.4
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 +79 -56
- package/dist/backup/index.d.ts +44 -45
- package/dist/backup/index.mjs +7 -7
- package/dist/backup-scheduler/index.d.ts +42 -41
- package/dist/backup-scheduler/index.mjs +4 -4
- package/dist/baseline-Co3iIsN_.d.ts +19 -0
- package/dist/change-tracker-DqCdeEyx.d.ts +128 -0
- package/dist/{chunk-VOSJBZ6Q.mjs → chunk-2ES3DT4A.mjs} +71 -33
- package/dist/{chunk-6CMCO7S4.mjs → chunk-2O3PP4CF.mjs} +1 -1
- package/dist/{chunk-E7Q7HE46.mjs → chunk-3BZLZ47R.mjs} +26 -26
- package/dist/chunk-4KASJKKK.mjs +2501 -0
- package/dist/{chunk-MYNI2WFS.mjs → chunk-52XW3Z76.mjs} +40 -8
- package/dist/chunk-55YRLSM2.mjs +57 -0
- package/dist/{chunk-KDAHYFW7.mjs → chunk-6AOJCNRU.mjs} +5 -5
- package/dist/{chunk-KXTYR2OQ.mjs → chunk-A3PHCO6W.mjs} +39 -111
- package/dist/chunk-ADNWEHGZ.mjs +299 -0
- package/dist/{chunk-2QLXDHAP.mjs → chunk-BLMRNSUG.mjs} +11 -2
- package/dist/chunk-BVMZJOVM.mjs +5 -0
- package/dist/{chunk-IWGIYDMZ.mjs → chunk-BZWDPF4G.mjs} +1 -1
- package/dist/{chunk-YVF2ZJTE.mjs → chunk-CZL4D3BM.mjs} +1 -1
- package/dist/{chunk-7SC2TZAS.mjs → chunk-DX4WGJL7.mjs} +2 -2
- package/dist/{chunk-45MKELYA.mjs → chunk-ESTZZPT5.mjs} +132 -13
- package/dist/{chunk-QP5L5DFJ.mjs → chunk-FJPR2CJX.mjs} +3 -3
- package/dist/chunk-HR7K3GZW.mjs +36 -0
- package/dist/{chunk-LXCGPZPA.mjs → chunk-IEKIYO4V.mjs} +1 -1
- package/dist/{chunk-TUD5CJ76.mjs → chunk-KB4OLJAD.mjs} +26 -19
- package/dist/{chunk-PBRXXISQ.mjs → chunk-KINDFBQP.mjs} +3 -2
- package/dist/{chunk-E6IMX7RS.mjs → chunk-KJLNYOOL.mjs} +2 -2
- package/dist/{chunk-BTTFW4Z4.mjs → chunk-KU32OT4H.mjs} +2 -10
- package/dist/{chunk-JQWGHSEH.mjs → chunk-NVSQ2T5Q.mjs} +71 -70
- package/dist/{chunk-W4EUTJNT.mjs → chunk-NZGSDALX.mjs} +94 -96
- package/dist/{chunk-U445A3C4.mjs → chunk-QGVRRKB2.mjs} +1 -1
- package/dist/{chunk-GYCEJNU4.mjs → chunk-R7GBXNTO.mjs} +1 -1
- package/dist/{chunk-7C36BCSN.mjs → chunk-VDN6FHU4.mjs} +1 -1
- package/dist/{chunk-6IYS4WLH.mjs → chunk-VO3ZWA2G.mjs} +142 -115
- package/dist/chunk-WOMADPM4.mjs +12 -0
- package/dist/{chunk-OUSWVNWT.mjs → chunk-YCMB6GR4.mjs} +1 -1
- package/dist/{chunk-YNTDOFTN.mjs → chunk-YW7IKQ7T.mjs} +1 -1
- package/dist/{chunk-M2BEAH5I.mjs → chunk-ZN4HI5FK.mjs} +126 -72
- package/dist/{chunk-SBL6GN43.mjs → chunk-ZYWI45EN.mjs} +1 -1
- package/dist/client/index.d.ts +135 -137
- package/dist/client/index.mjs +177 -82
- package/dist/client/topology.d.ts +31 -31
- package/dist/client/topology.mjs +42 -23
- package/dist/client-base-BxTocU1x.d.ts +398 -0
- package/dist/client-streams-YKANXMWO.mjs +112 -0
- package/dist/codegen/cli.mjs +4 -4
- package/dist/codegen/index.d.ts +36 -29
- package/dist/codegen/index.mjs +4 -4
- package/dist/core/index.d.ts +130 -115
- package/dist/core/index.mjs +387 -297
- package/dist/core/writer-worker.mjs +3 -3
- package/dist/database-CGDsXW1K.d.ts +959 -0
- package/dist/database-backup-DcLhxoa1.d.ts +143 -0
- package/dist/driver/better-sqlite3.d.ts +11 -10
- package/dist/driver/better-sqlite3.mjs +11 -11
- package/dist/driver/bun.d.ts +9 -8
- package/dist/driver/expo.d.ts +6 -7
- package/dist/driver/node.d.ts +11 -10
- package/dist/driver/node.mjs +13 -11
- package/dist/driver/wa-sqlite.d.ts +6 -6
- package/dist/errors-CJUkQ-KL.d.ts +199 -0
- package/dist/file-migrations/index.d.ts +20 -18
- package/dist/file-migrations/index.mjs +3 -3
- package/dist/forward-rpc-VRWFZXHN.mjs +44 -0
- package/dist/operation-registry-CGhNT-Sn.d.ts +96 -0
- package/dist/primary-wins-CHNOYuN7.d.ts +81 -0
- package/dist/protocol-DFpNNsIT.d.ts +153 -0
- package/dist/query-types-5B02ixH0.d.ts +129 -0
- package/dist/react/index.d.ts +22 -22
- package/dist/replication/coordinator/etcd.d.ts +37 -31
- package/dist/replication/coordinator/etcd.mjs +296 -131
- package/dist/replication/index.d.ts +201 -161
- package/dist/replication/index.mjs +630 -529
- package/dist/restore-options-CH8mE-Tb.d.ts +84 -0
- package/dist/server/index.d.ts +201 -191
- package/dist/server/index.mjs +736 -619
- package/dist/server-options-aYtCkefX.d.ts +317 -0
- package/dist/server-streams-24XL4T43.mjs +254 -0
- package/dist/sirannon-J3EgPVD7.d.ts +179 -0
- package/dist/transport/grpc.d.ts +37 -24
- package/dist/transport/grpc.mjs +83 -3199
- package/dist/transport/memory.d.ts +25 -26
- package/dist/transport/memory.mjs +18 -9
- package/dist/types-BrAiDGOb.d.ts +505 -0
- package/dist/types-CEJG0eFS.d.ts +88 -0
- package/dist/types-Dc8oWlxj.d.ts +1580 -0
- package/dist/types-DqODKeOq.d.ts +123 -0
- package/dist/types-guEKSzMr.d.ts +367 -0
- package/package.json +7 -7
- package/dist/baseline-D93hcIEE.d.ts +0 -17
- package/dist/change-tracker-CjRFuZdB.d.ts +0 -144
- package/dist/client-base-Cmd9EVme.d.ts +0 -397
- package/dist/database-BLQvqjsw.d.ts +0 -917
- package/dist/database-backup-B7oszQ2n.d.ts +0 -141
- package/dist/errors-Dei4GdBb.d.ts +0 -196
- package/dist/operation-registry-oiPuSYI6.d.ts +0 -94
- package/dist/primary-wins-B0np8JS3.d.ts +0 -85
- package/dist/protocol-HI-nY8DD.d.ts +0 -152
- package/dist/query-types-p1liOP-b.d.ts +0 -127
- package/dist/restore-options-CksYsH1l.d.ts +0 -83
- package/dist/server-options-Bj5iJnjx.d.ts +0 -292
- package/dist/sirannon-BPxC5tC2.d.ts +0 -178
- package/dist/types-B0EhgDbr.d.ts +0 -340
- package/dist/types-CbjfRwrK.d.ts +0 -499
- package/dist/types-CjhxcjhA.d.ts +0 -123
- package/dist/types-DPSmjU54.d.ts +0 -1518
- package/dist/types-DTO4CYhW.d.ts +0 -88
package/dist/server/index.mjs
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { restoreBackup, MAX_RESTORE_BATCH_SIZE } from '../chunk-
|
|
2
|
-
import { tablesInFkOrder, dumpSchema, dumpTablePages } from '../chunk-
|
|
3
|
-
import {
|
|
4
|
-
import '../chunk-
|
|
5
|
-
import
|
|
6
|
-
import '../chunk-
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
1
|
+
import { restoreBackup, MAX_RESTORE_BATCH_SIZE } from '../chunk-3BZLZ47R.mjs';
|
|
2
|
+
import { tablesInFkOrder, dumpSchema, dumpTablePages } from '../chunk-FJPR2CJX.mjs';
|
|
3
|
+
import { optionalDependencyError } from '../chunk-WOMADPM4.mjs';
|
|
4
|
+
import { resolveDeviceRetentionPolicy, ChangeTracker, SubscriptionManager, ensureCdcEpoch, migrationChecksum, applyDeviceCursorBoundary, needsResync, filteredChange, TransactionGrouper, isBulkLoadDurability, PrimedSubscription } from '../chunk-ZN4HI5FK.mjs';
|
|
5
|
+
import '../chunk-YW7IKQ7T.mjs';
|
|
6
|
+
import { SEQ_STRING_RE, highestMigrationVersion, selectCountTableRows, selectTableExists, selectAppliedMigrations, PkResolver, ensureDeviceCursorsTable, upsertDeviceCursor, HLC, selectMaxChangeSeq, canonicaliseForChecksum, IDENTIFIER_RE } from '../chunk-ESTZZPT5.mjs';
|
|
7
|
+
import '../chunk-BLMRNSUG.mjs';
|
|
8
|
+
import { encodeTaggedValues, MIGRATIONS_TABLE, decodeTaggedValues, encodeWireRowsInPlace, CHANGES_TABLE, INTERNAL_TABLE_PREFIX } from '../chunk-2O3PP4CF.mjs';
|
|
9
|
+
import { createOperationSource, operationRegistryDigest, findRead, isRefusal, resolveArguments, findWrite } from '../chunk-KB4OLJAD.mjs';
|
|
9
10
|
import '../chunk-GVCNMPOS.mjs';
|
|
10
|
-
import { discardStagedChain } from '../chunk-
|
|
11
|
-
import '../chunk-
|
|
12
|
-
import '../chunk-
|
|
13
|
-
import '../chunk-
|
|
14
|
-
import '../chunk-
|
|
15
|
-
import '../chunk-
|
|
11
|
+
import { discardStagedChain } from '../chunk-6AOJCNRU.mjs';
|
|
12
|
+
import '../chunk-QGVRRKB2.mjs';
|
|
13
|
+
import '../chunk-KJLNYOOL.mjs';
|
|
14
|
+
import '../chunk-R7GBXNTO.mjs';
|
|
15
|
+
import '../chunk-YCMB6GR4.mjs';
|
|
16
|
+
import '../chunk-CZL4D3BM.mjs';
|
|
16
17
|
import '../chunk-QBBLSQHQ.mjs';
|
|
17
|
-
import { SQLITE_USER_VERSION_MAX } from '../chunk-
|
|
18
|
-
import { SirannonError, WriteOverloadError } from '../chunk-
|
|
19
|
-
import uWS from 'uWebSockets.js';
|
|
18
|
+
import { SQLITE_USER_VERSION_MAX } from '../chunk-BZWDPF4G.mjs';
|
|
19
|
+
import { SirannonError, WriteOverloadError } from '../chunk-KINDFBQP.mjs';
|
|
20
20
|
import { createHash } from 'crypto';
|
|
21
21
|
|
|
22
22
|
// src/server/protocol.ts
|
|
@@ -148,11 +148,11 @@ function describeFailure(err) {
|
|
|
148
148
|
var BackupRestoreRuns = class {
|
|
149
149
|
runs = /* @__PURE__ */ new Map();
|
|
150
150
|
/**
|
|
151
|
-
* Marks a restore of one database as
|
|
151
|
+
* Marks a restore of one database as started, unless another restore of it is in progress.
|
|
152
152
|
*
|
|
153
|
-
* @param databaseId -
|
|
154
|
-
* @param moment -
|
|
155
|
-
* @returns True
|
|
153
|
+
* @param databaseId - The identifier of the database that the server rebuilds.
|
|
154
|
+
* @param moment - The moment, in epoch milliseconds, that the caller asked to restore to.
|
|
155
|
+
* @returns True when this call claims the database, and false when another restore of it is in progress.
|
|
156
156
|
*/
|
|
157
157
|
claim(databaseId, moment) {
|
|
158
158
|
if (this.runs.get(databaseId)?.state === "running") return false;
|
|
@@ -160,9 +160,9 @@ var BackupRestoreRuns = class {
|
|
|
160
160
|
return true;
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
|
-
* Records
|
|
163
|
+
* Records the progress of one database's restore.
|
|
164
164
|
*
|
|
165
|
-
* @param databaseId -
|
|
165
|
+
* @param databaseId - The identifier of the database that the server is rebuilding.
|
|
166
166
|
* @param progress - Pieces fetched, bytes fetched, and change pieces replayed.
|
|
167
167
|
*/
|
|
168
168
|
progressed(databaseId, progress) {
|
|
@@ -172,13 +172,14 @@ var BackupRestoreRuns = class {
|
|
|
172
172
|
/**
|
|
173
173
|
* Records a restore that rebuilt the database.
|
|
174
174
|
*
|
|
175
|
-
*
|
|
176
|
-
* `done`, because Sirannon replaced the data
|
|
177
|
-
* only the failure
|
|
175
|
+
* When the database fails to open again, this method records that error beside
|
|
176
|
+
* the report and keeps the state `done`, because Sirannon replaced the data
|
|
177
|
+
* either way, and an operator who saw only the failure could believe that the
|
|
178
|
+
* data was unchanged.
|
|
178
179
|
*
|
|
179
|
-
* @param databaseId -
|
|
180
|
-
* @param report -
|
|
181
|
-
* @param reopenFailure -
|
|
180
|
+
* @param databaseId - The identifier of the rebuilt database.
|
|
181
|
+
* @param report - The restore's report.
|
|
182
|
+
* @param reopenFailure - The error that stopped the database from opening again, if there was one.
|
|
182
183
|
*/
|
|
183
184
|
finished(databaseId, report, reopenFailure) {
|
|
184
185
|
this.runs.set(databaseId, {
|
|
@@ -191,15 +192,15 @@ var BackupRestoreRuns = class {
|
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
194
|
/**
|
|
194
|
-
* Records a restore
|
|
195
|
+
* Records a failed restore under the code of the error that stopped it.
|
|
195
196
|
*
|
|
196
|
-
*
|
|
197
|
-
* the
|
|
198
|
-
* runtime error
|
|
199
|
-
*
|
|
197
|
+
* The record keeps the message of a `SirannonError` only. For any other error,
|
|
198
|
+
* it stores the message that the HTTP layer sends for an unexpected failure,
|
|
199
|
+
* because a runtime error can include file paths and other internal details of
|
|
200
|
+
* the server's machine.
|
|
200
201
|
*
|
|
201
|
-
* @param databaseId -
|
|
202
|
-
* @param err -
|
|
202
|
+
* @param databaseId - The identifier of the database that the server was rebuilding.
|
|
203
|
+
* @param err - The error that stopped the restore.
|
|
203
204
|
*/
|
|
204
205
|
failed(databaseId, err) {
|
|
205
206
|
const previous = this.runs.get(databaseId);
|
|
@@ -212,10 +213,10 @@ var BackupRestoreRuns = class {
|
|
|
212
213
|
});
|
|
213
214
|
}
|
|
214
215
|
/**
|
|
215
|
-
*
|
|
216
|
+
* Returns the status of one database's restore.
|
|
216
217
|
*
|
|
217
|
-
* @param databaseId -
|
|
218
|
-
* @returns The restore, or an idle
|
|
218
|
+
* @param databaseId - The identifier of the database.
|
|
219
|
+
* @returns The restore's status, or an `idle` status when no restore has started.
|
|
219
220
|
*/
|
|
220
221
|
read(databaseId) {
|
|
221
222
|
return this.runs.get(databaseId) ?? { state: "idle" };
|
|
@@ -224,6 +225,8 @@ var BackupRestoreRuns = class {
|
|
|
224
225
|
|
|
225
226
|
// src/server/http-common.ts
|
|
226
227
|
var SQL_NOT_ACCEPTED_MESSAGE = "This server does not accept SQL statements over the wire; call a registered operation by name";
|
|
228
|
+
var TABLE_SUBSCRIPTION_NOT_ACCEPTED_MESSAGE = "This server keeps table subscriptions off; subscribe to a registered read by name, or give the server acceptSql: true or an onBeforeSubscribe hook";
|
|
229
|
+
var DEVICE_SYNC_NOT_ACCEPTED_MESSAGE = "This server keeps device sync off; the server has to set acceptDeviceSync before a device can sync";
|
|
227
230
|
function initAbortHandler(res) {
|
|
228
231
|
const listeners = [];
|
|
229
232
|
let aborted = false;
|
|
@@ -428,7 +431,7 @@ var SERVER_CAPABILITIES = [...REQUIRED_DEVICE_SYNC_CAPABILITIES, STAGED_STREAM_C
|
|
|
428
431
|
var NAMED_QUERY_CAPABILITY = "query.named";
|
|
429
432
|
var SQL_QUERY_CAPABILITY = "query.sql";
|
|
430
433
|
function buildCapabilitiesResponse(options) {
|
|
431
|
-
const capabilities = [...SERVER_CAPABILITIES];
|
|
434
|
+
const capabilities = options?.acceptDeviceSync === true ? [...SERVER_CAPABILITIES] : [];
|
|
432
435
|
if (options?.registryDigest !== void 0) capabilities.push(NAMED_QUERY_CAPABILITY);
|
|
433
436
|
if (options?.acceptSql === true) capabilities.push(SQL_QUERY_CAPABILITY);
|
|
434
437
|
const response = { capabilities };
|
|
@@ -472,6 +475,408 @@ function writeCorsOrigin(res, cors, requestOrigin) {
|
|
|
472
475
|
}
|
|
473
476
|
}
|
|
474
477
|
|
|
478
|
+
// src/server/wire-rows.ts
|
|
479
|
+
async function queryWireRows(target, sql, params, options) {
|
|
480
|
+
if (target.queryForWire) {
|
|
481
|
+
return target.queryForWire(sql, params, options);
|
|
482
|
+
}
|
|
483
|
+
return encodeTaggedValues(await target.query(sql, params, options));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/server/http-handler.ts
|
|
487
|
+
function decodeBatchParams(res, raw) {
|
|
488
|
+
const decoded = [];
|
|
489
|
+
for (const entry of raw) {
|
|
490
|
+
const result = decodeBoundParams(entry, "paramsBatch");
|
|
491
|
+
if (!result.ok) {
|
|
492
|
+
sendError(res, 400, "INVALID_REQUEST", result.message);
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
decoded.push(result.value);
|
|
496
|
+
}
|
|
497
|
+
return decoded;
|
|
498
|
+
}
|
|
499
|
+
function handleQuery(sirannon, resolveTarget) {
|
|
500
|
+
return async (res, dbId, rawBody, abort) => {
|
|
501
|
+
const body = parseBody(res, rawBody);
|
|
502
|
+
if (!body) return;
|
|
503
|
+
if (!body.sql || typeof body.sql !== "string") {
|
|
504
|
+
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const readConcern = parseReadConcern(res, body.readConcern);
|
|
508
|
+
if (!readConcern.ok) return;
|
|
509
|
+
const params = decodeBoundParams(body.params, "params");
|
|
510
|
+
if (!params.ok) {
|
|
511
|
+
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
515
|
+
if (!target) return;
|
|
516
|
+
try {
|
|
517
|
+
const rows = await queryWireRows(
|
|
518
|
+
target,
|
|
519
|
+
body.sql,
|
|
520
|
+
params.value,
|
|
521
|
+
readConcern.value ? { readConcern: readConcern.value } : void 0
|
|
522
|
+
);
|
|
523
|
+
if (abort.aborted) return;
|
|
524
|
+
sendJson(res, { rows });
|
|
525
|
+
} catch (err) {
|
|
526
|
+
sendCaughtError(res, abort, err);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function handleExecute(sirannon, resolveTarget) {
|
|
531
|
+
return async (res, dbId, rawBody, abort) => {
|
|
532
|
+
const body = parseBody(res, rawBody);
|
|
533
|
+
if (!body) return;
|
|
534
|
+
if (!body.sql || typeof body.sql !== "string") {
|
|
535
|
+
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const writeConcern = parseWriteConcern(res, body.writeConcern);
|
|
539
|
+
if (!writeConcern.ok) return;
|
|
540
|
+
const params = decodeBoundParams(body.params, "params");
|
|
541
|
+
if (!params.ok) {
|
|
542
|
+
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
546
|
+
if (!target) return;
|
|
547
|
+
try {
|
|
548
|
+
const result = await target.execute(
|
|
549
|
+
body.sql,
|
|
550
|
+
params.value,
|
|
551
|
+
writeConcern.value ? { writeConcern: writeConcern.value } : void 0
|
|
552
|
+
);
|
|
553
|
+
if (abort.aborted) return;
|
|
554
|
+
sendJson(res, toExecuteResponse(result));
|
|
555
|
+
} catch (err) {
|
|
556
|
+
sendCaughtError(res, abort, err);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function handleTransaction(sirannon, resolveTarget) {
|
|
561
|
+
return async (res, dbId, rawBody, abort) => {
|
|
562
|
+
const body = parseBody(res, rawBody);
|
|
563
|
+
if (!body) return;
|
|
564
|
+
const statementsError = transactionStatementsValidationError(body.statements);
|
|
565
|
+
if (statementsError !== null) {
|
|
566
|
+
sendError(res, 400, "INVALID_REQUEST", statementsError);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const writeConcern = parseWriteConcern(res, body.writeConcern);
|
|
570
|
+
if (!writeConcern.ok) return;
|
|
571
|
+
const statements = [];
|
|
572
|
+
for (const stmt of body.statements) {
|
|
573
|
+
const params = decodeBoundParams(stmt.params, "params");
|
|
574
|
+
if (!params.ok) {
|
|
575
|
+
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
statements.push({ sql: stmt.sql, params: params.value });
|
|
579
|
+
}
|
|
580
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
581
|
+
if (!target) return;
|
|
582
|
+
const txOptions = writeConcern.value ? { writeConcern: writeConcern.value } : void 0;
|
|
583
|
+
try {
|
|
584
|
+
const results = target.executeTransaction ? await target.executeTransaction(statements, txOptions) : await target.transaction(async (tx) => {
|
|
585
|
+
const txResults = [];
|
|
586
|
+
for (const stmt of statements) {
|
|
587
|
+
txResults.push(await tx.execute(stmt.sql, stmt.params));
|
|
588
|
+
}
|
|
589
|
+
return txResults;
|
|
590
|
+
}, txOptions);
|
|
591
|
+
if (abort.aborted) return;
|
|
592
|
+
sendJson(res, {
|
|
593
|
+
results: results.map(toExecuteResponse)
|
|
594
|
+
});
|
|
595
|
+
} catch (err) {
|
|
596
|
+
sendCaughtError(res, abort, err);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function handleBatch(sirannon, resolveTarget) {
|
|
601
|
+
return async (res, dbId, rawBody, abort) => {
|
|
602
|
+
const body = parseBody(res, rawBody);
|
|
603
|
+
if (!body) return;
|
|
604
|
+
if (!body.sql || typeof body.sql !== "string") {
|
|
605
|
+
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const paramsBatchError = paramsBatchValidationError(body.paramsBatch);
|
|
609
|
+
if (paramsBatchError !== null) {
|
|
610
|
+
sendError(res, 400, "INVALID_REQUEST", paramsBatchError);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const writeConcern = parseWriteConcern(res, body.writeConcern);
|
|
614
|
+
if (!writeConcern.ok) return;
|
|
615
|
+
const paramsBatch = decodeBatchParams(res, body.paramsBatch);
|
|
616
|
+
if (paramsBatch === null) return;
|
|
617
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
618
|
+
if (!target) return;
|
|
619
|
+
try {
|
|
620
|
+
const results = await target.transaction(
|
|
621
|
+
async (tx) => tx.executeBatch(body.sql, paramsBatch),
|
|
622
|
+
writeConcern.value ? { writeConcern: writeConcern.value } : void 0
|
|
623
|
+
);
|
|
624
|
+
if (abort.aborted) return;
|
|
625
|
+
sendJson(res, {
|
|
626
|
+
results: results.map(toExecuteResponse)
|
|
627
|
+
});
|
|
628
|
+
} catch (err) {
|
|
629
|
+
sendCaughtError(res, abort, err);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function handleLoad(sirannon, resolveTarget) {
|
|
634
|
+
return async (res, dbId, rawBody, abort) => {
|
|
635
|
+
const body = parseBody(res, rawBody);
|
|
636
|
+
if (!body) return;
|
|
637
|
+
if (!body.sql || typeof body.sql !== "string") {
|
|
638
|
+
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const paramsBatchError = paramsBatchValidationError(body.paramsBatch);
|
|
642
|
+
if (paramsBatchError !== null) {
|
|
643
|
+
sendError(res, 400, "INVALID_REQUEST", paramsBatchError);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const durabilityError = loadDurabilityValidationError(body.durability);
|
|
647
|
+
if (durabilityError !== null) {
|
|
648
|
+
sendError(res, 400, "INVALID_REQUEST", durabilityError);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const checkpointError = loadCheckpointValidationError(body.checkpoint);
|
|
652
|
+
if (checkpointError !== null) {
|
|
653
|
+
sendError(res, 400, "INVALID_REQUEST", checkpointError);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const paramsBatch = decodeBatchParams(res, body.paramsBatch);
|
|
657
|
+
if (paramsBatch === null) return;
|
|
658
|
+
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
659
|
+
if (!target) return;
|
|
660
|
+
const bulkLoad = target.bulkLoad;
|
|
661
|
+
if (typeof bulkLoad !== "function") {
|
|
662
|
+
sendError(res, 501, "BULK_LOAD_UNSUPPORTED", "The execution target for this database does not support bulk load");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
const summary = await bulkLoad.call(target, body.sql, paramsBatch, toBulkLoadOptions(body));
|
|
667
|
+
if (abort.aborted) return;
|
|
668
|
+
sendJson(res, summary);
|
|
669
|
+
} catch (err) {
|
|
670
|
+
sendCaughtError(res, abort, err);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function sendClusterUnavailable(res, dbId) {
|
|
675
|
+
sendError(res, 404, "NOT_FOUND", `Cluster status is not available for database '${dbId}'`);
|
|
676
|
+
}
|
|
677
|
+
function handleClusterStatus(getClusterStatus, authorizeClusterStatus) {
|
|
678
|
+
return async (res, dbId, ctx, abort) => {
|
|
679
|
+
if (!getClusterStatus || !authorizeClusterStatus) {
|
|
680
|
+
sendClusterUnavailable(res, dbId);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
let allowed;
|
|
684
|
+
try {
|
|
685
|
+
allowed = await authorizeClusterStatus(ctx);
|
|
686
|
+
} catch {
|
|
687
|
+
if (abort.aborted) return;
|
|
688
|
+
sendError(res, 500, "HOOK_ERROR", "authorizeClusterStatus threw an error");
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (abort.aborted) return;
|
|
692
|
+
if (!allowed) {
|
|
693
|
+
sendClusterUnavailable(res, dbId);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const status = getClusterStatus(dbId);
|
|
697
|
+
if (!status) {
|
|
698
|
+
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${dbId}' not found`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
sendJson(res, toClusterStatusResponse(status));
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function toClusterStatusResponse(status) {
|
|
705
|
+
return {
|
|
706
|
+
...status,
|
|
707
|
+
currentPrimary: status.currentPrimary ? { ...status.currentPrimary } : status.currentPrimary,
|
|
708
|
+
readEndpoints: status.readEndpoints?.map((endpoint) => ({
|
|
709
|
+
...endpoint,
|
|
710
|
+
readConcerns: [...endpoint.readConcerns]
|
|
711
|
+
})),
|
|
712
|
+
primaryTerm: status.primaryTerm?.toString()
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/core/ws-handshake.ts
|
|
717
|
+
var SIRANNON_WS_SUBPROTOCOL = "sirannon.v1";
|
|
718
|
+
var WS_CLOSE_UNAUTHENTICATED = 4401;
|
|
719
|
+
var WS_CLOSE_FORBIDDEN = 4403;
|
|
720
|
+
function selectSubprotocol(header) {
|
|
721
|
+
const offered = header.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
|
722
|
+
if (offered.length === 0) return { ok: true, protocol: "" };
|
|
723
|
+
if (offered.includes(SIRANNON_WS_SUBPROTOCOL)) return { ok: true, protocol: SIRANNON_WS_SUBPROTOCOL };
|
|
724
|
+
return { ok: false };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/server/request-hook.ts
|
|
728
|
+
function decodeRemoteAddress(res) {
|
|
729
|
+
return Buffer.from(res.getRemoteAddressAsText()).toString();
|
|
730
|
+
}
|
|
731
|
+
var CLOSE_REASON_MAX_BYTES = 123;
|
|
732
|
+
var RETURNED_REFUSAL_MESSAGE = "The authenticate hook returned a refusal object rather than an identity. A hook refuses a request by throwing a SirannonError; every value it returns becomes the caller identity.";
|
|
733
|
+
function looksLikeRefusal(value) {
|
|
734
|
+
if (typeof value !== "object" || value === null) return false;
|
|
735
|
+
const candidate = value;
|
|
736
|
+
return typeof candidate.status === "number" && typeof candidate.code === "string" && typeof candidate.message === "string";
|
|
737
|
+
}
|
|
738
|
+
async function invokeAuthenticate(ctx, hook) {
|
|
739
|
+
try {
|
|
740
|
+
const identity = await hook(ctx);
|
|
741
|
+
if (looksLikeRefusal(identity)) return { status: "returned-refusal" };
|
|
742
|
+
return { status: "identity", identity };
|
|
743
|
+
} catch (error) {
|
|
744
|
+
return { status: "threw", error };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function sendRefusal(res, abort, outcome) {
|
|
748
|
+
if (!abort.claim()) return;
|
|
749
|
+
if (outcome.status === "returned-refusal") {
|
|
750
|
+
sendError(res, 500, "HOOK_ERROR", RETURNED_REFUSAL_MESSAGE);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (outcome.status === "threw" && outcome.error instanceof SirannonError) {
|
|
754
|
+
sendCaughtError(res, abort, outcome.error);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
sendError(res, 500, "HOOK_ERROR", "authenticate hook threw an error");
|
|
758
|
+
}
|
|
759
|
+
function truncateCloseReason(reason) {
|
|
760
|
+
if (Buffer.byteLength(reason, "utf-8") <= CLOSE_REASON_MAX_BYTES) return reason;
|
|
761
|
+
let truncated = reason.slice(0, CLOSE_REASON_MAX_BYTES);
|
|
762
|
+
while (truncated.length > 0 && Buffer.byteLength(truncated, "utf-8") > CLOSE_REASON_MAX_BYTES) {
|
|
763
|
+
truncated = truncated.slice(0, -1);
|
|
764
|
+
}
|
|
765
|
+
return truncated;
|
|
766
|
+
}
|
|
767
|
+
function upgradeRefusalFor(error) {
|
|
768
|
+
if (!(error instanceof SirannonError)) return void 0;
|
|
769
|
+
const status = httpStatusForError(error);
|
|
770
|
+
if (status !== 401 && status !== 403) return void 0;
|
|
771
|
+
return {
|
|
772
|
+
code: status === 403 ? WS_CLOSE_FORBIDDEN : WS_CLOSE_UNAUTHENTICATED,
|
|
773
|
+
reason: truncateCloseReason(`${error.code}: ${error.message}`)
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
async function runAuthenticate(res, abort, ctx, hook) {
|
|
777
|
+
const outcome = await invokeAuthenticate(ctx, hook);
|
|
778
|
+
if (outcome.status === "identity") return { ok: true, identity: outcome.identity };
|
|
779
|
+
sendRefusal(res, abort, outcome);
|
|
780
|
+
return { ok: false };
|
|
781
|
+
}
|
|
782
|
+
async function authenticateUpgrade(res, abort, ctx, hook) {
|
|
783
|
+
const outcome = await invokeAuthenticate(ctx, hook);
|
|
784
|
+
if (outcome.status === "identity") return { ok: true, identity: outcome.identity };
|
|
785
|
+
if (outcome.status === "threw") {
|
|
786
|
+
const refusal = upgradeRefusalFor(outcome.error);
|
|
787
|
+
if (refusal) return { ok: false, refusal };
|
|
788
|
+
}
|
|
789
|
+
sendRefusal(res, abort, outcome);
|
|
790
|
+
return { ok: false, refusal: void 0 };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/server/db-route.ts
|
|
794
|
+
function readHeaders(req) {
|
|
795
|
+
const headers = {};
|
|
796
|
+
req.forEach((key, value) => {
|
|
797
|
+
headers[key] = value;
|
|
798
|
+
});
|
|
799
|
+
return headers;
|
|
800
|
+
}
|
|
801
|
+
function wrapDbRoute(deps, handler) {
|
|
802
|
+
const { authenticateHook, cors, maxBodyBytes } = deps;
|
|
803
|
+
return (res, req) => {
|
|
804
|
+
const dbId = req.getParameter(0) ?? "";
|
|
805
|
+
const method = req.getMethod();
|
|
806
|
+
const path = req.getUrl();
|
|
807
|
+
if (cors) {
|
|
808
|
+
writeCorsOrigin(res, cors, req.getHeader("origin"));
|
|
809
|
+
}
|
|
810
|
+
const abort = initAbortHandler(res);
|
|
811
|
+
const bodyPromise = readBody(res, maxBodyBytes, abort);
|
|
812
|
+
if (!authenticateHook) {
|
|
813
|
+
bodyPromise.then(async (rawBody) => {
|
|
814
|
+
if (!abort.claim()) return;
|
|
815
|
+
try {
|
|
816
|
+
await handler(res, dbId, rawBody, abort, void 0);
|
|
817
|
+
} catch {
|
|
818
|
+
if (!abort.aborted) {
|
|
819
|
+
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}).catch(() => {
|
|
823
|
+
});
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const ctx = {
|
|
827
|
+
headers: readHeaders(req),
|
|
828
|
+
method,
|
|
829
|
+
path,
|
|
830
|
+
databaseId: dbId,
|
|
831
|
+
remoteAddress: decodeRemoteAddress(res)
|
|
832
|
+
};
|
|
833
|
+
const hookPromise = runAuthenticate(res, abort, ctx, authenticateHook);
|
|
834
|
+
Promise.all([bodyPromise, hookPromise]).then(async ([rawBody, authenticated]) => {
|
|
835
|
+
if (!authenticated.ok || !abort.claim()) return;
|
|
836
|
+
try {
|
|
837
|
+
await handler(res, dbId, rawBody, abort, authenticated.identity);
|
|
838
|
+
} catch {
|
|
839
|
+
if (!abort.aborted) {
|
|
840
|
+
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}).catch(() => {
|
|
844
|
+
});
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function wrapDbGetRoute(deps, handler) {
|
|
848
|
+
const { authenticateHook, cors } = deps;
|
|
849
|
+
return (res, req) => {
|
|
850
|
+
const dbId = req.getParameter(0) ?? "";
|
|
851
|
+
const method = req.getMethod();
|
|
852
|
+
const path = req.getUrl();
|
|
853
|
+
if (cors) {
|
|
854
|
+
writeCorsOrigin(res, cors, req.getHeader("origin"));
|
|
855
|
+
}
|
|
856
|
+
const ctx = {
|
|
857
|
+
headers: readHeaders(req),
|
|
858
|
+
method,
|
|
859
|
+
path,
|
|
860
|
+
databaseId: dbId,
|
|
861
|
+
remoteAddress: decodeRemoteAddress(res)
|
|
862
|
+
};
|
|
863
|
+
const abort = initAbortHandler(res);
|
|
864
|
+
const run = async () => {
|
|
865
|
+
if (authenticateHook && !(await runAuthenticate(res, abort, ctx, authenticateHook)).ok) return;
|
|
866
|
+
if (!abort.claim()) return;
|
|
867
|
+
try {
|
|
868
|
+
await handler(res, dbId, ctx, abort);
|
|
869
|
+
} catch {
|
|
870
|
+
if (!abort.aborted) {
|
|
871
|
+
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
run().catch(() => {
|
|
876
|
+
});
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
475
880
|
// src/server/health.ts
|
|
476
881
|
function handleLiveness() {
|
|
477
882
|
const payload = JSON.stringify({ status: "ok" });
|
|
@@ -881,204 +1286,49 @@ function toChangesResponse(result) {
|
|
|
881
1286
|
return { applied: result.applied, skipped: result.skipped, conflicts: result.conflicts };
|
|
882
1287
|
}
|
|
883
1288
|
|
|
884
|
-
// src/server/
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
return encodeTaggedValues(await target.query(sql, params, options));
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// src/server/http-handler.ts
|
|
893
|
-
function decodeBatchParams(res, raw) {
|
|
894
|
-
const decoded = [];
|
|
895
|
-
for (const entry of raw) {
|
|
896
|
-
const result = decodeBoundParams(entry, "paramsBatch");
|
|
897
|
-
if (!result.ok) {
|
|
898
|
-
sendError(res, 400, "INVALID_REQUEST", result.message);
|
|
899
|
-
return null;
|
|
900
|
-
}
|
|
901
|
-
decoded.push(result.value);
|
|
902
|
-
}
|
|
903
|
-
return decoded;
|
|
904
|
-
}
|
|
905
|
-
function handleQuery(sirannon, resolveTarget) {
|
|
906
|
-
return async (res, dbId, rawBody, abort) => {
|
|
907
|
-
const body = parseBody(res, rawBody);
|
|
908
|
-
if (!body) return;
|
|
909
|
-
if (!body.sql || typeof body.sql !== "string") {
|
|
910
|
-
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
const readConcern = parseReadConcern(res, body.readConcern);
|
|
914
|
-
if (!readConcern.ok) return;
|
|
915
|
-
const params = decodeBoundParams(body.params, "params");
|
|
916
|
-
if (!params.ok) {
|
|
917
|
-
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
921
|
-
if (!target) return;
|
|
922
|
-
try {
|
|
923
|
-
const rows = await queryWireRows(
|
|
924
|
-
target,
|
|
925
|
-
body.sql,
|
|
926
|
-
params.value,
|
|
927
|
-
readConcern.value ? { readConcern: readConcern.value } : void 0
|
|
928
|
-
);
|
|
929
|
-
if (abort.aborted) return;
|
|
930
|
-
sendJson(res, { rows });
|
|
931
|
-
} catch (err) {
|
|
932
|
-
sendCaughtError(res, abort, err);
|
|
933
|
-
}
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
function handleExecute(sirannon, resolveTarget) {
|
|
937
|
-
return async (res, dbId, rawBody, abort) => {
|
|
938
|
-
const body = parseBody(res, rawBody);
|
|
939
|
-
if (!body) return;
|
|
940
|
-
if (!body.sql || typeof body.sql !== "string") {
|
|
941
|
-
sendError(res, 400, "INVALID_REQUEST", 'Field "sql" is required and must be a string');
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
const writeConcern = parseWriteConcern(res, body.writeConcern);
|
|
945
|
-
if (!writeConcern.ok) return;
|
|
946
|
-
const params = decodeBoundParams(body.params, "params");
|
|
947
|
-
if (!params.ok) {
|
|
948
|
-
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
952
|
-
if (!target) return;
|
|
1289
|
+
// src/server/http-changes.ts
|
|
1290
|
+
var MAX_DEVICE_CLOCK_AHEAD_MS = 3e5;
|
|
1291
|
+
function changeClockRefusal(batch, nowMs) {
|
|
1292
|
+
for (const [index, change] of batch.changes.entries()) {
|
|
1293
|
+
let wallMs;
|
|
953
1294
|
try {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
writeConcern.value ? { writeConcern: writeConcern.value } : void 0
|
|
958
|
-
);
|
|
959
|
-
if (abort.aborted) return;
|
|
960
|
-
sendJson(res, toExecuteResponse(result));
|
|
961
|
-
} catch (err) {
|
|
962
|
-
sendCaughtError(res, abort, err);
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
function handleTransaction(sirannon, resolveTarget) {
|
|
967
|
-
return async (res, dbId, rawBody, abort) => {
|
|
968
|
-
const body = parseBody(res, rawBody);
|
|
969
|
-
if (!body) return;
|
|
970
|
-
const statementsError = transactionStatementsValidationError(body.statements);
|
|
971
|
-
if (statementsError !== null) {
|
|
972
|
-
sendError(res, 400, "INVALID_REQUEST", statementsError);
|
|
973
|
-
return;
|
|
1295
|
+
wallMs = HLC.decode(change.hlc).wallMs;
|
|
1296
|
+
} catch {
|
|
1297
|
+
wallMs = Number.NaN;
|
|
974
1298
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const statements = [];
|
|
978
|
-
for (const stmt of body.statements) {
|
|
979
|
-
const params = decodeBoundParams(stmt.params, "params");
|
|
980
|
-
if (!params.ok) {
|
|
981
|
-
sendError(res, 400, "INVALID_REQUEST", params.message);
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
statements.push({ sql: stmt.sql, params: params.value });
|
|
1299
|
+
if (!Number.isFinite(wallMs)) {
|
|
1300
|
+
return { code: "INVALID_REQUEST", message: `changes[${index}].hlc is not a valid timestamp` };
|
|
985
1301
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const txResults = [];
|
|
992
|
-
for (const stmt of statements) {
|
|
993
|
-
txResults.push(await tx.execute(stmt.sql, stmt.params));
|
|
994
|
-
}
|
|
995
|
-
return txResults;
|
|
996
|
-
}, txOptions);
|
|
997
|
-
if (abort.aborted) return;
|
|
998
|
-
sendJson(res, {
|
|
999
|
-
results: results.map(toExecuteResponse)
|
|
1000
|
-
});
|
|
1001
|
-
} catch (err) {
|
|
1002
|
-
sendCaughtError(res, abort, err);
|
|
1302
|
+
if (wallMs > nowMs + MAX_DEVICE_CLOCK_AHEAD_MS) {
|
|
1303
|
+
return {
|
|
1304
|
+
code: "DEVICE_CLOCK_AHEAD",
|
|
1305
|
+
message: `changes[${index}] is stamped more than ${MAX_DEVICE_CLOCK_AHEAD_MS} ms ahead of the server clock; correct the device clock`
|
|
1306
|
+
};
|
|
1003
1307
|
}
|
|
1004
|
-
}
|
|
1308
|
+
}
|
|
1309
|
+
return null;
|
|
1005
1310
|
}
|
|
1006
|
-
function
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1014
|
-
const paramsBatchError = paramsBatchValidationError(body.paramsBatch);
|
|
1015
|
-
if (paramsBatchError !== null) {
|
|
1016
|
-
sendError(res, 400, "INVALID_REQUEST", paramsBatchError);
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
const writeConcern = parseWriteConcern(res, body.writeConcern);
|
|
1020
|
-
if (!writeConcern.ok) return;
|
|
1021
|
-
const paramsBatch = decodeBatchParams(res, body.paramsBatch);
|
|
1022
|
-
if (paramsBatch === null) return;
|
|
1023
|
-
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
1024
|
-
if (!target) return;
|
|
1025
|
-
try {
|
|
1026
|
-
const results = await target.transaction(
|
|
1027
|
-
async (tx) => tx.executeBatch(body.sql, paramsBatch),
|
|
1028
|
-
writeConcern.value ? { writeConcern: writeConcern.value } : void 0
|
|
1029
|
-
);
|
|
1030
|
-
if (abort.aborted) return;
|
|
1031
|
-
sendJson(res, {
|
|
1032
|
-
results: results.map(toExecuteResponse)
|
|
1033
|
-
});
|
|
1034
|
-
} catch (err) {
|
|
1035
|
-
sendCaughtError(res, abort, err);
|
|
1036
|
-
}
|
|
1037
|
-
};
|
|
1311
|
+
async function refuseUngrantedPush(sirannon, databaseId, batch, identity) {
|
|
1312
|
+
if (!sirannon.hookRegistry.has("beforePush")) return;
|
|
1313
|
+
const tables = new Set(batch.changes.map((change) => change.table));
|
|
1314
|
+
for (const table of tables) {
|
|
1315
|
+
const ctx = { databaseId, table, deviceId: batch.sourceNodeId, identity };
|
|
1316
|
+
await sirannon.hookRegistry.invoke("beforePush", ctx);
|
|
1317
|
+
}
|
|
1038
1318
|
}
|
|
1039
|
-
function
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
sendError(res, 400, "INVALID_REQUEST", paramsBatchError);
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
const durabilityError = loadDurabilityValidationError(body.durability);
|
|
1053
|
-
if (durabilityError !== null) {
|
|
1054
|
-
sendError(res, 400, "INVALID_REQUEST", durabilityError);
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
const checkpointError = loadCheckpointValidationError(body.checkpoint);
|
|
1058
|
-
if (checkpointError !== null) {
|
|
1059
|
-
sendError(res, 400, "INVALID_REQUEST", checkpointError);
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
const paramsBatch = decodeBatchParams(res, body.paramsBatch);
|
|
1063
|
-
if (paramsBatch === null) return;
|
|
1064
|
-
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
1065
|
-
if (!target) return;
|
|
1066
|
-
const bulkLoad = target.bulkLoad;
|
|
1067
|
-
if (typeof bulkLoad !== "function") {
|
|
1068
|
-
sendError(res, 501, "BULK_LOAD_UNSUPPORTED", "The execution target for this database does not support bulk load");
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
try {
|
|
1072
|
-
const summary = await bulkLoad.call(target, body.sql, paramsBatch, toBulkLoadOptions(body));
|
|
1073
|
-
if (abort.aborted) return;
|
|
1074
|
-
sendJson(res, summary);
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
sendCaughtError(res, abort, err);
|
|
1077
|
-
}
|
|
1078
|
-
};
|
|
1319
|
+
async function refuseSchemaMismatch(res, abort, target, deviceVersion) {
|
|
1320
|
+
const appliedMigrations = target.appliedMigrations;
|
|
1321
|
+
if (typeof appliedMigrations !== "function") return false;
|
|
1322
|
+
const serverVersion = highestMigrationVersion(await appliedMigrations.call(target));
|
|
1323
|
+
const refusal = schemaVersionGateRefusal(deviceVersion, serverVersion);
|
|
1324
|
+
if (refusal === null) return false;
|
|
1325
|
+
if (!abort.aborted) {
|
|
1326
|
+
sendError(res, 409, refusal.code, refusal.message, { serverVersion });
|
|
1327
|
+
}
|
|
1328
|
+
return true;
|
|
1079
1329
|
}
|
|
1080
1330
|
function handleChanges(sirannon, resolveTarget) {
|
|
1081
|
-
return async (res, dbId, rawBody, abort) => {
|
|
1331
|
+
return async (res, dbId, rawBody, abort, identity) => {
|
|
1082
1332
|
const body = parseBody(res, rawBody);
|
|
1083
1333
|
if (!body) return;
|
|
1084
1334
|
const batchError = syncBatchValidationError(body.batch);
|
|
@@ -1093,33 +1343,26 @@ function handleChanges(sirannon, resolveTarget) {
|
|
|
1093
1343
|
}
|
|
1094
1344
|
const target = await resolveExecutionTarget(res, abort, sirannon, dbId, resolveTarget);
|
|
1095
1345
|
if (!target) return;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
if (abort.aborted) return;
|
|
1103
|
-
sendError(res, 409, refusal.code, refusal.message, { serverVersion });
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
} catch (err) {
|
|
1107
|
-
sendCaughtError(res, abort, err);
|
|
1346
|
+
try {
|
|
1347
|
+
const batch = decodeSyncBatch(body.batch);
|
|
1348
|
+
await refuseUngrantedPush(sirannon, dbId, batch, identity);
|
|
1349
|
+
const clockRefusal = changeClockRefusal(batch, Date.now());
|
|
1350
|
+
if (clockRefusal !== null) {
|
|
1351
|
+
sendError(res, 400, clockRefusal.code, clockRefusal.message);
|
|
1108
1352
|
return;
|
|
1109
1353
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
const result = await applyChanges.call(target, decodeSyncBatch(body.batch));
|
|
1354
|
+
if (await refuseSchemaMismatch(res, abort, target, body.schemaVersion ?? 0)) return;
|
|
1355
|
+
const applyChanges = target.applyChanges;
|
|
1356
|
+
if (typeof applyChanges !== "function") {
|
|
1357
|
+
sendError(
|
|
1358
|
+
res,
|
|
1359
|
+
501,
|
|
1360
|
+
"SYNC_UNSUPPORTED",
|
|
1361
|
+
"The execution target for this database does not support applying sync changes"
|
|
1362
|
+
);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const result = await applyChanges.call(target, batch);
|
|
1123
1366
|
if (abort.aborted) return;
|
|
1124
1367
|
sendJson(res, toChangesResponse(result));
|
|
1125
1368
|
} catch (err) {
|
|
@@ -1127,47 +1370,6 @@ function handleChanges(sirannon, resolveTarget) {
|
|
|
1127
1370
|
}
|
|
1128
1371
|
};
|
|
1129
1372
|
}
|
|
1130
|
-
function sendClusterUnavailable(res, dbId) {
|
|
1131
|
-
sendError(res, 404, "NOT_FOUND", `Cluster status is not available for database '${dbId}'`);
|
|
1132
|
-
}
|
|
1133
|
-
function handleClusterStatus(getClusterStatus, authorizeClusterStatus) {
|
|
1134
|
-
return async (res, dbId, ctx, abort) => {
|
|
1135
|
-
if (!getClusterStatus || !authorizeClusterStatus) {
|
|
1136
|
-
sendClusterUnavailable(res, dbId);
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
let allowed;
|
|
1140
|
-
try {
|
|
1141
|
-
allowed = await authorizeClusterStatus(ctx);
|
|
1142
|
-
} catch {
|
|
1143
|
-
if (abort.aborted) return;
|
|
1144
|
-
sendError(res, 500, "HOOK_ERROR", "authorizeClusterStatus threw an error");
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
if (abort.aborted) return;
|
|
1148
|
-
if (!allowed) {
|
|
1149
|
-
sendClusterUnavailable(res, dbId);
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
const status = getClusterStatus(dbId);
|
|
1153
|
-
if (!status) {
|
|
1154
|
-
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${dbId}' not found`);
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
sendJson(res, toClusterStatusResponse(status));
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
function toClusterStatusResponse(status) {
|
|
1161
|
-
return {
|
|
1162
|
-
...status,
|
|
1163
|
-
currentPrimary: status.currentPrimary ? { ...status.currentPrimary } : status.currentPrimary,
|
|
1164
|
-
readEndpoints: status.readEndpoints?.map((endpoint) => ({
|
|
1165
|
-
...endpoint,
|
|
1166
|
-
readConcerns: [...endpoint.readConcerns]
|
|
1167
|
-
})),
|
|
1168
|
-
primaryTerm: status.primaryTerm?.toString()
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
1373
|
|
|
1172
1374
|
// src/server/http-migrations.ts
|
|
1173
1375
|
function handleMigrationList(sirannon) {
|
|
@@ -1232,9 +1434,9 @@ function decodeArguments(res, raw) {
|
|
|
1232
1434
|
}
|
|
1233
1435
|
return decoded.value ?? {};
|
|
1234
1436
|
}
|
|
1235
|
-
function handleOperationQuery(sirannon,
|
|
1437
|
+
function handleOperationQuery(sirannon, sets, resolveTarget) {
|
|
1236
1438
|
return async (res, dbId, name, identity, rawBody, abort) => {
|
|
1237
|
-
const operation = findRead(
|
|
1439
|
+
const operation = findRead(sets, dbId, name);
|
|
1238
1440
|
if (isRefusal(operation)) {
|
|
1239
1441
|
sendError(res, operation.status, operation.code, operation.message);
|
|
1240
1442
|
return;
|
|
@@ -1273,9 +1475,9 @@ function handleOperationQuery(sirannon, registry, resolveTarget) {
|
|
|
1273
1475
|
}
|
|
1274
1476
|
};
|
|
1275
1477
|
}
|
|
1276
|
-
function handleOperationExecute(sirannon,
|
|
1478
|
+
function handleOperationExecute(sirannon, sets, resolveTarget) {
|
|
1277
1479
|
return async (res, dbId, name, identity, rawBody, abort) => {
|
|
1278
|
-
const operation = findWrite(
|
|
1480
|
+
const operation = findWrite(sets, dbId, name);
|
|
1279
1481
|
if (isRefusal(operation)) {
|
|
1280
1482
|
sendError(res, operation.status, operation.code, operation.message);
|
|
1281
1483
|
return;
|
|
@@ -1514,6 +1716,14 @@ function assertBackupRestoreAuthenticated(acceptBackupRestore, hasAuthenticate)
|
|
|
1514
1716
|
);
|
|
1515
1717
|
}
|
|
1516
1718
|
}
|
|
1719
|
+
function assertDeviceSyncAuthenticated(acceptDeviceSync, hasAuthenticate) {
|
|
1720
|
+
if (acceptDeviceSync && !hasAuthenticate) {
|
|
1721
|
+
throw new SirannonError(
|
|
1722
|
+
"ServerOptions.acceptDeviceSync opens a route that writes rows into every table of a database, so it needs an authenticate hook to name the caller",
|
|
1723
|
+
"INVALID_DEVICE_SYNC"
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1517
1727
|
function resolveMaxBodyBytes(value) {
|
|
1518
1728
|
if (value === void 0) return DEFAULT_MAX_BODY_BYTES;
|
|
1519
1729
|
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
@@ -1553,83 +1763,6 @@ function resolveWsBackpressure(value, maxBodyBytes) {
|
|
|
1553
1763
|
return resolved;
|
|
1554
1764
|
}
|
|
1555
1765
|
|
|
1556
|
-
// src/core/ws-handshake.ts
|
|
1557
|
-
var SIRANNON_WS_SUBPROTOCOL = "sirannon.v1";
|
|
1558
|
-
var WS_CLOSE_UNAUTHENTICATED = 4401;
|
|
1559
|
-
var WS_CLOSE_FORBIDDEN = 4403;
|
|
1560
|
-
function selectSubprotocol(header) {
|
|
1561
|
-
const offered = header.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
|
|
1562
|
-
if (offered.length === 0) return { ok: true, protocol: "" };
|
|
1563
|
-
if (offered.includes(SIRANNON_WS_SUBPROTOCOL)) return { ok: true, protocol: SIRANNON_WS_SUBPROTOCOL };
|
|
1564
|
-
return { ok: false };
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
// src/server/request-hook.ts
|
|
1568
|
-
function decodeRemoteAddress(res) {
|
|
1569
|
-
return Buffer.from(res.getRemoteAddressAsText()).toString();
|
|
1570
|
-
}
|
|
1571
|
-
var CLOSE_REASON_MAX_BYTES = 123;
|
|
1572
|
-
var RETURNED_REFUSAL_MESSAGE = "The authenticate hook returned a refusal object rather than an identity. A hook refuses a request by throwing a SirannonError; every value it returns becomes the caller identity.";
|
|
1573
|
-
function looksLikeRefusal(value) {
|
|
1574
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1575
|
-
const candidate = value;
|
|
1576
|
-
return typeof candidate.status === "number" && typeof candidate.code === "string" && typeof candidate.message === "string";
|
|
1577
|
-
}
|
|
1578
|
-
async function invokeAuthenticate(ctx, hook) {
|
|
1579
|
-
try {
|
|
1580
|
-
const identity = await hook(ctx);
|
|
1581
|
-
if (looksLikeRefusal(identity)) return { status: "returned-refusal" };
|
|
1582
|
-
return { status: "identity", identity };
|
|
1583
|
-
} catch (error) {
|
|
1584
|
-
return { status: "threw", error };
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
function sendRefusal(res, abort, outcome) {
|
|
1588
|
-
if (!abort.claim()) return;
|
|
1589
|
-
if (outcome.status === "returned-refusal") {
|
|
1590
|
-
sendError(res, 500, "HOOK_ERROR", RETURNED_REFUSAL_MESSAGE);
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
if (outcome.status === "threw" && outcome.error instanceof SirannonError) {
|
|
1594
|
-
sendCaughtError(res, abort, outcome.error);
|
|
1595
|
-
return;
|
|
1596
|
-
}
|
|
1597
|
-
sendError(res, 500, "HOOK_ERROR", "authenticate hook threw an error");
|
|
1598
|
-
}
|
|
1599
|
-
function truncateCloseReason(reason) {
|
|
1600
|
-
if (Buffer.byteLength(reason, "utf-8") <= CLOSE_REASON_MAX_BYTES) return reason;
|
|
1601
|
-
let truncated = reason.slice(0, CLOSE_REASON_MAX_BYTES);
|
|
1602
|
-
while (truncated.length > 0 && Buffer.byteLength(truncated, "utf-8") > CLOSE_REASON_MAX_BYTES) {
|
|
1603
|
-
truncated = truncated.slice(0, -1);
|
|
1604
|
-
}
|
|
1605
|
-
return truncated;
|
|
1606
|
-
}
|
|
1607
|
-
function upgradeRefusalFor(error) {
|
|
1608
|
-
if (!(error instanceof SirannonError)) return void 0;
|
|
1609
|
-
const status = httpStatusForError(error);
|
|
1610
|
-
if (status !== 401 && status !== 403) return void 0;
|
|
1611
|
-
return {
|
|
1612
|
-
code: status === 403 ? WS_CLOSE_FORBIDDEN : WS_CLOSE_UNAUTHENTICATED,
|
|
1613
|
-
reason: truncateCloseReason(`${error.code}: ${error.message}`)
|
|
1614
|
-
};
|
|
1615
|
-
}
|
|
1616
|
-
async function runAuthenticate(res, abort, ctx, hook) {
|
|
1617
|
-
const outcome = await invokeAuthenticate(ctx, hook);
|
|
1618
|
-
if (outcome.status === "identity") return { ok: true, identity: outcome.identity };
|
|
1619
|
-
sendRefusal(res, abort, outcome);
|
|
1620
|
-
return { ok: false };
|
|
1621
|
-
}
|
|
1622
|
-
async function authenticateUpgrade(res, abort, ctx, hook) {
|
|
1623
|
-
const outcome = await invokeAuthenticate(ctx, hook);
|
|
1624
|
-
if (outcome.status === "identity") return { ok: true, identity: outcome.identity };
|
|
1625
|
-
if (outcome.status === "threw") {
|
|
1626
|
-
const refusal = upgradeRefusalFor(outcome.error);
|
|
1627
|
-
if (refusal) return { ok: false, refusal };
|
|
1628
|
-
}
|
|
1629
|
-
sendRefusal(res, abort, outcome);
|
|
1630
|
-
return { ok: false, refusal: void 0 };
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
1766
|
// src/server/operation-route.ts
|
|
1634
1767
|
function decodePathSegment(value) {
|
|
1635
1768
|
try {
|
|
@@ -1685,37 +1818,33 @@ function wrapOperationRoute(deps, handler) {
|
|
|
1685
1818
|
};
|
|
1686
1819
|
}
|
|
1687
1820
|
|
|
1821
|
+
// src/server/uws-loader.ts
|
|
1822
|
+
var UWEBSOCKETS_DEPENDENCY = {
|
|
1823
|
+
packageName: "uWebSockets.js",
|
|
1824
|
+
neededBy: "The Sirannon server",
|
|
1825
|
+
installCommand: 'pnpm add -E "uWebSockets.js@github:uNetworking/uWebSockets.js#v20.69.0"',
|
|
1826
|
+
code: "SERVER_DEPENDENCY_MISSING"
|
|
1827
|
+
};
|
|
1828
|
+
async function loadUWebSockets() {
|
|
1829
|
+
try {
|
|
1830
|
+
return await import('uWebSockets.js');
|
|
1831
|
+
} catch (err) {
|
|
1832
|
+
throw optionalDependencyError(UWEBSOCKETS_DEPENDENCY, err);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1688
1836
|
// src/server/device-cursors.ts
|
|
1689
|
-
var DEFAULT_DEVICE_CURSOR_RETENTION_MS = 30 * 24 * 36e5;
|
|
1690
1837
|
async function upsertDeviceAck(conn, deviceId, seq) {
|
|
1691
1838
|
await ensureDeviceCursorsTable(conn);
|
|
1692
1839
|
await upsertDeviceCursor(conn, deviceId, seq, Date.now() / 1e3);
|
|
1693
1840
|
}
|
|
1694
|
-
function evictStaleDeviceCursors(conn, retentionMs) {
|
|
1695
|
-
return deleteDeviceCursorsUpdatedBefore(conn, Date.now() / 1e3 - retentionMs / 1e3);
|
|
1696
|
-
}
|
|
1697
|
-
async function effectiveMinDeviceCursor(conn, retentionMs, changesTable = CHANGES_TABLE) {
|
|
1698
|
-
if (!await selectTableExists(conn, DEVICE_CURSORS_TABLE)) return null;
|
|
1699
|
-
await evictStaleDeviceCursors(conn, retentionMs);
|
|
1700
|
-
const cursors = await selectDeviceCursors(conn);
|
|
1701
|
-
if (cursors.length === 0) return null;
|
|
1702
|
-
const nextForeignStmt = await conn.prepare(selectMinForeignChangeSeqSql(changesTable));
|
|
1703
|
-
const maxSeq = await selectMaxChangeSeq(conn, changesTable);
|
|
1704
|
-
let min = null;
|
|
1705
|
-
for (const cursor of cursors) {
|
|
1706
|
-
const acked = cursor.ackedSeq;
|
|
1707
|
-
const foreignRow = await nextForeignStmt.get(acked.toString(), cursor.deviceId);
|
|
1708
|
-
const nextForeign = foreignRow?.seq === null || foreignRow?.seq === void 0 ? null : BigInt(foreignRow.seq);
|
|
1709
|
-
const effective = nextForeign === null ? maxSeq : nextForeign - 1n;
|
|
1710
|
-
if (min === null || effective < min) {
|
|
1711
|
-
min = effective;
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
return min;
|
|
1715
|
-
}
|
|
1716
1841
|
|
|
1717
1842
|
// src/server/ws-ack.ts
|
|
1718
1843
|
async function handleAckMessage(deps, conn, state, msg, id) {
|
|
1844
|
+
if (!deps.acceptDeviceSync) {
|
|
1845
|
+
deps.sendError(conn, id, "DEVICE_SYNC_NOT_ACCEPTED", DEVICE_SYNC_NOT_ACCEPTED_MESSAGE);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1719
1848
|
if (!isValidDeviceId(msg.deviceId)) {
|
|
1720
1849
|
deps.sendError(conn, id, "INVALID_MESSAGE", '"deviceId" must be a 32-hex device id');
|
|
1721
1850
|
return;
|
|
@@ -1725,13 +1854,21 @@ async function handleAckMessage(deps, conn, state, msg, id) {
|
|
|
1725
1854
|
return;
|
|
1726
1855
|
}
|
|
1727
1856
|
const deviceId = msg.deviceId;
|
|
1857
|
+
const streams = [...state.deviceStreams.values()].filter((stream) => stream.deviceId === deviceId);
|
|
1858
|
+
if (streams.length === 0) {
|
|
1859
|
+
deps.sendError(
|
|
1860
|
+
conn,
|
|
1861
|
+
id,
|
|
1862
|
+
"DEVICE_NOT_SUBSCRIBED",
|
|
1863
|
+
"An acknowledgement names a device this connection holds no subscription for"
|
|
1864
|
+
);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1728
1867
|
const seq = BigInt(msg.seq);
|
|
1729
1868
|
try {
|
|
1730
1869
|
await state.database.runCdcMaintenance((writer) => upsertDeviceAck(writer, deviceId, seq));
|
|
1731
|
-
for (const stream of
|
|
1732
|
-
|
|
1733
|
-
stream.onAck(seq);
|
|
1734
|
-
}
|
|
1870
|
+
for (const stream of streams) {
|
|
1871
|
+
stream.onAck(seq);
|
|
1735
1872
|
}
|
|
1736
1873
|
deps.sendResult(conn, id, { acked: true, seq: seq.toString() });
|
|
1737
1874
|
} catch (err) {
|
|
@@ -1742,17 +1879,24 @@ async function handleAckMessage(deps, conn, state, msg, id) {
|
|
|
1742
1879
|
// src/server/ws-cdc.ts
|
|
1743
1880
|
var DEFAULT_POLL_INTERVAL_MS = 50;
|
|
1744
1881
|
var CLEANUP_INTERVAL_TICKS = 100;
|
|
1882
|
+
function toChangeRetentionOptions(options) {
|
|
1883
|
+
return {
|
|
1884
|
+
...options?.cdcRetentionMs === void 0 ? {} : { cdcRetention: options.cdcRetentionMs },
|
|
1885
|
+
...options?.deviceCursorRetentionMs === void 0 ? {} : { deviceCursorRetention: options.deviceCursorRetentionMs },
|
|
1886
|
+
...options?.maxChangesHeldForDevice === void 0 ? {} : { maxChangesHeldForDevice: options.maxChangesHeldForDevice }
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1745
1889
|
var CdcContextRegistry = class {
|
|
1746
1890
|
sirannon;
|
|
1747
1891
|
retentionMs;
|
|
1748
|
-
|
|
1892
|
+
serverDeviceRetention;
|
|
1749
1893
|
contexts = /* @__PURE__ */ new Map();
|
|
1750
1894
|
pending = /* @__PURE__ */ new Map();
|
|
1751
1895
|
closed = false;
|
|
1752
|
-
constructor(sirannon,
|
|
1896
|
+
constructor(sirannon, serverRetention = {}) {
|
|
1753
1897
|
this.sirannon = sirannon;
|
|
1754
|
-
this.retentionMs =
|
|
1755
|
-
this.
|
|
1898
|
+
this.retentionMs = serverRetention.cdcRetention;
|
|
1899
|
+
this.serverDeviceRetention = serverRetention;
|
|
1756
1900
|
}
|
|
1757
1901
|
async ensure(databaseId, database) {
|
|
1758
1902
|
const existing = this.contexts.get(databaseId);
|
|
@@ -1800,8 +1944,10 @@ var CdcContextRegistry = class {
|
|
|
1800
1944
|
}
|
|
1801
1945
|
async createContext(database) {
|
|
1802
1946
|
const cdcConn = await this.sirannon.driver.open(database.path, { walMode: true });
|
|
1803
|
-
const
|
|
1804
|
-
const
|
|
1947
|
+
const retention = database.changeRetention.cdcRetention ?? this.retentionMs;
|
|
1948
|
+
const deviceRetention = resolveDeviceRetentionPolicy(database.changeRetention, this.serverDeviceRetention);
|
|
1949
|
+
const tracker = new ChangeTracker(retention === void 0 ? void 0 : { retention });
|
|
1950
|
+
const manager = new SubscriptionManager(this.sirannon.metrics?.observeDispatch(database.id));
|
|
1805
1951
|
let epoch = "";
|
|
1806
1952
|
try {
|
|
1807
1953
|
await tracker.advanceToLatest(cdcConn);
|
|
@@ -1840,12 +1986,7 @@ var CdcContextRegistry = class {
|
|
|
1840
1986
|
if (tickCount >= CLEANUP_INTERVAL_TICKS) {
|
|
1841
1987
|
tickCount = 0;
|
|
1842
1988
|
await database.runCdcMaintenance(async (writer) => {
|
|
1843
|
-
|
|
1844
|
-
if (minCursor === null) {
|
|
1845
|
-
tracker.clearPruneBoundary();
|
|
1846
|
-
} else {
|
|
1847
|
-
tracker.setPruneBoundary(minCursor);
|
|
1848
|
-
}
|
|
1989
|
+
await applyDeviceCursorBoundary(writer, tracker, deviceRetention);
|
|
1849
1990
|
await tracker.cleanup(writer);
|
|
1850
1991
|
}).catch(() => {
|
|
1851
1992
|
});
|
|
@@ -2015,14 +2156,15 @@ var DeviceChangeStream = class {
|
|
|
2015
2156
|
this.deps.packer?.clear();
|
|
2016
2157
|
}
|
|
2017
2158
|
/**
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2025
|
-
*
|
|
2159
|
+
* Sends a ping on a socket whose buffered bytes have stopped flushing.
|
|
2160
|
+
* uWebSockets keeps the rest of a partial write queued until the next write
|
|
2161
|
+
* on that socket and fires no drain event in the meantime, so the last
|
|
2162
|
+
* changes of a stream with nothing more to send could stay in the buffer.
|
|
2163
|
+
* When the buffered count is above zero, unchanged since the previous poll
|
|
2164
|
+
* batch, and below the congestion threshold, this method sends a ping so that
|
|
2165
|
+
* uWebSockets flushes the rest. It skips a congested socket, because that
|
|
2166
|
+
* socket drains under its own flow control and one more frame could push it
|
|
2167
|
+
* past the backpressure limit.
|
|
2026
2168
|
*/
|
|
2027
2169
|
nudgeStalledSocket() {
|
|
2028
2170
|
const buffered = this.deps.socketBuffered();
|
|
@@ -2067,25 +2209,24 @@ var DeviceChangeStream = class {
|
|
|
2067
2209
|
return true;
|
|
2068
2210
|
}
|
|
2069
2211
|
/**
|
|
2070
|
-
*
|
|
2071
|
-
* uWebSockets reports `buffered` for
|
|
2072
|
-
* including one that
|
|
2073
|
-
*
|
|
2074
|
-
*
|
|
2075
|
-
* remainder
|
|
2076
|
-
* never come.
|
|
2212
|
+
* Makes the stream wait for a drain event only while the socket is congested.
|
|
2213
|
+
* uWebSockets reports `buffered` for any send that it cannot finish at once,
|
|
2214
|
+
* including one that leaves only a few hundred bytes of a frame queued, and
|
|
2215
|
+
* it fires a drain event only when the socket becomes writable again. If the
|
|
2216
|
+
* stream waited on every `buffered` outcome, it could wait on a small
|
|
2217
|
+
* remainder for a drain event that never fires.
|
|
2077
2218
|
*/
|
|
2078
2219
|
parkOnSocket() {
|
|
2079
2220
|
this.socketWait = this.deps.socketCongested();
|
|
2080
2221
|
}
|
|
2081
2222
|
/**
|
|
2082
|
-
*
|
|
2083
|
-
* The grouper
|
|
2084
|
-
* watermarks, so
|
|
2085
|
-
* after the
|
|
2086
|
-
* event that
|
|
2087
|
-
*
|
|
2088
|
-
* the device's own echoes
|
|
2223
|
+
* Discards the in-flight stream state and switches to reading the change log.
|
|
2224
|
+
* The event that the grouper holds back is still unqueued, and its seq is
|
|
2225
|
+
* above both watermarks, so discarding it loses no change. The catch-up read
|
|
2226
|
+
* starts after the later of two positions. `highestQueuedSeq` is the last
|
|
2227
|
+
* event that the stream queued on the socket, and `processedSeq` can be further
|
|
2228
|
+
* ahead when the transform suppresses every event after that one, so the
|
|
2229
|
+
* stream skips a long sequence of the device's own echoes on every pause.
|
|
2089
2230
|
*/
|
|
2090
2231
|
enterCatchup() {
|
|
2091
2232
|
const flushed = this.deps.packer?.flush() ?? "queued";
|
|
@@ -2106,10 +2247,10 @@ var DeviceChangeStream = class {
|
|
|
2106
2247
|
}
|
|
2107
2248
|
}
|
|
2108
2249
|
/**
|
|
2109
|
-
* Starts the catch-up read, or
|
|
2110
|
-
* already
|
|
2111
|
-
*
|
|
2112
|
-
*
|
|
2250
|
+
* Starts the catch-up read, or sets a flag for another read when one is
|
|
2251
|
+
* already in progress. Without the flag, a read that stops partway because
|
|
2252
|
+
* of backpressure that clears before the read ends could leave the stream in
|
|
2253
|
+
* catch-up mode with no read scheduled to resume it.
|
|
2113
2254
|
*/
|
|
2114
2255
|
requestDrain() {
|
|
2115
2256
|
if (this.draining) {
|
|
@@ -2119,24 +2260,25 @@ var DeviceChangeStream = class {
|
|
|
2119
2260
|
void this.drain();
|
|
2120
2261
|
}
|
|
2121
2262
|
/**
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2124
|
-
* boundary under `perTransaction` pacing.
|
|
2125
|
-
*
|
|
2126
|
-
*
|
|
2127
|
-
*
|
|
2263
|
+
* Returns true when the catch-up read must pause for a full delivery window,
|
|
2264
|
+
* which happens at any event under `perEvent` pacing and only at a
|
|
2265
|
+
* transaction boundary under `perTransaction` pacing. A `perTransaction`
|
|
2266
|
+
* device acknowledges only whole transactions that it has applied, so if
|
|
2267
|
+
* the stream held back the rest of an open transaction, it would wait for
|
|
2268
|
+
* an acknowledgement that the device never sends.
|
|
2128
2269
|
*/
|
|
2129
2270
|
readGateClosed() {
|
|
2130
2271
|
if (!this.windowClosed()) return false;
|
|
2131
2272
|
return this.deps.pacing === "perEvent" || !this.midTransaction;
|
|
2132
2273
|
}
|
|
2133
2274
|
/**
|
|
2134
|
-
*
|
|
2135
|
-
*
|
|
2136
|
-
*
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2275
|
+
* Sets the size of the next catch-up read to twice the number of events that
|
|
2276
|
+
* the stream offered from the last read, between 32 and 1,000. When a read stops
|
|
2277
|
+
* at a full socket, the stream discards every event after the one where it
|
|
2278
|
+
* stopped, so with a larger fixed batch the server would decode the same rows again on
|
|
2279
|
+
* every pause for a device on a congested link. Doubling the count gives a
|
|
2280
|
+
* caught-up device full batches and a paced device batches close to the
|
|
2281
|
+
* number of events that its socket accepts.
|
|
2140
2282
|
*/
|
|
2141
2283
|
resizeCatchupBatch(accepted) {
|
|
2142
2284
|
const doubled = accepted * 2;
|
|
@@ -2209,11 +2351,12 @@ var DeviceChangeStream = class {
|
|
|
2209
2351
|
}
|
|
2210
2352
|
}
|
|
2211
2353
|
/**
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2354
|
+
* Switches the stream back to the live feed. This method is synchronous and
|
|
2355
|
+
* follows a log read directly, so that no poller tick can dispatch events
|
|
2356
|
+
* between the caught-up check and the switch to `live` mode. The stream keeps its grouper across the switch,
|
|
2357
|
+
* and when the poller's last batch ends at a transaction boundary, the
|
|
2358
|
+
* boundary flush releases the event that the grouper holds, as it does for
|
|
2359
|
+
* an ordinary subscription that resumes.
|
|
2217
2360
|
*/
|
|
2218
2361
|
goLive() {
|
|
2219
2362
|
this.mode = "live";
|
|
@@ -2697,6 +2840,14 @@ async function subscribeDevice(deps, conn, state, request) {
|
|
|
2697
2840
|
// src/server/ws-subscribe.ts
|
|
2698
2841
|
var MAX_SUBSCRIBED_TABLES = 500;
|
|
2699
2842
|
async function handleSubscribeMessage(deps, conn, state, msg, id) {
|
|
2843
|
+
if (msg.deviceId !== void 0 && !deps.acceptDeviceSync) {
|
|
2844
|
+
deps.sendError(conn, id, "DEVICE_SYNC_NOT_ACCEPTED", DEVICE_SYNC_NOT_ACCEPTED_MESSAGE);
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
if (msg.deviceId === void 0 && !deps.acceptSql && !deps.hasSubscribeHook()) {
|
|
2848
|
+
deps.sendError(conn, id, "SQL_NOT_ACCEPTED", TABLE_SUBSCRIPTION_NOT_ACCEPTED_MESSAGE);
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2700
2851
|
const tables = readTableSet(msg);
|
|
2701
2852
|
if (typeof tables === "string") {
|
|
2702
2853
|
deps.sendError(conn, id, "INVALID_MESSAGE", tables);
|
|
@@ -2763,7 +2914,13 @@ async function handleSubscribeMessage(deps, conn, state, msg, id) {
|
|
|
2763
2914
|
if (deps.hasSubscribeHook()) {
|
|
2764
2915
|
for (const table2 of tables) {
|
|
2765
2916
|
try {
|
|
2766
|
-
await deps.beforeSubscribe({
|
|
2917
|
+
await deps.beforeSubscribe({
|
|
2918
|
+
databaseId: state.databaseId,
|
|
2919
|
+
table: table2,
|
|
2920
|
+
filter,
|
|
2921
|
+
identity: state.identity,
|
|
2922
|
+
deviceId
|
|
2923
|
+
});
|
|
2767
2924
|
} catch (err) {
|
|
2768
2925
|
deps.sendSirannonError(conn, id, err);
|
|
2769
2926
|
return;
|
|
@@ -2944,17 +3101,19 @@ var WSHandler = class {
|
|
|
2944
3101
|
maxUnacknowledgedChanges;
|
|
2945
3102
|
socketResumeBytes;
|
|
2946
3103
|
acceptSql;
|
|
3104
|
+
acceptDeviceSync;
|
|
2947
3105
|
operations;
|
|
2948
3106
|
closed = false;
|
|
2949
3107
|
constructor(sirannon, options) {
|
|
2950
3108
|
this.sirannon = sirannon;
|
|
2951
3109
|
this.acceptSql = options?.acceptSql === true;
|
|
2952
|
-
this.
|
|
3110
|
+
this.acceptDeviceSync = options?.acceptDeviceSync === true;
|
|
3111
|
+
this.operations = createOperationSource(options?.operations, options?.sharedOperations);
|
|
2953
3112
|
this.maxPayloadLength = options?.maxPayloadLength ?? DEFAULT_MAX_PAYLOAD_LENGTH;
|
|
2954
3113
|
this.maxUnacknowledgedChanges = options?.maxUnacknowledgedChanges ?? DEFAULT_MAX_UNACKNOWLEDGED_CHANGES;
|
|
2955
3114
|
this.socketResumeBytes = Math.ceil((options?.maxBackpressureBytes ?? DEFAULT_MAX_BACKPRESSURE_BYTES) / 2);
|
|
2956
3115
|
this.resolveExecutionTarget = options?.resolveExecutionTarget;
|
|
2957
|
-
this.cdc = new CdcContextRegistry(sirannon, options
|
|
3116
|
+
this.cdc = new CdcContextRegistry(sirannon, toChangeRetentionOptions(options));
|
|
2958
3117
|
}
|
|
2959
3118
|
async handleOpen(conn, databaseId, identity) {
|
|
2960
3119
|
if (this.closed) {
|
|
@@ -2997,9 +3156,9 @@ var WSHandler = class {
|
|
|
2997
3156
|
});
|
|
2998
3157
|
}
|
|
2999
3158
|
/**
|
|
3000
|
-
*
|
|
3001
|
-
* client reject
|
|
3002
|
-
*
|
|
3159
|
+
* Closes a connection after the server drops a frame on it at the backpressure limit,
|
|
3160
|
+
* so the client can reject its pending requests and reconnect. The `overloaded` flag
|
|
3161
|
+
* makes the handler close the socket once, however many frames the server drops in a burst.
|
|
3003
3162
|
*/
|
|
3004
3163
|
handleOverload(conn) {
|
|
3005
3164
|
const state = this.connections.get(conn);
|
|
@@ -3137,6 +3296,8 @@ var WSHandler = class {
|
|
|
3137
3296
|
subscribeDeps() {
|
|
3138
3297
|
return {
|
|
3139
3298
|
cdc: this.cdc,
|
|
3299
|
+
acceptSql: this.acceptSql,
|
|
3300
|
+
acceptDeviceSync: this.acceptDeviceSync,
|
|
3140
3301
|
maxUnacknowledgedChanges: this.maxUnacknowledgedChanges,
|
|
3141
3302
|
socketResumeBytes: this.socketResumeBytes,
|
|
3142
3303
|
hasSubscribeHook: () => this.sirannon.hookRegistry.has("beforeSubscribe"),
|
|
@@ -3359,11 +3520,21 @@ function registerWebSocketRoute(options) {
|
|
|
3359
3520
|
|
|
3360
3521
|
// src/server/server.ts
|
|
3361
3522
|
var SQL_ROUTES = ["/db/:id/query", "/db/:id/execute", "/db/:id/transaction", "/db/:id/batch", "/db/:id/load"];
|
|
3523
|
+
var DEVICE_SYNC_ROUTES = [
|
|
3524
|
+
"/db/:id/changes",
|
|
3525
|
+
"/db/:id/migrations",
|
|
3526
|
+
"/db/:id/snapshot",
|
|
3527
|
+
"/db/:id/snapshot/page"
|
|
3528
|
+
];
|
|
3362
3529
|
function refuseSql(res) {
|
|
3363
3530
|
sendError(res, 403, "SQL_NOT_ACCEPTED", SQL_NOT_ACCEPTED_MESSAGE);
|
|
3364
3531
|
}
|
|
3532
|
+
function refuseDeviceSync(res) {
|
|
3533
|
+
sendError(res, 403, "DEVICE_SYNC_NOT_ACCEPTED", DEVICE_SYNC_NOT_ACCEPTED_MESSAGE);
|
|
3534
|
+
}
|
|
3365
3535
|
var SirannonServer = class {
|
|
3366
|
-
|
|
3536
|
+
runtime = null;
|
|
3537
|
+
runtimeLoad = null;
|
|
3367
3538
|
listenSocket = null;
|
|
3368
3539
|
host;
|
|
3369
3540
|
port;
|
|
@@ -3371,6 +3542,7 @@ var SirannonServer = class {
|
|
|
3371
3542
|
authenticateHook;
|
|
3372
3543
|
acceptSql;
|
|
3373
3544
|
acceptBackupRestore;
|
|
3545
|
+
acceptDeviceSync;
|
|
3374
3546
|
restoreRuns = new BackupRestoreRuns();
|
|
3375
3547
|
operations;
|
|
3376
3548
|
registryDigest;
|
|
@@ -3391,8 +3563,10 @@ var SirannonServer = class {
|
|
|
3391
3563
|
this.acceptSql = options?.acceptSql === true;
|
|
3392
3564
|
this.acceptBackupRestore = options?.acceptBackupRestore === true;
|
|
3393
3565
|
assertBackupRestoreAuthenticated(this.acceptBackupRestore, this.authenticateHook !== void 0);
|
|
3394
|
-
this.
|
|
3395
|
-
this.
|
|
3566
|
+
this.acceptDeviceSync = options?.acceptDeviceSync === true;
|
|
3567
|
+
assertDeviceSyncAuthenticated(this.acceptDeviceSync, this.authenticateHook !== void 0);
|
|
3568
|
+
this.operations = { registry: options?.operations, shared: options?.sharedOperations };
|
|
3569
|
+
this.registryDigest = operationRegistryDigest(this.operations.registry, this.operations.shared);
|
|
3396
3570
|
this.resolveExecutionTarget = options?.resolveExecutionTarget;
|
|
3397
3571
|
this.getReplicationStatus = options?.getReplicationStatus;
|
|
3398
3572
|
this.getClusterStatus = options?.getClusterStatus;
|
|
@@ -3405,21 +3579,24 @@ var SirannonServer = class {
|
|
|
3405
3579
|
maxBackpressureBytes: this.maxWsBackpressureBytes,
|
|
3406
3580
|
cdcRetentionMs: options?.cdcRetentionMs,
|
|
3407
3581
|
deviceCursorRetentionMs: options?.deviceCursorRetentionMs,
|
|
3582
|
+
maxChangesHeldForDevice: options?.maxChangesHeldForDevice,
|
|
3408
3583
|
maxUnacknowledgedChanges: options?.maxUnacknowledgedChanges,
|
|
3409
3584
|
acceptSql: this.acceptSql,
|
|
3410
|
-
|
|
3585
|
+
acceptDeviceSync: this.acceptDeviceSync,
|
|
3586
|
+
operations: this.operations.registry,
|
|
3587
|
+
sharedOperations: this.operations.shared
|
|
3411
3588
|
});
|
|
3412
|
-
this.app = uWS.App();
|
|
3413
|
-
this.registerRoutes();
|
|
3414
3589
|
}
|
|
3415
3590
|
/**
|
|
3416
|
-
*
|
|
3591
|
+
* Loads uWebSockets.js and starts serving on the configured host and port.
|
|
3417
3592
|
*
|
|
3418
|
-
* @throws
|
|
3593
|
+
* @throws A `SirannonError` with code `SERVER_DEPENDENCY_MISSING` when this process cannot load uWebSockets.js.
|
|
3594
|
+
* @throws An `Error` when uWebSockets.js cannot listen on the host and port, for example because the port is already in use.
|
|
3419
3595
|
*/
|
|
3420
|
-
listen() {
|
|
3421
|
-
|
|
3422
|
-
|
|
3596
|
+
async listen() {
|
|
3597
|
+
const { app } = await this.loadRuntime();
|
|
3598
|
+
await new Promise((resolve, reject) => {
|
|
3599
|
+
app.listen(this.host, this.port, (socket) => {
|
|
3423
3600
|
if (socket) {
|
|
3424
3601
|
this.listenSocket = socket;
|
|
3425
3602
|
resolve();
|
|
@@ -3430,29 +3607,47 @@ var SirannonServer = class {
|
|
|
3430
3607
|
});
|
|
3431
3608
|
}
|
|
3432
3609
|
/**
|
|
3433
|
-
* Stops
|
|
3610
|
+
* Stops listening for new connections and closes every open WebSocket connection.
|
|
3434
3611
|
*/
|
|
3435
3612
|
async close() {
|
|
3436
3613
|
try {
|
|
3437
3614
|
await this.wsHandler.close();
|
|
3438
3615
|
} finally {
|
|
3439
|
-
if (this.listenSocket) {
|
|
3440
|
-
|
|
3616
|
+
if (this.listenSocket && this.runtime) {
|
|
3617
|
+
this.runtime.uws.us_listen_socket_close(this.listenSocket);
|
|
3441
3618
|
this.listenSocket = null;
|
|
3442
3619
|
}
|
|
3443
3620
|
}
|
|
3444
3621
|
}
|
|
3445
3622
|
/**
|
|
3446
|
-
*
|
|
3623
|
+
* The port that the server listens on, which the operating system chooses when you set `port` to 0, or -1 while the server is not listening.
|
|
3447
3624
|
*/
|
|
3448
3625
|
get listeningPort() {
|
|
3449
|
-
if (!this.listenSocket) return -1;
|
|
3450
|
-
return
|
|
3626
|
+
if (!this.listenSocket || !this.runtime) return -1;
|
|
3627
|
+
return this.runtime.uws.us_socket_local_port(this.listenSocket);
|
|
3628
|
+
}
|
|
3629
|
+
loadRuntime() {
|
|
3630
|
+
if (!this.runtimeLoad) {
|
|
3631
|
+
this.runtimeLoad = loadUWebSockets().then(
|
|
3632
|
+
(uws) => {
|
|
3633
|
+
const app = uws.App();
|
|
3634
|
+
this.registerRoutes(app);
|
|
3635
|
+
const runtime = { uws, app };
|
|
3636
|
+
this.runtime = runtime;
|
|
3637
|
+
return runtime;
|
|
3638
|
+
},
|
|
3639
|
+
(err) => {
|
|
3640
|
+
this.runtimeLoad = null;
|
|
3641
|
+
throw err;
|
|
3642
|
+
}
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
return this.runtimeLoad;
|
|
3451
3646
|
}
|
|
3452
|
-
registerRoutes() {
|
|
3647
|
+
registerRoutes(app) {
|
|
3453
3648
|
if (this.cors) {
|
|
3454
3649
|
const cors = this.cors;
|
|
3455
|
-
|
|
3650
|
+
app.options("/*", (res, req) => {
|
|
3456
3651
|
const requestOrigin = req.getHeader("origin");
|
|
3457
3652
|
res.cork(() => {
|
|
3458
3653
|
res.writeStatus("204 No Content");
|
|
@@ -3461,68 +3656,74 @@ var SirannonServer = class {
|
|
|
3461
3656
|
});
|
|
3462
3657
|
});
|
|
3463
3658
|
}
|
|
3464
|
-
|
|
3659
|
+
app.get(
|
|
3465
3660
|
"/capabilities",
|
|
3466
|
-
this.withCors(
|
|
3661
|
+
this.withCors(
|
|
3662
|
+
handleCapabilities({
|
|
3663
|
+
registryDigest: this.registryDigest,
|
|
3664
|
+
acceptSql: this.acceptSql,
|
|
3665
|
+
acceptDeviceSync: this.acceptDeviceSync
|
|
3666
|
+
})
|
|
3667
|
+
)
|
|
3467
3668
|
);
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3669
|
+
app.get("/health", this.withCors(handleLiveness()));
|
|
3670
|
+
app.get("/health/ready", this.withCors(handleReadiness(this.sirannon, this.getReplicationStatus)));
|
|
3671
|
+
app.get(
|
|
3471
3672
|
"/db/:id/cluster",
|
|
3472
3673
|
this.wrapDbGetRoute(handleClusterStatus(this.getClusterStatus, this.authorizeClusterStatus))
|
|
3473
3674
|
);
|
|
3474
3675
|
if (this.acceptSql) {
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
);
|
|
3481
|
-
this.app.post("/db/:id/batch", this.wrapDbRoute(handleBatch(this.sirannon, this.resolveExecutionTarget)));
|
|
3482
|
-
this.app.post("/db/:id/load", this.wrapDbRoute(handleLoad(this.sirannon, this.resolveExecutionTarget)));
|
|
3676
|
+
app.post("/db/:id/query", this.wrapDbRoute(handleQuery(this.sirannon, this.resolveExecutionTarget)));
|
|
3677
|
+
app.post("/db/:id/execute", this.wrapDbRoute(handleExecute(this.sirannon, this.resolveExecutionTarget)));
|
|
3678
|
+
app.post("/db/:id/transaction", this.wrapDbRoute(handleTransaction(this.sirannon, this.resolveExecutionTarget)));
|
|
3679
|
+
app.post("/db/:id/batch", this.wrapDbRoute(handleBatch(this.sirannon, this.resolveExecutionTarget)));
|
|
3680
|
+
app.post("/db/:id/load", this.wrapDbRoute(handleLoad(this.sirannon, this.resolveExecutionTarget)));
|
|
3483
3681
|
} else {
|
|
3484
3682
|
for (const route of SQL_ROUTES) {
|
|
3485
|
-
|
|
3683
|
+
app.post(route, this.withCors(refuseSql));
|
|
3486
3684
|
}
|
|
3487
3685
|
}
|
|
3488
|
-
|
|
3686
|
+
app.post(
|
|
3489
3687
|
"/db/:id/query/:name",
|
|
3490
3688
|
this.wrapOperationRoute(handleOperationQuery(this.sirannon, this.operations, this.resolveExecutionTarget))
|
|
3491
3689
|
);
|
|
3492
|
-
|
|
3690
|
+
app.post(
|
|
3493
3691
|
"/db/:id/execute/:name",
|
|
3494
3692
|
this.wrapOperationRoute(handleOperationExecute(this.sirannon, this.operations, this.resolveExecutionTarget))
|
|
3495
3693
|
);
|
|
3496
|
-
this.registerBackupRoutes();
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
this.app.post("/db/:id/backup", this.wrapDbRoute(handleBackupTrigger(this.sirannon)));
|
|
3508
|
-
this.app.get("/db/:id/backup", this.wrapDbGetRoute(handleBackupStatus(this.sirannon)));
|
|
3509
|
-
this.app.get("/db/:id/backup/chain", this.wrapDbGetRoute(handleBackupChain(this.sirannon)));
|
|
3510
|
-
this.app.post("/db/:id/backup/verify", this.wrapDbRoute(handleBackupVerify(this.sirannon)));
|
|
3511
|
-
this.app.post("/db/:id/backup/safe-to-delete", this.wrapDbRoute(handleBackupSafeToDelete(this.sirannon)));
|
|
3512
|
-
this.app.post(
|
|
3513
|
-
"/db/:id/backup/restore",
|
|
3514
|
-
this.wrapDbRoute(handleBackupRestore(this.sirannon, this.acceptBackupRestore, this.restoreRuns))
|
|
3515
|
-
);
|
|
3516
|
-
this.app.get("/db/:id/backup/restore", this.wrapDbGetRoute(handleBackupRestoreStatus(this.restoreRuns)));
|
|
3517
|
-
}
|
|
3518
|
-
registerWebSocketRoute() {
|
|
3694
|
+
this.registerBackupRoutes(app);
|
|
3695
|
+
if (this.acceptDeviceSync) {
|
|
3696
|
+
app.post("/db/:id/changes", this.wrapDbRoute(handleChanges(this.sirannon, this.resolveExecutionTarget)));
|
|
3697
|
+
app.post("/db/:id/migrations", this.wrapDbRoute(handleMigrationList(this.sirannon)));
|
|
3698
|
+
app.post("/db/:id/snapshot", this.wrapDbRoute(handleSnapshotManifest(this.sirannon)));
|
|
3699
|
+
app.post("/db/:id/snapshot/page", this.wrapDbRoute(handleSnapshotPage(this.sirannon)));
|
|
3700
|
+
} else {
|
|
3701
|
+
for (const route of DEVICE_SYNC_ROUTES) {
|
|
3702
|
+
app.post(route, this.withCors(refuseDeviceSync));
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3519
3705
|
registerWebSocketRoute({
|
|
3520
|
-
app
|
|
3706
|
+
app,
|
|
3521
3707
|
wsHandler: this.wsHandler,
|
|
3522
3708
|
authenticateHook: this.authenticateHook,
|
|
3523
3709
|
maxBodyBytes: this.maxBodyBytes,
|
|
3524
3710
|
maxBackpressureBytes: this.maxWsBackpressureBytes
|
|
3525
3711
|
});
|
|
3712
|
+
app.any("/*", (res) => {
|
|
3713
|
+
sendError(res, 404, "NOT_FOUND", "Route not found");
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
registerBackupRoutes(app) {
|
|
3717
|
+
app.post("/db/:id/backup", this.wrapDbRoute(handleBackupTrigger(this.sirannon)));
|
|
3718
|
+
app.get("/db/:id/backup", this.wrapDbGetRoute(handleBackupStatus(this.sirannon)));
|
|
3719
|
+
app.get("/db/:id/backup/chain", this.wrapDbGetRoute(handleBackupChain(this.sirannon)));
|
|
3720
|
+
app.post("/db/:id/backup/verify", this.wrapDbRoute(handleBackupVerify(this.sirannon)));
|
|
3721
|
+
app.post("/db/:id/backup/safe-to-delete", this.wrapDbRoute(handleBackupSafeToDelete(this.sirannon)));
|
|
3722
|
+
app.post(
|
|
3723
|
+
"/db/:id/backup/restore",
|
|
3724
|
+
this.wrapDbRoute(handleBackupRestore(this.sirannon, this.acceptBackupRestore, this.restoreRuns))
|
|
3725
|
+
);
|
|
3726
|
+
app.get("/db/:id/backup/restore", this.wrapDbGetRoute(handleBackupRestoreStatus(this.restoreRuns)));
|
|
3526
3727
|
}
|
|
3527
3728
|
withCors(handler) {
|
|
3528
3729
|
if (!this.cors) return handler;
|
|
@@ -3532,105 +3733,21 @@ var SirannonServer = class {
|
|
|
3532
3733
|
handler(res, req);
|
|
3533
3734
|
};
|
|
3534
3735
|
}
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
const dbId = req.getParameter(0) ?? "";
|
|
3541
|
-
const method = req.getMethod();
|
|
3542
|
-
const path = req.getUrl();
|
|
3543
|
-
if (corsHeaders) {
|
|
3544
|
-
writeCorsOrigin(res, corsHeaders, req.getHeader("origin"));
|
|
3545
|
-
}
|
|
3546
|
-
const abort = initAbortHandler(res);
|
|
3547
|
-
const bodyPromise = readBody(res, maxBody, abort);
|
|
3548
|
-
if (!authenticateHook) {
|
|
3549
|
-
bodyPromise.then(async (rawBody) => {
|
|
3550
|
-
if (!abort.claim()) return;
|
|
3551
|
-
try {
|
|
3552
|
-
await handler(res, dbId, rawBody, abort, void 0);
|
|
3553
|
-
} catch {
|
|
3554
|
-
if (!abort.aborted) {
|
|
3555
|
-
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
}).catch(() => {
|
|
3559
|
-
});
|
|
3560
|
-
return;
|
|
3561
|
-
}
|
|
3562
|
-
const headers = {};
|
|
3563
|
-
req.forEach((key, value) => {
|
|
3564
|
-
headers[key] = value;
|
|
3565
|
-
});
|
|
3566
|
-
const remoteAddress = decodeRemoteAddress(res);
|
|
3567
|
-
const ctx = {
|
|
3568
|
-
headers,
|
|
3569
|
-
method,
|
|
3570
|
-
path,
|
|
3571
|
-
databaseId: dbId,
|
|
3572
|
-
remoteAddress
|
|
3573
|
-
};
|
|
3574
|
-
const hookPromise = runAuthenticate(res, abort, ctx, authenticateHook);
|
|
3575
|
-
Promise.all([bodyPromise, hookPromise]).then(async ([rawBody, authenticated]) => {
|
|
3576
|
-
if (!authenticated.ok || !abort.claim()) return;
|
|
3577
|
-
try {
|
|
3578
|
-
await handler(res, dbId, rawBody, abort, authenticated.identity);
|
|
3579
|
-
} catch {
|
|
3580
|
-
if (!abort.aborted) {
|
|
3581
|
-
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
}).catch(() => {
|
|
3585
|
-
});
|
|
3736
|
+
routeDeps() {
|
|
3737
|
+
return {
|
|
3738
|
+
cors: this.cors,
|
|
3739
|
+
maxBodyBytes: this.maxBodyBytes,
|
|
3740
|
+
authenticateHook: this.authenticateHook
|
|
3586
3741
|
};
|
|
3587
3742
|
}
|
|
3588
|
-
|
|
3589
|
-
return
|
|
3590
|
-
{
|
|
3591
|
-
cors: this.cors,
|
|
3592
|
-
maxBodyBytes: this.maxBodyBytes,
|
|
3593
|
-
authenticateHook: this.authenticateHook
|
|
3594
|
-
},
|
|
3595
|
-
handler
|
|
3596
|
-
);
|
|
3743
|
+
wrapDbRoute(handler) {
|
|
3744
|
+
return wrapDbRoute(this.routeDeps(), handler);
|
|
3597
3745
|
}
|
|
3598
3746
|
wrapDbGetRoute(handler) {
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
const method = req.getMethod();
|
|
3604
|
-
const path = req.getUrl();
|
|
3605
|
-
if (corsHeaders) {
|
|
3606
|
-
writeCorsOrigin(res, corsHeaders, req.getHeader("origin"));
|
|
3607
|
-
}
|
|
3608
|
-
const headers = {};
|
|
3609
|
-
req.forEach((key, value) => {
|
|
3610
|
-
headers[key] = value;
|
|
3611
|
-
});
|
|
3612
|
-
const ctx = {
|
|
3613
|
-
headers,
|
|
3614
|
-
method,
|
|
3615
|
-
path,
|
|
3616
|
-
databaseId: dbId,
|
|
3617
|
-
remoteAddress: decodeRemoteAddress(res)
|
|
3618
|
-
};
|
|
3619
|
-
const abort = initAbortHandler(res);
|
|
3620
|
-
const run = async () => {
|
|
3621
|
-
if (authenticateHook && !(await runAuthenticate(res, abort, ctx, authenticateHook)).ok) return;
|
|
3622
|
-
if (!abort.claim()) return;
|
|
3623
|
-
try {
|
|
3624
|
-
await handler(res, dbId, ctx, abort);
|
|
3625
|
-
} catch {
|
|
3626
|
-
if (!abort.aborted) {
|
|
3627
|
-
sendError(res, 500, "INTERNAL_ERROR", "An unexpected error occurred");
|
|
3628
|
-
}
|
|
3629
|
-
}
|
|
3630
|
-
};
|
|
3631
|
-
run().catch(() => {
|
|
3632
|
-
});
|
|
3633
|
-
};
|
|
3747
|
+
return wrapDbGetRoute(this.routeDeps(), handler);
|
|
3748
|
+
}
|
|
3749
|
+
wrapOperationRoute(handler) {
|
|
3750
|
+
return wrapOperationRoute(this.routeDeps(), handler);
|
|
3634
3751
|
}
|
|
3635
3752
|
};
|
|
3636
3753
|
function createServer(sirannon, options) {
|