@zq-silk/yui 0.6.12 → 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
- renameSync(committedDbPath, backupPath);
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
- renameSync(backupPath, committedDbPath);
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
- renameSync(backupPath, committedDbPath);
212
+ removeSqliteFileSet(committedDbPath);
213
+ moveSqliteFileSet(backupPath, committedDbPath);
212
214
  }
213
215
  else {
214
- rmSync(committedDbPath, { force: true });
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
- renameSync(committedDbPath, backupPath);
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
- promoteRename(backupPath, committedDbPath);
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
- promoteRename(backupPath, committedDbPath);
179
+ removeSqliteFileSet(committedDbPath);
180
+ moveSqliteFileSet(backupPath, committedDbPath, promoteRename);
179
181
  }
180
182
  else {
181
- rmSync(committedDbPath, { force: true });
183
+ removeSqliteFileSet(committedDbPath);
182
184
  }
183
185
  }
184
186
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.12",
3
+ "version": "0.6.13",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,