comfyui-node 1.4.1 → 1.4.2

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,438 +1,456 @@
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
- const DEFAULT_MAX_ATTEMPTS = 3;
12
- const DEFAULT_RETRY_DELAY = 1000;
13
- export class WorkflowPool extends TypedEventTarget {
14
- queue;
15
- strategy;
16
- clientManager;
17
- opts;
18
- jobStore = new Map();
19
- initPromise;
20
- processing = false;
21
- activeJobs = new Map();
22
- constructor(clients, opts) {
23
- super();
24
- this.strategy = opts?.failoverStrategy ?? new SmartFailoverStrategy();
25
- this.queue = opts?.queueAdapter ?? new MemoryQueueAdapter();
26
- this.clientManager = new ClientManager(this.strategy, {
27
- healthCheckIntervalMs: opts?.healthCheckIntervalMs ?? 30000
28
- });
29
- this.opts = opts ?? {};
30
- this.clientManager.on("client:state", (ev) => {
31
- this.dispatchEvent(new CustomEvent("client:state", { detail: ev.detail }));
32
- });
33
- this.clientManager.on("client:blocked_workflow", (ev) => {
34
- this.dispatchEvent(new CustomEvent("client:blocked_workflow", { detail: ev.detail }));
35
- });
36
- this.clientManager.on("client:unblocked_workflow", (ev) => {
37
- this.dispatchEvent(new CustomEvent("client:unblocked_workflow", { detail: ev.detail }));
38
- });
39
- this.initPromise = this.clientManager
40
- .initialize(clients)
41
- .then(() => {
42
- this.dispatchEvent(new CustomEvent("pool:ready", {
43
- detail: { clientIds: this.clientManager.list().map((c) => c.id) }
44
- }));
45
- })
46
- .catch((error) => {
47
- this.dispatchEvent(new CustomEvent("pool:error", { detail: { error } }));
48
- });
49
- }
50
- async ready() {
51
- await this.initPromise;
52
- }
53
- async enqueue(workflowInput, options) {
54
- await this.ready();
55
- const workflowJson = this.normalizeWorkflow(workflowInput);
56
- const workflowHash = hashWorkflow(workflowJson);
57
- const jobId = options?.jobId ?? this.generateJobId();
58
- // Extract workflow metadata (outputAliases, outputNodeIds, etc.) if input is a Workflow instance
59
- let workflowMeta;
60
- if (workflowInput instanceof Workflow) {
61
- workflowMeta = {
62
- outputNodeIds: workflowInput.outputNodeIds ?? [],
63
- outputAliases: workflowInput.outputAliases ?? {}
64
- };
65
- }
66
- const payload = {
67
- jobId,
68
- workflow: workflowJson,
69
- workflowHash,
70
- attempts: 0,
71
- enqueuedAt: Date.now(),
72
- workflowMeta,
73
- options: {
74
- maxAttempts: options?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
75
- retryDelayMs: options?.retryDelayMs ?? DEFAULT_RETRY_DELAY,
76
- priority: options?.priority ?? 0,
77
- preferredClientIds: options?.preferredClientIds ?? [],
78
- excludeClientIds: options?.excludeClientIds ?? [],
79
- metadata: options?.metadata ?? {},
80
- includeOutputs: options?.includeOutputs ?? []
81
- }
82
- };
83
- const record = {
84
- ...payload,
85
- attachments: options?.attachments,
86
- status: "queued"
87
- };
88
- this.jobStore.set(jobId, record);
89
- await this.queue.enqueue(payload, { priority: payload.options.priority });
90
- this.dispatchEvent(new CustomEvent("job:queued", { detail: { job: record } }));
91
- void this.processQueue();
92
- return jobId;
93
- }
94
- getJob(jobId) {
95
- return this.jobStore.get(jobId);
96
- }
97
- async cancel(jobId) {
98
- const record = this.jobStore.get(jobId);
99
- if (!record) {
100
- return false;
101
- }
102
- if (record.status === "queued") {
103
- const removed = await this.queue.remove(jobId);
104
- if (removed) {
105
- record.status = "cancelled";
106
- record.completedAt = Date.now();
107
- this.dispatchEvent(new CustomEvent("job:cancelled", { detail: { job: record } }));
108
- return true;
109
- }
110
- }
111
- const active = this.activeJobs.get(jobId);
112
- if (active?.cancel) {
113
- await active.cancel();
114
- record.status = "cancelled";
115
- record.completedAt = Date.now();
116
- this.dispatchEvent(new CustomEvent("job:cancelled", { detail: { job: record } }));
117
- return true;
118
- }
119
- return false;
120
- }
121
- async shutdown() {
122
- this.clientManager.destroy();
123
- await this.queue.shutdown();
124
- for (const [, ctx] of this.activeJobs) {
125
- ctx.release({ success: false });
126
- }
127
- this.activeJobs.clear();
128
- }
129
- async getQueueStats() {
130
- return this.queue.stats();
131
- }
132
- normalizeWorkflow(input) {
133
- if (typeof input === "string") {
134
- return JSON.parse(input);
135
- }
136
- if (input instanceof Workflow) {
137
- return cloneDeep(input.json ?? {});
138
- }
139
- if (typeof input?.toJSON === "function") {
140
- return cloneDeep(input.toJSON());
141
- }
142
- return cloneDeep(input);
143
- }
144
- generateJobId() {
145
- try {
146
- return randomUUID();
147
- }
148
- catch {
149
- return WorkflowPool.fallbackId();
150
- }
151
- }
152
- static fallbackId() {
153
- return (globalThis.crypto && "randomUUID" in globalThis.crypto)
154
- ? globalThis.crypto.randomUUID()
155
- : `job_${Math.random().toString(36).slice(2, 10)}`;
156
- }
157
- scheduleProcess(delayMs) {
158
- const wait = Math.max(delayMs, 10);
159
- setTimeout(() => {
160
- void this.processQueue();
161
- }, wait);
162
- }
163
- applyAutoSeed(workflow) {
164
- const autoSeeds = {};
165
- for (const [nodeId, nodeValue] of Object.entries(workflow)) {
166
- if (!nodeValue || typeof nodeValue !== "object")
167
- continue;
168
- const inputs = nodeValue.inputs;
169
- if (!inputs || typeof inputs !== "object")
170
- continue;
171
- if (typeof inputs.seed === "number" && inputs.seed === -1) {
172
- const val = Math.floor(Math.random() * 2_147_483_647);
173
- inputs.seed = val;
174
- autoSeeds[nodeId] = val;
175
- }
176
- }
177
- return autoSeeds;
178
- }
179
- async processQueue() {
180
- if (this.processing) {
181
- return;
182
- }
183
- this.processing = true;
184
- try {
185
- while (true) {
186
- const reservation = await this.queue.reserve();
187
- if (!reservation) {
188
- break;
189
- }
190
- const job = this.jobStore.get(reservation.payload.jobId);
191
- if (!job) {
192
- await this.queue.commit(reservation.reservationId);
193
- continue;
194
- }
195
- const lease = this.clientManager.claim(job);
196
- if (!lease) {
197
- await this.queue.retry(reservation.reservationId, { delayMs: job.options.retryDelayMs });
198
- this.scheduleProcess(job.options.retryDelayMs);
199
- break;
200
- }
201
- this.runJob({ reservation, job, clientId: lease.clientId, release: lease.release }).catch((error) => {
202
- console.error("[WorkflowPool] Unhandled job error", error);
203
- });
204
- }
205
- }
206
- finally {
207
- this.processing = false;
208
- }
209
- }
210
- async runJob(ctx) {
211
- const { reservation, job, clientId, release } = ctx;
212
- const managed = this.clientManager.getClient(clientId);
213
- const client = managed?.client;
214
- if (!client) {
215
- await this.queue.retry(reservation.reservationId, { delayMs: job.options.retryDelayMs });
216
- release({ success: false });
217
- return;
218
- }
219
- job.status = "running";
220
- job.clientId = clientId;
221
- job.attempts += 1;
222
- reservation.payload.attempts = job.attempts;
223
- job.startedAt = Date.now();
224
- this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
225
- const workflowPayload = cloneDeep(reservation.payload.workflow);
226
- if (job.attachments?.length) {
227
- for (const attachment of job.attachments) {
228
- const filename = attachment.filename ?? `${job.jobId}-${attachment.nodeId}-${attachment.inputName}.bin`;
229
- const blob = attachment.file instanceof Buffer ? new Blob([new Uint8Array(attachment.file)]) : attachment.file;
230
- await client.ext.file.uploadImage(blob, filename, { override: true });
231
- const node = workflowPayload[attachment.nodeId];
232
- if (node?.inputs) {
233
- node.inputs[attachment.inputName] = filename;
234
- }
235
- }
236
- }
237
- const autoSeeds = this.applyAutoSeed(workflowPayload);
238
- let wfInstance = Workflow.from(workflowPayload);
239
- if (job.options.includeOutputs?.length) {
240
- for (const nodeId of job.options.includeOutputs) {
241
- if (nodeId) {
242
- wfInstance = wfInstance.output(nodeId);
243
- }
244
- }
245
- }
246
- wfInstance.inferDefaultOutputs?.();
247
- // Use stored metadata if available (from Workflow instance), otherwise extract from recreated instance
248
- const outputNodeIds = reservation.payload.workflowMeta?.outputNodeIds ??
249
- wfInstance.outputNodeIds ??
250
- job.options.includeOutputs ?? [];
251
- const outputAliases = reservation.payload.workflowMeta?.outputAliases ??
252
- wfInstance.outputAliases ?? {};
253
- let promptBuilder = new PromptBuilder(wfInstance.json, wfInstance.inputPaths ?? [], outputNodeIds);
254
- for (const nodeId of outputNodeIds) {
255
- const alias = outputAliases[nodeId] ?? nodeId;
256
- promptBuilder = promptBuilder.setOutputNode(alias, nodeId);
257
- }
258
- const wrapper = new CallWrapper(client, promptBuilder);
259
- let pendingSettled = false;
260
- let resolvePending;
261
- let rejectPending;
262
- const pendingPromise = new Promise((resolve, reject) => {
263
- resolvePending = () => {
264
- if (!pendingSettled) {
265
- pendingSettled = true;
266
- resolve();
267
- }
268
- };
269
- rejectPending = (err) => {
270
- if (!pendingSettled) {
271
- pendingSettled = true;
272
- reject(err);
273
- }
274
- };
275
- });
276
- let resolveCompletion;
277
- let rejectCompletion;
278
- const completionPromise = new Promise((resolve, reject) => {
279
- resolveCompletion = resolve;
280
- rejectCompletion = reject;
281
- });
282
- wrapper.onProgress((progress, promptId) => {
283
- if (!job.promptId && promptId) {
284
- job.promptId = promptId;
285
- }
286
- this.dispatchEvent(new CustomEvent("job:progress", {
287
- detail: { jobId: job.jobId, clientId, progress }
288
- }));
289
- });
290
- wrapper.onPreview((blob, promptId) => {
291
- if (!job.promptId && promptId) {
292
- job.promptId = promptId;
293
- }
294
- this.dispatchEvent(new CustomEvent("job:preview", {
295
- detail: { jobId: job.jobId, clientId, blob }
296
- }));
297
- });
298
- wrapper.onPreviewMeta((payload, promptId) => {
299
- if (!job.promptId && promptId) {
300
- job.promptId = promptId;
301
- }
302
- this.dispatchEvent(new CustomEvent("job:preview_meta", {
303
- detail: { jobId: job.jobId, clientId, payload }
304
- }));
305
- });
306
- wrapper.onOutput((key, data, promptId) => {
307
- if (!job.promptId && promptId) {
308
- job.promptId = promptId;
309
- }
310
- this.dispatchEvent(new CustomEvent("job:output", {
311
- detail: { jobId: job.jobId, clientId, key: String(key), data }
312
- }));
313
- });
314
- wrapper.onPending((promptId) => {
315
- if (!job.promptId && promptId) {
316
- job.promptId = promptId;
317
- }
318
- this.dispatchEvent(new CustomEvent("job:accepted", { detail: { job } }));
319
- resolvePending?.();
320
- });
321
- wrapper.onStart((promptId) => {
322
- if (!job.promptId && promptId) {
323
- job.promptId = promptId;
324
- }
325
- });
326
- wrapper.onFinished((data, promptId) => {
327
- if (!job.promptId && promptId) {
328
- job.promptId = promptId;
329
- }
330
- job.status = "completed";
331
- job.lastError = undefined;
332
- const resultPayload = {};
333
- for (const nodeId of outputNodeIds) {
334
- const alias = outputAliases[nodeId] ?? nodeId;
335
- // CallWrapper uses alias keys when mapOutputKeys is configured, fallback to nodeId
336
- const nodeResult = data[alias];
337
- const fallbackResult = data[nodeId];
338
- const finalResult = nodeResult !== undefined ? nodeResult : fallbackResult;
339
- resultPayload[alias] = finalResult;
340
- }
341
- resultPayload._nodes = [...outputNodeIds];
342
- resultPayload._aliases = { ...outputAliases };
343
- if (job.promptId) {
344
- resultPayload._promptId = job.promptId;
345
- }
346
- if (Object.keys(autoSeeds).length) {
347
- resultPayload._autoSeeds = { ...autoSeeds };
348
- }
349
- job.result = resultPayload;
350
- job.completedAt = Date.now();
351
- this.dispatchEvent(new CustomEvent("job:completed", { detail: { job } }));
352
- resolveCompletion?.();
353
- });
354
- wrapper.onFailed((error, promptId) => {
355
- if (!job.promptId && promptId) {
356
- job.promptId = promptId;
357
- }
358
- job.lastError = error;
359
- rejectPending?.(error);
360
- rejectCompletion?.(error);
361
- });
362
- try {
363
- const exec = wrapper.run();
364
- await pendingPromise;
365
- this.activeJobs.set(job.jobId, {
366
- reservation,
367
- job,
368
- clientId,
369
- release,
370
- cancel: async () => {
371
- try {
372
- if (job.promptId) {
373
- await client.ext.queue.interrupt(job.promptId);
374
- }
375
- }
376
- finally {
377
- this.activeJobs.delete(job.jobId);
378
- await this.queue.discard(reservation.reservationId, new Error("cancelled"));
379
- release({ success: false });
380
- }
381
- }
382
- });
383
- const result = await exec;
384
- if (result === false) {
385
- // Execution failed - try to get the error from completionPromise rejection
386
- try {
387
- await completionPromise;
388
- }
389
- catch (err) {
390
- throw err;
391
- }
392
- throw job.lastError ?? new Error("Execution failed");
393
- }
394
- await completionPromise;
395
- await this.queue.commit(reservation.reservationId);
396
- release({ success: true });
397
- }
398
- catch (error) {
399
- const latestStatus = this.jobStore.get(job.jobId)?.status;
400
- if (latestStatus === "cancelled") {
401
- release({ success: false });
402
- return;
403
- }
404
- job.lastError = error;
405
- job.status = "failed";
406
- this.clientManager.recordFailure(clientId, job, error);
407
- const remainingAttempts = job.options.maxAttempts - job.attempts;
408
- const willRetry = remainingAttempts > 0;
409
- this.dispatchEvent(new CustomEvent("job:failed", {
410
- detail: { job, willRetry }
411
- }));
412
- if (willRetry) {
413
- const delay = this.opts.retryBackoffMs ?? job.options.retryDelayMs;
414
- this.dispatchEvent(new CustomEvent("job:retrying", { detail: { job, delayMs: delay } }));
415
- job.status = "queued";
416
- job.clientId = undefined;
417
- job.promptId = undefined;
418
- job.startedAt = undefined;
419
- job.completedAt = undefined;
420
- job.result = undefined;
421
- await this.queue.retry(reservation.reservationId, { delayMs: delay });
422
- this.dispatchEvent(new CustomEvent("job:queued", { detail: { job } }));
423
- this.scheduleProcess(delay);
424
- release({ success: false });
425
- }
426
- else {
427
- job.completedAt = Date.now();
428
- await this.queue.discard(reservation.reservationId, error);
429
- release({ success: false });
430
- }
431
- }
432
- finally {
433
- this.activeJobs.delete(job.jobId);
434
- void this.processQueue();
435
- }
436
- }
437
- }
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
+ const DEFAULT_MAX_ATTEMPTS = 3;
12
+ const DEFAULT_RETRY_DELAY = 1000;
13
+ export class WorkflowPool extends TypedEventTarget {
14
+ queue;
15
+ strategy;
16
+ clientManager;
17
+ opts;
18
+ jobStore = new Map();
19
+ initPromise;
20
+ processing = false;
21
+ activeJobs = new Map();
22
+ constructor(clients, opts) {
23
+ super();
24
+ this.strategy = opts?.failoverStrategy ?? new SmartFailoverStrategy();
25
+ this.queue = opts?.queueAdapter ?? new MemoryQueueAdapter();
26
+ this.clientManager = new ClientManager(this.strategy, {
27
+ healthCheckIntervalMs: opts?.healthCheckIntervalMs ?? 30000
28
+ });
29
+ this.opts = opts ?? {};
30
+ this.clientManager.on("client:state", (ev) => {
31
+ this.dispatchEvent(new CustomEvent("client:state", { detail: ev.detail }));
32
+ });
33
+ this.clientManager.on("client:blocked_workflow", (ev) => {
34
+ this.dispatchEvent(new CustomEvent("client:blocked_workflow", { detail: ev.detail }));
35
+ });
36
+ this.clientManager.on("client:unblocked_workflow", (ev) => {
37
+ this.dispatchEvent(new CustomEvent("client:unblocked_workflow", { detail: ev.detail }));
38
+ });
39
+ this.initPromise = this.clientManager
40
+ .initialize(clients)
41
+ .then(() => {
42
+ this.dispatchEvent(new CustomEvent("pool:ready", {
43
+ detail: { clientIds: this.clientManager.list().map((c) => c.id) }
44
+ }));
45
+ })
46
+ .catch((error) => {
47
+ this.dispatchEvent(new CustomEvent("pool:error", { detail: { error } }));
48
+ });
49
+ }
50
+ async ready() {
51
+ await this.initPromise;
52
+ }
53
+ async enqueue(workflowInput, options) {
54
+ await this.ready();
55
+ const workflowJson = this.normalizeWorkflow(workflowInput);
56
+ const workflowHash = hashWorkflow(workflowJson);
57
+ const jobId = options?.jobId ?? this.generateJobId();
58
+ // Extract workflow metadata (outputAliases, outputNodeIds, etc.) if input is a Workflow instance
59
+ let workflowMeta;
60
+ if (workflowInput instanceof Workflow) {
61
+ workflowMeta = {
62
+ outputNodeIds: workflowInput.outputNodeIds ?? [],
63
+ outputAliases: workflowInput.outputAliases ?? {}
64
+ };
65
+ }
66
+ const payload = {
67
+ jobId,
68
+ workflow: workflowJson,
69
+ workflowHash,
70
+ attempts: 0,
71
+ enqueuedAt: Date.now(),
72
+ workflowMeta,
73
+ options: {
74
+ maxAttempts: options?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
75
+ retryDelayMs: options?.retryDelayMs ?? DEFAULT_RETRY_DELAY,
76
+ priority: options?.priority ?? 0,
77
+ preferredClientIds: options?.preferredClientIds ?? [],
78
+ excludeClientIds: options?.excludeClientIds ?? [],
79
+ metadata: options?.metadata ?? {},
80
+ includeOutputs: options?.includeOutputs ?? []
81
+ }
82
+ };
83
+ const record = {
84
+ ...payload,
85
+ attachments: options?.attachments,
86
+ status: "queued"
87
+ };
88
+ this.jobStore.set(jobId, record);
89
+ await this.queue.enqueue(payload, { priority: payload.options.priority });
90
+ this.dispatchEvent(new CustomEvent("job:queued", { detail: { job: record } }));
91
+ void this.processQueue();
92
+ return jobId;
93
+ }
94
+ getJob(jobId) {
95
+ return this.jobStore.get(jobId);
96
+ }
97
+ async cancel(jobId) {
98
+ const record = this.jobStore.get(jobId);
99
+ if (!record) {
100
+ return false;
101
+ }
102
+ if (record.status === "queued") {
103
+ const removed = await this.queue.remove(jobId);
104
+ if (removed) {
105
+ record.status = "cancelled";
106
+ record.completedAt = Date.now();
107
+ this.dispatchEvent(new CustomEvent("job:cancelled", { detail: { job: record } }));
108
+ return true;
109
+ }
110
+ }
111
+ const active = this.activeJobs.get(jobId);
112
+ if (active?.cancel) {
113
+ await active.cancel();
114
+ record.status = "cancelled";
115
+ record.completedAt = Date.now();
116
+ this.dispatchEvent(new CustomEvent("job:cancelled", { detail: { job: record } }));
117
+ return true;
118
+ }
119
+ return false;
120
+ }
121
+ async shutdown() {
122
+ this.clientManager.destroy();
123
+ await this.queue.shutdown();
124
+ for (const [, ctx] of this.activeJobs) {
125
+ ctx.release({ success: false });
126
+ }
127
+ this.activeJobs.clear();
128
+ }
129
+ async getQueueStats() {
130
+ return this.queue.stats();
131
+ }
132
+ normalizeWorkflow(input) {
133
+ if (typeof input === "string") {
134
+ return JSON.parse(input);
135
+ }
136
+ if (input instanceof Workflow) {
137
+ return cloneDeep(input.json ?? {});
138
+ }
139
+ if (typeof input?.toJSON === "function") {
140
+ return cloneDeep(input.toJSON());
141
+ }
142
+ return cloneDeep(input);
143
+ }
144
+ generateJobId() {
145
+ try {
146
+ return randomUUID();
147
+ }
148
+ catch {
149
+ return WorkflowPool.fallbackId();
150
+ }
151
+ }
152
+ static fallbackId() {
153
+ return (globalThis.crypto && "randomUUID" in globalThis.crypto)
154
+ ? globalThis.crypto.randomUUID()
155
+ : `job_${Math.random().toString(36).slice(2, 10)}`;
156
+ }
157
+ scheduleProcess(delayMs) {
158
+ const wait = Math.max(delayMs, 10);
159
+ setTimeout(() => {
160
+ void this.processQueue();
161
+ }, wait);
162
+ }
163
+ applyAutoSeed(workflow) {
164
+ const autoSeeds = {};
165
+ for (const [nodeId, nodeValue] of Object.entries(workflow)) {
166
+ if (!nodeValue || typeof nodeValue !== "object")
167
+ continue;
168
+ const inputs = nodeValue.inputs;
169
+ if (!inputs || typeof inputs !== "object")
170
+ continue;
171
+ if (typeof inputs.seed === "number" && inputs.seed === -1) {
172
+ const val = Math.floor(Math.random() * 2_147_483_647);
173
+ inputs.seed = val;
174
+ autoSeeds[nodeId] = val;
175
+ }
176
+ }
177
+ return autoSeeds;
178
+ }
179
+ async processQueue() {
180
+ if (this.processing) {
181
+ return;
182
+ }
183
+ this.processing = true;
184
+ try {
185
+ while (true) {
186
+ const reservation = await this.queue.reserve();
187
+ if (!reservation) {
188
+ break;
189
+ }
190
+ const job = this.jobStore.get(reservation.payload.jobId);
191
+ if (!job) {
192
+ await this.queue.commit(reservation.reservationId);
193
+ continue;
194
+ }
195
+ const lease = this.clientManager.claim(job);
196
+ if (!lease) {
197
+ await this.queue.retry(reservation.reservationId, { delayMs: job.options.retryDelayMs });
198
+ this.scheduleProcess(job.options.retryDelayMs);
199
+ break;
200
+ }
201
+ this.runJob({ reservation, job, clientId: lease.clientId, release: lease.release }).catch((error) => {
202
+ console.error("[WorkflowPool] Unhandled job error", error);
203
+ });
204
+ }
205
+ }
206
+ finally {
207
+ this.processing = false;
208
+ }
209
+ }
210
+ async runJob(ctx) {
211
+ const { reservation, job, clientId, release } = ctx;
212
+ const managed = this.clientManager.getClient(clientId);
213
+ const client = managed?.client;
214
+ if (!client) {
215
+ await this.queue.retry(reservation.reservationId, { delayMs: job.options.retryDelayMs });
216
+ release({ success: false });
217
+ return;
218
+ }
219
+ job.status = "running";
220
+ job.clientId = clientId;
221
+ job.attempts += 1;
222
+ reservation.payload.attempts = job.attempts;
223
+ job.startedAt = Date.now();
224
+ // Don't dispatch job:started here - wait until we have promptId in onPending
225
+ // this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
226
+ const workflowPayload = cloneDeep(reservation.payload.workflow);
227
+ if (job.attachments?.length) {
228
+ for (const attachment of job.attachments) {
229
+ const filename = attachment.filename ?? `${job.jobId}-${attachment.nodeId}-${attachment.inputName}.bin`;
230
+ const blob = attachment.file instanceof Buffer ? new Blob([new Uint8Array(attachment.file)]) : attachment.file;
231
+ await client.ext.file.uploadImage(blob, filename, { override: true });
232
+ const node = workflowPayload[attachment.nodeId];
233
+ if (node?.inputs) {
234
+ node.inputs[attachment.inputName] = filename;
235
+ }
236
+ }
237
+ }
238
+ const autoSeeds = this.applyAutoSeed(workflowPayload);
239
+ let wfInstance = Workflow.from(workflowPayload);
240
+ if (job.options.includeOutputs?.length) {
241
+ for (const nodeId of job.options.includeOutputs) {
242
+ if (nodeId) {
243
+ wfInstance = wfInstance.output(nodeId);
244
+ }
245
+ }
246
+ }
247
+ wfInstance.inferDefaultOutputs?.();
248
+ // Use stored metadata if available (from Workflow instance), otherwise extract from recreated instance
249
+ const outputNodeIds = reservation.payload.workflowMeta?.outputNodeIds ??
250
+ wfInstance.outputNodeIds ??
251
+ job.options.includeOutputs ?? [];
252
+ const outputAliases = reservation.payload.workflowMeta?.outputAliases ??
253
+ wfInstance.outputAliases ?? {};
254
+ let promptBuilder = new PromptBuilder(wfInstance.json, wfInstance.inputPaths ?? [], outputNodeIds);
255
+ for (const nodeId of outputNodeIds) {
256
+ const alias = outputAliases[nodeId] ?? nodeId;
257
+ promptBuilder = promptBuilder.setOutputNode(alias, nodeId);
258
+ }
259
+ const wrapper = new CallWrapper(client, promptBuilder);
260
+ let pendingSettled = false;
261
+ let resolvePending;
262
+ let rejectPending;
263
+ const pendingPromise = new Promise((resolve, reject) => {
264
+ resolvePending = () => {
265
+ if (!pendingSettled) {
266
+ pendingSettled = true;
267
+ resolve();
268
+ }
269
+ };
270
+ rejectPending = (err) => {
271
+ if (!pendingSettled) {
272
+ pendingSettled = true;
273
+ reject(err);
274
+ }
275
+ };
276
+ });
277
+ let resolveCompletion;
278
+ let rejectCompletion;
279
+ const completionPromise = new Promise((resolve, reject) => {
280
+ resolveCompletion = resolve;
281
+ rejectCompletion = reject;
282
+ });
283
+ let jobStartedDispatched = false;
284
+ wrapper.onProgress((progress, promptId) => {
285
+ if (!job.promptId && promptId) {
286
+ job.promptId = promptId;
287
+ }
288
+ // Dispatch job:started on first progress update with promptId
289
+ if (!jobStartedDispatched && job.promptId) {
290
+ jobStartedDispatched = true;
291
+ this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
292
+ }
293
+ this.dispatchEvent(new CustomEvent("job:progress", {
294
+ detail: { jobId: job.jobId, clientId, progress }
295
+ }));
296
+ });
297
+ wrapper.onPreview((blob, promptId) => {
298
+ if (!job.promptId && promptId) {
299
+ job.promptId = promptId;
300
+ }
301
+ // Dispatch job:started on first preview with promptId
302
+ if (!jobStartedDispatched && job.promptId) {
303
+ jobStartedDispatched = true;
304
+ this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
305
+ }
306
+ this.dispatchEvent(new CustomEvent("job:preview", {
307
+ detail: { jobId: job.jobId, clientId, blob }
308
+ }));
309
+ });
310
+ wrapper.onPreviewMeta((payload, promptId) => {
311
+ if (!job.promptId && promptId) {
312
+ job.promptId = promptId;
313
+ }
314
+ // Dispatch job:started on first preview_meta with promptId
315
+ if (!jobStartedDispatched && job.promptId) {
316
+ jobStartedDispatched = true;
317
+ this.dispatchEvent(new CustomEvent("job:started", { detail: { job } }));
318
+ }
319
+ this.dispatchEvent(new CustomEvent("job:preview_meta", {
320
+ detail: { jobId: job.jobId, clientId, payload }
321
+ }));
322
+ });
323
+ wrapper.onOutput((key, data, promptId) => {
324
+ if (!job.promptId && promptId) {
325
+ job.promptId = promptId;
326
+ }
327
+ this.dispatchEvent(new CustomEvent("job:output", {
328
+ detail: { jobId: job.jobId, clientId, key: String(key), data }
329
+ }));
330
+ });
331
+ wrapper.onPending((promptId) => {
332
+ if (!job.promptId && promptId) {
333
+ job.promptId = promptId;
334
+ }
335
+ // Don't dispatch job:started here - wait for first progress/preview with promptId
336
+ this.dispatchEvent(new CustomEvent("job:accepted", { detail: { job } }));
337
+ resolvePending?.();
338
+ });
339
+ wrapper.onStart((promptId) => {
340
+ if (!job.promptId && promptId) {
341
+ job.promptId = promptId;
342
+ }
343
+ });
344
+ wrapper.onFinished((data, promptId) => {
345
+ if (!job.promptId && promptId) {
346
+ job.promptId = promptId;
347
+ }
348
+ job.status = "completed";
349
+ job.lastError = undefined;
350
+ const resultPayload = {};
351
+ for (const nodeId of outputNodeIds) {
352
+ const alias = outputAliases[nodeId] ?? nodeId;
353
+ // CallWrapper uses alias keys when mapOutputKeys is configured, fallback to nodeId
354
+ const nodeResult = data[alias];
355
+ const fallbackResult = data[nodeId];
356
+ const finalResult = nodeResult !== undefined ? nodeResult : fallbackResult;
357
+ resultPayload[alias] = finalResult;
358
+ }
359
+ resultPayload._nodes = [...outputNodeIds];
360
+ resultPayload._aliases = { ...outputAliases };
361
+ if (job.promptId) {
362
+ resultPayload._promptId = job.promptId;
363
+ }
364
+ if (Object.keys(autoSeeds).length) {
365
+ resultPayload._autoSeeds = { ...autoSeeds };
366
+ }
367
+ job.result = resultPayload;
368
+ job.completedAt = Date.now();
369
+ this.dispatchEvent(new CustomEvent("job:completed", { detail: { job } }));
370
+ resolveCompletion?.();
371
+ });
372
+ wrapper.onFailed((error, promptId) => {
373
+ if (!job.promptId && promptId) {
374
+ job.promptId = promptId;
375
+ }
376
+ job.lastError = error;
377
+ rejectPending?.(error);
378
+ rejectCompletion?.(error);
379
+ });
380
+ try {
381
+ const exec = wrapper.run();
382
+ await pendingPromise;
383
+ this.activeJobs.set(job.jobId, {
384
+ reservation,
385
+ job,
386
+ clientId,
387
+ release,
388
+ cancel: async () => {
389
+ try {
390
+ if (job.promptId) {
391
+ await client.ext.queue.interrupt(job.promptId);
392
+ }
393
+ }
394
+ finally {
395
+ this.activeJobs.delete(job.jobId);
396
+ await this.queue.discard(reservation.reservationId, new Error("cancelled"));
397
+ release({ success: false });
398
+ }
399
+ }
400
+ });
401
+ const result = await exec;
402
+ if (result === false) {
403
+ // Execution failed - try to get the error from completionPromise rejection
404
+ try {
405
+ await completionPromise;
406
+ }
407
+ catch (err) {
408
+ throw err;
409
+ }
410
+ throw job.lastError ?? new Error("Execution failed");
411
+ }
412
+ await completionPromise;
413
+ await this.queue.commit(reservation.reservationId);
414
+ release({ success: true });
415
+ }
416
+ catch (error) {
417
+ const latestStatus = this.jobStore.get(job.jobId)?.status;
418
+ if (latestStatus === "cancelled") {
419
+ release({ success: false });
420
+ return;
421
+ }
422
+ job.lastError = error;
423
+ job.status = "failed";
424
+ this.clientManager.recordFailure(clientId, job, error);
425
+ const remainingAttempts = job.options.maxAttempts - job.attempts;
426
+ const willRetry = remainingAttempts > 0;
427
+ this.dispatchEvent(new CustomEvent("job:failed", {
428
+ detail: { job, willRetry }
429
+ }));
430
+ if (willRetry) {
431
+ const delay = this.opts.retryBackoffMs ?? job.options.retryDelayMs;
432
+ this.dispatchEvent(new CustomEvent("job:retrying", { detail: { job, delayMs: delay } }));
433
+ job.status = "queued";
434
+ job.clientId = undefined;
435
+ job.promptId = undefined;
436
+ job.startedAt = undefined;
437
+ job.completedAt = undefined;
438
+ job.result = undefined;
439
+ await this.queue.retry(reservation.reservationId, { delayMs: delay });
440
+ this.dispatchEvent(new CustomEvent("job:queued", { detail: { job } }));
441
+ this.scheduleProcess(delay);
442
+ release({ success: false });
443
+ }
444
+ else {
445
+ job.completedAt = Date.now();
446
+ await this.queue.discard(reservation.reservationId, error);
447
+ release({ success: false });
448
+ }
449
+ }
450
+ finally {
451
+ this.activeJobs.delete(job.jobId);
452
+ void this.processQueue();
453
+ }
454
+ }
455
+ }
438
456
  //# sourceMappingURL=WorkflowPool.js.map