feat(dev-host): persist the auto-reload preference across restarts

Auto-reload lived only in the dev host process, so every restart reset
it to off and re-enabling it cost a confirmation dialog each session.
The setting now persists to settings.json in the development data
directory (same atomic write pattern as connections.json) and is
restored — and re-armed — at startup.
This commit is contained in:
t8y2
2026-09-16 15:29:39 +08:00
parent 197ba97f73
commit 0239d2c56b
2 changed files with 51 additions and 3 deletions
+27 -3
View File
@@ -1,7 +1,7 @@
import http from "node:http";
import { randomUUID, randomBytes } from "node:crypto";
import { readFile, realpath, watch } from "node:fs/promises";
import { resolve, relative, isAbsolute, sep } from "node:path";
import { readFile, realpath, watch, mkdir, writeFile, rename } from "node:fs/promises";
import { resolve, relative, isAbsolute, sep, join } from "node:path";
import semver from "semver";
import { Sidecar, protocolName } from "./sidecar.mjs";
import { ConnectionStore, lifecyclePayload, providerFor, summary, validateRecord } from "./connections.mjs";
@@ -84,7 +84,25 @@ export async function createMockHost(options) {
lifecycleQueue = task.catch(() => {});
return task;
};
let autoReload = false;
// Developer preferences such as auto-reload outlive a dev host restart;
// persist them next to the connection store with the same atomic pattern.
const settingsDirectory = resolve(options.dataDir),
settingsFile = join(settingsDirectory, "settings.json");
const readSettings = async () => {
try {
const parsed = JSON.parse(await readFile(settingsFile, "utf8"));
return parsed && typeof parsed === "object" ? parsed : {};
} catch {
return {};
}
};
const writeSettings = async (settings) => {
await mkdir(settingsDirectory, { recursive: true, mode: 0o700 });
const temporary = join(settingsDirectory, `.settings-${randomUUID()}.tmp`);
await writeFile(temporary, JSON.stringify({ version: 1, ...settings }, null, 2), { mode: 0o600, flag: "wx" });
await rename(temporary, settingsFile);
};
let autoReload = (await readSettings()).autoReload === true;
const backendReload = new AutoReload(async () => {
try {
await serialize(async () => {
@@ -94,6 +112,7 @@ export async function createMockHost(options) {
broadcast("auto-reload-error", {});
}
});
backendReload.enable(autoReload);
const broadcast = (type, payload) => {
const message = `data: ${JSON.stringify({ type, ...payload })}\n\n`;
for (const stream of streams) {
@@ -250,6 +269,11 @@ export async function createMockHost(options) {
if (typeof p.enabled !== "boolean") throw new Error("Invalid automatic reload setting");
autoReload = p.enabled;
backendReload.enable(autoReload);
try {
await writeSettings({ autoReload });
} catch (error) {
diagnostics.record("error", "build", "自动重载设置保存失败", { reason: String(error.message || error) });
}
broadcast("auto-reload", { enabled: autoReload });
diagnostics.record("info", "build", autoReload ? "自动重载已启用" : "自动重载已关闭");
return { enabled: autoReload };
@@ -262,6 +262,30 @@ test("source watching is opt-in and automatic restarts preserve saved connection
await new Promise((resolve) => setTimeout(resolve, 650));
assert.equal(builds, 1);
});
test("auto-reload preference persists across dev host restarts", async (t) => {
const first = await fixture(t);
assert.equal((await first.request("auto-reload", { enabled: true })).value.enabled, true);
await first.host.close();
const second = await createMockHost({
project: first.root,
uiRoot: "ui",
backend: process.execPath,
diagnostics: new Diagnostics(() => {}),
backendArgs: [fileURLToPath(new URL("./echo-sidecar.mjs", import.meta.url))],
dataDir: join(first.root, "data"),
shellHtml: join(first.root, "ui/index.html"),
port: 0,
backendWatch: first.root,
buildBackend: async () => {},
});
t.after(async () => {
await second.close();
await rm(first.root, { recursive: true, force: true });
});
const bootstrap = await (await fetch(`${second.origin}/api/bootstrap`)).json();
assert.equal(bootstrap.autoReload, true);
});
test("agent diagnostics is read-only, bounded and does not log its own polling", async (t) => {
const { host } = await fixture(t, true);
const url = `${host.origin}/api/diagnostics`,