Files
2026-11-05 15:36:42 +02:00

63 lines
4.5 KiB
JavaScript

// What the steps that rewrite a finished render (upscale, interpolate) share: naming a file, waiting for its background job, swapping it under every
// scene that plays it, and remembering which steps a file has been through so two "always" modes can never feed each other forever. Pure of the page.
export const keyOf = (m) => `${m.type || "output"}/${m.subfolder || ""}/${m.filename}`;
const active = new Map(); // id -> its stop call, for the Upscale / Interpolate jobs this page is waiting for
const asked = new Set(); // ids Stop was pressed for: whatever ends them afterwards is a stop
const listeners = [];
const changed = () => { for (const fn of listeners) { try { fn(); } catch { /* one listener must not stop the others */ } } };
/** How many jobs the Stop button can stop right now. */
export const jobsRunning = () => active.size;
/** -> a function that stops listening. */
export const onJobs = (fn) => { listeners.push(fn); return () => { const i = listeners.indexOf(fn); if (i >= 0) listeners.splice(i, 1); }; };
/** The Stop button: ask every job this page is waiting for to stop, each through its own module's route. */
export const stopJobs = () => Promise.all([...active].map(async ([id, stop]) => {
try { const r = await stop(id); if (r && r.stopped) asked.add(id); } catch { /* the request failed: the job runs on, and a real failure later is still a failure */ }
}));
/** What waitJob throws for a job that was stopped: not a failure, so callers say "stopped" and not "failed". */
export class JobStopped extends Error { constructor() { super("Stopped."); this.stopped = true; } }
/** Wait for a FunUI background job. `read(id)` -> {state: running | done | error | stopped, media, detail}. -> the media, or throws a sentence (a JobStopped when it was stopped). `sleep` is injectable so a test need not wait. */
export async function waitJob(read, id, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), stop = () => {}) {
let lost = 0;
active.set(id, stop); changed();
try {
for (;;) {
await sleep(1000);
let r;
try { r = await read(id); } catch (err) {
if (err.status === 404) throw new Error("ComfyUI restarted while it was working");
if (++lost < 30) continue; // a dropped reply is not a failed job
throw new Error("lost contact with ComfyUI");
}
lost = 0;
if (r.state === "stopped" || (r.state === "error" && asked.has(id))) throw new JobStopped(); // a chunk that failed while Stop was landing is still a stop
if (r.state === "error") throw new Error(r.detail || "failed inside ComfyUI");
if (r.state === "done") return r.media;
}
} finally { active.delete(id); asked.delete(id); changed(); }
}
/** One file backs a whole chain run, so every scene render and ghost that plays `from` gets `to`. -> how many were swapped. */
export function swap(p, from, to) {
const same = (m) => !!m && keyOf(m) === keyOf(from);
let n = 0;
for (const [id, r] of Object.entries(p.scene_renders || {})) if (r && same(r.media)) { p.scene_renders[id] = { ...r, media: to }; n += 1; }
for (const list of Object.values(p.scene_variants || {})) list.forEach((t) => { if (same(t.media)) { t.media = to; n += 1; } }); // a take stepped back to must be the new file too
p.scene_ghosts = (p.scene_ghosts || []).map((g) => (same(g.media) ? (n += 1, { ...g, media: to }) : g));
return n;
}
/** Where each step writes its files (modules/output/render/upscale.py and modules/output/interpolate/nodes.py say the same). */
export const FOLDERS = { upscale: "funui_upscaled", interpolate: "funui_interpolated" };
const steps = new Map(); // file key -> the steps it has been through (kept for this page's life)
const doneOf = (m) => new Set([...(steps.get(keyOf(m)) || []), ...Object.keys(FOLDERS).filter((k) => FOLDERS[k] === m.subfolder)]);
/** Whether `media` already went through `step`. */
export const wentThrough = (media, step) => doneOf(media).has(step);
/** `to` came out of `from` by `step`: it has been through all of `from`'s steps and this one. */
export const derived = (from, to, step) => { steps.set(keyOf(to), doneOf(from).add(step)); };
const gaveUpWatchers = [];
/** A step that was due on `media` (an "always" mode) could not run: whoever waited for it may go on without it. */
export const gaveUp = (media, step) => { for (const fn of gaveUpWatchers) fn(media, step); };
export const onGaveUp = (fn) => { gaveUpWatchers.push(fn); };