- Typed serverless pools validate the unpruned workflow, so a preview on any new slot failed the whole run; both serverless compilers now accept every Collector output slot (COLLECTOR_OUTPUT_COUNT). - Serverless pools are config workers: their serverless:<pool>:<job> participants now share the pool's worker_N slot instead of overflowing. - A worker returning only empty frames no longer raises in the preview split (the combined batch already skipped them). - Worker previews are views into the combined batch, not a second copy, and a frame-count guard skips them rather than misattribute frames. - Frontend: one shared config fetch per load; unnamed slots drop their saved label instead of storing a redundant one. - Tooltip notes that the per-GPU outputs are unavailable downstream of Ultimate SD Upscale Distributed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
95 lines
3.0 KiB
JavaScript
95 lines
3.0 KiB
JavaScript
import { app } from "/scripts/app.js";
|
|
import { ENDPOINTS, NODE_CLASSES } from "./constants.js";
|
|
|
|
// worker_N previews the Nth configured worker (see nodes/collector.py).
|
|
const WORKER_OUTPUT_PREFIX = "worker_";
|
|
const WORKERS_CHANGED_EVENT = "distributed:workers-changed";
|
|
|
|
const trackedNodes = new Set();
|
|
let workers = [];
|
|
let workersPromise = null;
|
|
let listenerAttached = false;
|
|
|
|
async function fetchWorkers() {
|
|
try {
|
|
const resp = await fetch(ENDPOINTS.CONFIG);
|
|
if (!resp.ok) return [];
|
|
const config = await resp.json();
|
|
return Array.isArray(config.workers) ? config.workers : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Collectors created together (a workflow load) share one config fetch.
|
|
function loadWorkers() {
|
|
if (!workersPromise) {
|
|
workersPromise = fetchWorkers().then((list) => {
|
|
workers = list;
|
|
if (list.length === 0) workersPromise = null;
|
|
return list;
|
|
});
|
|
}
|
|
return workersPromise;
|
|
}
|
|
|
|
// Only the displayed label changes; slot names stay fixed so links survive.
|
|
// Unnamed slots drop the label so saved workflows carry no redundant copies.
|
|
function labelWorkerOutputs(node, configWorkers) {
|
|
for (const output of node.outputs || []) {
|
|
if (!output?.name?.startsWith(WORKER_OUTPUT_PREFIX)) continue;
|
|
const index = parseInt(output.name.slice(WORKER_OUTPUT_PREFIX.length), 10) - 1;
|
|
const worker = configWorkers[index];
|
|
if (worker?.name) {
|
|
output.label = `${output.name} (${worker.name})`;
|
|
} else {
|
|
delete output.label;
|
|
}
|
|
}
|
|
node.setDirtyCanvas?.(true, true);
|
|
}
|
|
|
|
function relabelAll() {
|
|
for (const node of trackedNodes) labelWorkerOutputs(node, workers);
|
|
}
|
|
|
|
function attachListener() {
|
|
if (listenerAttached || typeof window === "undefined") return;
|
|
window.addEventListener(WORKERS_CHANGED_EVENT, (event) => {
|
|
if (Array.isArray(event?.detail?.workers)) {
|
|
workers = event.detail.workers;
|
|
relabelAll();
|
|
}
|
|
});
|
|
listenerAttached = true;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "Distributed.CollectorOutputs",
|
|
async nodeCreated(node) {
|
|
if (node.comfyClass !== NODE_CLASSES.DISTRIBUTED_COLLECTOR) return;
|
|
try {
|
|
attachListener();
|
|
trackedNodes.add(node);
|
|
|
|
const originalConfigure = node.configure;
|
|
node.configure = function (data) {
|
|
const result = originalConfigure ? originalConfigure.call(this, data) : undefined;
|
|
labelWorkerOutputs(this, workers);
|
|
return result;
|
|
};
|
|
|
|
const originalOnRemoved = node.onRemoved;
|
|
node.onRemoved = function () {
|
|
trackedNodes.delete(this);
|
|
return originalOnRemoved ? originalOnRemoved.call(this) : undefined;
|
|
};
|
|
|
|
if (workers.length === 0) await loadWorkers();
|
|
labelWorkerOutputs(node, workers);
|
|
} catch (error) {
|
|
console.error("Error in DistributedCollector outputs extension:", error);
|
|
}
|
|
},
|
|
});
|