comfyui-node 1.4.2 → 1.4.4
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 +16 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/call-wrapper.js +567 -567
- 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 +2 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pool/WorkflowPool.d.ts +80 -0
- package/dist/pool/WorkflowPool.d.ts.map +1 -1
- package/dist/pool/WorkflowPool.js +608 -455
- package/dist/pool/WorkflowPool.js.map +1 -1
- package/dist/pool/client/ClientManager.d.ts +15 -1
- package/dist/pool/client/ClientManager.d.ts.map +1 -1
- package/dist/pool/client/ClientManager.js +35 -6
- package/dist/pool/client/ClientManager.js.map +1 -1
- package/dist/pool/failover/SmartFailoverStrategy.js +1 -1
- package/dist/pool/failover/SmartFailoverStrategy.js.map +1 -1
- package/dist/pool/index.d.ts +1 -0
- package/dist/pool/index.d.ts.map +1 -1
- package/dist/pool/profiling/JobProfiler.d.ts +130 -0
- package/dist/pool/profiling/JobProfiler.d.ts.map +1 -0
- package/dist/pool/profiling/JobProfiler.js +225 -0
- package/dist/pool/profiling/JobProfiler.js.map +1 -0
- package/dist/pool/queue/QueueAdapter.d.ts +30 -30
- package/dist/pool/queue/adapters/memory.d.ts +20 -20
- package/dist/pool/types/job.d.ts +3 -0
- package/dist/pool/types/job.d.ts.map +1 -1
- package/dist/pool/utils/hash.d.ts +13 -1
- package/dist/pool/utils/hash.d.ts.map +1 -1
- package/dist/pool/utils/hash.js +14 -1
- package/dist/pool/utils/hash.js.map +1 -1
- package/dist/pool.d.ts +180 -180
- package/dist/workflow.d.ts +27 -4
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +30 -7
- package/dist/workflow.js.map +1 -1
- package/package.json +2 -2
- package/README.OLD.md +0 -1395
|
@@ -1,456 +1,609 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { TypedEventTarget } from "../typed-event-target.js";
|
|
3
|
-
import { Workflow } from "../workflow.js";
|
|
4
|
-
import { PromptBuilder } from "../prompt-builder.js";
|
|
5
|
-
import { CallWrapper } from "../call-wrapper.js";
|
|
6
|
-
import { MemoryQueueAdapter } from "./queue/adapters/memory.js";
|
|
7
|
-
import { SmartFailoverStrategy } from "./failover/SmartFailoverStrategy.js";
|
|
8
|
-
import { ClientManager } from "./client/ClientManager.js";
|
|
9
|
-
import { hashWorkflow } from "./utils/hash.js";
|
|
10
|
-
import { cloneDeep } from "./utils/clone.js";
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
let
|
|
60
|
-
if (workflowInput instanceof Workflow) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
if (!
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
job
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
let
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
detail
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
job.clientId
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { TypedEventTarget } from "../typed-event-target.js";
|
|
3
|
+
import { Workflow } from "../workflow.js";
|
|
4
|
+
import { PromptBuilder } from "../prompt-builder.js";
|
|
5
|
+
import { CallWrapper } from "../call-wrapper.js";
|
|
6
|
+
import { MemoryQueueAdapter } from "./queue/adapters/memory.js";
|
|
7
|
+
import { SmartFailoverStrategy } from "./failover/SmartFailoverStrategy.js";
|
|
8
|
+
import { ClientManager } from "./client/ClientManager.js";
|
|
9
|
+
import { hashWorkflow } from "./utils/hash.js";
|
|
10
|
+
import { cloneDeep } from "./utils/clone.js";
|
|
11
|
+
import { JobProfiler } from "./profiling/JobProfiler.js";
|
|
12
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
13
|
+
const DEFAULT_RETRY_DELAY = 1000;
|
|
14
|
+
export class WorkflowPool extends TypedEventTarget {
|
|
15
|
+
queue;
|
|
16
|
+
strategy;
|
|
17
|
+
clientManager;
|
|
18
|
+
opts;
|
|
19
|
+
jobStore = new Map();
|
|
20
|
+
initPromise;
|
|
21
|
+
processing = false;
|
|
22
|
+
activeJobs = new Map();
|
|
23
|
+
constructor(clients, opts) {
|
|
24
|
+
super();
|
|
25
|
+
this.strategy = opts?.failoverStrategy ?? new SmartFailoverStrategy();
|
|
26
|
+
this.queue = opts?.queueAdapter ?? new MemoryQueueAdapter();
|
|
27
|
+
this.clientManager = new ClientManager(this.strategy, {
|
|
28
|
+
healthCheckIntervalMs: opts?.healthCheckIntervalMs ?? 30000
|
|
29
|
+
});
|
|
30
|
+
this.opts = opts ?? {};
|
|
31
|
+
this.clientManager.on("client:state", (ev) => {
|
|
32
|
+
this.dispatchEvent(new CustomEvent("client:state", { detail: ev.detail }));
|
|
33
|
+
});
|
|
34
|
+
this.clientManager.on("client:blocked_workflow", (ev) => {
|
|
35
|
+
this.dispatchEvent(new CustomEvent("client:blocked_workflow", { detail: ev.detail }));
|
|
36
|
+
});
|
|
37
|
+
this.clientManager.on("client:unblocked_workflow", (ev) => {
|
|
38
|
+
this.dispatchEvent(new CustomEvent("client:unblocked_workflow", { detail: ev.detail }));
|
|
39
|
+
});
|
|
40
|
+
this.initPromise = this.clientManager
|
|
41
|
+
.initialize(clients)
|
|
42
|
+
.then(() => {
|
|
43
|
+
this.dispatchEvent(new CustomEvent("pool:ready", {
|
|
44
|
+
detail: { clientIds: this.clientManager.list().map((c) => c.id) }
|
|
45
|
+
}));
|
|
46
|
+
})
|
|
47
|
+
.catch((error) => {
|
|
48
|
+
this.dispatchEvent(new CustomEvent("pool:error", { detail: { error } }));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async ready() {
|
|
52
|
+
await this.initPromise;
|
|
53
|
+
}
|
|
54
|
+
async enqueue(workflowInput, options) {
|
|
55
|
+
await this.ready();
|
|
56
|
+
const workflowJson = this.normalizeWorkflow(workflowInput);
|
|
57
|
+
// Use the workflow's pre-computed structureHash if available (from Workflow instance)
|
|
58
|
+
// Otherwise compute it from the JSON
|
|
59
|
+
let workflowHash;
|
|
60
|
+
if (workflowInput instanceof Workflow) {
|
|
61
|
+
workflowHash = workflowInput.structureHash ?? hashWorkflow(workflowJson);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
workflowHash = hashWorkflow(workflowJson);
|
|
65
|
+
}
|
|
66
|
+
const jobId = options?.jobId ?? this.generateJobId();
|
|
67
|
+
// Extract workflow metadata (outputAliases, outputNodeIds, etc.) if input is a Workflow instance
|
|
68
|
+
let workflowMeta;
|
|
69
|
+
if (workflowInput instanceof Workflow) {
|
|
70
|
+
workflowMeta = {
|
|
71
|
+
outputNodeIds: workflowInput.outputNodeIds ?? [],
|
|
72
|
+
outputAliases: workflowInput.outputAliases ?? {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const payload = {
|
|
76
|
+
jobId,
|
|
77
|
+
workflow: workflowJson,
|
|
78
|
+
workflowHash,
|
|
79
|
+
attempts: 0,
|
|
80
|
+
enqueuedAt: Date.now(),
|
|
81
|
+
workflowMeta,
|
|
82
|
+
options: {
|
|
83
|
+
maxAttempts: options?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
84
|
+
retryDelayMs: options?.retryDelayMs ?? DEFAULT_RETRY_DELAY,
|
|
85
|
+
priority: options?.priority ?? 0,
|
|
86
|
+
preferredClientIds: options?.preferredClientIds ?? [],
|
|
87
|
+
excludeClientIds: options?.excludeClientIds ?? [],
|
|
88
|
+
metadata: options?.metadata ?? {},
|
|
89
|
+
includeOutputs: options?.includeOutputs ?? []
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const record = {
|
|
93
|
+
...payload,
|
|
94
|
+
attachments: options?.attachments,
|
|
95
|
+
status: "queued"
|
|
96
|
+
};
|
|
97
|
+
this.jobStore.set(jobId, record);
|
|
98
|
+
await this.queue.enqueue(payload, { priority: payload.options.priority });
|
|
99
|
+
this.dispatchEvent(new CustomEvent("job:queued", { detail: { job: record } }));
|
|
100
|
+
void this.processQueue();
|
|
101
|
+
return jobId;
|
|
102
|
+
}
|
|
103
|
+
getJob(jobId) {
|
|
104
|
+
return this.jobStore.get(jobId);
|
|
105
|
+
}
|
|
106
|
+
async cancel(jobId) {
|
|
107
|
+
const record = this.jobStore.get(jobId);
|
|
108
|
+
if (!record) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (record.status === "queued") {
|
|
112
|
+
const removed = await this.queue.remove(jobId);
|
|
113
|
+
if (removed) {
|
|
114
|
+
record.status = "cancelled";
|
|
115
|
+
record.completedAt = Date.now();
|
|
116
|
+
this.dispatchEvent(new CustomEvent("job:cancelled", { detail: { job: record } }));
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const active = this.activeJobs.get(jobId);
|
|
121
|
+
if (active?.cancel) {
|
|
122
|
+
await active.cancel();
|
|
123
|
+
record.status = "cancelled";
|
|
124
|
+
record.completedAt = Date.now();
|
|
125
|
+
this.dispatchEvent(new CustomEvent("job:cancelled", { detail: { job: record } }));
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
async shutdown() {
|
|
131
|
+
this.clientManager.destroy();
|
|
132
|
+
await this.queue.shutdown();
|
|
133
|
+
for (const [, ctx] of Array.from(this.activeJobs)) {
|
|
134
|
+
ctx.release({ success: false });
|
|
135
|
+
}
|
|
136
|
+
this.activeJobs.clear();
|
|
137
|
+
}
|
|
138
|
+
async getQueueStats() {
|
|
139
|
+
return this.queue.stats();
|
|
140
|
+
}
|
|
141
|
+
normalizeWorkflow(input) {
|
|
142
|
+
if (typeof input === "string") {
|
|
143
|
+
return JSON.parse(input);
|
|
144
|
+
}
|
|
145
|
+
if (input instanceof Workflow) {
|
|
146
|
+
return cloneDeep(input.json ?? {});
|
|
147
|
+
}
|
|
148
|
+
if (typeof input?.toJSON === "function") {
|
|
149
|
+
return cloneDeep(input.toJSON());
|
|
150
|
+
}
|
|
151
|
+
return cloneDeep(input);
|
|
152
|
+
}
|
|
153
|
+
generateJobId() {
|
|
154
|
+
try {
|
|
155
|
+
return randomUUID();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return WorkflowPool.fallbackId();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
static fallbackId() {
|
|
162
|
+
return (globalThis.crypto && "randomUUID" in globalThis.crypto)
|
|
163
|
+
? globalThis.crypto.randomUUID()
|
|
164
|
+
: `job_${Math.random().toString(36).slice(2, 10)}`;
|
|
165
|
+
}
|
|
166
|
+
scheduleProcess(delayMs) {
|
|
167
|
+
const wait = Math.max(delayMs, 10);
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
void this.processQueue();
|
|
170
|
+
}, wait);
|
|
171
|
+
}
|
|
172
|
+
applyAutoSeed(workflow) {
|
|
173
|
+
const autoSeeds = {};
|
|
174
|
+
for (const [nodeId, nodeValue] of Object.entries(workflow)) {
|
|
175
|
+
if (!nodeValue || typeof nodeValue !== "object")
|
|
176
|
+
continue;
|
|
177
|
+
const inputs = nodeValue.inputs;
|
|
178
|
+
if (!inputs || typeof inputs !== "object")
|
|
179
|
+
continue;
|
|
180
|
+
if (typeof inputs.seed === "number" && inputs.seed === -1) {
|
|
181
|
+
const val = Math.floor(Math.random() * 2_147_483_647);
|
|
182
|
+
inputs.seed = val;
|
|
183
|
+
autoSeeds[nodeId] = val;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return autoSeeds;
|
|
187
|
+
}
|
|
188
|
+
async processQueue() {
|
|
189
|
+
if (this.processing) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.processing = true;
|
|
193
|
+
try {
|
|
194
|
+
while (true) {
|
|
195
|
+
const reservation = await this.queue.reserve();
|
|
196
|
+
if (!reservation) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const job = this.jobStore.get(reservation.payload.jobId);
|
|
200
|
+
if (!job) {
|
|
201
|
+
await this.queue.commit(reservation.reservationId);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const lease = this.clientManager.claim(job);
|
|
205
|
+
if (!lease) {
|
|
206
|
+
await this.queue.retry(reservation.reservationId, { delayMs: job.options.retryDelayMs });
|
|
207
|
+
this.scheduleProcess(job.options.retryDelayMs);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
this.runJob({ reservation, job, clientId: lease.clientId, release: lease.release }).catch((error) => {
|
|
211
|
+
console.error("[WorkflowPool] Unhandled job error", error);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
this.processing = false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async runJob(ctx) {
|
|
220
|
+
const { reservation, job, clientId, release } = ctx;
|
|
221
|
+
const managed = this.clientManager.getClient(clientId);
|
|
222
|
+
const client = managed?.client;
|
|
223
|
+
if (!client) {
|
|
224
|
+
await this.queue.retry(reservation.reservationId, { delayMs: job.options.retryDelayMs });
|
|
225
|
+
release({ success: false });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
job.status = "running";
|
|
229
|
+
job.clientId = clientId;
|
|
230
|
+
job.attempts += 1;
|
|
231
|
+
reservation.payload.attempts = job.attempts;
|
|
232
|
+
job.startedAt = Date.now();
|
|
233
|
+
// Don't dispatch job:started here - wait until we have promptId in onPending
|
|
234
|
+
// this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
|
|
235
|
+
const workflowPayload = cloneDeep(reservation.payload.workflow);
|
|
236
|
+
if (job.attachments?.length) {
|
|
237
|
+
for (const attachment of job.attachments) {
|
|
238
|
+
const filename = attachment.filename ?? `${job.jobId}-${attachment.nodeId}-${attachment.inputName}.bin`;
|
|
239
|
+
const blob = attachment.file instanceof Buffer ? new Blob([new Uint8Array(attachment.file)]) : attachment.file;
|
|
240
|
+
await client.ext.file.uploadImage(blob, filename, { override: true });
|
|
241
|
+
const node = workflowPayload[attachment.nodeId];
|
|
242
|
+
if (node?.inputs) {
|
|
243
|
+
node.inputs[attachment.inputName] = filename;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const autoSeeds = this.applyAutoSeed(workflowPayload);
|
|
248
|
+
let wfInstance = Workflow.from(workflowPayload);
|
|
249
|
+
if (job.options.includeOutputs?.length) {
|
|
250
|
+
for (const nodeId of job.options.includeOutputs) {
|
|
251
|
+
if (nodeId) {
|
|
252
|
+
wfInstance = wfInstance.output(nodeId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
wfInstance.inferDefaultOutputs?.();
|
|
257
|
+
// Use stored metadata if available (from Workflow instance), otherwise extract from recreated instance
|
|
258
|
+
const outputNodeIds = reservation.payload.workflowMeta?.outputNodeIds ??
|
|
259
|
+
wfInstance.outputNodeIds ??
|
|
260
|
+
job.options.includeOutputs ?? [];
|
|
261
|
+
const outputAliases = reservation.payload.workflowMeta?.outputAliases ??
|
|
262
|
+
wfInstance.outputAliases ?? {};
|
|
263
|
+
let promptBuilder = new PromptBuilder(wfInstance.json, wfInstance.inputPaths ?? [], outputNodeIds);
|
|
264
|
+
for (const nodeId of outputNodeIds) {
|
|
265
|
+
const alias = outputAliases[nodeId] ?? nodeId;
|
|
266
|
+
promptBuilder = promptBuilder.setOutputNode(alias, nodeId);
|
|
267
|
+
}
|
|
268
|
+
const wrapper = new CallWrapper(client, promptBuilder);
|
|
269
|
+
// Setup profiling if enabled
|
|
270
|
+
const profiler = this.opts.enableProfiling
|
|
271
|
+
? new JobProfiler(job.enqueuedAt, workflowPayload)
|
|
272
|
+
: undefined;
|
|
273
|
+
// Setup node execution timeout tracking
|
|
274
|
+
const nodeExecutionTimeout = this.opts.nodeExecutionTimeoutMs ?? 300000; // 5 minutes default
|
|
275
|
+
let nodeTimeoutId;
|
|
276
|
+
let lastNodeStartTime;
|
|
277
|
+
let currentExecutingNode = null;
|
|
278
|
+
const resetNodeTimeout = (nodeName) => {
|
|
279
|
+
if (nodeTimeoutId) {
|
|
280
|
+
clearTimeout(nodeTimeoutId);
|
|
281
|
+
nodeTimeoutId = undefined;
|
|
282
|
+
}
|
|
283
|
+
if (nodeExecutionTimeout > 0 && nodeName !== null) {
|
|
284
|
+
lastNodeStartTime = Date.now();
|
|
285
|
+
currentExecutingNode = nodeName || null;
|
|
286
|
+
nodeTimeoutId = setTimeout(() => {
|
|
287
|
+
const elapsed = Date.now() - (lastNodeStartTime || 0);
|
|
288
|
+
const nodeInfo = currentExecutingNode ? ` (node: ${currentExecutingNode})` : '';
|
|
289
|
+
rejectCompletion?.(new Error(`Node execution timeout: took longer than ${nodeExecutionTimeout}ms${nodeInfo}. ` +
|
|
290
|
+
`Actual time: ${elapsed}ms. Server may be stuck or node is too slow for configured timeout.`));
|
|
291
|
+
}, nodeExecutionTimeout);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const clearNodeTimeout = () => {
|
|
295
|
+
if (nodeTimeoutId) {
|
|
296
|
+
clearTimeout(nodeTimeoutId);
|
|
297
|
+
nodeTimeoutId = undefined;
|
|
298
|
+
}
|
|
299
|
+
currentExecutingNode = null;
|
|
300
|
+
lastNodeStartTime = undefined;
|
|
301
|
+
};
|
|
302
|
+
// Setup profiling event listeners on the raw ComfyUI client
|
|
303
|
+
if (profiler) {
|
|
304
|
+
const onExecutionStart = (event) => {
|
|
305
|
+
const promptId = event.detail?.prompt_id;
|
|
306
|
+
if (promptId) {
|
|
307
|
+
profiler.onExecutionStart(promptId);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const onExecutionCached = (event) => {
|
|
311
|
+
const nodes = event.detail?.nodes;
|
|
312
|
+
if (Array.isArray(nodes)) {
|
|
313
|
+
profiler.onCachedNodes(nodes.map(String));
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const onExecuting = (event) => {
|
|
317
|
+
const node = event.detail?.node;
|
|
318
|
+
if (node === null) {
|
|
319
|
+
// Workflow completed
|
|
320
|
+
profiler.onExecutionComplete();
|
|
321
|
+
}
|
|
322
|
+
else if (node !== undefined) {
|
|
323
|
+
profiler.onNodeExecuting(String(node));
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const onExecutionError = (event) => {
|
|
327
|
+
const detail = event.detail || {};
|
|
328
|
+
if (detail.node !== undefined) {
|
|
329
|
+
profiler.onNodeError(String(detail.node), detail.exception_message || 'Execution error');
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
// Attach listeners to client
|
|
333
|
+
client.addEventListener('execution_start', onExecutionStart);
|
|
334
|
+
client.addEventListener('execution_cached', onExecutionCached);
|
|
335
|
+
client.addEventListener('executing', onExecuting);
|
|
336
|
+
client.addEventListener('execution_error', onExecutionError);
|
|
337
|
+
// Cleanup function to remove listeners
|
|
338
|
+
const cleanupProfiler = () => {
|
|
339
|
+
client.removeEventListener('execution_start', onExecutionStart);
|
|
340
|
+
client.removeEventListener('execution_cached', onExecutionCached);
|
|
341
|
+
client.removeEventListener('executing', onExecuting);
|
|
342
|
+
client.removeEventListener('execution_error', onExecutionError);
|
|
343
|
+
};
|
|
344
|
+
// Ensure cleanup happens when job finishes
|
|
345
|
+
wrapper.onFinished(() => cleanupProfiler());
|
|
346
|
+
wrapper.onFailed(() => cleanupProfiler());
|
|
347
|
+
}
|
|
348
|
+
// Setup node execution timeout listeners (always active if timeout > 0)
|
|
349
|
+
const onNodeExecuting = (event) => {
|
|
350
|
+
const node = event.detail?.node;
|
|
351
|
+
if (node === null) {
|
|
352
|
+
// Workflow completed - clear timeout
|
|
353
|
+
clearNodeTimeout();
|
|
354
|
+
}
|
|
355
|
+
else if (node !== undefined) {
|
|
356
|
+
// New node started - reset timeout
|
|
357
|
+
resetNodeTimeout(String(node));
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const onNodeProgress = (event) => {
|
|
361
|
+
// Progress event means node is still working - reset timeout
|
|
362
|
+
if (event.detail?.node) {
|
|
363
|
+
resetNodeTimeout(String(event.detail.node));
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const onExecutionStarted = (event) => {
|
|
367
|
+
// Execution started - reset timeout for first node
|
|
368
|
+
resetNodeTimeout('execution_start');
|
|
369
|
+
};
|
|
370
|
+
if (nodeExecutionTimeout > 0) {
|
|
371
|
+
client.addEventListener('execution_start', onExecutionStarted);
|
|
372
|
+
client.addEventListener('executing', onNodeExecuting);
|
|
373
|
+
client.addEventListener('progress', onNodeProgress);
|
|
374
|
+
}
|
|
375
|
+
const cleanupNodeTimeout = () => {
|
|
376
|
+
clearNodeTimeout();
|
|
377
|
+
if (nodeExecutionTimeout > 0) {
|
|
378
|
+
client.removeEventListener('execution_start', onExecutionStarted);
|
|
379
|
+
client.removeEventListener('executing', onNodeExecuting);
|
|
380
|
+
client.removeEventListener('progress', onNodeProgress);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
let pendingSettled = false;
|
|
384
|
+
let resolvePending;
|
|
385
|
+
let rejectPending;
|
|
386
|
+
const pendingPromise = new Promise((resolve, reject) => {
|
|
387
|
+
resolvePending = () => {
|
|
388
|
+
if (!pendingSettled) {
|
|
389
|
+
pendingSettled = true;
|
|
390
|
+
resolve();
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
rejectPending = (err) => {
|
|
394
|
+
if (!pendingSettled) {
|
|
395
|
+
pendingSettled = true;
|
|
396
|
+
reject(err);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
let resolveCompletion;
|
|
401
|
+
let rejectCompletion;
|
|
402
|
+
const completionPromise = new Promise((resolve, reject) => {
|
|
403
|
+
resolveCompletion = resolve;
|
|
404
|
+
rejectCompletion = reject;
|
|
405
|
+
});
|
|
406
|
+
let jobStartedDispatched = false;
|
|
407
|
+
wrapper.onProgress((progress, promptId) => {
|
|
408
|
+
if (!job.promptId && promptId) {
|
|
409
|
+
job.promptId = promptId;
|
|
410
|
+
}
|
|
411
|
+
// Dispatch job:started on first progress update with promptId
|
|
412
|
+
if (!jobStartedDispatched && job.promptId) {
|
|
413
|
+
jobStartedDispatched = true;
|
|
414
|
+
this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
|
|
415
|
+
}
|
|
416
|
+
// Feed progress to profiler
|
|
417
|
+
if (profiler) {
|
|
418
|
+
profiler.onProgress(progress);
|
|
419
|
+
}
|
|
420
|
+
this.dispatchEvent(new CustomEvent("job:progress", {
|
|
421
|
+
detail: { jobId: job.jobId, clientId, progress }
|
|
422
|
+
}));
|
|
423
|
+
});
|
|
424
|
+
wrapper.onPreview((blob, promptId) => {
|
|
425
|
+
if (!job.promptId && promptId) {
|
|
426
|
+
job.promptId = promptId;
|
|
427
|
+
}
|
|
428
|
+
// Dispatch job:started on first preview with promptId
|
|
429
|
+
if (!jobStartedDispatched && job.promptId) {
|
|
430
|
+
jobStartedDispatched = true;
|
|
431
|
+
this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
|
|
432
|
+
}
|
|
433
|
+
this.dispatchEvent(new CustomEvent("job:preview", {
|
|
434
|
+
detail: { jobId: job.jobId, clientId, blob }
|
|
435
|
+
}));
|
|
436
|
+
});
|
|
437
|
+
wrapper.onPreviewMeta((payload, promptId) => {
|
|
438
|
+
if (!job.promptId && promptId) {
|
|
439
|
+
job.promptId = promptId;
|
|
440
|
+
}
|
|
441
|
+
// Dispatch job:started on first preview_meta with promptId
|
|
442
|
+
if (!jobStartedDispatched && job.promptId) {
|
|
443
|
+
jobStartedDispatched = true;
|
|
444
|
+
this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
|
|
445
|
+
}
|
|
446
|
+
this.dispatchEvent(new CustomEvent("job:preview_meta", {
|
|
447
|
+
detail: { jobId: job.jobId, clientId, payload }
|
|
448
|
+
}));
|
|
449
|
+
});
|
|
450
|
+
wrapper.onOutput((key, data, promptId) => {
|
|
451
|
+
if (!job.promptId && promptId) {
|
|
452
|
+
job.promptId = promptId;
|
|
453
|
+
}
|
|
454
|
+
this.dispatchEvent(new CustomEvent("job:output", {
|
|
455
|
+
detail: { jobId: job.jobId, clientId, key: String(key), data }
|
|
456
|
+
}));
|
|
457
|
+
});
|
|
458
|
+
wrapper.onPending((promptId) => {
|
|
459
|
+
if (!job.promptId && promptId) {
|
|
460
|
+
job.promptId = promptId;
|
|
461
|
+
}
|
|
462
|
+
// Don't dispatch job:started here - wait for first progress/preview with promptId
|
|
463
|
+
this.dispatchEvent(new CustomEvent("job:accepted", { detail: { job } }));
|
|
464
|
+
resolvePending?.();
|
|
465
|
+
});
|
|
466
|
+
wrapper.onStart((promptId) => {
|
|
467
|
+
if (!job.promptId && promptId) {
|
|
468
|
+
job.promptId = promptId;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
wrapper.onFinished((data, promptId) => {
|
|
472
|
+
if (!job.promptId && promptId) {
|
|
473
|
+
job.promptId = promptId;
|
|
474
|
+
}
|
|
475
|
+
job.status = "completed";
|
|
476
|
+
job.lastError = undefined;
|
|
477
|
+
const resultPayload = {};
|
|
478
|
+
for (const nodeId of outputNodeIds) {
|
|
479
|
+
const alias = outputAliases[nodeId] ?? nodeId;
|
|
480
|
+
// CallWrapper uses alias keys when mapOutputKeys is configured, fallback to nodeId
|
|
481
|
+
const nodeResult = data[alias];
|
|
482
|
+
const fallbackResult = data[nodeId];
|
|
483
|
+
const finalResult = nodeResult !== undefined ? nodeResult : fallbackResult;
|
|
484
|
+
resultPayload[alias] = finalResult;
|
|
485
|
+
}
|
|
486
|
+
resultPayload._nodes = [...outputNodeIds];
|
|
487
|
+
resultPayload._aliases = { ...outputAliases };
|
|
488
|
+
if (job.promptId) {
|
|
489
|
+
resultPayload._promptId = job.promptId;
|
|
490
|
+
}
|
|
491
|
+
if (Object.keys(autoSeeds).length) {
|
|
492
|
+
resultPayload._autoSeeds = { ...autoSeeds };
|
|
493
|
+
}
|
|
494
|
+
job.result = resultPayload;
|
|
495
|
+
job.completedAt = Date.now();
|
|
496
|
+
// Cleanup timeouts
|
|
497
|
+
cleanupNodeTimeout();
|
|
498
|
+
// Attach profiling stats if profiling was enabled
|
|
499
|
+
if (profiler) {
|
|
500
|
+
job.profileStats = profiler.getStats();
|
|
501
|
+
}
|
|
502
|
+
this.dispatchEvent(new CustomEvent("job:completed", { detail: { job } }));
|
|
503
|
+
resolveCompletion?.();
|
|
504
|
+
});
|
|
505
|
+
wrapper.onFailed((error, promptId) => {
|
|
506
|
+
if (!job.promptId && promptId) {
|
|
507
|
+
job.promptId = promptId;
|
|
508
|
+
}
|
|
509
|
+
job.lastError = error;
|
|
510
|
+
// Cleanup timeouts
|
|
511
|
+
cleanupNodeTimeout();
|
|
512
|
+
rejectPending?.(error);
|
|
513
|
+
rejectCompletion?.(error);
|
|
514
|
+
});
|
|
515
|
+
try {
|
|
516
|
+
const exec = wrapper.run();
|
|
517
|
+
// Add timeout for execution start to prevent jobs getting stuck
|
|
518
|
+
const executionStartTimeout = this.opts.executionStartTimeoutMs ?? 5000;
|
|
519
|
+
let pendingTimeoutId;
|
|
520
|
+
if (executionStartTimeout > 0) {
|
|
521
|
+
const pendingWithTimeout = Promise.race([
|
|
522
|
+
pendingPromise,
|
|
523
|
+
new Promise((_, reject) => {
|
|
524
|
+
pendingTimeoutId = setTimeout(() => {
|
|
525
|
+
reject(new Error(`Execution failed to start within ${executionStartTimeout}ms. ` +
|
|
526
|
+
`Server may be stuck or unresponsive.`));
|
|
527
|
+
}, executionStartTimeout);
|
|
528
|
+
})
|
|
529
|
+
]);
|
|
530
|
+
await pendingWithTimeout;
|
|
531
|
+
clearTimeout(pendingTimeoutId);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
await pendingPromise;
|
|
535
|
+
}
|
|
536
|
+
this.activeJobs.set(job.jobId, {
|
|
537
|
+
reservation,
|
|
538
|
+
job,
|
|
539
|
+
clientId,
|
|
540
|
+
release,
|
|
541
|
+
cancel: async () => {
|
|
542
|
+
try {
|
|
543
|
+
if (job.promptId) {
|
|
544
|
+
await client.ext.queue.interrupt(job.promptId);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
finally {
|
|
548
|
+
this.activeJobs.delete(job.jobId);
|
|
549
|
+
await this.queue.discard(reservation.reservationId, new Error("cancelled"));
|
|
550
|
+
release({ success: false });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
const result = await exec;
|
|
555
|
+
if (result === false) {
|
|
556
|
+
// Execution failed - try to get the error from completionPromise rejection
|
|
557
|
+
try {
|
|
558
|
+
await completionPromise;
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
throw err;
|
|
562
|
+
}
|
|
563
|
+
throw job.lastError ?? new Error("Execution failed");
|
|
564
|
+
}
|
|
565
|
+
await completionPromise;
|
|
566
|
+
await this.queue.commit(reservation.reservationId);
|
|
567
|
+
release({ success: true });
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
const latestStatus = this.jobStore.get(job.jobId)?.status;
|
|
571
|
+
if (latestStatus === "cancelled") {
|
|
572
|
+
release({ success: false });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
job.lastError = error;
|
|
576
|
+
job.status = "failed";
|
|
577
|
+
this.clientManager.recordFailure(clientId, job, error);
|
|
578
|
+
const remainingAttempts = job.options.maxAttempts - job.attempts;
|
|
579
|
+
const willRetry = remainingAttempts > 0;
|
|
580
|
+
this.dispatchEvent(new CustomEvent("job:failed", {
|
|
581
|
+
detail: { job, willRetry }
|
|
582
|
+
}));
|
|
583
|
+
if (willRetry) {
|
|
584
|
+
const delay = this.opts.retryBackoffMs ?? job.options.retryDelayMs;
|
|
585
|
+
this.dispatchEvent(new CustomEvent("job:retrying", { detail: { job, delayMs: delay } }));
|
|
586
|
+
job.status = "queued";
|
|
587
|
+
job.clientId = undefined;
|
|
588
|
+
job.promptId = undefined;
|
|
589
|
+
job.startedAt = undefined;
|
|
590
|
+
job.completedAt = undefined;
|
|
591
|
+
job.result = undefined;
|
|
592
|
+
await this.queue.retry(reservation.reservationId, { delayMs: delay });
|
|
593
|
+
this.dispatchEvent(new CustomEvent("job:queued", { detail: { job } }));
|
|
594
|
+
this.scheduleProcess(delay);
|
|
595
|
+
release({ success: false });
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
job.completedAt = Date.now();
|
|
599
|
+
await this.queue.discard(reservation.reservationId, error);
|
|
600
|
+
release({ success: false });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
finally {
|
|
604
|
+
this.activeJobs.delete(job.jobId);
|
|
605
|
+
void this.processQueue();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
456
609
|
//# sourceMappingURL=WorkflowPool.js.map
|