UNPKG

comfyui-node

Version:
860 lines 39.1 kB
import { FailedCacheError, WentMissingError, EnqueueFailedError, DisconnectedError, CustomEventError, ExecutionFailedError, ExecutionInterruptedError, MissingNodeError } from "./types/error.js"; import { buildEnqueueFailedError } from "./utils/response-error.js"; const DISCONNECT_FAILURE_GRACE_MS = 5000; const CALL_WRAPPER_DEBUG = process.env.WORKFLOW_POOL_DEBUG === "1"; /** * Represents a wrapper class for making API calls using the ComfyApi client. * Provides methods for setting callback functions and executing the job. */ export class CallWrapper { client; prompt; started = false; isCompletingSuccessfully = false; promptId; output = {}; onPreviewFn; onPreviewMetaFn; onPendingFn; onStartFn; onOutputFn; onFinishedFn; onFailedFn; onProgressFn; jobResolveFn; jobDoneResolved = false; pendingCompletion = null; cancellationRequested = false; promptLoadTrigger = null; disconnectRecoveryActive = false; disconnectFailureTimer = null; onReconnectHandlerOffFn; onReconnectFailedHandlerOffFn; onDisconnectedHandlerOffFn; checkExecutingOffFn; checkExecutedOffFn; progressHandlerOffFn; previewHandlerOffFn; executionHandlerOffFn; errorHandlerOffFn; executionEndSuccessOffFn; statusHandlerOffFn; interruptionHandlerOffFn; /** * Constructs a new CallWrapper instance. * @param client The ComfyApi client. * @param workflow The workflow object. */ constructor(client, workflow) { this.client = client; this.prompt = workflow; return this; } /** * Set the callback function to be called when a preview event occurs. * * @param fn - The callback function to be called. It receives a Blob object representing the event and an optional promptId string. * @returns The current instance of the CallWrapper. */ onPreview(fn) { this.onPreviewFn = fn; return this; } /** * Set the callback function to be called when a preview-with-metadata event occurs. */ onPreviewMeta(fn) { this.onPreviewMetaFn = fn; return this; } /** * Set a callback function to be executed when the job is queued. * @param {Function} fn - The callback function to be executed. * @returns The current instance of the CallWrapper. */ onPending(fn) { this.onPendingFn = fn; return this; } /** * Set the callback function to be executed when the job start. * * @param fn - The callback function to be executed. It can optionally receive a `promptId` parameter. * @returns The current instance of the CallWrapper. */ onStart(fn) { this.onStartFn = fn; return this; } /** * Sets the callback function to handle the output node when the workflow is executing. This is * useful when you want to handle the output of each nodes as they are being processed. * * All the nodes defined in the `mapOutputKeys` will be passed to this function when node is executed. * * @param fn - The callback function to handle the output. * @returns The current instance of the class. */ onOutput(fn) { this.onOutputFn = fn; return this; } /** * Set the callback function to be executed when the asynchronous operation is finished. * * @param fn - The callback function to be executed. It receives the data returned by the operation * and an optional promptId parameter. * @returns The current instance of the CallWrapper. */ onFinished(fn) { this.onFinishedFn = fn; return this; } /** * Set the callback function to be executed when the API call fails. * * @param fn - The callback function to be executed when the API call fails. * It receives an `Error` object as the first parameter and an optional `promptId` as the second parameter. * @returns The current instance of the CallWrapper. */ onFailed(fn) { this.onFailedFn = fn; return this; } /** * Set a callback function to be called when progress information is available. * @param fn - The callback function to be called with the progress information. * @returns The current instance of the CallWrapper. */ onProgress(fn) { this.onProgressFn = fn; return this; } /** * Run the call wrapper and returns the output of the executed job. * If the job is already cached, it returns the cached output. * If the job is not cached, it executes the job and returns the output. * * @returns A promise that resolves to the output of the executed job, * or `undefined` if the job is not found, * or `false` if the job execution fails. */ async run() { /** * Start the job execution. */ this.emitLog("CallWrapper.run", "enqueue start"); this.pendingCompletion = null; this.jobResolveFn = undefined; this.jobDoneResolved = false; this.cancellationRequested = false; this.promptLoadTrigger = null; const job = await this.enqueueJob(); if (!job) { // enqueueJob already invoked onFailed with a rich error instance; just abort. this.emitLog("CallWrapper.run", "enqueue failed -> abort"); return false; } const promptLoadCached = new Promise((resolve) => { this.promptLoadTrigger = (value) => { if (this.promptLoadTrigger) { this.promptLoadTrigger = null; } resolve(value); }; }); const jobDonePromise = new Promise((resolve) => { this.jobDoneResolved = false; this.jobResolveFn = (value) => { resolve(value); }; if (this.pendingCompletion !== null) { const pending = this.pendingCompletion; this.pendingCompletion = null; this.jobResolveFn?.(pending); } }); /** * Declare the function to check if the job is executing. */ const checkExecutingFn = (event) => { // Defensive null check for event detail if (!event.detail) { this.emitLog("CallWrapper.run", "executing event received with no detail"); return; } if (event.detail.prompt_id === job.prompt_id) { this.emitLog("CallWrapper.run", "executing observed", { node: event.detail.node }); this.resolvePromptLoad(false); } }; /** * Declare the function to check if the job is cached. */ const checkExecutionCachedFn = (event) => { // Defensive null checks for event detail if (!event.detail || !event.detail.nodes) { this.emitLog("CallWrapper.run", "execution_cached event received with invalid structure"); return; } const outputNodes = Object.values(this.prompt.mapOutputKeys).filter((n) => !!n); if (event.detail.nodes.length > 0 && event.detail.prompt_id === job.prompt_id) { /** * Cached is true if all output nodes are included in the cached nodes. */ const cached = outputNodes.every((node) => event.detail.nodes.includes(node)); this.emitLog("CallWrapper.run", "execution_cached observed", { cached, nodes: event.detail.nodes, expected: outputNodes }); this.resolvePromptLoad(cached); } }; /** * Listen to the executing event. */ this.checkExecutingOffFn = this.client.on("executing", checkExecutingFn); this.checkExecutedOffFn = this.client.on("execution_cached", checkExecutionCachedFn); // race condition handling let wentMissing = false; let cachedOutputDone = false; let cachedOutputPromise = Promise.resolve(null); const statusHandler = async () => { const queue = await this.client.getQueue(); const queueItems = [...queue.queue_pending, ...queue.queue_running]; this.emitLog("CallWrapper.status", "queue snapshot", { running: queue.queue_running.length, pending: queue.queue_pending.length }); for (const queueItem of queueItems) { if (queueItem[1] === job.prompt_id) { return; } } await cachedOutputPromise; if (cachedOutputDone) { this.emitLog("CallWrapper.status", "cached output already handled"); return; } if (this.cancellationRequested) { this.emitLog("CallWrapper.status", "job missing after cancellation", { prompt_id: job.prompt_id }); this.resolvePromptLoad(false); this.resolveJob(false); this.cleanupListeners("status handler cancellation"); return; } wentMissing = true; const output = await this.handleCachedOutput(job.prompt_id); if (output) { cachedOutputDone = true; this.emitLog("CallWrapper.status", "output from history after missing", { prompt_id: job.prompt_id }); this.resolvePromptLoad(false); this.resolveJob(output); this.cleanupListeners("status handler resolved from history"); return; } if (this.disconnectRecoveryActive) { this.emitLog("CallWrapper.status", "job missing but disconnect recovery active -> waiting", { prompt_id: job.prompt_id }); this.resolvePromptLoad(false); void this.attemptHistoryCompletion("status_missing"); return; } cachedOutputDone = true; this.emitLog("CallWrapper.status", "job missing without cached output", { prompt_id: job.prompt_id }); this.resolvePromptLoad(false); this.resolveJob(false); this.cleanupListeners("status handler missing"); this.emitFailure(new WentMissingError("The job went missing!"), job.prompt_id); }; this.statusHandlerOffFn = this.client.on("status", statusHandler); // Attach execution listeners immediately so fast jobs cannot finish before we subscribe this.handleJobExecution(job.prompt_id); await promptLoadCached; if (wentMissing) { return jobDonePromise; } cachedOutputPromise = this.handleCachedOutput(job.prompt_id); const output = await cachedOutputPromise; if (output) { cachedOutputDone = true; this.cleanupListeners("no cached output values returned"); this.resolveJob(output); return output; } if (output === false) { cachedOutputDone = true; this.cleanupListeners("cached output ready before execution listeners"); this.emitFailure(new FailedCacheError("Failed to get cached output"), this.promptId); this.resolveJob(false); return false; } this.emitLog("CallWrapper.run", "no cached output -> proceed with execution listeners"); return jobDonePromise; } async bypassWorkflowNodes(workflow) { const nodeDefs = {}; // cache node definitions for (const nodeId of this.prompt.bypassNodes) { if (!workflow[nodeId]) { throw new MissingNodeError(`Node ${nodeId.toString()} is missing from the workflow!`); } const classType = workflow[nodeId].class_type; // Directly use feature namespace to avoid deprecated internal call const def = nodeDefs[classType] || (await this.client.ext.node.getNodeDefs(classType))?.[classType]; if (!def) { throw new MissingNodeError(`Node type ${workflow[nodeId].class_type} is missing from server!`); } nodeDefs[classType] = def; const connections = new Map(); const connectedInputs = []; // connect output nodes to matching input nodes for (const [outputIdx, outputType] of Array.from(def.output.entries())) { for (const [inputName, inputValue] of Object.entries(workflow[nodeId].inputs)) { if (connectedInputs.includes(inputName)) { continue; } if (def.input.required[inputName]?.[0] === outputType) { connections.set(outputIdx, inputValue); connectedInputs.push(inputName); break; } if (def.input.optional?.[inputName]?.[0] === outputType) { connections.set(outputIdx, inputValue); connectedInputs.push(inputName); break; } } } // search and replace all nodes' inputs referencing this node based on matching output type, or remove reference // if no matching output type was found for (const [conNodeId, conNode] of Object.entries(workflow)) { for (const [conInputName, conInputValue] of Object.entries(conNode.inputs)) { if (!Array.isArray(conInputValue) || conInputValue[0] !== nodeId) { continue; } if (connections.has(conInputValue[1])) { workflow[conNodeId].inputs[conInputName] = connections.get(conInputValue[1]); } else { delete workflow[conNodeId].inputs[conInputName]; } } } delete workflow[nodeId]; } return workflow; } async enqueueJob() { let workflow = structuredClone(this.prompt.workflow); if (this.prompt.bypassNodes.length > 0) { try { workflow = await this.bypassWorkflowNodes(workflow); } catch (e) { if (e instanceof Response) { this.emitFailure(new MissingNodeError("Failed to get workflow node definitions", { cause: await e.json() }), this.promptId); } else { this.emitFailure(new MissingNodeError("There was a missing node in the workflow bypass.", { cause: e }), this.promptId); } return null; } } let job; try { job = await this.client.ext.queue.appendPrompt(workflow); } catch (e) { try { if (e instanceof EnqueueFailedError) { this.emitFailure(e, this.promptId); } else if (e instanceof Response) { const err = await buildEnqueueFailedError(e); this.emitFailure(err, this.promptId); } else if (e && typeof e === "object" && "response" in e && e.response instanceof Response) { const err = await buildEnqueueFailedError(e.response); this.emitFailure(err, this.promptId); } else { this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: e, reason: e?.message }), this.promptId); } } catch (inner) { this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: inner }), this.promptId); } job = null; } if (!job) { return; } this.promptId = job.prompt_id; console.log(`[CallWrapper] Enqueued with promptId=${this.promptId?.substring(0, 8)}...`); console.log(`[CallWrapper] Full job object:`, JSON.stringify({ promptId: job.prompt_id }, null, 2)); this.emitLog("CallWrapper.enqueueJob", "queued", { prompt_id: this.promptId }); this.onPendingFn?.(this.promptId); this.onDisconnectedHandlerOffFn = this.client.on("disconnected", () => { if (this.isCompletingSuccessfully) { this.emitLog("CallWrapper.enqueueJob", "disconnected during success completion -> ignored"); return; } this.emitLog("CallWrapper.enqueueJob", "socket disconnected -> enter recovery", { promptId: this.promptId }); this.startDisconnectRecovery(); }); this.onReconnectHandlerOffFn = this.client.on("reconnected", () => { if (!this.disconnectRecoveryActive) { return; } this.emitLog("CallWrapper.enqueueJob", "socket reconnected", { promptId: this.promptId }); this.stopDisconnectRecovery(); void this.attemptHistoryCompletion("reconnected"); }); this.onReconnectFailedHandlerOffFn = this.client.on("reconnection_failed", () => { if (!this.disconnectRecoveryActive) { return; } this.emitLog("CallWrapper.enqueueJob", "reconnection failed", { promptId: this.promptId }); this.failDisconnected("reconnection_failed"); }); return job; } resolvePromptLoad(value) { const trigger = this.promptLoadTrigger; if (!trigger) { return; } this.promptLoadTrigger = null; try { trigger(value); } catch (error) { this.emitLog("CallWrapper.resolvePromptLoad", "prompt load trigger threw", { error: error instanceof Error ? error.message : String(error), promptId: this.promptId }); } } resolveJob(value) { if (CALL_WRAPPER_DEBUG) { console.log("[debug] resolveJob", this.promptId, value, Boolean(this.jobResolveFn), this.jobDoneResolved); } if (this.jobResolveFn && !this.jobDoneResolved) { this.jobDoneResolved = true; this.jobResolveFn(value); if (CALL_WRAPPER_DEBUG) { console.log("[debug] jobResolveFn invoked", this.promptId); } } else if (!this.jobResolveFn) { this.pendingCompletion = value; } } emitFailure(error, promptId) { const fn = this.onFailedFn; if (!fn) { return; } const targetPromptId = promptId ?? this.promptId; try { if (CALL_WRAPPER_DEBUG) { console.log("[debug] emitFailure start", error.name); } fn(error, targetPromptId); if (CALL_WRAPPER_DEBUG) { console.log("[debug] emitFailure end", error.name); } } catch (callbackError) { this.emitLog("CallWrapper.emitFailure", "onFailed callback threw", { prompt_id: targetPromptId, error: callbackError instanceof Error ? callbackError.message : String(callbackError) }); } } cancel(reason = "cancelled") { if (this.cancellationRequested) { this.emitLog("CallWrapper.cancel", "cancel already requested", { promptId: this.promptId, reason }); return; } this.cancellationRequested = true; this.emitLog("CallWrapper.cancel", "cancel requested", { promptId: this.promptId, reason }); this.resolvePromptLoad(false); this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: { reason } }), this.promptId); this.cleanupListeners("cancel requested"); this.resolveJob(false); } startDisconnectRecovery() { if (this.disconnectRecoveryActive || this.cancellationRequested) { return; } this.disconnectRecoveryActive = true; if (this.disconnectFailureTimer) { clearTimeout(this.disconnectFailureTimer); } this.disconnectFailureTimer = setTimeout(() => this.failDisconnected("timeout"), DISCONNECT_FAILURE_GRACE_MS); void this.attemptHistoryCompletion("disconnect_start"); } stopDisconnectRecovery() { if (!this.disconnectRecoveryActive) { return; } this.disconnectRecoveryActive = false; if (this.disconnectFailureTimer) { clearTimeout(this.disconnectFailureTimer); this.disconnectFailureTimer = null; } } async attemptHistoryCompletion(reason) { if (!this.promptId || this.cancellationRequested) { return false; } try { const output = await this.handleCachedOutput(this.promptId); if (output && output !== false) { this.emitLog("CallWrapper.historyRecovery", "completed from history", { reason, promptId: this.promptId }); this.stopDisconnectRecovery(); this.isCompletingSuccessfully = true; this.resolvePromptLoad(false); this.resolveJob(output); this.cleanupListeners(`history recovery (${reason})`); return true; } } catch (error) { this.emitLog("CallWrapper.historyRecovery", "history fetch failed", { reason, error: String(error) }); } return false; } failDisconnected(reason) { if (!this.disconnectRecoveryActive || this.isCompletingSuccessfully) { return; } this.stopDisconnectRecovery(); this.emitLog("CallWrapper.enqueueJob", "disconnect recovery failed", { reason, promptId: this.promptId }); this.resolvePromptLoad(false); this.resolveJob(false); this.cleanupListeners("disconnect failure"); this.emitFailure(new DisconnectedError("Disconnected"), this.promptId); } async handleCachedOutput(promptId) { const hisData = await this.client.ext.history.getHistory(promptId); this.emitLog("CallWrapper.handleCachedOutput", "history fetched", { promptId, status: hisData?.status?.status_str, completed: hisData?.status?.completed, outputKeys: hisData?.outputs ? Object.keys(hisData.outputs) : [], hasOutputs: !!(hisData && hisData.outputs && Object.keys(hisData.outputs).length > 0) }); // Only return outputs if execution is actually completed if (hisData && hisData.status?.completed && hisData.outputs) { const output = this.mapOutput(hisData.outputs); const hasDefinedValue = Object.entries(output).some(([key, value]) => { if (key === "_raw") { return value !== undefined && value !== null && Object.keys(value).length > 0; } return value !== undefined; }); if (hasDefinedValue) { this.emitLog("CallWrapper.handleCachedOutput", "returning completed outputs"); this.onFinishedFn?.(output, this.promptId); return output; } else { this.emitLog("CallWrapper.handleCachedOutput", "cached output missing defined values", { promptId, outputKeys: Object.keys(hisData.outputs ?? {}), mappedKeys: this.prompt.mapOutputKeys }); return false; } } if (hisData && hisData.status?.completed && !hisData.outputs) { this.emitLog("CallWrapper.handleCachedOutput", "history completed without outputs", { promptId }); return false; } if (hisData && !hisData.status?.completed) { this.emitLog("CallWrapper.handleCachedOutput", "history not completed yet"); } if (!hisData) { this.emitLog("CallWrapper.handleCachedOutput", "history entry not available"); } return null; } mapOutput(outputNodes) { const outputMapped = this.prompt.mapOutputKeys; const output = {}; for (const key in outputMapped) { const node = outputMapped[key]; if (node) { output[key] = outputNodes[node]; } else { if (!output._raw) { output._raw = {}; } output._raw[key] = outputNodes[key]; } } return output; } handleJobExecution(promptId) { if (this.executionHandlerOffFn) { return; } const reverseOutputMapped = this.reverseMapOutputKeys(); const mapOutputKeys = this.prompt.mapOutputKeys; console.log(`[CallWrapper] handleJobExecution for ${promptId.substring(0, 8)}... - mapOutputKeys:`, mapOutputKeys, "reverseOutputMapped:", reverseOutputMapped); this.progressHandlerOffFn = this.client.on("progress", (ev) => this.handleProgress(ev, promptId)); this.previewHandlerOffFn = this.client.on("b_preview", (ev) => { // Note: b_preview events don't include prompt_id. They're scoped per connection. // If multiple jobs use the same connection, they will all receive preview events. // This is a limitation of the ComfyUI protocol - previews are not separated by prompt_id. this.onPreviewFn?.(ev.detail, this.promptId); }); // Also forward preview with metadata if available const offPreviewMeta = this.client.on("b_preview_meta", (ev) => { // Validate prompt_id from metadata if available to prevent cross-user preview leakage const metadata = ev.detail.metadata; const metaPromptId = metadata?.prompt_id; if (metaPromptId && metaPromptId !== promptId) { console.log(`[CallWrapper] Ignoring b_preview_meta for wrong prompt. Expected ${promptId.substring(0, 8)}..., got ${metaPromptId.substring(0, 8)}...`); return; } this.onPreviewMetaFn?.(ev.detail, this.promptId); }); const prevCleanup = this.previewHandlerOffFn; this.previewHandlerOffFn = () => { prevCleanup?.(); offPreviewMeta?.(); }; const totalOutput = Object.keys(reverseOutputMapped).length; let remainingOutput = totalOutput; console.log(`[CallWrapper] totalOutput=${totalOutput}, remainingOutput=${remainingOutput}`); const executionHandler = (ev) => { console.log(`[CallWrapper.executionHandler] received executed event for promptId=${ev.detail.prompt_id?.substring(0, 8)}..., node=${ev.detail.node}, waitingFor=${promptId.substring(0, 8)}...`); const eventPromptId = ev.detail.prompt_id; const isCorrectPrompt = eventPromptId === promptId; // STRICT: Only accept events where prompt_id matches our expected promptId if (!isCorrectPrompt) { console.log(`[CallWrapper.executionHandler] REJECTED - prompt_id mismatch (expected ${promptId.substring(0, 8)}..., got ${eventPromptId?.substring(0, 8)}...)`); return; } const outputKey = reverseOutputMapped[ev.detail.node]; console.log(`[CallWrapper] executionHandler - promptId: ${promptId.substring(0, 8)}... (event says: ${ev.detail.prompt_id?.substring(0, 8)}...), node: ${ev.detail.node}, outputKey: ${outputKey}, output:`, JSON.stringify(ev.detail.output)); if (outputKey) { this.output[outputKey] = ev.detail.output; this.onOutputFn?.(outputKey, ev.detail.output, this.promptId); remainingOutput--; } else { this.output._raw = this.output._raw || {}; this.output._raw[ev.detail.node] = ev.detail.output; this.onOutputFn?.(ev.detail.node, ev.detail.output, this.promptId); } console.log(`[CallWrapper] afterProcessing - remainingAfter: ${remainingOutput}, willTriggerCompletion: ${remainingOutput === 0}`); if (remainingOutput === 0) { console.log(`[CallWrapper] all outputs collected for ${promptId.substring(0, 8)}...`); // Mark as successfully completing BEFORE cleanup to prevent race condition with disconnection handler this.isCompletingSuccessfully = true; this.cleanupListeners("all outputs collected"); this.onFinishedFn?.(this.output, this.promptId); this.resolveJob(this.output); } }; const executedEnd = async () => { console.log(`[CallWrapper] execution_success fired for ${promptId.substring(0, 8)}..., remainingOutput=${remainingOutput}, totalOutput=${totalOutput}`); // If we've already marked this as successfully completing, don't fail it again if (this.isCompletingSuccessfully) { console.log(`[CallWrapper] Already marked as successfully completing, ignoring this execution_success`); return; } if (remainingOutput === 0) { console.log(`[CallWrapper] all outputs already collected, nothing to do`); return; } // Wait briefly for outputs that might be arriving due to prompt ID mismatch await new Promise((resolve) => setTimeout(resolve, 100)); console.log(`[CallWrapper] After wait - remainingOutput=${remainingOutput}, this.output keys:`, Object.keys(this.output)); // Check if outputs arrived while we were waiting if (remainingOutput === 0) { console.log(`[CallWrapper] Outputs arrived during wait - marking as complete`); this.isCompletingSuccessfully = true; this.cleanupListeners("executedEnd - outputs complete after wait"); this.onFinishedFn?.(this.output, this.promptId); this.resolveJob(this.output); return; } // Check if we have collected all outputs (even if prompt ID mismatch) const hasAllOutputs = Object.keys(reverseOutputMapped).every((nodeId) => this.output[reverseOutputMapped[nodeId]] !== undefined); if (hasAllOutputs) { console.log(`[CallWrapper] Have all required outputs despite promptId mismatch - marking as complete`); this.isCompletingSuccessfully = true; this.cleanupListeners("executedEnd - outputs complete despite promptId mismatch"); this.onFinishedFn?.(this.output, this.promptId); this.resolveJob(this.output); return; } // Try to fetch from history with retry logic let hisData = null; for (let retries = 0; retries < 5; retries++) { hisData = await this.client.ext.history.getHistory(promptId); console.log(`[CallWrapper] History query result for ${promptId.substring(0, 8)}... (attempt ${retries + 1}) - status:`, hisData?.status, "outputs:", Object.keys(hisData?.outputs ?? {}).length); if (hisData?.status?.completed && hisData.outputs) { console.log(`[CallWrapper] Found completed job in history with outputs - attempting to populate from history`); break; } if (retries < 4) { console.log(`[CallWrapper] History not ready yet, waiting 100ms before retry...`); await new Promise((resolve) => setTimeout(resolve, 100)); } } if (hisData?.status?.completed && hisData.outputs) { // Try to extract outputs from history data let populatedCount = 0; for (const [nodeIdStr, nodeOutput] of Object.entries(hisData.outputs)) { const nodeId = parseInt(nodeIdStr, 10); const outputKey = reverseOutputMapped[nodeId]; if (outputKey && nodeOutput) { // nodeOutput is typically { images: [...] } or similar - take the first property const outputValue = Array.isArray(nodeOutput) ? nodeOutput[0] : Object.values(nodeOutput)[0]; if (outputValue !== undefined) { this.output[outputKey] = outputValue; this.onOutputFn?.(outputKey, outputValue, this.promptId); populatedCount++; remainingOutput--; console.log(`[CallWrapper] Populated ${outputKey} from history`); } } } if (remainingOutput === 0) { console.log(`[CallWrapper] Successfully populated all outputs from history for ${promptId.substring(0, 8)}...`); this.isCompletingSuccessfully = true; this.cleanupListeners("executedEnd - populated from history"); this.onFinishedFn?.(this.output, this.promptId); this.resolveJob(this.output); return; } if (populatedCount > 0) { console.log(`[CallWrapper] Populated ${populatedCount} outputs from history (remainingOutput=${remainingOutput})`); if (remainingOutput === 0) { this.isCompletingSuccessfully = true; this.cleanupListeners("executedEnd - all outputs from history"); this.onFinishedFn?.(this.output, this.promptId); this.resolveJob(this.output); return; } } } console.log(`[CallWrapper] execution failed due to missing outputs - remainingOutput=${remainingOutput}, totalOutput=${totalOutput}`); this.emitFailure(new ExecutionFailedError("Execution failed"), this.promptId); this.resolvePromptLoad(false); this.cleanupListeners("executedEnd missing outputs"); this.resolveJob(false); }; this.executionEndSuccessOffFn = this.client.on("execution_success", executedEnd); this.executionHandlerOffFn = this.client.on("executed", executionHandler); console.log(`[CallWrapper] Registered listeners for ${promptId.substring(0, 8)}... - executionHandler and executedEnd`); this.errorHandlerOffFn = this.client.on("execution_error", (ev) => this.handleError(ev, promptId)); this.interruptionHandlerOffFn = this.client.on("execution_interrupted", (ev) => { if (ev.detail.prompt_id !== promptId) return; this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: ev.detail }), ev.detail.prompt_id); this.resolvePromptLoad(false); this.cleanupListeners("execution interrupted"); this.resolveJob(false); }); } reverseMapOutputKeys() { const outputMapped = this.prompt.mapOutputKeys; return Object.entries(outputMapped).reduce((acc, [k, v]) => { if (v) acc[v] = k; return acc; }, {}); } handleProgress(ev, promptId) { if (ev.detail.prompt_id === promptId && !this.started) { this.started = true; this.onStartFn?.(this.promptId); } this.onProgressFn?.(ev.detail, this.promptId); } handleError(ev, promptId) { if (ev.detail.prompt_id !== promptId) return; this.emitLog("CallWrapper.handleError", ev.detail.exception_type, { prompt_id: ev.detail.prompt_id, node_id: ev.detail?.node_id }); this.emitFailure(new CustomEventError(ev.detail.exception_type, { cause: ev.detail }), ev.detail.prompt_id); if (CALL_WRAPPER_DEBUG) { console.log("[debug] handleError after emitFailure"); } this.resolvePromptLoad(false); if (CALL_WRAPPER_DEBUG) { console.log("[debug] handleError before cleanup"); } this.cleanupListeners("execution_error received"); if (CALL_WRAPPER_DEBUG) { console.log("[debug] handleError after cleanup"); } this.resolveJob(false); } emitLog(fnName, message, data) { const detail = { fnName, message, data }; const customEvent = new CustomEvent("log", { detail }); const clientAny = this.client; if (typeof clientAny.emit === "function") { clientAny.emit("log", customEvent); return; } clientAny.dispatchEvent?.(customEvent); } cleanupListeners(reason) { const debugPayload = { reason, promptId: this.promptId }; this.emitLog("CallWrapper.cleanupListeners", "removing listeners", debugPayload); this.resolvePromptLoad(false); this.stopDisconnectRecovery(); this.onReconnectHandlerOffFn?.(); this.onReconnectHandlerOffFn = undefined; this.onReconnectFailedHandlerOffFn?.(); this.onReconnectFailedHandlerOffFn = undefined; this.disconnectFailureTimer = null; this.onDisconnectedHandlerOffFn?.(); this.onDisconnectedHandlerOffFn = undefined; this.checkExecutingOffFn?.(); this.checkExecutingOffFn = undefined; this.checkExecutedOffFn?.(); this.checkExecutedOffFn = undefined; this.progressHandlerOffFn?.(); this.progressHandlerOffFn = undefined; this.previewHandlerOffFn?.(); this.previewHandlerOffFn = undefined; this.executionHandlerOffFn?.(); this.executionHandlerOffFn = undefined; this.errorHandlerOffFn?.(); this.errorHandlerOffFn = undefined; this.executionEndSuccessOffFn?.(); this.executionEndSuccessOffFn = undefined; this.interruptionHandlerOffFn?.(); this.interruptionHandlerOffFn = undefined; this.statusHandlerOffFn?.(); this.statusHandlerOffFn = undefined; } } //# sourceMappingURL=call-wrapper.js.map