comfyui-node 1.6.4 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,857 +1,860 @@
1
- import { FailedCacheError, WentMissingError, EnqueueFailedError, DisconnectedError, CustomEventError, ExecutionFailedError, ExecutionInterruptedError, MissingNodeError } from "./types/error.js";
2
- import { buildEnqueueFailedError } from "./utils/response-error.js";
3
- const DISCONNECT_FAILURE_GRACE_MS = 5000;
4
- const CALL_WRAPPER_DEBUG = process.env.WORKFLOW_POOL_DEBUG === "1";
5
- /**
6
- * Represents a wrapper class for making API calls using the ComfyApi client.
7
- * Provides methods for setting callback functions and executing the job.
8
- */
9
- export class CallWrapper {
10
- client;
11
- prompt;
12
- started = false;
13
- isCompletingSuccessfully = false;
14
- promptId;
15
- output = {};
16
- onPreviewFn;
17
- onPreviewMetaFn;
18
- onPendingFn;
19
- onStartFn;
20
- onOutputFn;
21
- onFinishedFn;
22
- onFailedFn;
23
- onProgressFn;
24
- jobResolveFn;
25
- jobDoneResolved = false;
26
- pendingCompletion = null;
27
- cancellationRequested = false;
28
- promptLoadTrigger = null;
29
- disconnectRecoveryActive = false;
30
- disconnectFailureTimer = null;
31
- onReconnectHandlerOffFn;
32
- onReconnectFailedHandlerOffFn;
33
- onDisconnectedHandlerOffFn;
34
- checkExecutingOffFn;
35
- checkExecutedOffFn;
36
- progressHandlerOffFn;
37
- previewHandlerOffFn;
38
- executionHandlerOffFn;
39
- errorHandlerOffFn;
40
- executionEndSuccessOffFn;
41
- statusHandlerOffFn;
42
- interruptionHandlerOffFn;
43
- /**
44
- * Constructs a new CallWrapper instance.
45
- * @param client The ComfyApi client.
46
- * @param workflow The workflow object.
47
- */
48
- constructor(client, workflow) {
49
- this.client = client;
50
- this.prompt = workflow;
51
- return this;
52
- }
53
- /**
54
- * Set the callback function to be called when a preview event occurs.
55
- *
56
- * @param fn - The callback function to be called. It receives a Blob object representing the event and an optional promptId string.
57
- * @returns The current instance of the CallWrapper.
58
- */
59
- onPreview(fn) {
60
- this.onPreviewFn = fn;
61
- return this;
62
- }
63
- /**
64
- * Set the callback function to be called when a preview-with-metadata event occurs.
65
- */
66
- onPreviewMeta(fn) {
67
- this.onPreviewMetaFn = fn;
68
- return this;
69
- }
70
- /**
71
- * Set a callback function to be executed when the job is queued.
72
- * @param {Function} fn - The callback function to be executed.
73
- * @returns The current instance of the CallWrapper.
74
- */
75
- onPending(fn) {
76
- this.onPendingFn = fn;
77
- return this;
78
- }
79
- /**
80
- * Set the callback function to be executed when the job start.
81
- *
82
- * @param fn - The callback function to be executed. It can optionally receive a `promptId` parameter.
83
- * @returns The current instance of the CallWrapper.
84
- */
85
- onStart(fn) {
86
- this.onStartFn = fn;
87
- return this;
88
- }
89
- /**
90
- * Sets the callback function to handle the output node when the workflow is executing. This is
91
- * useful when you want to handle the output of each nodes as they are being processed.
92
- *
93
- * All the nodes defined in the `mapOutputKeys` will be passed to this function when node is executed.
94
- *
95
- * @param fn - The callback function to handle the output.
96
- * @returns The current instance of the class.
97
- */
98
- onOutput(fn) {
99
- this.onOutputFn = fn;
100
- return this;
101
- }
102
- /**
103
- * Set the callback function to be executed when the asynchronous operation is finished.
104
- *
105
- * @param fn - The callback function to be executed. It receives the data returned by the operation
106
- * and an optional promptId parameter.
107
- * @returns The current instance of the CallWrapper.
108
- */
109
- onFinished(fn) {
110
- this.onFinishedFn = fn;
111
- return this;
112
- }
113
- /**
114
- * Set the callback function to be executed when the API call fails.
115
- *
116
- * @param fn - The callback function to be executed when the API call fails.
117
- * It receives an `Error` object as the first parameter and an optional `promptId` as the second parameter.
118
- * @returns The current instance of the CallWrapper.
119
- */
120
- onFailed(fn) {
121
- this.onFailedFn = fn;
122
- return this;
123
- }
124
- /**
125
- * Set a callback function to be called when progress information is available.
126
- * @param fn - The callback function to be called with the progress information.
127
- * @returns The current instance of the CallWrapper.
128
- */
129
- onProgress(fn) {
130
- this.onProgressFn = fn;
131
- return this;
132
- }
133
- /**
134
- * Run the call wrapper and returns the output of the executed job.
135
- * If the job is already cached, it returns the cached output.
136
- * If the job is not cached, it executes the job and returns the output.
137
- *
138
- * @returns A promise that resolves to the output of the executed job,
139
- * or `undefined` if the job is not found,
140
- * or `false` if the job execution fails.
141
- */
142
- async run() {
143
- /**
144
- * Start the job execution.
145
- */
146
- this.emitLog("CallWrapper.run", "enqueue start");
147
- this.pendingCompletion = null;
148
- this.jobResolveFn = undefined;
149
- this.jobDoneResolved = false;
150
- this.cancellationRequested = false;
151
- this.promptLoadTrigger = null;
152
- const job = await this.enqueueJob();
153
- if (!job) {
154
- // enqueueJob already invoked onFailed with a rich error instance; just abort.
155
- this.emitLog("CallWrapper.run", "enqueue failed -> abort");
156
- return false;
157
- }
158
- const promptLoadCached = new Promise((resolve) => {
159
- this.promptLoadTrigger = (value) => {
160
- if (this.promptLoadTrigger) {
161
- this.promptLoadTrigger = null;
162
- }
163
- resolve(value);
164
- };
165
- });
166
- const jobDonePromise = new Promise((resolve) => {
167
- this.jobDoneResolved = false;
168
- this.jobResolveFn = (value) => {
169
- if (this.jobDoneResolved) {
170
- return;
171
- }
172
- this.jobDoneResolved = true;
173
- resolve(value);
174
- };
175
- if (this.pendingCompletion !== null) {
176
- const pending = this.pendingCompletion;
177
- this.pendingCompletion = null;
178
- this.jobResolveFn?.(pending);
179
- }
180
- });
181
- /**
182
- * Declare the function to check if the job is executing.
183
- */
184
- const checkExecutingFn = (event) => {
185
- if (event.detail && event.detail.prompt_id === job.prompt_id) {
186
- this.emitLog("CallWrapper.run", "executing observed", { node: event.detail.node });
187
- this.resolvePromptLoad(false);
188
- }
189
- };
190
- /**
191
- * Declare the function to check if the job is cached.
192
- */
193
- const checkExecutionCachedFn = (event) => {
194
- const outputNodes = Object.values(this.prompt.mapOutputKeys).filter((n) => !!n);
195
- if (event.detail.nodes.length > 0 && event.detail.prompt_id === job.prompt_id) {
196
- /**
197
- * Cached is true if all output nodes are included in the cached nodes.
198
- */
199
- const cached = outputNodes.every((node) => event.detail.nodes.includes(node));
200
- this.emitLog("CallWrapper.run", "execution_cached observed", {
201
- cached,
202
- nodes: event.detail.nodes,
203
- expected: outputNodes
204
- });
205
- this.resolvePromptLoad(cached);
206
- }
207
- };
208
- /**
209
- * Listen to the executing event.
210
- */
211
- this.checkExecutingOffFn = this.client.on("executing", checkExecutingFn);
212
- this.checkExecutedOffFn = this.client.on("execution_cached", checkExecutionCachedFn);
213
- // race condition handling
214
- let wentMissing = false;
215
- let cachedOutputDone = false;
216
- let cachedOutputPromise = Promise.resolve(null);
217
- const statusHandler = async () => {
218
- const queue = await this.client.getQueue();
219
- const queueItems = [...queue.queue_pending, ...queue.queue_running];
220
- this.emitLog("CallWrapper.status", "queue snapshot", {
221
- running: queue.queue_running.length,
222
- pending: queue.queue_pending.length
223
- });
224
- for (const queueItem of queueItems) {
225
- if (queueItem[1] === job.prompt_id) {
226
- return;
227
- }
228
- }
229
- await cachedOutputPromise;
230
- if (cachedOutputDone) {
231
- this.emitLog("CallWrapper.status", "cached output already handled");
232
- return;
233
- }
234
- if (this.cancellationRequested) {
235
- this.emitLog("CallWrapper.status", "job missing after cancellation", {
236
- prompt_id: job.prompt_id
237
- });
238
- this.resolvePromptLoad(false);
239
- this.resolveJob(false);
240
- this.cleanupListeners("status handler cancellation");
241
- return;
242
- }
243
- wentMissing = true;
244
- const output = await this.handleCachedOutput(job.prompt_id);
245
- if (output) {
246
- cachedOutputDone = true;
247
- this.emitLog("CallWrapper.status", "output from history after missing", {
248
- prompt_id: job.prompt_id
249
- });
250
- this.resolvePromptLoad(false);
251
- this.resolveJob(output);
252
- this.cleanupListeners("status handler resolved from history");
253
- return;
254
- }
255
- if (this.disconnectRecoveryActive) {
256
- this.emitLog("CallWrapper.status", "job missing but disconnect recovery active -> waiting", {
257
- prompt_id: job.prompt_id
258
- });
259
- this.resolvePromptLoad(false);
260
- void this.attemptHistoryCompletion("status_missing");
261
- return;
262
- }
263
- cachedOutputDone = true;
264
- this.emitLog("CallWrapper.status", "job missing without cached output", {
265
- prompt_id: job.prompt_id
266
- });
267
- this.resolvePromptLoad(false);
268
- this.resolveJob(false);
269
- this.cleanupListeners("status handler missing");
270
- this.emitFailure(new WentMissingError("The job went missing!"), job.prompt_id);
271
- };
272
- this.statusHandlerOffFn = this.client.on("status", statusHandler);
273
- // Attach execution listeners immediately so fast jobs cannot finish before we subscribe
274
- this.handleJobExecution(job.prompt_id);
275
- await promptLoadCached;
276
- if (wentMissing) {
277
- return jobDonePromise;
278
- }
279
- cachedOutputPromise = this.handleCachedOutput(job.prompt_id);
280
- const output = await cachedOutputPromise;
281
- if (output) {
282
- cachedOutputDone = true;
283
- this.cleanupListeners("no cached output values returned");
284
- this.resolveJob(output);
285
- return output;
286
- }
287
- if (output === false) {
288
- cachedOutputDone = true;
289
- this.cleanupListeners("cached output ready before execution listeners");
290
- this.emitFailure(new FailedCacheError("Failed to get cached output"), this.promptId);
291
- this.resolveJob(false);
292
- return false;
293
- }
294
- this.emitLog("CallWrapper.run", "no cached output -> proceed with execution listeners");
295
- return jobDonePromise;
296
- }
297
- async bypassWorkflowNodes(workflow) {
298
- const nodeDefs = {}; // cache node definitions
299
- for (const nodeId of this.prompt.bypassNodes) {
300
- if (!workflow[nodeId]) {
301
- throw new MissingNodeError(`Node ${nodeId.toString()} is missing from the workflow!`);
302
- }
303
- const classType = workflow[nodeId].class_type;
304
- // Directly use feature namespace to avoid deprecated internal call
305
- const def = nodeDefs[classType] || (await this.client.ext.node.getNodeDefs(classType))?.[classType];
306
- if (!def) {
307
- throw new MissingNodeError(`Node type ${workflow[nodeId].class_type} is missing from server!`);
308
- }
309
- nodeDefs[classType] = def;
310
- const connections = new Map();
311
- const connectedInputs = [];
312
- // connect output nodes to matching input nodes
313
- for (const [outputIdx, outputType] of Array.from(def.output.entries())) {
314
- for (const [inputName, inputValue] of Object.entries(workflow[nodeId].inputs)) {
315
- if (connectedInputs.includes(inputName)) {
316
- continue;
317
- }
318
- if (def.input.required[inputName]?.[0] === outputType) {
319
- connections.set(outputIdx, inputValue);
320
- connectedInputs.push(inputName);
321
- break;
322
- }
323
- if (def.input.optional?.[inputName]?.[0] === outputType) {
324
- connections.set(outputIdx, inputValue);
325
- connectedInputs.push(inputName);
326
- break;
327
- }
328
- }
329
- }
330
- // search and replace all nodes' inputs referencing this node based on matching output type, or remove reference
331
- // if no matching output type was found
332
- for (const [conNodeId, conNode] of Object.entries(workflow)) {
333
- for (const [conInputName, conInputValue] of Object.entries(conNode.inputs)) {
334
- if (!Array.isArray(conInputValue) || conInputValue[0] !== nodeId) {
335
- continue;
336
- }
337
- if (connections.has(conInputValue[1])) {
338
- workflow[conNodeId].inputs[conInputName] = connections.get(conInputValue[1]);
339
- }
340
- else {
341
- delete workflow[conNodeId].inputs[conInputName];
342
- }
343
- }
344
- }
345
- delete workflow[nodeId];
346
- }
347
- return workflow;
348
- }
349
- async enqueueJob() {
350
- let workflow = structuredClone(this.prompt.workflow);
351
- if (this.prompt.bypassNodes.length > 0) {
352
- try {
353
- workflow = await this.bypassWorkflowNodes(workflow);
354
- }
355
- catch (e) {
356
- if (e instanceof Response) {
357
- this.emitFailure(new MissingNodeError("Failed to get workflow node definitions", { cause: await e.json() }), this.promptId);
358
- }
359
- else {
360
- this.emitFailure(new MissingNodeError("There was a missing node in the workflow bypass.", { cause: e }), this.promptId);
361
- }
362
- return null;
363
- }
364
- }
365
- let job;
366
- try {
367
- job = await this.client.ext.queue.appendPrompt(workflow);
368
- }
369
- catch (e) {
370
- try {
371
- if (e instanceof EnqueueFailedError) {
372
- this.emitFailure(e, this.promptId);
373
- }
374
- else if (e instanceof Response) {
375
- const err = await buildEnqueueFailedError(e);
376
- this.emitFailure(err, this.promptId);
377
- }
378
- else if (e && typeof e === "object" && "response" in e && e.response instanceof Response) {
379
- const err = await buildEnqueueFailedError(e.response);
380
- this.emitFailure(err, this.promptId);
381
- }
382
- else {
383
- this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: e, reason: e?.message }), this.promptId);
384
- }
385
- }
386
- catch (inner) {
387
- this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: inner }), this.promptId);
388
- }
389
- job = null;
390
- }
391
- if (!job) {
392
- return;
393
- }
394
- this.promptId = job.prompt_id;
395
- console.log(`[CallWrapper] Enqueued with promptId=${this.promptId?.substring(0, 8)}...`);
396
- console.log(`[CallWrapper] Full job object:`, JSON.stringify({ promptId: job.prompt_id }, null, 2));
397
- this.emitLog("CallWrapper.enqueueJob", "queued", { prompt_id: this.promptId });
398
- this.onPendingFn?.(this.promptId);
399
- this.onDisconnectedHandlerOffFn = this.client.on("disconnected", () => {
400
- if (this.isCompletingSuccessfully) {
401
- this.emitLog("CallWrapper.enqueueJob", "disconnected during success completion -> ignored");
402
- return;
403
- }
404
- this.emitLog("CallWrapper.enqueueJob", "socket disconnected -> enter recovery", { promptId: this.promptId });
405
- this.startDisconnectRecovery();
406
- });
407
- this.onReconnectHandlerOffFn = this.client.on("reconnected", () => {
408
- if (!this.disconnectRecoveryActive) {
409
- return;
410
- }
411
- this.emitLog("CallWrapper.enqueueJob", "socket reconnected", { promptId: this.promptId });
412
- this.stopDisconnectRecovery();
413
- void this.attemptHistoryCompletion("reconnected");
414
- });
415
- this.onReconnectFailedHandlerOffFn = this.client.on("reconnection_failed", () => {
416
- if (!this.disconnectRecoveryActive) {
417
- return;
418
- }
419
- this.emitLog("CallWrapper.enqueueJob", "reconnection failed", { promptId: this.promptId });
420
- this.failDisconnected("reconnection_failed");
421
- });
422
- return job;
423
- }
424
- resolvePromptLoad(value) {
425
- const trigger = this.promptLoadTrigger;
426
- if (!trigger) {
427
- return;
428
- }
429
- this.promptLoadTrigger = null;
430
- try {
431
- trigger(value);
432
- }
433
- catch (error) {
434
- this.emitLog("CallWrapper.resolvePromptLoad", "prompt load trigger threw", {
435
- error: error instanceof Error ? error.message : String(error),
436
- promptId: this.promptId
437
- });
438
- }
439
- }
440
- resolveJob(value) {
441
- if (CALL_WRAPPER_DEBUG) {
442
- console.log("[debug] resolveJob", this.promptId, value, Boolean(this.jobResolveFn), this.jobDoneResolved);
443
- }
444
- if (this.jobResolveFn) {
445
- if (this.jobDoneResolved) {
446
- return;
447
- }
448
- this.jobDoneResolved = true;
449
- this.jobResolveFn(value);
450
- if (CALL_WRAPPER_DEBUG) {
451
- console.log("[debug] jobResolveFn invoked", this.promptId);
452
- }
453
- }
454
- else {
455
- this.pendingCompletion = value;
456
- }
457
- }
458
- emitFailure(error, promptId) {
459
- const fn = this.onFailedFn;
460
- if (!fn) {
461
- return;
462
- }
463
- const targetPromptId = promptId ?? this.promptId;
464
- try {
465
- if (CALL_WRAPPER_DEBUG) {
466
- console.log("[debug] emitFailure start", error.name);
467
- }
468
- fn(error, targetPromptId);
469
- if (CALL_WRAPPER_DEBUG) {
470
- console.log("[debug] emitFailure end", error.name);
471
- }
472
- }
473
- catch (callbackError) {
474
- this.emitLog("CallWrapper.emitFailure", "onFailed callback threw", {
475
- prompt_id: targetPromptId,
476
- error: callbackError instanceof Error ? callbackError.message : String(callbackError)
477
- });
478
- }
479
- }
480
- cancel(reason = "cancelled") {
481
- if (this.cancellationRequested) {
482
- this.emitLog("CallWrapper.cancel", "cancel already requested", {
483
- promptId: this.promptId,
484
- reason
485
- });
486
- return;
487
- }
488
- this.cancellationRequested = true;
489
- this.emitLog("CallWrapper.cancel", "cancel requested", {
490
- promptId: this.promptId,
491
- reason
492
- });
493
- this.resolvePromptLoad(false);
494
- this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: { reason } }), this.promptId);
495
- this.cleanupListeners("cancel requested");
496
- this.resolveJob(false);
497
- }
498
- startDisconnectRecovery() {
499
- if (this.disconnectRecoveryActive || this.cancellationRequested) {
500
- return;
501
- }
502
- this.disconnectRecoveryActive = true;
503
- if (this.disconnectFailureTimer) {
504
- clearTimeout(this.disconnectFailureTimer);
505
- }
506
- this.disconnectFailureTimer = setTimeout(() => this.failDisconnected("timeout"), DISCONNECT_FAILURE_GRACE_MS);
507
- void this.attemptHistoryCompletion("disconnect_start");
508
- }
509
- stopDisconnectRecovery() {
510
- if (!this.disconnectRecoveryActive) {
511
- return;
512
- }
513
- this.disconnectRecoveryActive = false;
514
- if (this.disconnectFailureTimer) {
515
- clearTimeout(this.disconnectFailureTimer);
516
- this.disconnectFailureTimer = null;
517
- }
518
- }
519
- async attemptHistoryCompletion(reason) {
520
- if (!this.promptId || this.cancellationRequested) {
521
- return false;
522
- }
523
- try {
524
- const output = await this.handleCachedOutput(this.promptId);
525
- if (output && output !== false) {
526
- this.emitLog("CallWrapper.historyRecovery", "completed from history", { reason, promptId: this.promptId });
527
- this.stopDisconnectRecovery();
528
- this.isCompletingSuccessfully = true;
529
- this.resolvePromptLoad(false);
530
- this.resolveJob(output);
531
- this.cleanupListeners(`history recovery (${reason})`);
532
- return true;
533
- }
534
- }
535
- catch (error) {
536
- this.emitLog("CallWrapper.historyRecovery", "history fetch failed", { reason, error: String(error) });
537
- }
538
- return false;
539
- }
540
- failDisconnected(reason) {
541
- if (!this.disconnectRecoveryActive || this.isCompletingSuccessfully) {
542
- return;
543
- }
544
- this.stopDisconnectRecovery();
545
- this.emitLog("CallWrapper.enqueueJob", "disconnect recovery failed", { reason, promptId: this.promptId });
546
- this.resolvePromptLoad(false);
547
- this.resolveJob(false);
548
- this.cleanupListeners("disconnect failure");
549
- this.emitFailure(new DisconnectedError("Disconnected"), this.promptId);
550
- }
551
- async handleCachedOutput(promptId) {
552
- const hisData = await this.client.ext.history.getHistory(promptId);
553
- this.emitLog("CallWrapper.handleCachedOutput", "history fetched", {
554
- promptId,
555
- status: hisData?.status?.status_str,
556
- completed: hisData?.status?.completed,
557
- outputKeys: hisData?.outputs ? Object.keys(hisData.outputs) : [],
558
- hasOutputs: !!(hisData && hisData.outputs && Object.keys(hisData.outputs).length > 0)
559
- });
560
- // Only return outputs if execution is actually completed
561
- if (hisData && hisData.status?.completed && hisData.outputs) {
562
- const output = this.mapOutput(hisData.outputs);
563
- const hasDefinedValue = Object.entries(output).some(([key, value]) => {
564
- if (key === "_raw") {
565
- return value !== undefined && value !== null && Object.keys(value).length > 0;
566
- }
567
- return value !== undefined;
568
- });
569
- if (hasDefinedValue) {
570
- this.emitLog("CallWrapper.handleCachedOutput", "returning completed outputs");
571
- this.onFinishedFn?.(output, this.promptId);
572
- return output;
573
- }
574
- else {
575
- this.emitLog("CallWrapper.handleCachedOutput", "cached output missing defined values", {
576
- promptId,
577
- outputKeys: Object.keys(hisData.outputs ?? {}),
578
- mappedKeys: this.prompt.mapOutputKeys
579
- });
580
- return false;
581
- }
582
- }
583
- if (hisData && hisData.status?.completed && !hisData.outputs) {
584
- this.emitLog("CallWrapper.handleCachedOutput", "history completed without outputs", { promptId });
585
- return false;
586
- }
587
- if (hisData && !hisData.status?.completed) {
588
- this.emitLog("CallWrapper.handleCachedOutput", "history not completed yet");
589
- }
590
- if (!hisData) {
591
- this.emitLog("CallWrapper.handleCachedOutput", "history entry not available");
592
- }
593
- return null;
594
- }
595
- mapOutput(outputNodes) {
596
- const outputMapped = this.prompt.mapOutputKeys;
597
- const output = {};
598
- for (const key in outputMapped) {
599
- const node = outputMapped[key];
600
- if (node) {
601
- output[key] = outputNodes[node];
602
- }
603
- else {
604
- if (!output._raw) {
605
- output._raw = {};
606
- }
607
- output._raw[key] = outputNodes[key];
608
- }
609
- }
610
- return output;
611
- }
612
- handleJobExecution(promptId) {
613
- if (this.executionHandlerOffFn) {
614
- return;
615
- }
616
- const reverseOutputMapped = this.reverseMapOutputKeys();
617
- const mapOutputKeys = this.prompt.mapOutputKeys;
618
- console.log(`[CallWrapper] handleJobExecution for ${promptId.substring(0, 8)}... - mapOutputKeys:`, mapOutputKeys, "reverseOutputMapped:", reverseOutputMapped);
619
- this.progressHandlerOffFn = this.client.on("progress", (ev) => this.handleProgress(ev, promptId));
620
- this.previewHandlerOffFn = this.client.on("b_preview", (ev) => {
621
- // Note: b_preview events don't include prompt_id. They're scoped per connection.
622
- // If multiple jobs use the same connection, they will all receive preview events.
623
- // This is a limitation of the ComfyUI protocol - previews are not separated by prompt_id.
624
- this.onPreviewFn?.(ev.detail, this.promptId);
625
- });
626
- // Also forward preview with metadata if available
627
- const offPreviewMeta = this.client.on("b_preview_meta", (ev) => {
628
- // Validate prompt_id from metadata if available to prevent cross-user preview leakage
629
- const metadata = ev.detail.metadata;
630
- const metaPromptId = metadata?.prompt_id;
631
- if (metaPromptId && metaPromptId !== promptId) {
632
- console.log(`[CallWrapper] Ignoring b_preview_meta for wrong prompt. Expected ${promptId.substring(0, 8)}..., got ${metaPromptId.substring(0, 8)}...`);
633
- return;
634
- }
635
- this.onPreviewMetaFn?.(ev.detail, this.promptId);
636
- });
637
- const prevCleanup = this.previewHandlerOffFn;
638
- this.previewHandlerOffFn = () => {
639
- prevCleanup?.();
640
- offPreviewMeta?.();
641
- };
642
- const totalOutput = Object.keys(reverseOutputMapped).length;
643
- let remainingOutput = totalOutput;
644
- console.log(`[CallWrapper] totalOutput=${totalOutput}, remainingOutput=${remainingOutput}`);
645
- const executionHandler = (ev) => {
646
- 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)}...`);
647
- const eventPromptId = ev.detail.prompt_id;
648
- const isCorrectPrompt = eventPromptId === promptId;
649
- // STRICT: Only accept events where prompt_id matches our expected promptId
650
- if (!isCorrectPrompt) {
651
- console.log(`[CallWrapper.executionHandler] REJECTED - prompt_id mismatch (expected ${promptId.substring(0, 8)}..., got ${eventPromptId?.substring(0, 8)}...)`);
652
- return;
653
- }
654
- const outputKey = reverseOutputMapped[ev.detail.node];
655
- 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));
656
- if (outputKey) {
657
- this.output[outputKey] = ev.detail.output;
658
- this.onOutputFn?.(outputKey, ev.detail.output, this.promptId);
659
- remainingOutput--;
660
- }
661
- else {
662
- this.output._raw = this.output._raw || {};
663
- this.output._raw[ev.detail.node] = ev.detail.output;
664
- this.onOutputFn?.(ev.detail.node, ev.detail.output, this.promptId);
665
- }
666
- console.log(`[CallWrapper] afterProcessing - remainingAfter: ${remainingOutput}, willTriggerCompletion: ${remainingOutput === 0}`);
667
- if (remainingOutput === 0) {
668
- console.log(`[CallWrapper] all outputs collected for ${promptId.substring(0, 8)}...`);
669
- // Mark as successfully completing BEFORE cleanup to prevent race condition with disconnection handler
670
- this.isCompletingSuccessfully = true;
671
- this.cleanupListeners("all outputs collected");
672
- this.onFinishedFn?.(this.output, this.promptId);
673
- this.resolveJob(this.output);
674
- }
675
- };
676
- const executedEnd = async () => {
677
- console.log(`[CallWrapper] execution_success fired for ${promptId.substring(0, 8)}..., remainingOutput=${remainingOutput}, totalOutput=${totalOutput}`);
678
- // If we've already marked this as successfully completing, don't fail it again
679
- if (this.isCompletingSuccessfully) {
680
- console.log(`[CallWrapper] Already marked as successfully completing, ignoring this execution_success`);
681
- return;
682
- }
683
- if (remainingOutput === 0) {
684
- console.log(`[CallWrapper] all outputs already collected, nothing to do`);
685
- return;
686
- }
687
- // Wait briefly for outputs that might be arriving due to prompt ID mismatch
688
- await new Promise(resolve => setTimeout(resolve, 100));
689
- console.log(`[CallWrapper] After wait - remainingOutput=${remainingOutput}, this.output keys:`, Object.keys(this.output));
690
- // Check if outputs arrived while we were waiting
691
- if (remainingOutput === 0) {
692
- console.log(`[CallWrapper] Outputs arrived during wait - marking as complete`);
693
- this.isCompletingSuccessfully = true;
694
- this.cleanupListeners("executedEnd - outputs complete after wait");
695
- this.onFinishedFn?.(this.output, this.promptId);
696
- this.resolveJob(this.output);
697
- return;
698
- }
699
- // Check if we have collected all outputs (even if prompt ID mismatch)
700
- const hasAllOutputs = Object.keys(reverseOutputMapped).every(nodeId => this.output[reverseOutputMapped[nodeId]] !== undefined);
701
- if (hasAllOutputs) {
702
- console.log(`[CallWrapper] Have all required outputs despite promptId mismatch - marking as complete`);
703
- this.isCompletingSuccessfully = true;
704
- this.cleanupListeners("executedEnd - outputs complete despite promptId mismatch");
705
- this.onFinishedFn?.(this.output, this.promptId);
706
- this.resolveJob(this.output);
707
- return;
708
- }
709
- // Try to fetch from history with retry logic
710
- let hisData = null;
711
- for (let retries = 0; retries < 5; retries++) {
712
- hisData = await this.client.ext.history.getHistory(promptId);
713
- console.log(`[CallWrapper] History query result for ${promptId.substring(0, 8)}... (attempt ${retries + 1}) - status:`, hisData?.status, 'outputs:', Object.keys(hisData?.outputs ?? {}).length);
714
- if (hisData?.status?.completed && hisData.outputs) {
715
- console.log(`[CallWrapper] Found completed job in history with outputs - attempting to populate from history`);
716
- break;
717
- }
718
- if (retries < 4) {
719
- console.log(`[CallWrapper] History not ready yet, waiting 100ms before retry...`);
720
- await new Promise(resolve => setTimeout(resolve, 100));
721
- }
722
- }
723
- if (hisData?.status?.completed && hisData.outputs) {
724
- // Try to extract outputs from history data
725
- let populatedCount = 0;
726
- for (const [nodeIdStr, nodeOutput] of Object.entries(hisData.outputs)) {
727
- const nodeId = parseInt(nodeIdStr, 10);
728
- const outputKey = reverseOutputMapped[nodeId];
729
- if (outputKey && nodeOutput) {
730
- // nodeOutput is typically { images: [...] } or similar - take the first property
731
- const outputValue = Array.isArray(nodeOutput) ? nodeOutput[0] : Object.values(nodeOutput)[0];
732
- if (outputValue !== undefined) {
733
- this.output[outputKey] = outputValue;
734
- this.onOutputFn?.(outputKey, outputValue, this.promptId);
735
- populatedCount++;
736
- remainingOutput--;
737
- console.log(`[CallWrapper] Populated ${outputKey} from history`);
738
- }
739
- }
740
- }
741
- if (remainingOutput === 0) {
742
- console.log(`[CallWrapper] Successfully populated all outputs from history for ${promptId.substring(0, 8)}...`);
743
- this.isCompletingSuccessfully = true;
744
- this.cleanupListeners("executedEnd - populated from history");
745
- this.onFinishedFn?.(this.output, this.promptId);
746
- this.resolveJob(this.output);
747
- return;
748
- }
749
- if (populatedCount > 0) {
750
- console.log(`[CallWrapper] Populated ${populatedCount} outputs from history (remainingOutput=${remainingOutput})`);
751
- if (remainingOutput === 0) {
752
- this.isCompletingSuccessfully = true;
753
- this.cleanupListeners("executedEnd - all outputs from history");
754
- this.onFinishedFn?.(this.output, this.promptId);
755
- this.resolveJob(this.output);
756
- return;
757
- }
758
- }
759
- }
760
- console.log(`[CallWrapper] execution failed due to missing outputs - remainingOutput=${remainingOutput}, totalOutput=${totalOutput}`);
761
- this.emitFailure(new ExecutionFailedError("Execution failed"), this.promptId);
762
- this.resolvePromptLoad(false);
763
- this.cleanupListeners("executedEnd missing outputs");
764
- this.resolveJob(false);
765
- };
766
- this.executionEndSuccessOffFn = this.client.on("execution_success", executedEnd);
767
- this.executionHandlerOffFn = this.client.on("executed", executionHandler);
768
- console.log(`[CallWrapper] Registered listeners for ${promptId.substring(0, 8)}... - executionHandler and executedEnd`);
769
- this.errorHandlerOffFn = this.client.on("execution_error", (ev) => this.handleError(ev, promptId));
770
- this.interruptionHandlerOffFn = this.client.on("execution_interrupted", (ev) => {
771
- if (ev.detail.prompt_id !== promptId)
772
- return;
773
- this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: ev.detail }), ev.detail.prompt_id);
774
- this.resolvePromptLoad(false);
775
- this.cleanupListeners("execution interrupted");
776
- this.resolveJob(false);
777
- });
778
- }
779
- reverseMapOutputKeys() {
780
- const outputMapped = this.prompt.mapOutputKeys;
781
- return Object.entries(outputMapped).reduce((acc, [k, v]) => {
782
- if (v)
783
- acc[v] = k;
784
- return acc;
785
- }, {});
786
- }
787
- handleProgress(ev, promptId) {
788
- if (ev.detail.prompt_id === promptId && !this.started) {
789
- this.started = true;
790
- this.onStartFn?.(this.promptId);
791
- }
792
- this.onProgressFn?.(ev.detail, this.promptId);
793
- }
794
- handleError(ev, promptId) {
795
- if (ev.detail.prompt_id !== promptId)
796
- return;
797
- this.emitLog("CallWrapper.handleError", ev.detail.exception_type, {
798
- prompt_id: ev.detail.prompt_id,
799
- node_id: ev.detail?.node_id
800
- });
801
- this.emitFailure(new CustomEventError(ev.detail.exception_type, { cause: ev.detail }), ev.detail.prompt_id);
802
- if (CALL_WRAPPER_DEBUG) {
803
- console.log("[debug] handleError after emitFailure");
804
- }
805
- this.resolvePromptLoad(false);
806
- if (CALL_WRAPPER_DEBUG) {
807
- console.log("[debug] handleError before cleanup");
808
- }
809
- this.cleanupListeners("execution_error received");
810
- if (CALL_WRAPPER_DEBUG) {
811
- console.log("[debug] handleError after cleanup");
812
- }
813
- this.resolveJob(false);
814
- }
815
- emitLog(fnName, message, data) {
816
- const detail = { fnName, message, data };
817
- const customEvent = new CustomEvent("log", { detail });
818
- const clientAny = this.client;
819
- if (typeof clientAny.emit === "function") {
820
- clientAny.emit("log", customEvent);
821
- return;
822
- }
823
- clientAny.dispatchEvent?.(customEvent);
824
- }
825
- cleanupListeners(reason) {
826
- const debugPayload = { reason, promptId: this.promptId };
827
- this.emitLog("CallWrapper.cleanupListeners", "removing listeners", debugPayload);
828
- this.resolvePromptLoad(false);
829
- this.stopDisconnectRecovery();
830
- this.onReconnectHandlerOffFn?.();
831
- this.onReconnectHandlerOffFn = undefined;
832
- this.onReconnectFailedHandlerOffFn?.();
833
- this.onReconnectFailedHandlerOffFn = undefined;
834
- this.disconnectFailureTimer = null;
835
- this.onDisconnectedHandlerOffFn?.();
836
- this.onDisconnectedHandlerOffFn = undefined;
837
- this.checkExecutingOffFn?.();
838
- this.checkExecutingOffFn = undefined;
839
- this.checkExecutedOffFn?.();
840
- this.checkExecutedOffFn = undefined;
841
- this.progressHandlerOffFn?.();
842
- this.progressHandlerOffFn = undefined;
843
- this.previewHandlerOffFn?.();
844
- this.previewHandlerOffFn = undefined;
845
- this.executionHandlerOffFn?.();
846
- this.executionHandlerOffFn = undefined;
847
- this.errorHandlerOffFn?.();
848
- this.errorHandlerOffFn = undefined;
849
- this.executionEndSuccessOffFn?.();
850
- this.executionEndSuccessOffFn = undefined;
851
- this.interruptionHandlerOffFn?.();
852
- this.interruptionHandlerOffFn = undefined;
853
- this.statusHandlerOffFn?.();
854
- this.statusHandlerOffFn = undefined;
855
- }
856
- }
1
+ import { FailedCacheError, WentMissingError, EnqueueFailedError, DisconnectedError, CustomEventError, ExecutionFailedError, ExecutionInterruptedError, MissingNodeError } from "./types/error.js";
2
+ import { buildEnqueueFailedError } from "./utils/response-error.js";
3
+ const DISCONNECT_FAILURE_GRACE_MS = 5000;
4
+ const CALL_WRAPPER_DEBUG = process.env.WORKFLOW_POOL_DEBUG === "1";
5
+ /**
6
+ * Represents a wrapper class for making API calls using the ComfyApi client.
7
+ * Provides methods for setting callback functions and executing the job.
8
+ */
9
+ export class CallWrapper {
10
+ client;
11
+ prompt;
12
+ started = false;
13
+ isCompletingSuccessfully = false;
14
+ promptId;
15
+ output = {};
16
+ onPreviewFn;
17
+ onPreviewMetaFn;
18
+ onPendingFn;
19
+ onStartFn;
20
+ onOutputFn;
21
+ onFinishedFn;
22
+ onFailedFn;
23
+ onProgressFn;
24
+ jobResolveFn;
25
+ jobDoneResolved = false;
26
+ pendingCompletion = null;
27
+ cancellationRequested = false;
28
+ promptLoadTrigger = null;
29
+ disconnectRecoveryActive = false;
30
+ disconnectFailureTimer = null;
31
+ onReconnectHandlerOffFn;
32
+ onReconnectFailedHandlerOffFn;
33
+ onDisconnectedHandlerOffFn;
34
+ checkExecutingOffFn;
35
+ checkExecutedOffFn;
36
+ progressHandlerOffFn;
37
+ previewHandlerOffFn;
38
+ executionHandlerOffFn;
39
+ errorHandlerOffFn;
40
+ executionEndSuccessOffFn;
41
+ statusHandlerOffFn;
42
+ interruptionHandlerOffFn;
43
+ /**
44
+ * Constructs a new CallWrapper instance.
45
+ * @param client The ComfyApi client.
46
+ * @param workflow The workflow object.
47
+ */
48
+ constructor(client, workflow) {
49
+ this.client = client;
50
+ this.prompt = workflow;
51
+ return this;
52
+ }
53
+ /**
54
+ * Set the callback function to be called when a preview event occurs.
55
+ *
56
+ * @param fn - The callback function to be called. It receives a Blob object representing the event and an optional promptId string.
57
+ * @returns The current instance of the CallWrapper.
58
+ */
59
+ onPreview(fn) {
60
+ this.onPreviewFn = fn;
61
+ return this;
62
+ }
63
+ /**
64
+ * Set the callback function to be called when a preview-with-metadata event occurs.
65
+ */
66
+ onPreviewMeta(fn) {
67
+ this.onPreviewMetaFn = fn;
68
+ return this;
69
+ }
70
+ /**
71
+ * Set a callback function to be executed when the job is queued.
72
+ * @param {Function} fn - The callback function to be executed.
73
+ * @returns The current instance of the CallWrapper.
74
+ */
75
+ onPending(fn) {
76
+ this.onPendingFn = fn;
77
+ return this;
78
+ }
79
+ /**
80
+ * Set the callback function to be executed when the job start.
81
+ *
82
+ * @param fn - The callback function to be executed. It can optionally receive a `promptId` parameter.
83
+ * @returns The current instance of the CallWrapper.
84
+ */
85
+ onStart(fn) {
86
+ this.onStartFn = fn;
87
+ return this;
88
+ }
89
+ /**
90
+ * Sets the callback function to handle the output node when the workflow is executing. This is
91
+ * useful when you want to handle the output of each nodes as they are being processed.
92
+ *
93
+ * All the nodes defined in the `mapOutputKeys` will be passed to this function when node is executed.
94
+ *
95
+ * @param fn - The callback function to handle the output.
96
+ * @returns The current instance of the class.
97
+ */
98
+ onOutput(fn) {
99
+ this.onOutputFn = fn;
100
+ return this;
101
+ }
102
+ /**
103
+ * Set the callback function to be executed when the asynchronous operation is finished.
104
+ *
105
+ * @param fn - The callback function to be executed. It receives the data returned by the operation
106
+ * and an optional promptId parameter.
107
+ * @returns The current instance of the CallWrapper.
108
+ */
109
+ onFinished(fn) {
110
+ this.onFinishedFn = fn;
111
+ return this;
112
+ }
113
+ /**
114
+ * Set the callback function to be executed when the API call fails.
115
+ *
116
+ * @param fn - The callback function to be executed when the API call fails.
117
+ * It receives an `Error` object as the first parameter and an optional `promptId` as the second parameter.
118
+ * @returns The current instance of the CallWrapper.
119
+ */
120
+ onFailed(fn) {
121
+ this.onFailedFn = fn;
122
+ return this;
123
+ }
124
+ /**
125
+ * Set a callback function to be called when progress information is available.
126
+ * @param fn - The callback function to be called with the progress information.
127
+ * @returns The current instance of the CallWrapper.
128
+ */
129
+ onProgress(fn) {
130
+ this.onProgressFn = fn;
131
+ return this;
132
+ }
133
+ /**
134
+ * Run the call wrapper and returns the output of the executed job.
135
+ * If the job is already cached, it returns the cached output.
136
+ * If the job is not cached, it executes the job and returns the output.
137
+ *
138
+ * @returns A promise that resolves to the output of the executed job,
139
+ * or `undefined` if the job is not found,
140
+ * or `false` if the job execution fails.
141
+ */
142
+ async run() {
143
+ /**
144
+ * Start the job execution.
145
+ */
146
+ this.emitLog("CallWrapper.run", "enqueue start");
147
+ this.pendingCompletion = null;
148
+ this.jobResolveFn = undefined;
149
+ this.jobDoneResolved = false;
150
+ this.cancellationRequested = false;
151
+ this.promptLoadTrigger = null;
152
+ const job = await this.enqueueJob();
153
+ if (!job) {
154
+ // enqueueJob already invoked onFailed with a rich error instance; just abort.
155
+ this.emitLog("CallWrapper.run", "enqueue failed -> abort");
156
+ return false;
157
+ }
158
+ const promptLoadCached = new Promise((resolve) => {
159
+ this.promptLoadTrigger = (value) => {
160
+ if (this.promptLoadTrigger) {
161
+ this.promptLoadTrigger = null;
162
+ }
163
+ resolve(value);
164
+ };
165
+ });
166
+ const jobDonePromise = new Promise((resolve) => {
167
+ this.jobDoneResolved = false;
168
+ this.jobResolveFn = (value) => {
169
+ resolve(value);
170
+ };
171
+ if (this.pendingCompletion !== null) {
172
+ const pending = this.pendingCompletion;
173
+ this.pendingCompletion = null;
174
+ this.jobResolveFn?.(pending);
175
+ }
176
+ });
177
+ /**
178
+ * Declare the function to check if the job is executing.
179
+ */
180
+ const checkExecutingFn = (event) => {
181
+ // Defensive null check for event detail
182
+ if (!event.detail) {
183
+ this.emitLog("CallWrapper.run", "executing event received with no detail");
184
+ return;
185
+ }
186
+ if (event.detail.prompt_id === job.prompt_id) {
187
+ this.emitLog("CallWrapper.run", "executing observed", { node: event.detail.node });
188
+ this.resolvePromptLoad(false);
189
+ }
190
+ };
191
+ /**
192
+ * Declare the function to check if the job is cached.
193
+ */
194
+ const checkExecutionCachedFn = (event) => {
195
+ // Defensive null checks for event detail
196
+ if (!event.detail || !event.detail.nodes) {
197
+ this.emitLog("CallWrapper.run", "execution_cached event received with invalid structure");
198
+ return;
199
+ }
200
+ const outputNodes = Object.values(this.prompt.mapOutputKeys).filter((n) => !!n);
201
+ if (event.detail.nodes.length > 0 && event.detail.prompt_id === job.prompt_id) {
202
+ /**
203
+ * Cached is true if all output nodes are included in the cached nodes.
204
+ */
205
+ const cached = outputNodes.every((node) => event.detail.nodes.includes(node));
206
+ this.emitLog("CallWrapper.run", "execution_cached observed", {
207
+ cached,
208
+ nodes: event.detail.nodes,
209
+ expected: outputNodes
210
+ });
211
+ this.resolvePromptLoad(cached);
212
+ }
213
+ };
214
+ /**
215
+ * Listen to the executing event.
216
+ */
217
+ this.checkExecutingOffFn = this.client.on("executing", checkExecutingFn);
218
+ this.checkExecutedOffFn = this.client.on("execution_cached", checkExecutionCachedFn);
219
+ // race condition handling
220
+ let wentMissing = false;
221
+ let cachedOutputDone = false;
222
+ let cachedOutputPromise = Promise.resolve(null);
223
+ const statusHandler = async () => {
224
+ const queue = await this.client.getQueue();
225
+ const queueItems = [...queue.queue_pending, ...queue.queue_running];
226
+ this.emitLog("CallWrapper.status", "queue snapshot", {
227
+ running: queue.queue_running.length,
228
+ pending: queue.queue_pending.length
229
+ });
230
+ for (const queueItem of queueItems) {
231
+ if (queueItem[1] === job.prompt_id) {
232
+ return;
233
+ }
234
+ }
235
+ await cachedOutputPromise;
236
+ if (cachedOutputDone) {
237
+ this.emitLog("CallWrapper.status", "cached output already handled");
238
+ return;
239
+ }
240
+ if (this.cancellationRequested) {
241
+ this.emitLog("CallWrapper.status", "job missing after cancellation", {
242
+ prompt_id: job.prompt_id
243
+ });
244
+ this.resolvePromptLoad(false);
245
+ this.resolveJob(false);
246
+ this.cleanupListeners("status handler cancellation");
247
+ return;
248
+ }
249
+ wentMissing = true;
250
+ const output = await this.handleCachedOutput(job.prompt_id);
251
+ if (output) {
252
+ cachedOutputDone = true;
253
+ this.emitLog("CallWrapper.status", "output from history after missing", {
254
+ prompt_id: job.prompt_id
255
+ });
256
+ this.resolvePromptLoad(false);
257
+ this.resolveJob(output);
258
+ this.cleanupListeners("status handler resolved from history");
259
+ return;
260
+ }
261
+ if (this.disconnectRecoveryActive) {
262
+ this.emitLog("CallWrapper.status", "job missing but disconnect recovery active -> waiting", {
263
+ prompt_id: job.prompt_id
264
+ });
265
+ this.resolvePromptLoad(false);
266
+ void this.attemptHistoryCompletion("status_missing");
267
+ return;
268
+ }
269
+ cachedOutputDone = true;
270
+ this.emitLog("CallWrapper.status", "job missing without cached output", {
271
+ prompt_id: job.prompt_id
272
+ });
273
+ this.resolvePromptLoad(false);
274
+ this.resolveJob(false);
275
+ this.cleanupListeners("status handler missing");
276
+ this.emitFailure(new WentMissingError("The job went missing!"), job.prompt_id);
277
+ };
278
+ this.statusHandlerOffFn = this.client.on("status", statusHandler);
279
+ // Attach execution listeners immediately so fast jobs cannot finish before we subscribe
280
+ this.handleJobExecution(job.prompt_id);
281
+ await promptLoadCached;
282
+ if (wentMissing) {
283
+ return jobDonePromise;
284
+ }
285
+ cachedOutputPromise = this.handleCachedOutput(job.prompt_id);
286
+ const output = await cachedOutputPromise;
287
+ if (output) {
288
+ cachedOutputDone = true;
289
+ this.cleanupListeners("no cached output values returned");
290
+ this.resolveJob(output);
291
+ return output;
292
+ }
293
+ if (output === false) {
294
+ cachedOutputDone = true;
295
+ this.cleanupListeners("cached output ready before execution listeners");
296
+ this.emitFailure(new FailedCacheError("Failed to get cached output"), this.promptId);
297
+ this.resolveJob(false);
298
+ return false;
299
+ }
300
+ this.emitLog("CallWrapper.run", "no cached output -> proceed with execution listeners");
301
+ return jobDonePromise;
302
+ }
303
+ async bypassWorkflowNodes(workflow) {
304
+ const nodeDefs = {}; // cache node definitions
305
+ for (const nodeId of this.prompt.bypassNodes) {
306
+ if (!workflow[nodeId]) {
307
+ throw new MissingNodeError(`Node ${nodeId.toString()} is missing from the workflow!`);
308
+ }
309
+ const classType = workflow[nodeId].class_type;
310
+ // Directly use feature namespace to avoid deprecated internal call
311
+ const def = nodeDefs[classType] || (await this.client.ext.node.getNodeDefs(classType))?.[classType];
312
+ if (!def) {
313
+ throw new MissingNodeError(`Node type ${workflow[nodeId].class_type} is missing from server!`);
314
+ }
315
+ nodeDefs[classType] = def;
316
+ const connections = new Map();
317
+ const connectedInputs = [];
318
+ // connect output nodes to matching input nodes
319
+ for (const [outputIdx, outputType] of Array.from(def.output.entries())) {
320
+ for (const [inputName, inputValue] of Object.entries(workflow[nodeId].inputs)) {
321
+ if (connectedInputs.includes(inputName)) {
322
+ continue;
323
+ }
324
+ if (def.input.required[inputName]?.[0] === outputType) {
325
+ connections.set(outputIdx, inputValue);
326
+ connectedInputs.push(inputName);
327
+ break;
328
+ }
329
+ if (def.input.optional?.[inputName]?.[0] === outputType) {
330
+ connections.set(outputIdx, inputValue);
331
+ connectedInputs.push(inputName);
332
+ break;
333
+ }
334
+ }
335
+ }
336
+ // search and replace all nodes' inputs referencing this node based on matching output type, or remove reference
337
+ // if no matching output type was found
338
+ for (const [conNodeId, conNode] of Object.entries(workflow)) {
339
+ for (const [conInputName, conInputValue] of Object.entries(conNode.inputs)) {
340
+ if (!Array.isArray(conInputValue) || conInputValue[0] !== nodeId) {
341
+ continue;
342
+ }
343
+ if (connections.has(conInputValue[1])) {
344
+ workflow[conNodeId].inputs[conInputName] = connections.get(conInputValue[1]);
345
+ }
346
+ else {
347
+ delete workflow[conNodeId].inputs[conInputName];
348
+ }
349
+ }
350
+ }
351
+ delete workflow[nodeId];
352
+ }
353
+ return workflow;
354
+ }
355
+ async enqueueJob() {
356
+ let workflow = structuredClone(this.prompt.workflow);
357
+ if (this.prompt.bypassNodes.length > 0) {
358
+ try {
359
+ workflow = await this.bypassWorkflowNodes(workflow);
360
+ }
361
+ catch (e) {
362
+ if (e instanceof Response) {
363
+ this.emitFailure(new MissingNodeError("Failed to get workflow node definitions", { cause: await e.json() }), this.promptId);
364
+ }
365
+ else {
366
+ this.emitFailure(new MissingNodeError("There was a missing node in the workflow bypass.", { cause: e }), this.promptId);
367
+ }
368
+ return null;
369
+ }
370
+ }
371
+ let job;
372
+ try {
373
+ job = await this.client.ext.queue.appendPrompt(workflow);
374
+ }
375
+ catch (e) {
376
+ try {
377
+ if (e instanceof EnqueueFailedError) {
378
+ this.emitFailure(e, this.promptId);
379
+ }
380
+ else if (e instanceof Response) {
381
+ const err = await buildEnqueueFailedError(e);
382
+ this.emitFailure(err, this.promptId);
383
+ }
384
+ else if (e && typeof e === "object" && "response" in e && e.response instanceof Response) {
385
+ const err = await buildEnqueueFailedError(e.response);
386
+ this.emitFailure(err, this.promptId);
387
+ }
388
+ else {
389
+ this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: e, reason: e?.message }), this.promptId);
390
+ }
391
+ }
392
+ catch (inner) {
393
+ this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: inner }), this.promptId);
394
+ }
395
+ job = null;
396
+ }
397
+ if (!job) {
398
+ return;
399
+ }
400
+ this.promptId = job.prompt_id;
401
+ console.log(`[CallWrapper] Enqueued with promptId=${this.promptId?.substring(0, 8)}...`);
402
+ console.log(`[CallWrapper] Full job object:`, JSON.stringify({ promptId: job.prompt_id }, null, 2));
403
+ this.emitLog("CallWrapper.enqueueJob", "queued", { prompt_id: this.promptId });
404
+ this.onPendingFn?.(this.promptId);
405
+ this.onDisconnectedHandlerOffFn = this.client.on("disconnected", () => {
406
+ if (this.isCompletingSuccessfully) {
407
+ this.emitLog("CallWrapper.enqueueJob", "disconnected during success completion -> ignored");
408
+ return;
409
+ }
410
+ this.emitLog("CallWrapper.enqueueJob", "socket disconnected -> enter recovery", { promptId: this.promptId });
411
+ this.startDisconnectRecovery();
412
+ });
413
+ this.onReconnectHandlerOffFn = this.client.on("reconnected", () => {
414
+ if (!this.disconnectRecoveryActive) {
415
+ return;
416
+ }
417
+ this.emitLog("CallWrapper.enqueueJob", "socket reconnected", { promptId: this.promptId });
418
+ this.stopDisconnectRecovery();
419
+ void this.attemptHistoryCompletion("reconnected");
420
+ });
421
+ this.onReconnectFailedHandlerOffFn = this.client.on("reconnection_failed", () => {
422
+ if (!this.disconnectRecoveryActive) {
423
+ return;
424
+ }
425
+ this.emitLog("CallWrapper.enqueueJob", "reconnection failed", { promptId: this.promptId });
426
+ this.failDisconnected("reconnection_failed");
427
+ });
428
+ return job;
429
+ }
430
+ resolvePromptLoad(value) {
431
+ const trigger = this.promptLoadTrigger;
432
+ if (!trigger) {
433
+ return;
434
+ }
435
+ this.promptLoadTrigger = null;
436
+ try {
437
+ trigger(value);
438
+ }
439
+ catch (error) {
440
+ this.emitLog("CallWrapper.resolvePromptLoad", "prompt load trigger threw", {
441
+ error: error instanceof Error ? error.message : String(error),
442
+ promptId: this.promptId
443
+ });
444
+ }
445
+ }
446
+ resolveJob(value) {
447
+ if (CALL_WRAPPER_DEBUG) {
448
+ console.log("[debug] resolveJob", this.promptId, value, Boolean(this.jobResolveFn), this.jobDoneResolved);
449
+ }
450
+ if (this.jobResolveFn && !this.jobDoneResolved) {
451
+ this.jobDoneResolved = true;
452
+ this.jobResolveFn(value);
453
+ if (CALL_WRAPPER_DEBUG) {
454
+ console.log("[debug] jobResolveFn invoked", this.promptId);
455
+ }
456
+ }
457
+ else if (!this.jobResolveFn) {
458
+ this.pendingCompletion = value;
459
+ }
460
+ }
461
+ emitFailure(error, promptId) {
462
+ const fn = this.onFailedFn;
463
+ if (!fn) {
464
+ return;
465
+ }
466
+ const targetPromptId = promptId ?? this.promptId;
467
+ try {
468
+ if (CALL_WRAPPER_DEBUG) {
469
+ console.log("[debug] emitFailure start", error.name);
470
+ }
471
+ fn(error, targetPromptId);
472
+ if (CALL_WRAPPER_DEBUG) {
473
+ console.log("[debug] emitFailure end", error.name);
474
+ }
475
+ }
476
+ catch (callbackError) {
477
+ this.emitLog("CallWrapper.emitFailure", "onFailed callback threw", {
478
+ prompt_id: targetPromptId,
479
+ error: callbackError instanceof Error ? callbackError.message : String(callbackError)
480
+ });
481
+ }
482
+ }
483
+ cancel(reason = "cancelled") {
484
+ if (this.cancellationRequested) {
485
+ this.emitLog("CallWrapper.cancel", "cancel already requested", {
486
+ promptId: this.promptId,
487
+ reason
488
+ });
489
+ return;
490
+ }
491
+ this.cancellationRequested = true;
492
+ this.emitLog("CallWrapper.cancel", "cancel requested", {
493
+ promptId: this.promptId,
494
+ reason
495
+ });
496
+ this.resolvePromptLoad(false);
497
+ this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: { reason } }), this.promptId);
498
+ this.cleanupListeners("cancel requested");
499
+ this.resolveJob(false);
500
+ }
501
+ startDisconnectRecovery() {
502
+ if (this.disconnectRecoveryActive || this.cancellationRequested) {
503
+ return;
504
+ }
505
+ this.disconnectRecoveryActive = true;
506
+ if (this.disconnectFailureTimer) {
507
+ clearTimeout(this.disconnectFailureTimer);
508
+ }
509
+ this.disconnectFailureTimer = setTimeout(() => this.failDisconnected("timeout"), DISCONNECT_FAILURE_GRACE_MS);
510
+ void this.attemptHistoryCompletion("disconnect_start");
511
+ }
512
+ stopDisconnectRecovery() {
513
+ if (!this.disconnectRecoveryActive) {
514
+ return;
515
+ }
516
+ this.disconnectRecoveryActive = false;
517
+ if (this.disconnectFailureTimer) {
518
+ clearTimeout(this.disconnectFailureTimer);
519
+ this.disconnectFailureTimer = null;
520
+ }
521
+ }
522
+ async attemptHistoryCompletion(reason) {
523
+ if (!this.promptId || this.cancellationRequested) {
524
+ return false;
525
+ }
526
+ try {
527
+ const output = await this.handleCachedOutput(this.promptId);
528
+ if (output && output !== false) {
529
+ this.emitLog("CallWrapper.historyRecovery", "completed from history", { reason, promptId: this.promptId });
530
+ this.stopDisconnectRecovery();
531
+ this.isCompletingSuccessfully = true;
532
+ this.resolvePromptLoad(false);
533
+ this.resolveJob(output);
534
+ this.cleanupListeners(`history recovery (${reason})`);
535
+ return true;
536
+ }
537
+ }
538
+ catch (error) {
539
+ this.emitLog("CallWrapper.historyRecovery", "history fetch failed", { reason, error: String(error) });
540
+ }
541
+ return false;
542
+ }
543
+ failDisconnected(reason) {
544
+ if (!this.disconnectRecoveryActive || this.isCompletingSuccessfully) {
545
+ return;
546
+ }
547
+ this.stopDisconnectRecovery();
548
+ this.emitLog("CallWrapper.enqueueJob", "disconnect recovery failed", { reason, promptId: this.promptId });
549
+ this.resolvePromptLoad(false);
550
+ this.resolveJob(false);
551
+ this.cleanupListeners("disconnect failure");
552
+ this.emitFailure(new DisconnectedError("Disconnected"), this.promptId);
553
+ }
554
+ async handleCachedOutput(promptId) {
555
+ const hisData = await this.client.ext.history.getHistory(promptId);
556
+ this.emitLog("CallWrapper.handleCachedOutput", "history fetched", {
557
+ promptId,
558
+ status: hisData?.status?.status_str,
559
+ completed: hisData?.status?.completed,
560
+ outputKeys: hisData?.outputs ? Object.keys(hisData.outputs) : [],
561
+ hasOutputs: !!(hisData && hisData.outputs && Object.keys(hisData.outputs).length > 0)
562
+ });
563
+ // Only return outputs if execution is actually completed
564
+ if (hisData && hisData.status?.completed && hisData.outputs) {
565
+ const output = this.mapOutput(hisData.outputs);
566
+ const hasDefinedValue = Object.entries(output).some(([key, value]) => {
567
+ if (key === "_raw") {
568
+ return value !== undefined && value !== null && Object.keys(value).length > 0;
569
+ }
570
+ return value !== undefined;
571
+ });
572
+ if (hasDefinedValue) {
573
+ this.emitLog("CallWrapper.handleCachedOutput", "returning completed outputs");
574
+ this.onFinishedFn?.(output, this.promptId);
575
+ return output;
576
+ }
577
+ else {
578
+ this.emitLog("CallWrapper.handleCachedOutput", "cached output missing defined values", {
579
+ promptId,
580
+ outputKeys: Object.keys(hisData.outputs ?? {}),
581
+ mappedKeys: this.prompt.mapOutputKeys
582
+ });
583
+ return false;
584
+ }
585
+ }
586
+ if (hisData && hisData.status?.completed && !hisData.outputs) {
587
+ this.emitLog("CallWrapper.handleCachedOutput", "history completed without outputs", { promptId });
588
+ return false;
589
+ }
590
+ if (hisData && !hisData.status?.completed) {
591
+ this.emitLog("CallWrapper.handleCachedOutput", "history not completed yet");
592
+ }
593
+ if (!hisData) {
594
+ this.emitLog("CallWrapper.handleCachedOutput", "history entry not available");
595
+ }
596
+ return null;
597
+ }
598
+ mapOutput(outputNodes) {
599
+ const outputMapped = this.prompt.mapOutputKeys;
600
+ const output = {};
601
+ for (const key in outputMapped) {
602
+ const node = outputMapped[key];
603
+ if (node) {
604
+ output[key] = outputNodes[node];
605
+ }
606
+ else {
607
+ if (!output._raw) {
608
+ output._raw = {};
609
+ }
610
+ output._raw[key] = outputNodes[key];
611
+ }
612
+ }
613
+ return output;
614
+ }
615
+ handleJobExecution(promptId) {
616
+ if (this.executionHandlerOffFn) {
617
+ return;
618
+ }
619
+ const reverseOutputMapped = this.reverseMapOutputKeys();
620
+ const mapOutputKeys = this.prompt.mapOutputKeys;
621
+ console.log(`[CallWrapper] handleJobExecution for ${promptId.substring(0, 8)}... - mapOutputKeys:`, mapOutputKeys, "reverseOutputMapped:", reverseOutputMapped);
622
+ this.progressHandlerOffFn = this.client.on("progress", (ev) => this.handleProgress(ev, promptId));
623
+ this.previewHandlerOffFn = this.client.on("b_preview", (ev) => {
624
+ // Note: b_preview events don't include prompt_id. They're scoped per connection.
625
+ // If multiple jobs use the same connection, they will all receive preview events.
626
+ // This is a limitation of the ComfyUI protocol - previews are not separated by prompt_id.
627
+ this.onPreviewFn?.(ev.detail, this.promptId);
628
+ });
629
+ // Also forward preview with metadata if available
630
+ const offPreviewMeta = this.client.on("b_preview_meta", (ev) => {
631
+ // Validate prompt_id from metadata if available to prevent cross-user preview leakage
632
+ const metadata = ev.detail.metadata;
633
+ const metaPromptId = metadata?.prompt_id;
634
+ if (metaPromptId && metaPromptId !== promptId) {
635
+ console.log(`[CallWrapper] Ignoring b_preview_meta for wrong prompt. Expected ${promptId.substring(0, 8)}..., got ${metaPromptId.substring(0, 8)}...`);
636
+ return;
637
+ }
638
+ this.onPreviewMetaFn?.(ev.detail, this.promptId);
639
+ });
640
+ const prevCleanup = this.previewHandlerOffFn;
641
+ this.previewHandlerOffFn = () => {
642
+ prevCleanup?.();
643
+ offPreviewMeta?.();
644
+ };
645
+ const totalOutput = Object.keys(reverseOutputMapped).length;
646
+ let remainingOutput = totalOutput;
647
+ console.log(`[CallWrapper] totalOutput=${totalOutput}, remainingOutput=${remainingOutput}`);
648
+ const executionHandler = (ev) => {
649
+ 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)}...`);
650
+ const eventPromptId = ev.detail.prompt_id;
651
+ const isCorrectPrompt = eventPromptId === promptId;
652
+ // STRICT: Only accept events where prompt_id matches our expected promptId
653
+ if (!isCorrectPrompt) {
654
+ console.log(`[CallWrapper.executionHandler] REJECTED - prompt_id mismatch (expected ${promptId.substring(0, 8)}..., got ${eventPromptId?.substring(0, 8)}...)`);
655
+ return;
656
+ }
657
+ const outputKey = reverseOutputMapped[ev.detail.node];
658
+ 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));
659
+ if (outputKey) {
660
+ this.output[outputKey] = ev.detail.output;
661
+ this.onOutputFn?.(outputKey, ev.detail.output, this.promptId);
662
+ remainingOutput--;
663
+ }
664
+ else {
665
+ this.output._raw = this.output._raw || {};
666
+ this.output._raw[ev.detail.node] = ev.detail.output;
667
+ this.onOutputFn?.(ev.detail.node, ev.detail.output, this.promptId);
668
+ }
669
+ console.log(`[CallWrapper] afterProcessing - remainingAfter: ${remainingOutput}, willTriggerCompletion: ${remainingOutput === 0}`);
670
+ if (remainingOutput === 0) {
671
+ console.log(`[CallWrapper] all outputs collected for ${promptId.substring(0, 8)}...`);
672
+ // Mark as successfully completing BEFORE cleanup to prevent race condition with disconnection handler
673
+ this.isCompletingSuccessfully = true;
674
+ this.cleanupListeners("all outputs collected");
675
+ this.onFinishedFn?.(this.output, this.promptId);
676
+ this.resolveJob(this.output);
677
+ }
678
+ };
679
+ const executedEnd = async () => {
680
+ console.log(`[CallWrapper] execution_success fired for ${promptId.substring(0, 8)}..., remainingOutput=${remainingOutput}, totalOutput=${totalOutput}`);
681
+ // If we've already marked this as successfully completing, don't fail it again
682
+ if (this.isCompletingSuccessfully) {
683
+ console.log(`[CallWrapper] Already marked as successfully completing, ignoring this execution_success`);
684
+ return;
685
+ }
686
+ if (remainingOutput === 0) {
687
+ console.log(`[CallWrapper] all outputs already collected, nothing to do`);
688
+ return;
689
+ }
690
+ // Wait briefly for outputs that might be arriving due to prompt ID mismatch
691
+ await new Promise((resolve) => setTimeout(resolve, 100));
692
+ console.log(`[CallWrapper] After wait - remainingOutput=${remainingOutput}, this.output keys:`, Object.keys(this.output));
693
+ // Check if outputs arrived while we were waiting
694
+ if (remainingOutput === 0) {
695
+ console.log(`[CallWrapper] Outputs arrived during wait - marking as complete`);
696
+ this.isCompletingSuccessfully = true;
697
+ this.cleanupListeners("executedEnd - outputs complete after wait");
698
+ this.onFinishedFn?.(this.output, this.promptId);
699
+ this.resolveJob(this.output);
700
+ return;
701
+ }
702
+ // Check if we have collected all outputs (even if prompt ID mismatch)
703
+ const hasAllOutputs = Object.keys(reverseOutputMapped).every((nodeId) => this.output[reverseOutputMapped[nodeId]] !== undefined);
704
+ if (hasAllOutputs) {
705
+ console.log(`[CallWrapper] Have all required outputs despite promptId mismatch - marking as complete`);
706
+ this.isCompletingSuccessfully = true;
707
+ this.cleanupListeners("executedEnd - outputs complete despite promptId mismatch");
708
+ this.onFinishedFn?.(this.output, this.promptId);
709
+ this.resolveJob(this.output);
710
+ return;
711
+ }
712
+ // Try to fetch from history with retry logic
713
+ let hisData = null;
714
+ for (let retries = 0; retries < 5; retries++) {
715
+ hisData = await this.client.ext.history.getHistory(promptId);
716
+ console.log(`[CallWrapper] History query result for ${promptId.substring(0, 8)}... (attempt ${retries + 1}) - status:`, hisData?.status, "outputs:", Object.keys(hisData?.outputs ?? {}).length);
717
+ if (hisData?.status?.completed && hisData.outputs) {
718
+ console.log(`[CallWrapper] Found completed job in history with outputs - attempting to populate from history`);
719
+ break;
720
+ }
721
+ if (retries < 4) {
722
+ console.log(`[CallWrapper] History not ready yet, waiting 100ms before retry...`);
723
+ await new Promise((resolve) => setTimeout(resolve, 100));
724
+ }
725
+ }
726
+ if (hisData?.status?.completed && hisData.outputs) {
727
+ // Try to extract outputs from history data
728
+ let populatedCount = 0;
729
+ for (const [nodeIdStr, nodeOutput] of Object.entries(hisData.outputs)) {
730
+ const nodeId = parseInt(nodeIdStr, 10);
731
+ const outputKey = reverseOutputMapped[nodeId];
732
+ if (outputKey && nodeOutput) {
733
+ // nodeOutput is typically { images: [...] } or similar - take the first property
734
+ const outputValue = Array.isArray(nodeOutput) ? nodeOutput[0] : Object.values(nodeOutput)[0];
735
+ if (outputValue !== undefined) {
736
+ this.output[outputKey] = outputValue;
737
+ this.onOutputFn?.(outputKey, outputValue, this.promptId);
738
+ populatedCount++;
739
+ remainingOutput--;
740
+ console.log(`[CallWrapper] Populated ${outputKey} from history`);
741
+ }
742
+ }
743
+ }
744
+ if (remainingOutput === 0) {
745
+ console.log(`[CallWrapper] Successfully populated all outputs from history for ${promptId.substring(0, 8)}...`);
746
+ this.isCompletingSuccessfully = true;
747
+ this.cleanupListeners("executedEnd - populated from history");
748
+ this.onFinishedFn?.(this.output, this.promptId);
749
+ this.resolveJob(this.output);
750
+ return;
751
+ }
752
+ if (populatedCount > 0) {
753
+ console.log(`[CallWrapper] Populated ${populatedCount} outputs from history (remainingOutput=${remainingOutput})`);
754
+ if (remainingOutput === 0) {
755
+ this.isCompletingSuccessfully = true;
756
+ this.cleanupListeners("executedEnd - all outputs from history");
757
+ this.onFinishedFn?.(this.output, this.promptId);
758
+ this.resolveJob(this.output);
759
+ return;
760
+ }
761
+ }
762
+ }
763
+ console.log(`[CallWrapper] execution failed due to missing outputs - remainingOutput=${remainingOutput}, totalOutput=${totalOutput}`);
764
+ this.emitFailure(new ExecutionFailedError("Execution failed"), this.promptId);
765
+ this.resolvePromptLoad(false);
766
+ this.cleanupListeners("executedEnd missing outputs");
767
+ this.resolveJob(false);
768
+ };
769
+ this.executionEndSuccessOffFn = this.client.on("execution_success", executedEnd);
770
+ this.executionHandlerOffFn = this.client.on("executed", executionHandler);
771
+ console.log(`[CallWrapper] Registered listeners for ${promptId.substring(0, 8)}... - executionHandler and executedEnd`);
772
+ this.errorHandlerOffFn = this.client.on("execution_error", (ev) => this.handleError(ev, promptId));
773
+ this.interruptionHandlerOffFn = this.client.on("execution_interrupted", (ev) => {
774
+ if (ev.detail.prompt_id !== promptId)
775
+ return;
776
+ this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: ev.detail }), ev.detail.prompt_id);
777
+ this.resolvePromptLoad(false);
778
+ this.cleanupListeners("execution interrupted");
779
+ this.resolveJob(false);
780
+ });
781
+ }
782
+ reverseMapOutputKeys() {
783
+ const outputMapped = this.prompt.mapOutputKeys;
784
+ return Object.entries(outputMapped).reduce((acc, [k, v]) => {
785
+ if (v)
786
+ acc[v] = k;
787
+ return acc;
788
+ }, {});
789
+ }
790
+ handleProgress(ev, promptId) {
791
+ if (ev.detail.prompt_id === promptId && !this.started) {
792
+ this.started = true;
793
+ this.onStartFn?.(this.promptId);
794
+ }
795
+ this.onProgressFn?.(ev.detail, this.promptId);
796
+ }
797
+ handleError(ev, promptId) {
798
+ if (ev.detail.prompt_id !== promptId)
799
+ return;
800
+ this.emitLog("CallWrapper.handleError", ev.detail.exception_type, {
801
+ prompt_id: ev.detail.prompt_id,
802
+ node_id: ev.detail?.node_id
803
+ });
804
+ this.emitFailure(new CustomEventError(ev.detail.exception_type, { cause: ev.detail }), ev.detail.prompt_id);
805
+ if (CALL_WRAPPER_DEBUG) {
806
+ console.log("[debug] handleError after emitFailure");
807
+ }
808
+ this.resolvePromptLoad(false);
809
+ if (CALL_WRAPPER_DEBUG) {
810
+ console.log("[debug] handleError before cleanup");
811
+ }
812
+ this.cleanupListeners("execution_error received");
813
+ if (CALL_WRAPPER_DEBUG) {
814
+ console.log("[debug] handleError after cleanup");
815
+ }
816
+ this.resolveJob(false);
817
+ }
818
+ emitLog(fnName, message, data) {
819
+ const detail = { fnName, message, data };
820
+ const customEvent = new CustomEvent("log", { detail });
821
+ const clientAny = this.client;
822
+ if (typeof clientAny.emit === "function") {
823
+ clientAny.emit("log", customEvent);
824
+ return;
825
+ }
826
+ clientAny.dispatchEvent?.(customEvent);
827
+ }
828
+ cleanupListeners(reason) {
829
+ const debugPayload = { reason, promptId: this.promptId };
830
+ this.emitLog("CallWrapper.cleanupListeners", "removing listeners", debugPayload);
831
+ this.resolvePromptLoad(false);
832
+ this.stopDisconnectRecovery();
833
+ this.onReconnectHandlerOffFn?.();
834
+ this.onReconnectHandlerOffFn = undefined;
835
+ this.onReconnectFailedHandlerOffFn?.();
836
+ this.onReconnectFailedHandlerOffFn = undefined;
837
+ this.disconnectFailureTimer = null;
838
+ this.onDisconnectedHandlerOffFn?.();
839
+ this.onDisconnectedHandlerOffFn = undefined;
840
+ this.checkExecutingOffFn?.();
841
+ this.checkExecutingOffFn = undefined;
842
+ this.checkExecutedOffFn?.();
843
+ this.checkExecutedOffFn = undefined;
844
+ this.progressHandlerOffFn?.();
845
+ this.progressHandlerOffFn = undefined;
846
+ this.previewHandlerOffFn?.();
847
+ this.previewHandlerOffFn = undefined;
848
+ this.executionHandlerOffFn?.();
849
+ this.executionHandlerOffFn = undefined;
850
+ this.errorHandlerOffFn?.();
851
+ this.errorHandlerOffFn = undefined;
852
+ this.executionEndSuccessOffFn?.();
853
+ this.executionEndSuccessOffFn = undefined;
854
+ this.interruptionHandlerOffFn?.();
855
+ this.interruptionHandlerOffFn = undefined;
856
+ this.statusHandlerOffFn?.();
857
+ this.statusHandlerOffFn = undefined;
858
+ }
859
+ }
857
860
  //# sourceMappingURL=call-wrapper.js.map