@delali/sirannon-db 0.2.3-next.37 → 0.2.3-next.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backup/index.d.ts +6 -84
- package/dist/backup/index.mjs +6 -584
- package/dist/backup-scheduler/index.d.ts +58 -8
- package/dist/backup-scheduler/index.mjs +3 -2
- package/dist/{change-tracker-DXHPFe9J.d.ts → change-tracker-uG9gWvsY.d.ts} +1 -1
- package/dist/chunk-2QCPVE67.mjs +143 -0
- package/dist/chunk-63VBFSCS.mjs +525 -0
- package/dist/chunk-7SC2TZAS.mjs +41 -0
- package/dist/{chunk-72I5UI5C.mjs → chunk-DVS4QSIZ.mjs} +51 -23
- package/dist/{chunk-BQTVNI5H.mjs → chunk-DXXCLTFF.mjs} +205 -45
- package/dist/{chunk-BQFQ65OL.mjs → chunk-LXCGPZPA.mjs} +9 -1
- package/dist/{chunk-ZKDBGX7H.mjs → chunk-OJQHM4MT.mjs} +365 -357
- package/dist/chunk-U445A3C4.mjs +88 -0
- package/dist/chunk-W77EEVOT.mjs +156 -0
- package/dist/client/index.d.ts +8 -8
- package/dist/client/topology.d.ts +4 -4
- package/dist/{client-base-CGBpjZUB.d.ts → client-base-C-B8u5Lm.d.ts} +2 -2
- package/dist/core/index.d.ts +9 -9
- package/dist/core/index.mjs +228 -48
- package/dist/{database-955qag_W.d.ts → database-BlhUdJAO.d.ts} +46 -39
- package/dist/{chain-queries-D5aZ-8v2.d.ts → database-backup-C090lMHc.d.ts} +52 -2
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +9 -6
- package/dist/driver/bun.d.ts +1 -1
- package/dist/driver/expo.d.ts +1 -1
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +9 -6
- package/dist/driver/wa-sqlite.d.ts +1 -1
- package/dist/file-migrations/index.d.ts +1 -1
- package/dist/{protocol-BfiujBsE.d.ts → protocol-QekTGoKn.d.ts} +2 -2
- package/dist/replication/index.d.ts +7 -7
- package/dist/restore-options-C9MW9gpm.d.ts +83 -0
- package/dist/server/index.d.ts +102 -9
- package/dist/server/index.mjs +355 -3
- package/dist/{server-options-D4VmfTHw.d.ts → server-options-DeK6ZG8u.d.ts} +8 -1
- package/dist/{sirannon-DaGUawHs.d.ts → sirannon-C9cTgFrw.d.ts} +48 -5
- package/dist/transport/grpc.d.ts +3 -3
- package/dist/transport/memory.d.ts +3 -3
- package/dist/{types-BEjS69rs.d.ts → types-C1yBGTdx.d.ts} +2 -2
- package/dist/{types-Xb0L3o3w.d.ts → types-DApb7dvw.d.ts} +266 -23
- package/package.json +8 -7
- package/dist/chunk-GBM4AU24.mjs +0 -42
package/dist/server/index.mjs
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import { restoreBackup, MAX_RESTORE_BATCH_SIZE } from '../chunk-63VBFSCS.mjs';
|
|
1
2
|
import { dumpSchema, tablesInFkOrder, dumpTablePages } from '../chunk-XF2HH5E6.mjs';
|
|
2
3
|
import { ChangeTracker, SubscriptionManager, ensureCdcEpoch, migrationChecksum, needsResync, filteredChange, TransactionGrouper, isBulkLoadDurability, PrimedSubscription } from '../chunk-ECEGYTVF.mjs';
|
|
4
|
+
import '../chunk-YNTDOFTN.mjs';
|
|
3
5
|
import { SEQ_STRING_RE, highestMigrationVersion, selectCountTableRows, selectTableExists, selectAppliedMigrations, PkResolver, ensureDeviceCursorsTable, upsertDeviceCursor, selectMaxChangeSeq, canonicaliseForChecksum, selectDeviceCursors, selectMinForeignChangeSeqSql, IDENTIFIER_RE, deleteDeviceCursorsUpdatedBefore } from '../chunk-7FQRQH5Z.mjs';
|
|
4
6
|
import '../chunk-2QLXDHAP.mjs';
|
|
5
7
|
import { encodeTaggedValues, MIGRATIONS_TABLE, decodeTaggedValues, encodeWireRowsInPlace, CHANGES_TABLE, DEVICE_CURSORS_TABLE, INTERNAL_TABLE_PREFIX } from '../chunk-7R4ER4FB.mjs';
|
|
6
8
|
import { createOperationSource, operationRegistryDigest, findRead, isRefusal, resolveArguments, findWrite } from '../chunk-TUD5CJ76.mjs';
|
|
7
9
|
import '../chunk-GVCNMPOS.mjs';
|
|
10
|
+
import { discardStagedChain } from '../chunk-W77EEVOT.mjs';
|
|
11
|
+
import '../chunk-U445A3C4.mjs';
|
|
12
|
+
import '../chunk-2QCPVE67.mjs';
|
|
13
|
+
import '../chunk-DVS4QSIZ.mjs';
|
|
8
14
|
import '../chunk-OUSWVNWT.mjs';
|
|
15
|
+
import '../chunk-YVF2ZJTE.mjs';
|
|
9
16
|
import '../chunk-V5FUJEYY.mjs';
|
|
10
17
|
import { SQLITE_USER_VERSION_MAX } from '../chunk-IWGIYDMZ.mjs';
|
|
11
18
|
import { SirannonError, WriteOverloadError } from '../chunk-PBRXXISQ.mjs';
|
|
@@ -131,6 +138,90 @@ function paramsBatchValidationError(value) {
|
|
|
131
138
|
return null;
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
// src/server/backup-restore-runs.ts
|
|
142
|
+
function describeFailure(err) {
|
|
143
|
+
return {
|
|
144
|
+
code: err instanceof SirannonError ? err.code : "INTERNAL_ERROR",
|
|
145
|
+
message: err instanceof SirannonError ? err.message : "An unexpected error occurred"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
var BackupRestoreRuns = class {
|
|
149
|
+
runs = /* @__PURE__ */ new Map();
|
|
150
|
+
/**
|
|
151
|
+
* Marks a restore of one database as under way, unless one is already running.
|
|
152
|
+
*
|
|
153
|
+
* @param databaseId - Identifier of the database being rebuilt.
|
|
154
|
+
* @param moment - Epoch milliseconds the caller asked to be taken back to.
|
|
155
|
+
* @returns True where this call claimed the database, false where a restore already has it.
|
|
156
|
+
*/
|
|
157
|
+
claim(databaseId, moment) {
|
|
158
|
+
if (this.runs.get(databaseId)?.state === "running") return false;
|
|
159
|
+
this.runs.set(databaseId, { state: "running", moment, startedAt: Date.now() });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Records how far the restore of one database has got.
|
|
164
|
+
*
|
|
165
|
+
* @param databaseId - Identifier of the database being rebuilt.
|
|
166
|
+
* @param progress - Pieces fetched, bytes fetched, and change pieces replayed.
|
|
167
|
+
*/
|
|
168
|
+
progressed(databaseId, progress) {
|
|
169
|
+
const run = this.runs.get(databaseId);
|
|
170
|
+
if (run?.state === "running") run.progress = progress;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Records a restore that rebuilt the database.
|
|
174
|
+
*
|
|
175
|
+
* A reopen that failed is reported beside the report, and the state stays
|
|
176
|
+
* `done`, because Sirannon replaced the data either way and an operator shown
|
|
177
|
+
* only the failure would believe their database untouched.
|
|
178
|
+
*
|
|
179
|
+
* @param databaseId - Identifier of the database that was rebuilt.
|
|
180
|
+
* @param report - What the restore produced.
|
|
181
|
+
* @param reopenFailure - What stopped the database opening again, where anything did.
|
|
182
|
+
*/
|
|
183
|
+
finished(databaseId, report, reopenFailure) {
|
|
184
|
+
this.runs.set(databaseId, {
|
|
185
|
+
state: "done",
|
|
186
|
+
moment: this.runs.get(databaseId)?.moment ?? report.restoresTo,
|
|
187
|
+
startedAt: report.startedAt,
|
|
188
|
+
finishedAt: report.finishedAt,
|
|
189
|
+
report,
|
|
190
|
+
...reopenFailure === void 0 ? {} : { reopenError: describeFailure(reopenFailure) }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Records a restore that stopped, under the code it stopped with.
|
|
195
|
+
*
|
|
196
|
+
* Only a `SirannonError` passes its message through. Anything else reports
|
|
197
|
+
* the same sentence the HTTP layer sends for an unexpected failure, since a
|
|
198
|
+
* runtime error names the paths and the internals of the machine serving the
|
|
199
|
+
* request.
|
|
200
|
+
*
|
|
201
|
+
* @param databaseId - Identifier of the database it was rebuilding.
|
|
202
|
+
* @param err - What stopped it.
|
|
203
|
+
*/
|
|
204
|
+
failed(databaseId, err) {
|
|
205
|
+
const previous = this.runs.get(databaseId);
|
|
206
|
+
this.runs.set(databaseId, {
|
|
207
|
+
state: "failed",
|
|
208
|
+
...previous?.moment === void 0 ? {} : { moment: previous.moment },
|
|
209
|
+
...previous?.startedAt === void 0 ? {} : { startedAt: previous.startedAt },
|
|
210
|
+
finishedAt: Date.now(),
|
|
211
|
+
error: describeFailure(err)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Reads where one database's restore stands.
|
|
216
|
+
*
|
|
217
|
+
* @param databaseId - Identifier of the database to report on.
|
|
218
|
+
* @returns The restore, or an idle answer where none has run.
|
|
219
|
+
*/
|
|
220
|
+
read(databaseId) {
|
|
221
|
+
return this.runs.get(databaseId) ?? { state: "idle" };
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
134
225
|
// src/server/http-common.ts
|
|
135
226
|
var SQL_NOT_ACCEPTED_MESSAGE = "This server does not accept SQL statements over the wire; call a registered operation by name";
|
|
136
227
|
function initAbortHandler(res) {
|
|
@@ -200,17 +291,23 @@ function parseBody(res, raw) {
|
|
|
200
291
|
sendError(res, 400, "EMPTY_BODY", "Request body is empty");
|
|
201
292
|
return null;
|
|
202
293
|
}
|
|
294
|
+
let parsed;
|
|
203
295
|
try {
|
|
204
|
-
|
|
296
|
+
parsed = JSON.parse(raw.toString("utf-8"));
|
|
205
297
|
} catch {
|
|
206
298
|
sendError(res, 400, "INVALID_JSON", "Request body is not valid JSON");
|
|
207
299
|
return null;
|
|
208
300
|
}
|
|
301
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
302
|
+
sendError(res, 400, "INVALID_REQUEST", "Request body must be a JSON object");
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
return parsed;
|
|
209
306
|
}
|
|
210
|
-
function sendJson(res, data) {
|
|
307
|
+
function sendJson(res, data, status = "200 OK") {
|
|
211
308
|
const payload = JSON.stringify(data);
|
|
212
309
|
res.cork(() => {
|
|
213
|
-
res.writeStatus(
|
|
310
|
+
res.writeStatus(status).writeHeader("Content-Type", "application/json").end(payload);
|
|
214
311
|
});
|
|
215
312
|
}
|
|
216
313
|
function sendError(res, status, code, message, details, headers) {
|
|
@@ -244,6 +341,12 @@ function httpStatusForError(err) {
|
|
|
244
341
|
return 409;
|
|
245
342
|
case "HOOK_DENIED":
|
|
246
343
|
return 403;
|
|
344
|
+
case "BACKUP_CHAIN_BROKEN":
|
|
345
|
+
return 409;
|
|
346
|
+
case "BACKUP_UNSUPPORTED":
|
|
347
|
+
return 501;
|
|
348
|
+
case "BACKUP_DESTINATION_ERROR":
|
|
349
|
+
return 502;
|
|
247
350
|
case "DATABASE_CLOSED":
|
|
248
351
|
case "SHUTDOWN":
|
|
249
352
|
case "READ_CONCERN_ERROR":
|
|
@@ -430,6 +533,230 @@ function readinessStatusForReplication(replication, current) {
|
|
|
430
533
|
return current;
|
|
431
534
|
}
|
|
432
535
|
|
|
536
|
+
// src/server/backup-protocol.ts
|
|
537
|
+
var BACKUP_RESTORE_NOT_ACCEPTED_MESSAGE = "This server does not restore databases over the wire; turn on acceptBackupRestore to open the route";
|
|
538
|
+
function wholeNumberError(field, value, atLeast, atMost) {
|
|
539
|
+
if (value === void 0) return null;
|
|
540
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < atLeast) {
|
|
541
|
+
return `Field "${field}" must be a whole number of at least ${atLeast}`;
|
|
542
|
+
}
|
|
543
|
+
if (atMost !== void 0 && value > atMost) {
|
|
544
|
+
return `Field "${field}" must be a whole number of at most ${atMost}`;
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
function verifyNameValidationError(value) {
|
|
549
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
550
|
+
return 'Field "name" is required and must be the name a chain record states';
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
function restorableFromValidationError(value) {
|
|
555
|
+
return wholeNumberError("restorableFrom", value, 0);
|
|
556
|
+
}
|
|
557
|
+
function restoreRequestValidationError(body) {
|
|
558
|
+
return wholeNumberError("moment", body.moment, 0) ?? wholeNumberError("batchSize", body.batchSize, 1, MAX_RESTORE_BATCH_SIZE);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/server/http-backups.ts
|
|
562
|
+
function parseOptionalBody(res, rawBody) {
|
|
563
|
+
if (rawBody.length === 0) return {};
|
|
564
|
+
return parseBody(res, rawBody);
|
|
565
|
+
}
|
|
566
|
+
async function resolveBackupDatabase(res, abort, sirannon, dbId) {
|
|
567
|
+
let database;
|
|
568
|
+
try {
|
|
569
|
+
database = await sirannon.resolve(dbId);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
sendCaughtError(res, abort, err);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
if (abort.aborted) return null;
|
|
575
|
+
if (!database) {
|
|
576
|
+
sendError(res, 404, "DATABASE_NOT_FOUND", `Database '${dbId}' not found`);
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
return database;
|
|
580
|
+
}
|
|
581
|
+
function refusedOnTheFirstTick(turn) {
|
|
582
|
+
const settled = turn.then(
|
|
583
|
+
() => void 0,
|
|
584
|
+
(err) => err ?? new SirannonError("The backup turn failed", "BACKUP_ERROR")
|
|
585
|
+
);
|
|
586
|
+
const tick = new Promise((resolve) => {
|
|
587
|
+
setImmediate(() => resolve(void 0));
|
|
588
|
+
});
|
|
589
|
+
return Promise.race([settled, tick]);
|
|
590
|
+
}
|
|
591
|
+
function readStatus(res, abort, database) {
|
|
592
|
+
try {
|
|
593
|
+
return database.backupStatus();
|
|
594
|
+
} catch (err) {
|
|
595
|
+
sendCaughtError(res, abort, err);
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function handleBackupTrigger(sirannon) {
|
|
600
|
+
return async (res, dbId, _rawBody, abort) => {
|
|
601
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
602
|
+
if (!database) return;
|
|
603
|
+
if (!readStatus(res, abort, database)) return;
|
|
604
|
+
const refusal = await refusedOnTheFirstTick(database.captureBackupChanges());
|
|
605
|
+
if (refusal !== void 0) {
|
|
606
|
+
sendCaughtError(res, abort, refusal);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (abort.aborted) return;
|
|
610
|
+
sendJson(res, { started: true }, "202 Accepted");
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function handleBackupStatus(sirannon) {
|
|
614
|
+
return async (res, dbId, _ctx, abort) => {
|
|
615
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
616
|
+
if (!database) return;
|
|
617
|
+
const status = readStatus(res, abort, database);
|
|
618
|
+
if (!status || abort.aborted) return;
|
|
619
|
+
sendJson(res, status);
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function handleBackupChain(sirannon) {
|
|
623
|
+
return async (res, dbId, _ctx, abort) => {
|
|
624
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
625
|
+
if (!database) return;
|
|
626
|
+
try {
|
|
627
|
+
const chains = await database.backupChain();
|
|
628
|
+
if (abort.aborted) return;
|
|
629
|
+
const body = { chains };
|
|
630
|
+
sendJson(res, body);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
sendCaughtError(res, abort, err);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function handleBackupVerify(sirannon) {
|
|
637
|
+
return async (res, dbId, rawBody, abort) => {
|
|
638
|
+
const body = parseOptionalBody(res, rawBody);
|
|
639
|
+
if (!body) return;
|
|
640
|
+
const nameError = verifyNameValidationError(body.name);
|
|
641
|
+
if (nameError !== null) {
|
|
642
|
+
sendError(res, 400, "INVALID_REQUEST", nameError);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
646
|
+
if (!database) return;
|
|
647
|
+
try {
|
|
648
|
+
const result = await database.verifyBackup(body.name);
|
|
649
|
+
if (abort.aborted) return;
|
|
650
|
+
sendJson(res, result);
|
|
651
|
+
} catch (err) {
|
|
652
|
+
sendCaughtError(res, abort, err);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function handleBackupSafeToDelete(sirannon) {
|
|
657
|
+
return async (res, dbId, rawBody, abort) => {
|
|
658
|
+
const body = parseOptionalBody(res, rawBody);
|
|
659
|
+
if (!body) return;
|
|
660
|
+
const restorableFromError = restorableFromValidationError(body.restorableFrom);
|
|
661
|
+
if (restorableFromError !== null) {
|
|
662
|
+
sendError(res, 400, "INVALID_REQUEST", restorableFromError);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
666
|
+
if (!database) return;
|
|
667
|
+
try {
|
|
668
|
+
const records = await database.backupPiecesSafeToDelete(
|
|
669
|
+
body.restorableFrom === void 0 ? void 0 : { restorableFrom: body.restorableFrom }
|
|
670
|
+
);
|
|
671
|
+
if (abort.aborted) return;
|
|
672
|
+
const answer = { records };
|
|
673
|
+
sendJson(res, answer);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
sendCaughtError(res, abort, err);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/server/http-backup-restore.ts
|
|
681
|
+
function restoreAlreadyRunning(databaseId) {
|
|
682
|
+
return `A restore of database '${databaseId}' is already under way, and it keeps that file to itself until it finishes`;
|
|
683
|
+
}
|
|
684
|
+
async function rebuildDatabase(request) {
|
|
685
|
+
const { sirannon, runs, databaseId, location } = request;
|
|
686
|
+
try {
|
|
687
|
+
const outcome = await sirannon.withDatabaseOffline(databaseId, async (destPath) => {
|
|
688
|
+
await discardStagedChain(location.stagingDir);
|
|
689
|
+
return restoreBackup({
|
|
690
|
+
destination: location.destination,
|
|
691
|
+
chainName: location.chainName,
|
|
692
|
+
driver: sirannon.driver,
|
|
693
|
+
destPath,
|
|
694
|
+
replaceExisting: true,
|
|
695
|
+
moment: request.moment,
|
|
696
|
+
...location.destinationTimeoutMs === void 0 ? {} : { destinationTimeoutMs: location.destinationTimeoutMs },
|
|
697
|
+
...request.batchSize === void 0 ? {} : { batchSize: request.batchSize },
|
|
698
|
+
onProgress: (progress) => runs.progressed(databaseId, progress)
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
if (!outcome.ok) {
|
|
702
|
+
runs.failed(databaseId, outcome.failure);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
runs.finished(databaseId, outcome.value, outcome.reopenFailure);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
runs.failed(databaseId, err);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function handleBackupRestore(sirannon, accepted, runs) {
|
|
711
|
+
return async (res, dbId, rawBody, abort) => {
|
|
712
|
+
if (!accepted) {
|
|
713
|
+
sendError(res, 403, "BACKUP_RESTORE_NOT_ACCEPTED", BACKUP_RESTORE_NOT_ACCEPTED_MESSAGE);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const body = parseOptionalBody(res, rawBody);
|
|
717
|
+
if (!body) return;
|
|
718
|
+
const validationError = restoreRequestValidationError(body);
|
|
719
|
+
if (validationError !== null) {
|
|
720
|
+
sendError(res, 400, "INVALID_REQUEST", validationError);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (runs.read(dbId).state === "running") {
|
|
724
|
+
sendError(res, 409, "BACKUP_RESTORE_IN_PROGRESS", restoreAlreadyRunning(dbId));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const database = await resolveBackupDatabase(res, abort, sirannon, dbId);
|
|
728
|
+
if (!database) return;
|
|
729
|
+
let location;
|
|
730
|
+
try {
|
|
731
|
+
location = database.backupLocation();
|
|
732
|
+
} catch (err) {
|
|
733
|
+
sendCaughtError(res, abort, err);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const moment = body.moment === void 0 ? Date.now() : body.moment;
|
|
737
|
+
if (!runs.claim(dbId, moment)) {
|
|
738
|
+
sendError(res, 409, "BACKUP_RESTORE_IN_PROGRESS", restoreAlreadyRunning(dbId));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
void rebuildDatabase({
|
|
742
|
+
sirannon,
|
|
743
|
+
runs,
|
|
744
|
+
databaseId: dbId,
|
|
745
|
+
location,
|
|
746
|
+
moment,
|
|
747
|
+
...body.batchSize === void 0 ? {} : { batchSize: body.batchSize }
|
|
748
|
+
});
|
|
749
|
+
if (abort.aborted) return;
|
|
750
|
+
sendJson(res, { started: true }, "202 Accepted");
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function handleBackupRestoreStatus(runs) {
|
|
754
|
+
return async (res, dbId, _ctx, abort) => {
|
|
755
|
+
if (abort.aborted) return;
|
|
756
|
+
sendJson(res, runs.read(dbId));
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
433
760
|
// src/server/sync-protocol.ts
|
|
434
761
|
var MAX_SYNC_BATCH_CHANGES = 1e3;
|
|
435
762
|
var NODE_ID_RE = /^[0-9a-f]{32}$/;
|
|
@@ -1170,6 +1497,14 @@ function handleSnapshotPage(sirannon) {
|
|
|
1170
1497
|
var DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
1171
1498
|
var DEFAULT_WS_BACKPRESSURE_BYTES = 16 * 1048576;
|
|
1172
1499
|
var UWS_MAX_LIMIT_BYTES = 4294967295;
|
|
1500
|
+
function assertBackupRestoreAuthenticated(acceptBackupRestore, hasAuthenticate) {
|
|
1501
|
+
if (acceptBackupRestore && !hasAuthenticate) {
|
|
1502
|
+
throw new SirannonError(
|
|
1503
|
+
"ServerOptions.acceptBackupRestore opens a route that replaces a running database, so it needs an authenticate hook to name the caller",
|
|
1504
|
+
"INVALID_BACKUP_RESTORE"
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1173
1508
|
function resolveMaxBodyBytes(value) {
|
|
1174
1509
|
if (value === void 0) return DEFAULT_MAX_BODY_BYTES;
|
|
1175
1510
|
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
@@ -2968,6 +3303,8 @@ var SirannonServer = class {
|
|
|
2968
3303
|
cors;
|
|
2969
3304
|
authenticateHook;
|
|
2970
3305
|
acceptSql;
|
|
3306
|
+
acceptBackupRestore;
|
|
3307
|
+
restoreRuns = new BackupRestoreRuns();
|
|
2971
3308
|
operations;
|
|
2972
3309
|
registryDigest;
|
|
2973
3310
|
resolveExecutionTarget;
|
|
@@ -2985,6 +3322,8 @@ var SirannonServer = class {
|
|
|
2985
3322
|
this.cors = resolveCors(options?.cors);
|
|
2986
3323
|
this.authenticateHook = options?.authenticate;
|
|
2987
3324
|
this.acceptSql = options?.acceptSql === true;
|
|
3325
|
+
this.acceptBackupRestore = options?.acceptBackupRestore === true;
|
|
3326
|
+
assertBackupRestoreAuthenticated(this.acceptBackupRestore, this.authenticateHook !== void 0);
|
|
2988
3327
|
this.operations = options?.operations;
|
|
2989
3328
|
this.registryDigest = operationRegistryDigest(options?.operations);
|
|
2990
3329
|
this.resolveExecutionTarget = options?.resolveExecutionTarget;
|
|
@@ -3087,6 +3426,7 @@ var SirannonServer = class {
|
|
|
3087
3426
|
"/db/:id/execute/:name",
|
|
3088
3427
|
this.wrapOperationRoute(handleOperationExecute(this.sirannon, this.operations, this.resolveExecutionTarget))
|
|
3089
3428
|
);
|
|
3429
|
+
this.registerBackupRoutes();
|
|
3090
3430
|
this.app.post("/db/:id/changes", this.wrapDbRoute(handleChanges(this.sirannon, this.resolveExecutionTarget)));
|
|
3091
3431
|
this.app.post("/db/:id/migrations", this.wrapDbRoute(handleMigrationList(this.sirannon)));
|
|
3092
3432
|
this.app.post("/db/:id/snapshot", this.wrapDbRoute(handleSnapshotManifest(this.sirannon)));
|
|
@@ -3096,6 +3436,18 @@ var SirannonServer = class {
|
|
|
3096
3436
|
sendError(res, 404, "NOT_FOUND", "Route not found");
|
|
3097
3437
|
});
|
|
3098
3438
|
}
|
|
3439
|
+
registerBackupRoutes() {
|
|
3440
|
+
this.app.post("/db/:id/backup", this.wrapDbRoute(handleBackupTrigger(this.sirannon)));
|
|
3441
|
+
this.app.get("/db/:id/backup", this.wrapDbGetRoute(handleBackupStatus(this.sirannon)));
|
|
3442
|
+
this.app.get("/db/:id/backup/chain", this.wrapDbGetRoute(handleBackupChain(this.sirannon)));
|
|
3443
|
+
this.app.post("/db/:id/backup/verify", this.wrapDbRoute(handleBackupVerify(this.sirannon)));
|
|
3444
|
+
this.app.post("/db/:id/backup/safe-to-delete", this.wrapDbRoute(handleBackupSafeToDelete(this.sirannon)));
|
|
3445
|
+
this.app.post(
|
|
3446
|
+
"/db/:id/backup/restore",
|
|
3447
|
+
this.wrapDbRoute(handleBackupRestore(this.sirannon, this.acceptBackupRestore, this.restoreRuns))
|
|
3448
|
+
);
|
|
3449
|
+
this.app.get("/db/:id/backup/restore", this.wrapDbGetRoute(handleBackupRestoreStatus(this.restoreRuns)));
|
|
3450
|
+
}
|
|
3099
3451
|
registerWebSocketRoute() {
|
|
3100
3452
|
registerWebSocketRoute({
|
|
3101
3453
|
app: this.app,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as OperationRegistry } from './operation-registry-C1FmwQWR.js';
|
|
2
2
|
import { R as ReplicationBatch, C as ConflictResolver, A as ApplyResult } from './types-CjhxcjhA.js';
|
|
3
|
-
import { N as NodeHealth, T as Transaction,
|
|
3
|
+
import { N as NodeHealth, T as Transaction, H as ClusterStatusInfo } from './types-DApb7dvw.js';
|
|
4
4
|
import { P as Params, Q as QueryOptions, E as ExecuteResult } from './query-types-Cv8N7pXj.js';
|
|
5
5
|
|
|
6
6
|
interface AppliedMigrationRow {
|
|
@@ -165,6 +165,13 @@ interface ServerOptions<Identity = unknown> {
|
|
|
165
165
|
operations?: OperationRegistry<Identity>;
|
|
166
166
|
/** Opens the five statement routes and their WebSocket messages. Default: false. */
|
|
167
167
|
acceptSql?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Opens the route that rebuilds a database from its backups. A restore
|
|
170
|
+
* replaces the database that is serving traffic, so the route stays shut
|
|
171
|
+
* until you open it here, and a server that opens it without an
|
|
172
|
+
* `authenticate` hook refuses to start. Default: false.
|
|
173
|
+
*/
|
|
174
|
+
acceptBackupRestore?: boolean;
|
|
168
175
|
/** Finds what the server runs a database's statements against. */
|
|
169
176
|
resolveExecutionTarget?: ServerExecutionTargetResolver;
|
|
170
177
|
/** Supplies the replication figures the readiness endpoint reports. */
|
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
import { D as Database } from './database-
|
|
2
|
-
import {
|
|
1
|
+
import { D as Database } from './database-BlhUdJAO.js';
|
|
2
|
+
import { a as SirannonOptions, S as SQLiteDriver, D as DatabaseOptions, M as Migration, b as BeforeQueryHook, A as AfterQueryHook, c as BeforeConnectHook, d as DatabaseOpenHook, e as DatabaseCloseHook } from './types-DApb7dvw.js';
|
|
3
|
+
|
|
4
|
+
/** What one database taken out of service produced.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
type OfflineOutcome<T> = {
|
|
8
|
+
/** True where the action returned. */
|
|
9
|
+
ok: true;
|
|
10
|
+
/** What the action produced. */
|
|
11
|
+
value: T;
|
|
12
|
+
/** What the reopen threw, where it threw. The action's own result still stands. */
|
|
13
|
+
reopenFailure?: unknown;
|
|
14
|
+
} | {
|
|
15
|
+
/** False where the action threw. */
|
|
16
|
+
ok: false;
|
|
17
|
+
/** What the action threw. */
|
|
18
|
+
failure: unknown;
|
|
19
|
+
/** What the reopen threw, where it threw. */
|
|
20
|
+
reopenFailure?: unknown;
|
|
21
|
+
};
|
|
3
22
|
|
|
4
23
|
/**
|
|
5
24
|
* A registry of open SQLite databases, keyed by identifier.
|
|
@@ -10,14 +29,17 @@ import { S as SirannonOptions, a as SQLiteDriver, D as DatabaseOptions, M as Mig
|
|
|
10
29
|
*/
|
|
11
30
|
declare class Sirannon {
|
|
12
31
|
private readonly dbs;
|
|
32
|
+
private readonly openedWith;
|
|
33
|
+
private readonly offline;
|
|
34
|
+
private readonly offlineWork;
|
|
13
35
|
private readonly opening;
|
|
14
36
|
private readonly resolving;
|
|
15
|
-
private migrationSet;
|
|
16
37
|
private _shutdown;
|
|
17
38
|
private readonly _driver;
|
|
18
39
|
private readonly hookRegistry;
|
|
19
40
|
private readonly metricsCollector;
|
|
20
41
|
private readonly lifecycleManager;
|
|
42
|
+
private readonly migrations;
|
|
21
43
|
/** The driver, hooks, metrics, lifecycle, migrations, and writer-worker default this registry was built with. */
|
|
22
44
|
readonly options: SirannonOptions;
|
|
23
45
|
/**
|
|
@@ -45,6 +67,24 @@ declare class Sirannon {
|
|
|
45
67
|
* @param id - Identifier of the database to close.
|
|
46
68
|
*/
|
|
47
69
|
close(id: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Closes one database, calls an action against the file behind it, and then
|
|
72
|
+
* opens that database again under the same identifier with the settings it
|
|
73
|
+
* had before.
|
|
74
|
+
*
|
|
75
|
+
* A restore rebuilds a database at the path it already occupies, so no
|
|
76
|
+
* connection may be open on that file while Sirannon replaces its bytes. The
|
|
77
|
+
* identifier answers nothing until that database is open again, though an
|
|
78
|
+
* action that failed still leaves a database open at the end.
|
|
79
|
+
*
|
|
80
|
+
* @param id - Identifier of the database to take offline.
|
|
81
|
+
* @param action - Runs with the database closed, and receives its file path.
|
|
82
|
+
* @returns Whether the action returned, what it produced or threw, and what a failed reopen threw.
|
|
83
|
+
* @throws When no database is open under the identifier, when it refuses writes, or when the close fails.
|
|
84
|
+
*
|
|
85
|
+
* @internal
|
|
86
|
+
*/
|
|
87
|
+
withDatabaseOffline<T>(id: string, action: (path: string) => Promise<T>): Promise<OfflineOutcome<T>>;
|
|
48
88
|
/**
|
|
49
89
|
* Returns an already-open database.
|
|
50
90
|
*
|
|
@@ -56,8 +96,6 @@ declare class Sirannon {
|
|
|
56
96
|
resolve(id: string): Promise<Database | undefined>;
|
|
57
97
|
/** @internal */
|
|
58
98
|
registryMigrations(): Promise<Migration[]>;
|
|
59
|
-
private applyRegistryMigrations;
|
|
60
|
-
private loadMigrationSet;
|
|
61
99
|
/**
|
|
62
100
|
* Reports whether a database is open under an identifier.
|
|
63
101
|
*
|
|
@@ -73,6 +111,10 @@ declare class Sirannon {
|
|
|
73
111
|
databases(): Map<string, Database>;
|
|
74
112
|
/**
|
|
75
113
|
* Closes every open database and stops the lifecycle timers.
|
|
114
|
+
*
|
|
115
|
+
* A database whose file Sirannon is replacing delays the shutdown until that
|
|
116
|
+
* work finishes, since a process that exits part-way through would leave the
|
|
117
|
+
* path with no database on it at all.
|
|
76
118
|
*/
|
|
77
119
|
shutdown(): Promise<void>;
|
|
78
120
|
/**
|
|
@@ -106,6 +148,7 @@ declare class Sirannon {
|
|
|
106
148
|
*/
|
|
107
149
|
onDatabaseClose(hook: DatabaseCloseHook): void;
|
|
108
150
|
private ensureRunning;
|
|
151
|
+
private ensureOpenAllowed;
|
|
109
152
|
}
|
|
110
153
|
|
|
111
154
|
export { Sirannon as S };
|
package/dist/transport/grpc.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BinaryWriter, BinaryReader } from '@bufbuild/protobuf/wire';
|
|
2
2
|
import { Client, ClientDuplexStream, CallOptions, Metadata, ServiceError, ClientUnaryCall, ChannelCredentials, ClientOptions, ServerDuplexStream, Server } from '@grpc/grpc-js';
|
|
3
3
|
import { HealthImplementation } from 'grpc-health-check';
|
|
4
|
-
import { R as ReplicationAck, F as ForwardedTransaction, a as ForwardedTransactionResult, N as NodeInfo, S as SyncRequest, b as SyncBatch, c as SyncComplete, d as SyncAck, e as ReplicationTransport, f as TopologyRole, T as TransportConfig } from '../types-
|
|
4
|
+
import { R as ReplicationAck, F as ForwardedTransaction, a as ForwardedTransactionResult, N as NodeInfo, S as SyncRequest, b as SyncBatch, c as SyncComplete, d as SyncAck, e as ReplicationTransport, f as TopologyRole, T as TransportConfig } from '../types-C1yBGTdx.js';
|
|
5
5
|
import { R as ReplicationBatch } from '../types-CjhxcjhA.js';
|
|
6
|
-
import '../change-tracker-
|
|
7
|
-
import '../types-
|
|
6
|
+
import '../change-tracker-uG9gWvsY.js';
|
|
7
|
+
import '../types-DApb7dvw.js';
|
|
8
8
|
import '../query-types-Cv8N7pXj.js';
|
|
9
9
|
import '../types-CMBcFPhb.js';
|
|
10
10
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { R as ReplicationAck, F as ForwardedTransaction, a as ForwardedTransactionResult, S as SyncRequest, b as SyncBatch, c as SyncComplete, d as SyncAck, N as NodeInfo, e as ReplicationTransport, T as TransportConfig } from '../types-
|
|
1
|
+
import { R as ReplicationAck, F as ForwardedTransaction, a as ForwardedTransactionResult, S as SyncRequest, b as SyncBatch, c as SyncComplete, d as SyncAck, N as NodeInfo, e as ReplicationTransport, T as TransportConfig } from '../types-C1yBGTdx.js';
|
|
2
2
|
import { R as ReplicationBatch } from '../types-CjhxcjhA.js';
|
|
3
|
-
import '../change-tracker-
|
|
4
|
-
import '../types-
|
|
3
|
+
import '../change-tracker-uG9gWvsY.js';
|
|
4
|
+
import '../types-DApb7dvw.js';
|
|
5
5
|
import '../query-types-Cv8N7pXj.js';
|
|
6
6
|
import '../types-CMBcFPhb.js';
|
|
7
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ChangeTracker } from './change-tracker-
|
|
2
|
-
import {
|
|
1
|
+
import { C as ChangeTracker } from './change-tracker-uG9gWvsY.js';
|
|
2
|
+
import { I as SQLiteConnection, N as NodeHealth } from './types-DApb7dvw.js';
|
|
3
3
|
import { S as SyncTableManifest, R as ReplicationBatch, C as ConflictResolver } from './types-CjhxcjhA.js';
|
|
4
4
|
import { C as ClusterCoordinator, j as CoordinatorCompatibilityMetadata, c as ReplicationGroupState } from './types-CMBcFPhb.js';
|
|
5
5
|
|