Files
olivv-cs-ComfyUI-FunPack/app/shell/pipeline.js
T
digital-garbage 5b59043fc8 core, app: a prompt you can type, on the main window
The prompt was a slot input on a CLIPTextEncode, reachable only by opening
the pipeline window and finding the node. A slot can now say that one of
its inputs belongs on the app's surface -- a place and a label -- and the
shell offers a region with that name or does not, exactly as a module's
panel works. Core validates that a role is names and nothing else.

Values for those inputs travel addressed by slot rather than as a whole
pipeline, so the window that owns the structure and a box that owns one
value cannot disagree about the slots between them. An address that is
not there is refused: a prompt typed into a box for a removed slot has
gone nowhere, and a run that starts anyway is not the run that was asked
for.

Region stand-ins are empty states now, taken down once something mounts --
'modules appear here' left above the modules that appeared explained
nothing to nobody.
2026-08-30 20:27:44 +03:00

102 lines
4.4 KiB
JavaScript

// What the server would queue, asked for rather than assembled here.
//
// The graph is built by core from the pipeline's slots, so the app never
// composes one: two builders would be two answers to "what runs", and the one
// the user sees would be whichever was written last. The app asks, and gets
// either a prompt or the reasons there is not one.
const ENDPOINT = "/funpack/api/pipeline";
const NODES = "/funpack/api/nodes";
/**
* check({slots, values}) -> { slots, refused, incomplete, notes, queueable, prompt }
*
* `refused` and `incomplete` are different things and stay apart: refused means
* the edit did not happen, incomplete means it did and the pipeline still is
* not ready -- an unset file picker on a fresh install is the normal case, not
* a failure. `notes` is a third thing again: the pipeline is fine and something
* about it is worth saying, such as settings that will not be applied.
*/
export async function check({ fetch: doFetch = globalThis.fetch, slots, values, inputs,
action = "check", slot, node } = {}) {
// `slots` and an action travel together on purpose: a remove is "take this
// one out of THIS pipeline", and sending the action without the pipeline the
// user is looking at would apply it to the server's defaults instead.
const body = { action };
if (slots) body.slots = slots;
// Sent with the pipeline, every time. The server holds no copy of what the
// panels say: two stores of "what the user picked" is two answers to what a
// run used, and the one believed would be whichever was written last.
if (values) body.values = values;
// Values for named inputs, from the controls the pipeline asked the main
// window to show. Only sent when there are some: an empty object is a
// request to override nothing, and saying it every time is noise on the wire.
if (inputs && Object.keys(inputs).length) body.inputs = inputs;
if (slot !== undefined) body.slot = slot;
if (node !== undefined) body.node = node;
const response = await doFetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
// A 400 here is a malformed REQUEST, which is the app's fault and not the
// user's -- so it is reported as a refusal rather than dressed up as an
// unfinished pipeline.
return {
slots: payload.slots || [],
refused: payload.problems || payload.refused || [`the pipeline could not be read (${response.status})`],
incomplete: [],
notes: [],
queueable: false,
prompt: null,
};
}
return {
slots: payload.slots || [],
refused: payload.refused || [],
incomplete: payload.incomplete || [],
notes: payload.notes || [],
queueable: Boolean(payload.queueable),
prompt: payload.prompt || null,
};
}
/** The pipeline as the server holds it, before anything has been edited. */
export async function load({ fetch: doFetch = globalThis.fetch } = {}) {
const response = await doFetch(ENDPOINT);
if (!response.ok) throw new Error(`the pipeline could not be read (${response.status})`);
const payload = await response.json();
return {
slots: payload.slots || [],
incomplete: payload.incomplete || [],
queueable: Boolean(payload.queueable),
};
}
/**
* describe(classes) -> { [className]: description | null }
*
* A null is an answer, not a gap: the slot points at a node this install does
* not have. The caller asked about it, so it is in the reply saying so.
*/
export async function describe(classes, { fetch: doFetch = globalThis.fetch } = {}) {
const wanted = [...new Set(classes)].filter(Boolean);
if (!wanted.length) return {};
const response = await doFetch(`${NODES}?classes=${encodeURIComponent(wanted.join(","))}`);
if (!response.ok) throw new Error(`those nodes could not be described (${response.status})`);
return (await response.json()).nodes || {};
}
/** Installed nodes matching a query, and how many there were before the cut. */
export async function search(query, { fetch: doFetch = globalThis.fetch, limit = 40 } = {}) {
const response = await doFetch(
`${NODES}/search?q=${encodeURIComponent(query || "")}&limit=${limit}`);
if (!response.ok) throw new Error(`nodes could not be searched (${response.status})`);
const payload = await response.json();
return { nodes: payload.nodes || [], total: payload.total || 0 };
}