Two faults from the round, and a third I introduced fixing them. ComfyUI keys its socket table by client id and pops the old entry when a new socket arrives with the same one, so two tabs sharing an id meant the second took the first's updates -- the first froze mid-progress while the GPU carried on -- and the socket's `finally` pops that id again on close, so closing the dead tab disconnected the live one. The id lives in sessionStorage now: it survives a reload of this tab, which is what reattach needs, and does not reach another tab. Duplicating a tab copies sessionStorage and is still a collision; said in the comment rather than pretended away. Generate only went dead when the RUN reached "queued", and reaching it takes a round trip to ask what to queue. For the whole of that round trip the button was live, so two clicks each posted their own /prompt: two jobs burning GPU time, with the UI following one. Both lived in boot.js, which runs on import and needs a document, so none of it could be driven by a test. That wiring is now in shell/session.js and tested -- and the first version of those tests used a hand-rolled transport whose draw() did nothing, which hid the fault the change introduced: the real draw() writes the status from the run state, and a refused attempt leaves the run idle, so the reason was overwritten with "Ready" in the same breath as it appeared. The tests use the real transport, and a refusal now outlives the redraw that follows it.
79 lines
2.9 KiB
JavaScript
79 lines
2.9 KiB
JavaScript
// Entry point.
|
|
//
|
|
// The whole of what the app does at startup: build the regions, ask the server
|
|
// what exists, and mount whatever announced itself. Nothing here names a module.
|
|
|
|
import { build } from "./shell/layout.js";
|
|
import { fetchManifest } from "./shell/manifest.js";
|
|
import { mountAll } from "./shell/panels.js";
|
|
import { all as allValues } from "./shell/values.js";
|
|
import { composer } from "./composer/composer.js";
|
|
import { createRun, viewUrl } from "./shell/run.js";
|
|
import { clientId, connect, runningFor, finishedFor } from "./shell/client.js";
|
|
import { createGenerator, reattach } from "./shell/session.js";
|
|
import { check } from "./shell/pipeline.js";
|
|
|
|
const root = document.querySelector("#app");
|
|
|
|
async function start() {
|
|
const id = clientId();
|
|
const run = createRun({ clientId: id, connect });
|
|
|
|
const page = build(root, {
|
|
onGenerate: () => generate(),
|
|
onCancel: () => run.cancel(),
|
|
});
|
|
const generate = createGenerator({ run, transport: page.transport, check });
|
|
|
|
// The socket opens at load, not at Generate. A generation queued before a
|
|
// reload keeps running on the server, and a page that only listens once it
|
|
// starts one of its own hears nothing about it -- sits at Ready, and queues a
|
|
// SECOND job if the user presses Generate believing nothing is running.
|
|
run.listen();
|
|
reattach(run, id, { runningFor, finishedFor });
|
|
|
|
run.subscribe((state) => {
|
|
page.transport.draw(state);
|
|
const last = state.images[state.images.length - 1];
|
|
if (last) page.viewer.setSource(viewUrl(last), kindOf(last));
|
|
});
|
|
|
|
let manifest;
|
|
try {
|
|
manifest = await fetchManifest();
|
|
} catch (err) {
|
|
// The one failure the user must see: with no manifest there is no app, so
|
|
// silence here would be an empty window with no explanation.
|
|
root.replaceChildren(composer.emptyState.default({
|
|
icon: "▲",
|
|
title: "Could not reach FunPack",
|
|
hint: `${err.message}. Is ComfyUI running?`,
|
|
}).node);
|
|
return;
|
|
}
|
|
|
|
const { mounted, hidden } = await mountAll(manifest);
|
|
const failed = manifest.failed || [];
|
|
|
|
console.info(`[FunPack] ${mounted.length} module(s) mounted`,
|
|
hidden.length ? `· ${hidden.length} hidden` : "",
|
|
failed.length ? `· ${failed.length} failed to load` : "");
|
|
for (const { id, why } of hidden) console.warn(`[FunPack] ${id} is hidden: ${why}`);
|
|
// These never reached the manifest, so no panel could be missing "in a way
|
|
// the user notices" -- which is exactly why they have to be said. A module
|
|
// that failed to import looks identical to one nobody installed.
|
|
for (const { where, why } of failed) console.warn(`[FunPack] ${where} did not load: ${why}`);
|
|
|
|
window.FunPack = {
|
|
manifest, values: allValues, failed, hidden, run,
|
|
mounted: mounted.map((m) => m.id),
|
|
};
|
|
}
|
|
|
|
/** Video and image results arrive the same way and cannot be shown the same way. */
|
|
function kindOf(image) {
|
|
return /\.(mp4|webm|mov|mkv)$/i.test(image.filename || "") ? "video" : "image";
|
|
}
|
|
|
|
start();
|