@zq-silk/yui 0.6.11 → 0.6.13
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.
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, renameSync, rmSync } from "node:fs";
|
|
2
|
+
const SQLITE_FILE_SUFFIXES = ["", "-wal", "-shm"];
|
|
3
|
+
/**
|
|
4
|
+
* Move a SQLite database together with any live WAL/SHM sidecars.
|
|
5
|
+
*
|
|
6
|
+
* A WAL database is one logical file set. Moving only the main file can leave
|
|
7
|
+
* the old WAL under the promoted database name, causing SQLite to replay it
|
|
8
|
+
* against an unrelated database. The migration fence guarantees there are no
|
|
9
|
+
* writers while this bounded rename runs.
|
|
10
|
+
*/
|
|
11
|
+
export function moveSqliteFileSet(sourcePath, targetPath, rename = renameSync) {
|
|
12
|
+
const suffixes = SQLITE_FILE_SUFFIXES.filter((suffix) => existsSync(`${sourcePath}${suffix}`));
|
|
13
|
+
for (const suffix of suffixes) {
|
|
14
|
+
if (existsSync(`${targetPath}${suffix}`)) {
|
|
15
|
+
throw new Error(`Refusing to overwrite an existing SQLite switch target: ${targetPath}${suffix}.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const moved = [];
|
|
19
|
+
try {
|
|
20
|
+
for (const suffix of suffixes) {
|
|
21
|
+
rename(`${sourcePath}${suffix}`, `${targetPath}${suffix}`);
|
|
22
|
+
moved.push(suffix);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
let rollbackError;
|
|
27
|
+
for (const suffix of moved.reverse()) {
|
|
28
|
+
try {
|
|
29
|
+
rename(`${targetPath}${suffix}`, `${sourcePath}${suffix}`);
|
|
30
|
+
}
|
|
31
|
+
catch (candidate) {
|
|
32
|
+
rollbackError ??= candidate;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (rollbackError !== undefined) {
|
|
36
|
+
throw new Error(`SQLite file-set move failed and rollback was incomplete: ${messageOf(error)}; `
|
|
37
|
+
+ `rollback failed: ${messageOf(rollbackError)}`);
|
|
38
|
+
}
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Remove a promoted SQLite file set before restoring its timestamped backup. */
|
|
43
|
+
export function removeSqliteFileSet(databasePath) {
|
|
44
|
+
for (const suffix of SQLITE_FILE_SUFFIXES) {
|
|
45
|
+
rmSync(`${databasePath}${suffix}`, { force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function messageOf(error) {
|
|
49
|
+
return error instanceof Error ? error.message : String(error);
|
|
50
|
+
}
|
|
@@ -42,6 +42,7 @@ import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspect
|
|
|
42
42
|
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState } from "./sqliteStateMigration.js";
|
|
43
43
|
import { latestStateBackupPath } from "./pseudoLayoutRepair.js";
|
|
44
44
|
import { migrationReceiptPath, writeMigrationReceipt } from "./migrationReceipt.js";
|
|
45
|
+
import { moveSqliteFileSet, removeSqliteFileSet } from "./sqliteFileSet.js";
|
|
45
46
|
/** Build the SQLite-backed migration target. */
|
|
46
47
|
export function createSqliteMigrationTarget(options) {
|
|
47
48
|
const home = options.home;
|
|
@@ -155,7 +156,7 @@ export function createSqliteMigrationTarget(options) {
|
|
|
155
156
|
if (existsSync(backupPath)) {
|
|
156
157
|
throw new Error(`Refusing to overwrite an existing database backup: ${backupPath}.`);
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
+
moveSqliteFileSet(committedDbPath, backupPath);
|
|
159
160
|
}
|
|
160
161
|
renameSync(stagedDbPath, committedDbPath);
|
|
161
162
|
// The staged connection may leave empty WAL/SHM sidecars behind
|
|
@@ -167,7 +168,7 @@ export function createSqliteMigrationTarget(options) {
|
|
|
167
168
|
// Pre-promotion failure: restore the original database if we moved it.
|
|
168
169
|
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
169
170
|
try {
|
|
170
|
-
|
|
171
|
+
moveSqliteFileSet(backupPath, committedDbPath);
|
|
171
172
|
}
|
|
172
173
|
catch {
|
|
173
174
|
throw new AmbiguousSwitchError({
|
|
@@ -208,10 +209,11 @@ export function createSqliteMigrationTarget(options) {
|
|
|
208
209
|
// is ambiguous and must be recovered manually.
|
|
209
210
|
try {
|
|
210
211
|
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
211
|
-
|
|
212
|
+
removeSqliteFileSet(committedDbPath);
|
|
213
|
+
moveSqliteFileSet(backupPath, committedDbPath);
|
|
212
214
|
}
|
|
213
215
|
else {
|
|
214
|
-
|
|
216
|
+
removeSqliteFileSet(committedDbPath);
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
219
|
catch {
|
|
@@ -37,6 +37,7 @@ import { planMigration } from "../migration/planner.js";
|
|
|
37
37
|
import { AmbiguousSwitchError } from "../migration/index.js";
|
|
38
38
|
import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState, inspectSnapshotVersionState } from "./homeMigrationTarget.js";
|
|
39
39
|
import { writeSwitchProgress } from "./switchProgress.js";
|
|
40
|
+
import { moveSqliteFileSet, removeSqliteFileSet } from "./sqliteFileSet.js";
|
|
40
41
|
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readStateFromSqlite } from "./sqliteStateMigration.js";
|
|
41
42
|
/** Build the SQLite-backed record-migration target. */
|
|
42
43
|
export function createSqliteRecordMigrationTarget(options) {
|
|
@@ -135,7 +136,7 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
135
136
|
if (existsSync(backupPath)) {
|
|
136
137
|
throw new Error(`Refusing to overwrite an existing database backup: ${backupPath}.`);
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
+
moveSqliteFileSet(committedDbPath, backupPath);
|
|
139
140
|
}
|
|
140
141
|
promoteRename(stagedDbPath, committedDbPath);
|
|
141
142
|
// The staged connection may leave empty WAL/SHM sidecars behind
|
|
@@ -147,7 +148,7 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
147
148
|
// Pre-promotion failure: restore the original database if we moved it.
|
|
148
149
|
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
149
150
|
try {
|
|
150
|
-
|
|
151
|
+
moveSqliteFileSet(backupPath, committedDbPath, promoteRename);
|
|
151
152
|
}
|
|
152
153
|
catch {
|
|
153
154
|
writeInterruptedMarker(home, backupPath, stagedDbPath, now);
|
|
@@ -175,10 +176,11 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
175
176
|
// is ambiguous and must be recovered manually.
|
|
176
177
|
try {
|
|
177
178
|
if (backupPath !== undefined && existsSync(backupPath)) {
|
|
178
|
-
|
|
179
|
+
removeSqliteFileSet(committedDbPath);
|
|
180
|
+
moveSqliteFileSet(backupPath, committedDbPath, promoteRename);
|
|
179
181
|
}
|
|
180
182
|
else {
|
|
181
|
-
|
|
183
|
+
removeSqliteFileSet(committedDbPath);
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
catch {
|
|
@@ -529,7 +529,6 @@ function hashPayloadTable(db, sql) {
|
|
|
529
529
|
const rows = db.prepare(sql).all();
|
|
530
530
|
return hashRecords(rows.map((row) => JSON.parse(row.payload)));
|
|
531
531
|
}
|
|
532
|
-
/** Reconstruct a WorkMailbox from the normalised mailboxes table columns. */
|
|
533
532
|
export function rowToMailbox(row) {
|
|
534
533
|
let target;
|
|
535
534
|
switch (row.target_kind) {
|
|
@@ -551,13 +550,25 @@ export function rowToMailbox(row) {
|
|
|
551
550
|
default:
|
|
552
551
|
target = { kind: row.target_kind };
|
|
553
552
|
}
|
|
554
|
-
|
|
555
|
-
schemaVersion: 1,
|
|
553
|
+
const common = {
|
|
556
554
|
target,
|
|
557
555
|
nextSequence: row.next_sequence,
|
|
558
556
|
processing: row.processing === null ? null : JSON.parse(row.processing),
|
|
559
557
|
pending: row.pending === null ? null : JSON.parse(row.pending)
|
|
560
558
|
};
|
|
559
|
+
return Object.hasOwn(row, "input_delivery")
|
|
560
|
+
? {
|
|
561
|
+
schemaVersion: 2,
|
|
562
|
+
...common,
|
|
563
|
+
inputDelivery: row.input_delivery === null
|
|
564
|
+
? null
|
|
565
|
+
: JSON.parse(row.input_delivery)
|
|
566
|
+
}
|
|
567
|
+
: { schemaVersion: 1, ...common };
|
|
568
|
+
}
|
|
569
|
+
function readWorkMailboxRows(db) {
|
|
570
|
+
const hasInputDelivery = db.prepare("PRAGMA table_info(mailboxes)").all().some(({ name }) => name === "input_delivery");
|
|
571
|
+
return db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending${hasInputDelivery ? ", input_delivery" : ""} FROM mailboxes ORDER BY target_key`).all();
|
|
561
572
|
}
|
|
562
573
|
/**
|
|
563
574
|
* Compute per-family checksums from the SQLite database. The database is
|
|
@@ -603,7 +614,7 @@ export function computeDbFamilyChecksums(home, databaseFilename) {
|
|
|
603
614
|
checksums.capabilityGrant = hashPayloadTable(db, "SELECT payload FROM capability_grants");
|
|
604
615
|
checksums.releaseWorkflow = hashPayloadTable(db, "SELECT payload FROM release_workflows");
|
|
605
616
|
// Mailboxes are reconstructed from typed columns (no payload column).
|
|
606
|
-
const mailboxRows = db
|
|
617
|
+
const mailboxRows = readWorkMailboxRows(db);
|
|
607
618
|
checksums.workMailbox = hashRecords(mailboxRows.map(rowToMailbox));
|
|
608
619
|
return checksums;
|
|
609
620
|
}
|
|
@@ -774,7 +785,7 @@ export function readStateFromSqlite(home) {
|
|
|
774
785
|
stored.idHighWaterMarks[row.kind] = row.high_water;
|
|
775
786
|
}
|
|
776
787
|
// Work mailboxes: reconstructed from typed columns (no payload column).
|
|
777
|
-
const mailboxRows = db
|
|
788
|
+
const mailboxRows = readWorkMailboxRows(db);
|
|
778
789
|
const mailboxes = state.mailboxes;
|
|
779
790
|
for (const row of mailboxRows) {
|
|
780
791
|
const mailbox = rowToMailbox(row);
|