124 lines
5.5 KiB
JavaScript
124 lines
5.5 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,
|
|
input, from_slot: fromSlot, from_output: fromOutput } = {}) {
|
|
// `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;
|
|
// wire's own fields, forwarded the same way -- an unlisted key here is one
|
|
// that reaches the server as though it was never sent, which is exactly how
|
|
// this dropped `input` silently until a live click on Unwire caught it.
|
|
if (input !== undefined) body.input = input;
|
|
if (fromSlot !== undefined) body.from_slot = fromSlot;
|
|
if (fromOutput !== undefined) body.from_output = fromOutput;
|
|
|
|
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),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* presets() -> [{ id, title, module, slots }]
|
|
*
|
|
* Alternate starting points a module offers beside the one true default --
|
|
* loaded into the same editable pipeline `load()` returns, not a different
|
|
* kind of thing. An empty list is a real answer (nothing offers one), not
|
|
* a failure to fetch.
|
|
*/
|
|
export async function presets({ fetch: doFetch = globalThis.fetch } = {}) {
|
|
const response = await doFetch(`${ENDPOINT}/presets`);
|
|
if (!response.ok) throw new Error(`presets could not be read (${response.status})`);
|
|
const payload = await response.json();
|
|
return payload.presets || [];
|
|
}
|
|
|
|
/**
|
|
* 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 };
|
|
}
|