comfyui-node 1.4.4 → 1.6.0
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.
- package/README.md +21 -16
- package/dist/.tsbuildinfo +1 -1
- package/dist/call-wrapper.d.ts +141 -124
- package/dist/call-wrapper.d.ts.map +1 -1
- package/dist/call-wrapper.js +353 -64
- package/dist/call-wrapper.js.map +1 -1
- package/dist/client.d.ts +290 -290
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +78 -19
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pool/SmartPool.d.ts +144 -0
- package/dist/pool/SmartPool.d.ts.map +1 -0
- package/dist/pool/SmartPool.js +677 -0
- package/dist/pool/SmartPool.js.map +1 -0
- package/dist/pool/SmartPoolV2.d.ts +120 -0
- package/dist/pool/SmartPoolV2.d.ts.map +1 -0
- package/dist/pool/SmartPoolV2.js +587 -0
- package/dist/pool/SmartPoolV2.js.map +1 -0
- package/dist/pool/WorkflowPool.d.ts +32 -2
- package/dist/pool/WorkflowPool.d.ts.map +1 -1
- package/dist/pool/WorkflowPool.js +298 -66
- package/dist/pool/WorkflowPool.js.map +1 -1
- package/dist/pool/client/ClientManager.d.ts +4 -2
- package/dist/pool/client/ClientManager.d.ts.map +1 -1
- package/dist/pool/client/ClientManager.js +29 -9
- package/dist/pool/client/ClientManager.js.map +1 -1
- package/dist/pool/index.d.ts +2 -0
- package/dist/pool/index.d.ts.map +1 -1
- package/dist/pool/index.js +2 -0
- package/dist/pool/index.js.map +1 -1
- package/dist/pool/queue/QueueAdapter.d.ts +32 -30
- package/dist/pool/queue/QueueAdapter.d.ts.map +1 -1
- package/dist/pool/queue/adapters/memory.d.ts +22 -20
- package/dist/pool/queue/adapters/memory.d.ts.map +1 -1
- package/dist/pool/queue/adapters/memory.js +14 -2
- package/dist/pool/queue/adapters/memory.js.map +1 -1
- package/dist/pool/types/affinity.d.ts +6 -0
- package/dist/pool/types/affinity.d.ts.map +1 -0
- package/dist/pool/types/affinity.js +2 -0
- package/dist/pool/types/affinity.js.map +1 -0
- package/dist/pool/types/job.d.ts.map +1 -1
- package/dist/pool/utils/failure-analysis.d.ts +14 -0
- package/dist/pool/utils/failure-analysis.d.ts.map +1 -0
- package/dist/pool/utils/failure-analysis.js +224 -0
- package/dist/pool/utils/failure-analysis.js.map +1 -0
- package/dist/pool.d.ts +180 -180
- package/dist/types/error.d.ts +31 -1
- package/dist/types/error.d.ts.map +1 -1
- package/dist/types/error.js +30 -0
- package/dist/types/error.js.map +1 -1
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +4 -1
- package/dist/workflow.js.map +1 -1
- package/package.json +4 -3
package/dist/call-wrapper.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { FailedCacheError, WentMissingError, EnqueueFailedError, DisconnectedError, CustomEventError, ExecutionFailedError, ExecutionInterruptedError, MissingNodeError } from "./types/error.js";
|
|
2
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";
|
|
3
5
|
/**
|
|
4
6
|
* Represents a wrapper class for making API calls using the ComfyApi client.
|
|
5
7
|
* Provides methods for setting callback functions and executing the job.
|
|
@@ -19,6 +21,15 @@ export class CallWrapper {
|
|
|
19
21
|
onFinishedFn;
|
|
20
22
|
onFailedFn;
|
|
21
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;
|
|
22
33
|
onDisconnectedHandlerOffFn;
|
|
23
34
|
checkExecutingOffFn;
|
|
24
35
|
checkExecutedOffFn;
|
|
@@ -133,19 +144,39 @@ export class CallWrapper {
|
|
|
133
144
|
* Start the job execution.
|
|
134
145
|
*/
|
|
135
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;
|
|
136
152
|
const job = await this.enqueueJob();
|
|
137
153
|
if (!job) {
|
|
138
154
|
// enqueueJob already invoked onFailed with a rich error instance; just abort.
|
|
139
155
|
this.emitLog("CallWrapper.run", "enqueue failed -> abort");
|
|
140
156
|
return false;
|
|
141
157
|
}
|
|
142
|
-
let promptLoadTrigger;
|
|
143
158
|
const promptLoadCached = new Promise((resolve) => {
|
|
144
|
-
promptLoadTrigger =
|
|
159
|
+
this.promptLoadTrigger = (value) => {
|
|
160
|
+
if (this.promptLoadTrigger) {
|
|
161
|
+
this.promptLoadTrigger = null;
|
|
162
|
+
}
|
|
163
|
+
resolve(value);
|
|
164
|
+
};
|
|
145
165
|
});
|
|
146
|
-
let jobDoneTrigger;
|
|
147
166
|
const jobDonePromise = new Promise((resolve) => {
|
|
148
|
-
|
|
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
|
+
}
|
|
149
180
|
});
|
|
150
181
|
/**
|
|
151
182
|
* Declare the function to check if the job is executing.
|
|
@@ -153,7 +184,7 @@ export class CallWrapper {
|
|
|
153
184
|
const checkExecutingFn = (event) => {
|
|
154
185
|
if (event.detail && event.detail.prompt_id === job.prompt_id) {
|
|
155
186
|
this.emitLog("CallWrapper.run", "executing observed", { node: event.detail.node });
|
|
156
|
-
|
|
187
|
+
this.resolvePromptLoad(false);
|
|
157
188
|
}
|
|
158
189
|
};
|
|
159
190
|
/**
|
|
@@ -171,7 +202,7 @@ export class CallWrapper {
|
|
|
171
202
|
nodes: event.detail.nodes,
|
|
172
203
|
expected: outputNodes
|
|
173
204
|
});
|
|
174
|
-
|
|
205
|
+
this.resolvePromptLoad(cached);
|
|
175
206
|
}
|
|
176
207
|
};
|
|
177
208
|
/**
|
|
@@ -200,6 +231,15 @@ export class CallWrapper {
|
|
|
200
231
|
this.emitLog("CallWrapper.status", "cached output already handled");
|
|
201
232
|
return;
|
|
202
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
|
+
}
|
|
203
243
|
wentMissing = true;
|
|
204
244
|
const output = await this.handleCachedOutput(job.prompt_id);
|
|
205
245
|
if (output) {
|
|
@@ -207,22 +247,31 @@ export class CallWrapper {
|
|
|
207
247
|
this.emitLog("CallWrapper.status", "output from history after missing", {
|
|
208
248
|
prompt_id: job.prompt_id
|
|
209
249
|
});
|
|
210
|
-
|
|
250
|
+
this.resolvePromptLoad(false);
|
|
251
|
+
this.resolveJob(output);
|
|
211
252
|
this.cleanupListeners("status handler resolved from history");
|
|
212
253
|
return;
|
|
213
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
|
+
}
|
|
214
263
|
cachedOutputDone = true;
|
|
215
264
|
this.emitLog("CallWrapper.status", "job missing without cached output", {
|
|
216
265
|
prompt_id: job.prompt_id
|
|
217
266
|
});
|
|
218
|
-
|
|
219
|
-
|
|
267
|
+
this.resolvePromptLoad(false);
|
|
268
|
+
this.resolveJob(false);
|
|
220
269
|
this.cleanupListeners("status handler missing");
|
|
221
|
-
this.
|
|
270
|
+
this.emitFailure(new WentMissingError("The job went missing!"), job.prompt_id);
|
|
222
271
|
};
|
|
223
272
|
this.statusHandlerOffFn = this.client.on("status", statusHandler);
|
|
224
273
|
// Attach execution listeners immediately so fast jobs cannot finish before we subscribe
|
|
225
|
-
this.handleJobExecution(job.prompt_id
|
|
274
|
+
this.handleJobExecution(job.prompt_id);
|
|
226
275
|
await promptLoadCached;
|
|
227
276
|
if (wentMissing) {
|
|
228
277
|
return jobDonePromise;
|
|
@@ -232,14 +281,14 @@ export class CallWrapper {
|
|
|
232
281
|
if (output) {
|
|
233
282
|
cachedOutputDone = true;
|
|
234
283
|
this.cleanupListeners("no cached output values returned");
|
|
235
|
-
|
|
284
|
+
this.resolveJob(output);
|
|
236
285
|
return output;
|
|
237
286
|
}
|
|
238
287
|
if (output === false) {
|
|
239
288
|
cachedOutputDone = true;
|
|
240
289
|
this.cleanupListeners("cached output ready before execution listeners");
|
|
241
|
-
this.
|
|
242
|
-
|
|
290
|
+
this.emitFailure(new FailedCacheError("Failed to get cached output"), this.promptId);
|
|
291
|
+
this.resolveJob(false);
|
|
243
292
|
return false;
|
|
244
293
|
}
|
|
245
294
|
this.emitLog("CallWrapper.run", "no cached output -> proceed with execution listeners");
|
|
@@ -305,10 +354,10 @@ export class CallWrapper {
|
|
|
305
354
|
}
|
|
306
355
|
catch (e) {
|
|
307
356
|
if (e instanceof Response) {
|
|
308
|
-
this.
|
|
357
|
+
this.emitFailure(new MissingNodeError("Failed to get workflow node definitions", { cause: await e.json() }), this.promptId);
|
|
309
358
|
}
|
|
310
359
|
else {
|
|
311
|
-
this.
|
|
360
|
+
this.emitFailure(new MissingNodeError("There was a missing node in the workflow bypass.", { cause: e }), this.promptId);
|
|
312
361
|
}
|
|
313
362
|
return null;
|
|
314
363
|
}
|
|
@@ -320,22 +369,22 @@ export class CallWrapper {
|
|
|
320
369
|
catch (e) {
|
|
321
370
|
try {
|
|
322
371
|
if (e instanceof EnqueueFailedError) {
|
|
323
|
-
this.
|
|
372
|
+
this.emitFailure(e, this.promptId);
|
|
324
373
|
}
|
|
325
374
|
else if (e instanceof Response) {
|
|
326
375
|
const err = await buildEnqueueFailedError(e);
|
|
327
|
-
this.
|
|
376
|
+
this.emitFailure(err, this.promptId);
|
|
328
377
|
}
|
|
329
378
|
else if (e && typeof e === "object" && "response" in e && e.response instanceof Response) {
|
|
330
379
|
const err = await buildEnqueueFailedError(e.response);
|
|
331
|
-
this.
|
|
380
|
+
this.emitFailure(err, this.promptId);
|
|
332
381
|
}
|
|
333
382
|
else {
|
|
334
|
-
this.
|
|
383
|
+
this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: e, reason: e?.message }), this.promptId);
|
|
335
384
|
}
|
|
336
385
|
}
|
|
337
386
|
catch (inner) {
|
|
338
|
-
this.
|
|
387
|
+
this.emitFailure(new EnqueueFailedError("Failed to queue prompt", { cause: inner }), this.promptId);
|
|
339
388
|
}
|
|
340
389
|
job = null;
|
|
341
390
|
}
|
|
@@ -343,20 +392,162 @@ export class CallWrapper {
|
|
|
343
392
|
return;
|
|
344
393
|
}
|
|
345
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));
|
|
346
397
|
this.emitLog("CallWrapper.enqueueJob", "queued", { prompt_id: this.promptId });
|
|
347
398
|
this.onPendingFn?.(this.promptId);
|
|
348
399
|
this.onDisconnectedHandlerOffFn = this.client.on("disconnected", () => {
|
|
349
|
-
// Ignore disconnection if we are already successfully completing
|
|
350
|
-
// This prevents a race condition where outputs are collected successfully
|
|
351
|
-
// but the WebSocket disconnects before cleanupListeners() is called
|
|
352
400
|
if (this.isCompletingSuccessfully) {
|
|
353
401
|
this.emitLog("CallWrapper.enqueueJob", "disconnected during success completion -> ignored");
|
|
354
402
|
return;
|
|
355
403
|
}
|
|
356
|
-
this.
|
|
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");
|
|
357
421
|
});
|
|
358
422
|
return job;
|
|
359
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
|
+
}
|
|
360
551
|
async handleCachedOutput(promptId) {
|
|
361
552
|
const hisData = await this.client.ext.history.getHistory(promptId);
|
|
362
553
|
this.emitLog("CallWrapper.handleCachedOutput", "history fetched", {
|
|
@@ -418,15 +609,31 @@ export class CallWrapper {
|
|
|
418
609
|
}
|
|
419
610
|
return output;
|
|
420
611
|
}
|
|
421
|
-
handleJobExecution(promptId
|
|
612
|
+
handleJobExecution(promptId) {
|
|
422
613
|
if (this.executionHandlerOffFn) {
|
|
423
614
|
return;
|
|
424
615
|
}
|
|
425
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);
|
|
426
619
|
this.progressHandlerOffFn = this.client.on("progress", (ev) => this.handleProgress(ev, promptId));
|
|
427
|
-
this.previewHandlerOffFn = this.client.on("b_preview", (ev) =>
|
|
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
|
+
});
|
|
428
626
|
// Also forward preview with metadata if available
|
|
429
|
-
const offPreviewMeta = this.client.on("b_preview_meta", (ev) =>
|
|
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
|
+
});
|
|
430
637
|
const prevCleanup = this.previewHandlerOffFn;
|
|
431
638
|
this.previewHandlerOffFn = () => {
|
|
432
639
|
prevCleanup?.();
|
|
@@ -434,16 +641,18 @@ export class CallWrapper {
|
|
|
434
641
|
};
|
|
435
642
|
const totalOutput = Object.keys(reverseOutputMapped).length;
|
|
436
643
|
let remainingOutput = totalOutput;
|
|
644
|
+
console.log(`[CallWrapper] totalOutput=${totalOutput}, remainingOutput=${remainingOutput}`);
|
|
437
645
|
const executionHandler = (ev) => {
|
|
438
|
-
|
|
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)}...)`);
|
|
439
652
|
return;
|
|
653
|
+
}
|
|
440
654
|
const outputKey = reverseOutputMapped[ev.detail.node];
|
|
441
|
-
|
|
442
|
-
node: ev.detail.node,
|
|
443
|
-
outputKey,
|
|
444
|
-
remainingBefore: remainingOutput,
|
|
445
|
-
isTrackedOutput: !!outputKey
|
|
446
|
-
});
|
|
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));
|
|
447
656
|
if (outputKey) {
|
|
448
657
|
this.output[outputKey] = ev.detail.output;
|
|
449
658
|
this.onOutputFn?.(outputKey, ev.detail.output, this.promptId);
|
|
@@ -454,54 +663,117 @@ export class CallWrapper {
|
|
|
454
663
|
this.output._raw[ev.detail.node] = ev.detail.output;
|
|
455
664
|
this.onOutputFn?.(ev.detail.node, ev.detail.output, this.promptId);
|
|
456
665
|
}
|
|
457
|
-
|
|
458
|
-
remainingAfter: remainingOutput,
|
|
459
|
-
willTriggerCompletion: remainingOutput === 0
|
|
460
|
-
});
|
|
666
|
+
console.log(`[CallWrapper] afterProcessing - remainingAfter: ${remainingOutput}, willTriggerCompletion: ${remainingOutput === 0}`);
|
|
461
667
|
if (remainingOutput === 0) {
|
|
462
|
-
|
|
668
|
+
console.log(`[CallWrapper] all outputs collected for ${promptId.substring(0, 8)}...`);
|
|
463
669
|
// Mark as successfully completing BEFORE cleanup to prevent race condition with disconnection handler
|
|
464
670
|
this.isCompletingSuccessfully = true;
|
|
465
671
|
this.cleanupListeners("all outputs collected");
|
|
466
672
|
this.onFinishedFn?.(this.output, this.promptId);
|
|
467
|
-
|
|
673
|
+
this.resolveJob(this.output);
|
|
468
674
|
}
|
|
469
675
|
};
|
|
470
676
|
const executedEnd = async () => {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
476
691
|
if (remainingOutput === 0) {
|
|
477
|
-
|
|
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);
|
|
478
707
|
return;
|
|
479
708
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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);
|
|
485
747
|
return;
|
|
486
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
|
+
}
|
|
487
759
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
});
|
|
492
|
-
this.onFailedFn?.(new ExecutionFailedError("Execution failed"), this.promptId);
|
|
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);
|
|
493
763
|
this.cleanupListeners("executedEnd missing outputs");
|
|
494
|
-
|
|
764
|
+
this.resolveJob(false);
|
|
495
765
|
};
|
|
496
766
|
this.executionEndSuccessOffFn = this.client.on("execution_success", executedEnd);
|
|
497
767
|
this.executionHandlerOffFn = this.client.on("executed", executionHandler);
|
|
498
|
-
|
|
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));
|
|
499
770
|
this.interruptionHandlerOffFn = this.client.on("execution_interrupted", (ev) => {
|
|
500
771
|
if (ev.detail.prompt_id !== promptId)
|
|
501
772
|
return;
|
|
502
|
-
this.
|
|
773
|
+
this.emitFailure(new ExecutionInterruptedError("The execution was interrupted!", { cause: ev.detail }), ev.detail.prompt_id);
|
|
774
|
+
this.resolvePromptLoad(false);
|
|
503
775
|
this.cleanupListeners("execution interrupted");
|
|
504
|
-
|
|
776
|
+
this.resolveJob(false);
|
|
505
777
|
});
|
|
506
778
|
}
|
|
507
779
|
reverseMapOutputKeys() {
|
|
@@ -519,16 +791,26 @@ export class CallWrapper {
|
|
|
519
791
|
}
|
|
520
792
|
this.onProgressFn?.(ev.detail, this.promptId);
|
|
521
793
|
}
|
|
522
|
-
handleError(ev, promptId
|
|
794
|
+
handleError(ev, promptId) {
|
|
523
795
|
if (ev.detail.prompt_id !== promptId)
|
|
524
796
|
return;
|
|
525
797
|
this.emitLog("CallWrapper.handleError", ev.detail.exception_type, {
|
|
526
798
|
prompt_id: ev.detail.prompt_id,
|
|
527
799
|
node_id: ev.detail?.node_id
|
|
528
800
|
});
|
|
529
|
-
this.
|
|
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
|
+
}
|
|
530
809
|
this.cleanupListeners("execution_error received");
|
|
531
|
-
|
|
810
|
+
if (CALL_WRAPPER_DEBUG) {
|
|
811
|
+
console.log("[debug] handleError after cleanup");
|
|
812
|
+
}
|
|
813
|
+
this.resolveJob(false);
|
|
532
814
|
}
|
|
533
815
|
emitLog(fnName, message, data) {
|
|
534
816
|
const detail = { fnName, message, data };
|
|
@@ -543,6 +825,13 @@ export class CallWrapper {
|
|
|
543
825
|
cleanupListeners(reason) {
|
|
544
826
|
const debugPayload = { reason, promptId: this.promptId };
|
|
545
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;
|
|
546
835
|
this.onDisconnectedHandlerOffFn?.();
|
|
547
836
|
this.onDisconnectedHandlerOffFn = undefined;
|
|
548
837
|
this.checkExecutingOffFn?.();
|