Files
chrmc40 8e3dceae99 ComfyUI-PromptChain
A prompt-authoring and image-iteration suite for ComfyUI.
2026-06-17 19:35:25 -07:00

145 lines
4.8 KiB
JavaScript

// History — client-side image history cache with subscribe pattern.
// Tracks generated images per workflow, backed by the /promptchain API.
import { api } from "../../../scripts/api.js";
const _cache = new Map(); // workflow_id → ImageEntry[]
const _subscribers = new Set();
// Prompt ids whose outputs are recorded by a dedicated tracker (background
// upscales). main.js's executed-event recorder skips these — its node-id maps
// were built from whatever graph was ACTIVE at execution_start, which is the
// user's workflow, not the background prompt's, so small-int node-id
// collisions would mis-record the output under the wrong workflow.
export const externallyTrackedPrompts = new Set();
// A background run's prompt id isn't known until its queue POST resolves, but
// the server can emit execution_start over the websocket BEFORE that lands.
// Without a guard the main-graph progress handler adopts the run and shows a
// node-panel spinner that never clears (the run's executed event is later
// skipped as external). A runner ARMS this window before queuing; the first
// unknown prompt seen by execution_start while it's open is claimed as external.
let _armedExternalQueues = 0;
export function armExternalQueue() { _armedExternalQueues++; }
export function disarmExternalQueue() { if (_armedExternalQueues > 0) _armedExternalQueues--; }
export function isExternalPrompt(promptId) {
if (!promptId) return false;
if (externallyTrackedPrompts.has(promptId)) return true;
if (_armedExternalQueues > 0) {
externallyTrackedPrompts.add(promptId);
_armedExternalQueues--;
return true;
}
return false;
}
function notify(workflowId) {
for (const fn of _subscribers) {
try { fn(workflowId); } catch {}
}
}
export function subscribe(fn) {
_subscribers.add(fn);
return () => _subscribers.delete(fn);
}
export async function recordGeneration(workflowId, filename, subfolder, sourceType, metadata) {
if (!workflowId || !filename) return null;
try {
const resp = await api.fetchApi(`/promptchain/generation/${encodeURIComponent(workflowId)}`, {
method: "POST",
body: JSON.stringify({
filename,
subfolder: subfolder || "",
source_type: sourceType || "output",
...metadata,
}),
headers: { "Content-Type": "application/json" },
});
if (!resp.ok) return null;
const entry = await resp.json();
if (!_cache.has(workflowId)) _cache.set(workflowId, []);
const list = _cache.get(workflowId);
if (!list.some(e => e.hash === entry.hash)) list.unshift(entry);
notify(workflowId);
refreshEntryWorkflows(entry, workflowId);
return entry;
} catch {
return null;
}
}
// A derived image joins its parent's workflows server-side (record_image
// returns them as entry.workflows) — drop those caches and notify so any
// open gallery reloads with the family reordered. The window event carries
// the full entry so node preview panels can show externally tracked renders
// (viewer edit/inpaint/upscale), whose `executed` events main.js skips.
export function refreshEntryWorkflows(entry, recordedWorkflowId = null) {
for (const wid of entry?.workflows || []) {
if (wid === recordedWorkflowId) continue;
_cache.delete(wid);
notify(wid);
}
if (entry?.workflows?.length) {
window.dispatchEvent(new CustomEvent("promptchain:generation-recorded", {
detail: { entry, recordedWorkflowId },
}));
}
}
export async function fetchWorkflowImages(workflowId) {
if (!workflowId) return [];
if (_cache.has(workflowId)) return _cache.get(workflowId);
try {
const resp = await api.fetchApi(`/promptchain/workflow/${encodeURIComponent(workflowId)}?limit=10000`);
if (!resp.ok) return [];
const data = await resp.json();
_cache.set(workflowId, data.images || []);
return _cache.get(workflowId);
} catch {
return [];
}
}
export function getCachedImages(workflowId) {
return _cache.get(workflowId) || [];
}
export function invalidateCache(workflowId) {
_cache.delete(workflowId);
}
export async function fetchWorkflowCount(workflowId) {
if (!workflowId) return 0;
if (_cache.has(workflowId)) return _cache.get(workflowId).length;
try {
const resp = await api.fetchApi(`/promptchain/count/${encodeURIComponent(workflowId)}`);
if (!resp.ok) return 0;
const data = await resp.json();
return data.count || 0;
} catch {
return 0;
}
}
export function thumbnailUrl(hash) {
return api.apiURL(`/promptchain/thumb/${hash}`);
}
export async function checkOrphans(hashes) {
if (!hashes?.length) return [];
try {
const resp = await api.fetchApi("/promptchain/check-orphans", {
method: "POST",
body: JSON.stringify({ hashes }),
headers: { "Content-Type": "application/json" },
});
if (!resp.ok) return [];
const data = await resp.json();
return data.orphaned || [];
} catch {
return [];
}
}