修复服务器断线时自动刷新误报暂存失败

This commit is contained in:
magicwang1111
2026-10-28 11:06:27 +08:00
parent f9106f9b2a
commit e6f31f2bf7
2 changed files with 110 additions and 2 deletions
+89
View File
@@ -0,0 +1,89 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
function harness() {
const events = new Map(), storage = new Map(), warnings = [], timers = new Map();
let extension, reloads = 0;
const api = {
addEventListener: (name, fn) => events.set(name, fn),
fetchApi: async () => ({ ok: true, json: async () => ({ revision: '200', user_id: 'a' }) }),
};
const app = {
isGraphReady: true,
graphToPrompt: async () => ({ workflow: { nodes: [] } }),
extensionManager: {
workflow: { openWorkflows: [] },
toast: { add: item => warnings.push(item) },
setting: { settings: {} },
},
registerExtension: value => { extension = value; },
};
const sessionStorage = {
getItem: key => storage.get(key), setItem: (key, value) => storage.set(key, value),
removeItem: key => storage.delete(key),
};
const source = fs.readFileSync(require.resolve('../web/js/autoRefresh.js'), 'utf8')
.replace(/^import .*;\r?\n/gm, '')
.replace('import.meta.url', JSON.stringify('http://localhost/autoRefresh.js?am_revision=100'));
vm.runInNewContext(source, {
app, api, sessionStorage, URL, AbortSignal, console: { error() {} },
setTimeout: fn => { const id = {}; timers.set(id, fn); return id; },
clearTimeout: id => timers.delete(id),
window: { addEventListener() {}, location: { reload: () => reloads++ } },
document: { addEventListener() {} },
});
extension.setup();
return { app, api, warnings, storage, sessionStorage, events, reloads: () => reloads };
}
test('server unavailable does not warn or reload', async () => {
const h = harness();
h.api.fetchApi = async () => { throw new TypeError('Failed to fetch'); };
await h.events.get('reconnected')();
assert.equal(h.warnings.length, 0);
assert.equal(h.reloads(), 0);
});
for (const fail of [true, false]) {
test(`disconnect during snapshot (${fail ? 'failure' : 'success'}) waits for reconnect`, async () => {
const h = harness();
h.app.graphToPrompt = async () => {
h.events.get('reconnecting')?.();
if (fail) throw new TypeError('Failed to fetch');
return { workflow: { nodes: [] } };
};
await h.events.get('reconnected')();
assert.equal(h.warnings.length, 0);
assert.equal(h.reloads(), 0);
assert.equal(h.storage.has('AccountManager.RefreshRevision'), false);
h.app.graphToPrompt = async () => ({ workflow: { nodes: [] } });
await h.events.get('reconnected')();
assert.equal(h.reloads(), 1);
});
}
test('real local storage failure still pauses refresh', async () => {
const h = harness();
h.sessionStorage.setItem = () => { throw new Error('Quota exceeded'); };
await h.events.get('reconnected')();
await h.events.get('reconnected')();
assert.equal(h.warnings.length, 1);
assert.equal(h.reloads(), 0);
});
test('disconnect while version response is pending discards the response', async () => {
const h = harness();
const fetch = h.api.fetchApi;
h.api.fetchApi = async () => {
h.events.get('reconnecting')();
return fetch();
};
await h.events.get('reconnected')();
assert.equal(h.reloads(), 0);
assert.equal(h.warnings.length, 0);
h.api.fetchApi = fetch;
await h.events.get('reconnected')();
assert.equal(h.reloads(), 1);
});
+21 -2
View File
@@ -8,6 +8,8 @@ let busy = false;
let pending = false;
let reloading = false;
let paused = false;
let disconnected = false;
let connectionEpoch = 0;
let retryTimer;
function readStored(key) {
@@ -80,7 +82,7 @@ async function restoreWorkflows(info) {
}
async function checkForUpdate() {
if (reloading || paused) return;
if (reloading || paused || disconnected) return;
if (busy) {
pending = true;
return;
@@ -91,6 +93,7 @@ async function checkForUpdate() {
return;
}
busy = true;
const epoch = connectionEpoch;
try {
let info;
try {
@@ -102,13 +105,16 @@ async function checkForUpdate() {
} catch {
return; // A stopped server or expired login must not trigger a reload.
}
if (epoch !== connectionEpoch || disconnected) return;
if (!/^\d+$/.test(info.revision) || !info.user_id) return;
try {
await restoreWorkflows(info);
} catch (error) {
if (epoch !== connectionEpoch || disconnected) return;
pauseRefresh("工作流恢复未完成,备份已保留。请先检查并保存当前工作流。", error);
return;
}
if (epoch !== connectionEpoch || disconnected) return;
let seen;
try {
seen = readStored(SEEN_KEY);
@@ -125,9 +131,11 @@ async function checkForUpdate() {
if (BigInt(info.revision) <= baseline) return;
try {
const backup = await snapshotWorkflows(info);
if (epoch !== connectionEpoch || disconnected) return;
sessionStorage.setItem(BACKUP_KEY, JSON.stringify(backup));
sessionStorage.setItem(SEEN_KEY, JSON.stringify({ revision: info.revision, user_id: info.user_id }));
} catch (error) {
if (epoch !== connectionEpoch || disconnected) return;
pauseRefresh("无法安全暂存工作流,请先保存后手动刷新。", error);
return;
}
@@ -148,7 +156,18 @@ async function checkForUpdate() {
app.registerExtension({
name: "ComfyUI.AccountManager.AutoRefresh",
setup() {
api.addEventListener("reconnected", checkForUpdate);
// ComfyUI already displays connection status; do not add a save warning while offline.
const onDisconnect = () => {
disconnected = true;
connectionEpoch++;
clearTimeout(retryTimer);
};
api.addEventListener("reconnecting", onDisconnect);
window.addEventListener("offline", onDisconnect);
api.addEventListener("reconnected", () => {
disconnected = false;
return checkForUpdate();
});
window.addEventListener("online", checkForUpdate);
window.addEventListener("pageshow", checkForUpdate);
document.addEventListener("visibilitychange", () => {