ComfyUI's own queue and its own socket, not a second one alongside them. A
FunPack queue would be a second thing tracking what is running, and the one the
user believes is whichever spoke last -- so this speaks to /prompt, /interrupt
and /ws as any ComfyUI client does and holds only a view of the server's state.
The graph is asked for, never assembled here, for the same reason.
The client id is remembered: ComfyUI addresses progress to the client that
queued the run, so a reload with a fresh id hears nothing about the generation
already in flight -- the run continues, the GPU time is spent, and the app says
Ready.
Faults found on the way, each in code that already had passing tests:
* `[hidden]` was inert across the whole kit. The UA rule is element-level, so
`.cx-btn { display: inline-flex }` beats it -- Cancel was marked hidden, sat
there, and nothing reported anything. jsdom cannot see it: the attribute was
always correct. One reset rule, and a browser test over four element kinds.
* A panel passed no height to its body and a split pane passed none to its
panes, so the viewer asked for 100% of `auto` and collapsed to a single line
inside a panel that was the right size around it.
* subscribe() delivered the first state outside the guard every later delivery
gets, so one bad listener took down whoever was wiring the UI up -- at
startup, before anything existed to report it.
* A whole tests directory was never run: the test script globbed
composer/tests only. `node --test` discovers them itself now, so there is no
list to forget.
* `empty` was not in the content rule's key list, so three elements were never
asked to prove their empty-state text comes back as text rather than markup.
The dev server now mounts the REAL routes -- register() already took a route
table -- instead of reimplementing them over http.server. It answered POST
/api/pipeline with 501, so the edit-and-queue path could not be looked at, which
is the one thing the dev server exists for.
90 lines
3.2 KiB
JavaScript
90 lines
3.2 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 } from "./shell/client.js";
|
|
import { check } from "./shell/pipeline.js";
|
|
|
|
const root = document.querySelector("#app");
|
|
|
|
async function start() {
|
|
const run = createRun({ clientId: clientId(), connect });
|
|
|
|
const page = build(root, {
|
|
async onGenerate() {
|
|
// Asked for, not assembled: core builds the graph from the pipeline's
|
|
// slots, and a refusal comes back as a reason rather than as a failure
|
|
// several stages later.
|
|
let plan;
|
|
try {
|
|
plan = await check({});
|
|
} catch (err) {
|
|
page.transport.status.setText(`The pipeline could not be read: ${err.message}`);
|
|
return;
|
|
}
|
|
const stopping = [...plan.refused, ...plan.incomplete];
|
|
if (!plan.queueable || !plan.prompt) {
|
|
page.transport.status.setText(stopping[0] || "This pipeline is not ready to run.");
|
|
return;
|
|
}
|
|
try {
|
|
await run.start(plan.prompt);
|
|
} catch { /* the run's own state carries the reason, and draw() shows it */ }
|
|
},
|
|
onCancel: () => run.cancel(),
|
|
});
|
|
|
|
// One subscription draws everything the run affects. Two would be two places
|
|
// that can disagree about what is happening.
|
|
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();
|