@delali/sirannon-db 0.2.3-next.31 → 0.2.3-next.34
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 +4 -3
- package/dist/backup/index.mjs +2 -0
- package/dist/backup-scheduler/index.d.ts +14 -2
- package/dist/backup-scheduler/index.mjs +1 -1
- package/dist/chain-queries-BFRYrmNE.d.ts +91 -0
- package/dist/{change-tracker-7wXnBjUN.d.ts → change-tracker-DNisL4aw.d.ts} +1 -1
- package/dist/chunk-4H45GMYI.mjs +118 -0
- package/dist/{chunk-23HKRYSM.mjs → chunk-7HVBKOII.mjs} +469 -59
- package/dist/{chunk-7X6E7UUF.mjs → chunk-ECEGYTVF.mjs} +1 -1
- package/dist/chunk-JFZXO5S2.mjs +1157 -0
- package/dist/chunk-YNTDOFTN.mjs +62 -0
- package/dist/client/index.d.ts +8 -9
- package/dist/client/topology.d.ts +4 -5
- package/dist/{client-base-TJj2LTmq.d.ts → client-base-0QBh8JC4.d.ts} +2 -2
- package/dist/core/index.d.ts +15 -10
- package/dist/core/index.mjs +209 -59
- package/dist/core/writer-worker.mjs +1 -0
- package/dist/{database-rRTtNLyK.d.ts → database-CKZP6SiT.d.ts} +115 -42
- package/dist/driver/better-sqlite3.d.ts +8 -2
- package/dist/driver/better-sqlite3.mjs +14 -5
- package/dist/driver/bun.d.ts +1 -2
- package/dist/driver/bun.mjs +3 -0
- package/dist/driver/expo.d.ts +1 -2
- package/dist/driver/expo.mjs +3 -0
- package/dist/driver/node.d.ts +8 -2
- package/dist/driver/node.mjs +18 -3
- package/dist/driver/wa-sqlite.d.ts +1 -2
- package/dist/driver/wa-sqlite.mjs +3 -0
- package/dist/file-migrations/index.d.ts +1 -2
- package/dist/{protocol-b7bwu6s4.d.ts → protocol-2vVZ4ijz.d.ts} +1 -1
- package/dist/replication/index.d.ts +7 -8
- package/dist/server/index.d.ts +8 -9
- package/dist/server/index.mjs +1 -1
- package/dist/{server-options-Dgvo1Fhv.d.ts → server-options-DxHqvZw6.d.ts} +1 -1
- package/dist/{sirannon-Xu6bjxsA.d.ts → sirannon-Clqx8Xyu.d.ts} +2 -2
- package/dist/transport/grpc.d.ts +3 -4
- package/dist/transport/memory.d.ts +3 -4
- package/dist/{types-B8dVULSQ.d.ts → types-DAemUqLQ.d.ts} +2 -2
- package/dist/{types-B1wQuNXV.d.ts → types-GuxbxlwL.d.ts} +470 -6
- package/package.json +10 -1
- package/dist/capabilities-DagBnGk8.d.ts +0 -21
- package/dist/chunk-ADBGZA6L.mjs +0 -420
- package/dist/report-BFsHdkKt.d.ts +0 -132
package/dist/backup/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
export { B as BackupCapabilities } from '../
|
|
1
|
+
import { e as BackupDestination, f as BackupRunReport } from '../types-GuxbxlwL.js';
|
|
2
|
+
export { g as BackupChain, h as BackupChainBase, i as BackupChainChange, j as BackupChainPosition, k as BackupChainRecord, l as BackupCycle, m as BackupCycleOptions, n as BackupPiece, o as BackupProgress, p as BackupToDestinationOptions, q as DEFAULT_CHAIN_NAME, r as chainLogName, s as readBackupChains } from '../types-GuxbxlwL.js';
|
|
3
|
+
export { B as BackupCapabilities, a as BackupRestorePlan, b as BackupSafeToDeleteOptions, c as backupPiecesSafeToDelete, p as planBackupRestore } from '../chain-queries-BFRYrmNE.js';
|
|
4
|
+
import '../query-types-Cv8N7pXj.js';
|
|
4
5
|
|
|
5
6
|
/** What one assembled file took to build.
|
|
6
7
|
* @public
|
package/dist/backup/index.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { backupPiecesSafeToDelete, planBackupRestore } from '../chunk-YNTDOFTN.mjs';
|
|
2
|
+
export { DEFAULT_CHAIN_NAME, chainLogName, readBackupChains } from '../chunk-4H45GMYI.mjs';
|
|
1
3
|
import { SirannonError } from '../chunk-PBRXXISQ.mjs';
|
|
2
4
|
import { createHash } from 'crypto';
|
|
3
5
|
import { open, rm } from 'fs/promises';
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { e as BackupRunRequest, a as BackupRunReport } from '../report-BFsHdkKt.js';
|
|
1
|
+
import { t as SQLiteConnection, u as BackupRunRequest, f as BackupRunReport, v as BackupScheduleOptions } from '../types-GuxbxlwL.js';
|
|
3
2
|
import '../query-types-Cv8N7pXj.js';
|
|
4
3
|
|
|
4
|
+
/** What one runtime needs before a copy can reach a destination without a local file.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
interface BackupStreamingSupport {
|
|
8
|
+
/** Absolute path of the compiled extension that carries the bytes. */
|
|
9
|
+
extensionPath: string;
|
|
10
|
+
/** Opens the connection the extension's statements run on. */
|
|
11
|
+
openConnection(): Promise<SQLiteConnection>;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
declare class BackupManager {
|
|
15
|
+
private readonly streaming?;
|
|
16
|
+
constructor(streaming?: BackupStreamingSupport | undefined);
|
|
6
17
|
backup(conn: SQLiteConnection, destPath: string, onFirstStep?: () => void): Promise<void>;
|
|
7
18
|
copyToDestination(conn: SQLiteConnection, request: BackupRunRequest): Promise<BackupRunReport>;
|
|
19
|
+
streamsToDestination(): boolean;
|
|
8
20
|
generateFilename(): string;
|
|
9
21
|
rotate(dir: string, maxFiles: number): void;
|
|
10
22
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { h as BackupChainBase, i as BackupChainChange, g as BackupChain, k as BackupChainRecord } from './types-GuxbxlwL.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What one runtime supports of the backup operations, so a caller learns
|
|
5
|
+
* before a run rather than at failure time. A runtime that hands over whole
|
|
6
|
+
* databases only reports no full copy at all.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
interface BackupCapabilities {
|
|
11
|
+
/** Whether this runtime copies an open database while writes continue. */
|
|
12
|
+
fullCopy: boolean;
|
|
13
|
+
/** Whether a full copy reaches the destination without a local file. */
|
|
14
|
+
streamedCopy: boolean;
|
|
15
|
+
/** Whether a full copy writes a local file and sends that file on. */
|
|
16
|
+
stagedCopy: boolean;
|
|
17
|
+
/** Local disk a full copy needs, which the staged route sets to the size of the backup. */
|
|
18
|
+
localDiskRequired: 'none' | 'equal-to-backup';
|
|
19
|
+
/** Whether this runtime repeats a full copy on a schedule. */
|
|
20
|
+
schedule: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What one restore has to read, and what it will produce.
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
interface BackupRestorePlan {
|
|
27
|
+
/** The chain to read. */
|
|
28
|
+
chainId: string;
|
|
29
|
+
/** The full copy to start from. */
|
|
30
|
+
base: BackupChainBase;
|
|
31
|
+
/** The change pieces to apply on top of it, oldest first. */
|
|
32
|
+
changes: BackupChainChange[];
|
|
33
|
+
/**
|
|
34
|
+
* Epoch milliseconds the restored database will reflect. This is when the
|
|
35
|
+
* last piece in the plan was captured, which is at or before the moment you
|
|
36
|
+
* asked for.
|
|
37
|
+
*/
|
|
38
|
+
restoresTo: number;
|
|
39
|
+
}
|
|
40
|
+
/** How far back you still want to be able to restore.
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
interface BackupSafeToDeleteOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Epoch milliseconds of the earliest moment a restore must still reach.
|
|
46
|
+
* Leave it out and the answer covers only the backups no restore could ever
|
|
47
|
+
* use.
|
|
48
|
+
*/
|
|
49
|
+
restorableFrom?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Works out what a restore to a given moment has to read: the newest full copy
|
|
53
|
+
* finished at or before it, then every change piece captured from that copy up
|
|
54
|
+
* to it.
|
|
55
|
+
*
|
|
56
|
+
* You get back the moment the result will actually reflect, which is when the
|
|
57
|
+
* last piece was captured. One piece covers every write in the interval it was
|
|
58
|
+
* taken over, so a restore arrives at a piece boundary, not at the exact
|
|
59
|
+
* millisecond you named.
|
|
60
|
+
*
|
|
61
|
+
* A gap in the chain fails the plan with `BACKUP_CHAIN_BROKEN` and names the
|
|
62
|
+
* missing piece, because a plan that stopped part-way through would leave a
|
|
63
|
+
* database nobody could trust.
|
|
64
|
+
*
|
|
65
|
+
* @param chains - The chains a destination holds, as {@link readBackupChains} returns them.
|
|
66
|
+
* @param moment - Epoch milliseconds you want back.
|
|
67
|
+
* @returns The full copy, the change pieces to apply, and the moment the result reflects.
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
declare function planBackupRestore(chains: readonly BackupChain[], moment: number): BackupRestorePlan;
|
|
72
|
+
/**
|
|
73
|
+
* Tells you which backups no restore still needs, so you can delete them
|
|
74
|
+
* knowing exactly what you give up.
|
|
75
|
+
*
|
|
76
|
+
* Two kinds are dead whatever you ask for: a chain whose full copy has gone,
|
|
77
|
+
* and every change piece after a gap, since nothing can be replayed past the
|
|
78
|
+
* gap. Name the earliest moment you still want to reach and the answer also
|
|
79
|
+
* covers the older chains a newer full copy already spans.
|
|
80
|
+
*
|
|
81
|
+
* Sirannon lists them and deletes nothing. The destination is yours.
|
|
82
|
+
*
|
|
83
|
+
* @param chains - The chains a destination holds, as {@link readBackupChains} returns them.
|
|
84
|
+
* @param options - How far back you still want to be able to restore.
|
|
85
|
+
* @returns The records you may delete, oldest first.
|
|
86
|
+
*
|
|
87
|
+
* @public
|
|
88
|
+
*/
|
|
89
|
+
declare function backupPiecesSafeToDelete(chains: readonly BackupChain[], options?: BackupSafeToDeleteOptions): BackupChainRecord[];
|
|
90
|
+
|
|
91
|
+
export { type BackupCapabilities as B, type BackupRestorePlan as a, type BackupSafeToDeleteOptions as b, backupPiecesSafeToDelete as c, planBackupRestore as p };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { SirannonError } from './chunk-PBRXXISQ.mjs';
|
|
2
|
+
|
|
3
|
+
// src/core/backup/chain-records.ts
|
|
4
|
+
function isWholeNumber(value) {
|
|
5
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
6
|
+
}
|
|
7
|
+
function isAtLeastOne(value) {
|
|
8
|
+
return isWholeNumber(value) && value >= 1;
|
|
9
|
+
}
|
|
10
|
+
function isTextOrAbsent(value) {
|
|
11
|
+
return value === void 0 || typeof value === "string";
|
|
12
|
+
}
|
|
13
|
+
function isBackupChainPosition(value) {
|
|
14
|
+
const position = value;
|
|
15
|
+
return isWholeNumber(position?.logSequence) && isWholeNumber(position.salt1) && isWholeNumber(position.salt2) && isAtLeastOne(position.firstFrame) && isAtLeastOne(position.lastFrame) && position.lastFrame >= position.firstFrame;
|
|
16
|
+
}
|
|
17
|
+
function isBackupChainHead(value) {
|
|
18
|
+
const head = value;
|
|
19
|
+
return typeof head?.chainId === "string" && isWholeNumber(head.startedAt) && isTextOrAbsent(head.previousChainId);
|
|
20
|
+
}
|
|
21
|
+
function isBackupChainBase(value) {
|
|
22
|
+
const base = value;
|
|
23
|
+
return base?.kind === "full" && typeof base.chainId === "string" && typeof base.name === "string" && typeof base.runId === "string" && isWholeNumber(base.finishedAt) && isWholeNumber(base.pieceCount) && isAtLeastOne(base.pieceBytes) && isWholeNumber(base.bytesWritten) && isTextOrAbsent(base.fingerprint);
|
|
24
|
+
}
|
|
25
|
+
function isBackupChainChange(value) {
|
|
26
|
+
const change = value;
|
|
27
|
+
return change?.kind === "change" && typeof change.chainId === "string" && typeof change.name === "string" && typeof change.runId === "string" && isAtLeastOne(change.sequence) && isBackupChainPosition(change.position) && isWholeNumber(change.capturedAt) && isAtLeastOne(change.frameCount) && isWholeNumber(change.pieceCount) && isAtLeastOne(change.pieceBytes) && isWholeNumber(change.bytesWritten) && typeof change.checkpointed === "boolean" && isTextOrAbsent(change.fingerprint);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/core/backup/chain.ts
|
|
31
|
+
var DEFAULT_CHAIN_NAME = "sirannon-backup-chain";
|
|
32
|
+
var encoder = new TextEncoder();
|
|
33
|
+
var decoder = new TextDecoder();
|
|
34
|
+
function destinationError(message, err) {
|
|
35
|
+
if (err instanceof SirannonError) return err;
|
|
36
|
+
const detail = err instanceof Error ? `: ${err.message}` : "";
|
|
37
|
+
return new SirannonError(`${message}${detail}`, "BACKUP_DESTINATION_ERROR");
|
|
38
|
+
}
|
|
39
|
+
function chainLogName(chainName, chainId) {
|
|
40
|
+
return `${chainName}.${chainId}`;
|
|
41
|
+
}
|
|
42
|
+
async function appendRecord(destination, name, index, record) {
|
|
43
|
+
try {
|
|
44
|
+
await destination.writePiece(name, index, encoder.encode(JSON.stringify(record)));
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw destinationError(`The destination refused record ${index} of '${name}'`, err);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function readRecords(destination, name) {
|
|
50
|
+
let listed;
|
|
51
|
+
try {
|
|
52
|
+
listed = await destination.listPieces(name);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw destinationError(`The destination could not list the records of '${name}'`, err);
|
|
55
|
+
}
|
|
56
|
+
const ordered = [...listed].sort((left, right) => left.index - right.index);
|
|
57
|
+
const records = [];
|
|
58
|
+
for (const piece of ordered) {
|
|
59
|
+
let bytes;
|
|
60
|
+
try {
|
|
61
|
+
bytes = await destination.readPiece(name, piece.index);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
throw destinationError(`The destination could not return record ${piece.index} of '${name}'`, err);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
records.push(JSON.parse(decoder.decode(bytes)));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
throw destinationError(`Record ${piece.index} of '${name}' is not a record Sirannon wrote`, err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return records;
|
|
72
|
+
}
|
|
73
|
+
function chainRecords(records, name) {
|
|
74
|
+
const kept = [];
|
|
75
|
+
for (const record of records) {
|
|
76
|
+
const kind = record?.kind;
|
|
77
|
+
if (kind !== "full" && kind !== "change") continue;
|
|
78
|
+
if (kind === "full" ? isBackupChainBase(record) : isBackupChainChange(record)) {
|
|
79
|
+
kept.push(record);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const label = kind === "full" ? "full copy" : "change piece";
|
|
83
|
+
throw destinationError(
|
|
84
|
+
`A ${label} record of '${name}' is missing fields Sirannon writes into every record it stores, so no restore can use this chain until you put that record back`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return kept;
|
|
88
|
+
}
|
|
89
|
+
async function appendChainHead(destination, chainName, head, index) {
|
|
90
|
+
await appendRecord(destination, chainName, index, head);
|
|
91
|
+
}
|
|
92
|
+
async function appendChainRecord(destination, chainName, record, index) {
|
|
93
|
+
await appendRecord(destination, chainLogName(chainName, record.chainId), index, record);
|
|
94
|
+
}
|
|
95
|
+
async function readChainHeads(destination, chainName) {
|
|
96
|
+
const records = await readRecords(destination, chainName);
|
|
97
|
+
return records.filter(isBackupChainHead).reverse();
|
|
98
|
+
}
|
|
99
|
+
async function readBackupChains(destination, chainName = DEFAULT_CHAIN_NAME) {
|
|
100
|
+
const heads = await readChainHeads(destination, chainName);
|
|
101
|
+
const chains = [];
|
|
102
|
+
for (const head of heads) {
|
|
103
|
+
const logName = chainLogName(chainName, head.chainId);
|
|
104
|
+
const records = chainRecords(await readRecords(destination, logName), logName);
|
|
105
|
+
const changes = records.filter((record) => record.kind === "change").sort((left, right) => left.sequence - right.sequence);
|
|
106
|
+
const base = records.find((record) => record.kind === "full");
|
|
107
|
+
chains.push({
|
|
108
|
+
chainId: head.chainId,
|
|
109
|
+
startedAt: head.startedAt,
|
|
110
|
+
...head.previousChainId ? { previousChainId: head.previousChainId } : {},
|
|
111
|
+
...base ? { base } : {},
|
|
112
|
+
changes
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return chains;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { DEFAULT_CHAIN_NAME, appendChainHead, appendChainRecord, chainLogName, isBackupChainPosition, readBackupChains, readChainHeads };
|