@comfyorg/sdk 0.1.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 +236 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/low/errors.d.ts +73 -0
- package/dist/low/errors.js +104 -0
- package/dist/low/generated/index.d.ts +1 -0
- package/dist/low/generated/index.js +2 -0
- package/dist/low/generated/types.gen.d.ts +528 -0
- package/dist/low/generated/types.gen.js +2 -0
- package/dist/low/generated/zod.gen.d.ts +455 -0
- package/dist/low/generated/zod.gen.js +204 -0
- package/dist/low/index.d.ts +21 -0
- package/dist/low/index.js +21 -0
- package/dist/low/models.d.ts +47 -0
- package/dist/low/models.js +16 -0
- package/dist/low/sse.d.ts +24 -0
- package/dist/low/sse.js +44 -0
- package/dist/low/transport.d.ts +128 -0
- package/dist/low/transport.js +285 -0
- package/dist/sdk/abortable-sleep.d.ts +13 -0
- package/dist/sdk/abortable-sleep.js +31 -0
- package/dist/sdk/assets.d.ts +74 -0
- package/dist/sdk/assets.js +200 -0
- package/dist/sdk/client.d.ts +70 -0
- package/dist/sdk/client.js +120 -0
- package/dist/sdk/core.d.ts +33 -0
- package/dist/sdk/core.js +89 -0
- package/dist/sdk/events.d.ts +60 -0
- package/dist/sdk/events.js +84 -0
- package/dist/sdk/exceptions.d.ts +79 -0
- package/dist/sdk/exceptions.js +114 -0
- package/dist/sdk/hashing.d.ts +14 -0
- package/dist/sdk/hashing.js +28 -0
- package/dist/sdk/index.d.ts +13 -0
- package/dist/sdk/index.js +12 -0
- package/dist/sdk/jobs.d.ts +47 -0
- package/dist/sdk/jobs.js +145 -0
- package/dist/sdk/outputs.d.ts +26 -0
- package/dist/sdk/outputs.js +48 -0
- package/dist/sdk/workflows.d.ts +32 -0
- package/dist/sdk/workflows.js +45 -0
- package/package.json +52 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idiomatic `sdk` exceptions.
|
|
3
|
+
*
|
|
4
|
+
* These wrap the protocol-level `low.ApiError` codes with names an
|
|
5
|
+
* integrator catches directly (`JobFailed`, `QueueFull`, ...). `toSdkError`
|
|
6
|
+
* maps a raised `ApiError` to the right subclass; anything unmapped stays a
|
|
7
|
+
* `ComfyError` carrying the original code. Mirrors `comfy_sdk.exceptions`
|
|
8
|
+
* in the Python SDK.
|
|
9
|
+
*/
|
|
10
|
+
import { ApiError } from "../low/index.js";
|
|
11
|
+
import type { JobError } from "../low/index.js";
|
|
12
|
+
export interface ComfyErrorOptions {
|
|
13
|
+
code?: string;
|
|
14
|
+
httpStatus?: number;
|
|
15
|
+
details?: Record<string, unknown> | null;
|
|
16
|
+
}
|
|
17
|
+
export declare class ComfyError extends Error {
|
|
18
|
+
readonly code?: string;
|
|
19
|
+
readonly httpStatus?: number;
|
|
20
|
+
readonly details: Record<string, unknown> | null;
|
|
21
|
+
constructor(message: string, options?: ComfyErrorOptions);
|
|
22
|
+
}
|
|
23
|
+
/** The surface rejected the request for lack of a valid key. Comfy Cloud
|
|
24
|
+
* and serverless require a key; a self-hosted proxy needs none. */
|
|
25
|
+
export declare class Unauthorized extends ComfyError {
|
|
26
|
+
}
|
|
27
|
+
export declare class Forbidden extends ComfyError {
|
|
28
|
+
}
|
|
29
|
+
export declare class NotFound extends ComfyError {
|
|
30
|
+
}
|
|
31
|
+
/** Structural/validation failure; `details` carries per-node errors. */
|
|
32
|
+
export declare class InvalidWorkflow extends ComfyError {
|
|
33
|
+
}
|
|
34
|
+
/** UI-export JSON was submitted instead of the API-format graph. */
|
|
35
|
+
export declare class WorkflowFormatUi extends InvalidWorkflow {
|
|
36
|
+
}
|
|
37
|
+
/** A `core/ASSET` reference was not usable (unknown/unscanned/not owned). */
|
|
38
|
+
export declare class MissingAsset extends ComfyError {
|
|
39
|
+
}
|
|
40
|
+
/** Uploaded bytes did not match the declared `expectedHash`. */
|
|
41
|
+
export declare class HashMismatch extends ComfyError {
|
|
42
|
+
}
|
|
43
|
+
/** from-hash / existence probe found no blob the caller can mint from. */
|
|
44
|
+
export declare class BlobNotFound extends ComfyError {
|
|
45
|
+
}
|
|
46
|
+
/** The idempotency key was reused. Keys are single-use (reject-on-duplicate,
|
|
47
|
+
* no replay): any second request with the same key — a retry, a concurrent
|
|
48
|
+
* duplicate, or the same key with a different body — is rejected. */
|
|
49
|
+
export declare class IdempotencyKeyReuse extends ComfyError {
|
|
50
|
+
}
|
|
51
|
+
export declare class InsufficientCredits extends ComfyError {
|
|
52
|
+
}
|
|
53
|
+
/** Backpressure: the queue is full. `retryAfter` is seconds to wait. */
|
|
54
|
+
export declare class QueueFull extends ComfyError {
|
|
55
|
+
readonly retryAfter: number;
|
|
56
|
+
constructor(message: string, options: ComfyErrorOptions & {
|
|
57
|
+
retryAfter: number;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A job reached a non-success terminal state. `error` carries the
|
|
62
|
+
* node-level detail (`code`, `nodeId`, `message`, `traceback`) when the
|
|
63
|
+
* platform provided one.
|
|
64
|
+
*/
|
|
65
|
+
export declare class JobFailed extends ComfyError {
|
|
66
|
+
readonly error: JobError | null;
|
|
67
|
+
constructor(message: string, options?: {
|
|
68
|
+
error?: JobError | null;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Translate a protocol `ApiError` into the idiomatic SDK exception. */
|
|
72
|
+
export declare function toSdkError(exc: ApiError): ComfyError;
|
|
73
|
+
/**
|
|
74
|
+
* Run `fn`, re-raising any protocol `ApiError` as its idiomatic SDK
|
|
75
|
+
* exception. Wrap every `sdk`-level operation that calls into `low` with
|
|
76
|
+
* this so integrators only ever catch `sdk` exceptions (`MissingAsset`,
|
|
77
|
+
* `HashMismatch`, `NotFound`, ...), never the raw protocol error.
|
|
78
|
+
*/
|
|
79
|
+
export declare function translate<T>(fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idiomatic `sdk` exceptions.
|
|
3
|
+
*
|
|
4
|
+
* These wrap the protocol-level `low.ApiError` codes with names an
|
|
5
|
+
* integrator catches directly (`JobFailed`, `QueueFull`, ...). `toSdkError`
|
|
6
|
+
* maps a raised `ApiError` to the right subclass; anything unmapped stays a
|
|
7
|
+
* `ComfyError` carrying the original code. Mirrors `comfy_sdk.exceptions`
|
|
8
|
+
* in the Python SDK.
|
|
9
|
+
*/
|
|
10
|
+
import { ApiError } from "../low/index.js";
|
|
11
|
+
export class ComfyError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
httpStatus;
|
|
14
|
+
details;
|
|
15
|
+
constructor(message, options = {}) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = new.target.name;
|
|
18
|
+
this.code = options.code;
|
|
19
|
+
this.httpStatus = options.httpStatus;
|
|
20
|
+
this.details = options.details ?? null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** The surface rejected the request for lack of a valid key. Comfy Cloud
|
|
24
|
+
* and serverless require a key; a self-hosted proxy needs none. */
|
|
25
|
+
export class Unauthorized extends ComfyError {
|
|
26
|
+
}
|
|
27
|
+
export class Forbidden extends ComfyError {
|
|
28
|
+
}
|
|
29
|
+
export class NotFound extends ComfyError {
|
|
30
|
+
}
|
|
31
|
+
/** Structural/validation failure; `details` carries per-node errors. */
|
|
32
|
+
export class InvalidWorkflow extends ComfyError {
|
|
33
|
+
}
|
|
34
|
+
/** UI-export JSON was submitted instead of the API-format graph. */
|
|
35
|
+
export class WorkflowFormatUi extends InvalidWorkflow {
|
|
36
|
+
}
|
|
37
|
+
/** A `core/ASSET` reference was not usable (unknown/unscanned/not owned). */
|
|
38
|
+
export class MissingAsset extends ComfyError {
|
|
39
|
+
}
|
|
40
|
+
/** Uploaded bytes did not match the declared `expectedHash`. */
|
|
41
|
+
export class HashMismatch extends ComfyError {
|
|
42
|
+
}
|
|
43
|
+
/** from-hash / existence probe found no blob the caller can mint from. */
|
|
44
|
+
export class BlobNotFound extends ComfyError {
|
|
45
|
+
}
|
|
46
|
+
/** The idempotency key was reused. Keys are single-use (reject-on-duplicate,
|
|
47
|
+
* no replay): any second request with the same key — a retry, a concurrent
|
|
48
|
+
* duplicate, or the same key with a different body — is rejected. */
|
|
49
|
+
export class IdempotencyKeyReuse extends ComfyError {
|
|
50
|
+
}
|
|
51
|
+
export class InsufficientCredits extends ComfyError {
|
|
52
|
+
}
|
|
53
|
+
/** Backpressure: the queue is full. `retryAfter` is seconds to wait. */
|
|
54
|
+
export class QueueFull extends ComfyError {
|
|
55
|
+
retryAfter;
|
|
56
|
+
constructor(message, options) {
|
|
57
|
+
super(message, options);
|
|
58
|
+
this.retryAfter = options.retryAfter;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A job reached a non-success terminal state. `error` carries the
|
|
63
|
+
* node-level detail (`code`, `nodeId`, `message`, `traceback`) when the
|
|
64
|
+
* platform provided one.
|
|
65
|
+
*/
|
|
66
|
+
export class JobFailed extends ComfyError {
|
|
67
|
+
error;
|
|
68
|
+
constructor(message, options = {}) {
|
|
69
|
+
super(message, { code: options.error?.code ?? "job_failed" });
|
|
70
|
+
this.error = options.error ?? null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const BY_CODE = {
|
|
74
|
+
invalid_workflow: InvalidWorkflow,
|
|
75
|
+
workflow_format_ui: WorkflowFormatUi,
|
|
76
|
+
missing_asset: MissingAsset,
|
|
77
|
+
hash_mismatch: HashMismatch,
|
|
78
|
+
blob_not_found: BlobNotFound,
|
|
79
|
+
idempotency_key_reuse: IdempotencyKeyReuse,
|
|
80
|
+
insufficient_credits: InsufficientCredits,
|
|
81
|
+
not_found: NotFound,
|
|
82
|
+
unauthorized: Unauthorized,
|
|
83
|
+
forbidden: Forbidden,
|
|
84
|
+
};
|
|
85
|
+
/** Translate a protocol `ApiError` into the idiomatic SDK exception. */
|
|
86
|
+
export function toSdkError(exc) {
|
|
87
|
+
if (exc.code === "queue_full") {
|
|
88
|
+
return new QueueFull(exc.message, {
|
|
89
|
+
retryAfter: exc.retryAfter ?? 0,
|
|
90
|
+
code: exc.code,
|
|
91
|
+
httpStatus: exc.httpStatus,
|
|
92
|
+
details: exc.details,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const cls = BY_CODE[exc.code] ?? ComfyError;
|
|
96
|
+
return new cls(exc.message, { code: exc.code, httpStatus: exc.httpStatus, details: exc.details });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run `fn`, re-raising any protocol `ApiError` as its idiomatic SDK
|
|
100
|
+
* exception. Wrap every `sdk`-level operation that calls into `low` with
|
|
101
|
+
* this so integrators only ever catch `sdk` exceptions (`MissingAsset`,
|
|
102
|
+
* `HashMismatch`, `NotFound`, ...), never the raw protocol error.
|
|
103
|
+
*/
|
|
104
|
+
export async function translate(fn) {
|
|
105
|
+
try {
|
|
106
|
+
return await fn();
|
|
107
|
+
}
|
|
108
|
+
catch (exc) {
|
|
109
|
+
if (exc instanceof ApiError) {
|
|
110
|
+
throw toSdkError(exc);
|
|
111
|
+
}
|
|
112
|
+
throw exc;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local blake3 content hashing (via `hash-wasm`, which runs anywhere Node
|
|
3
|
+
* runs — no native addon).
|
|
4
|
+
*
|
|
5
|
+
* The hash is computed client-side purely as a dedup hint — the server
|
|
6
|
+
* always recomputes it from the received bytes and that value is
|
|
7
|
+
* authoritative. Files are hashed by streaming in chunks so a multi-GB
|
|
8
|
+
* input is never buffered whole. Mirrors `comfy_sdk._hashing` in the
|
|
9
|
+
* Python SDK.
|
|
10
|
+
*/
|
|
11
|
+
/** `blake3:<hex>` of an in-memory buffer. */
|
|
12
|
+
export declare function hashBytes(data: Uint8Array): Promise<string>;
|
|
13
|
+
/** `blake3:<hex>` of a file on disk, streamed in chunks. */
|
|
14
|
+
export declare function hashFile(path: string): Promise<string>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local blake3 content hashing (via `hash-wasm`, which runs anywhere Node
|
|
3
|
+
* runs — no native addon).
|
|
4
|
+
*
|
|
5
|
+
* The hash is computed client-side purely as a dedup hint — the server
|
|
6
|
+
* always recomputes it from the received bytes and that value is
|
|
7
|
+
* authoritative. Files are hashed by streaming in chunks so a multi-GB
|
|
8
|
+
* input is never buffered whole. Mirrors `comfy_sdk._hashing` in the
|
|
9
|
+
* Python SDK.
|
|
10
|
+
*/
|
|
11
|
+
import { createReadStream } from "node:fs";
|
|
12
|
+
import { createBLAKE3 } from "hash-wasm";
|
|
13
|
+
/** `blake3:<hex>` of an in-memory buffer. */
|
|
14
|
+
export async function hashBytes(data) {
|
|
15
|
+
const hasher = await createBLAKE3();
|
|
16
|
+
hasher.init();
|
|
17
|
+
hasher.update(data);
|
|
18
|
+
return `blake3:${hasher.digest()}`;
|
|
19
|
+
}
|
|
20
|
+
/** `blake3:<hex>` of a file on disk, streamed in chunks. */
|
|
21
|
+
export async function hashFile(path) {
|
|
22
|
+
const hasher = await createBLAKE3();
|
|
23
|
+
hasher.init();
|
|
24
|
+
for await (const chunk of createReadStream(path)) {
|
|
25
|
+
hasher.update(chunk);
|
|
26
|
+
}
|
|
27
|
+
return `blake3:${hasher.digest()}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sdk` — the idiomatic, hand-written layer integrators import (the
|
|
3
|
+
* package's default export surface, re-exported from the package root).
|
|
4
|
+
* Mirrors `comfy_sdk`'s `__init__.py` in the Python SDK, minus the
|
|
5
|
+
* sync/async duplication (JS is async-native).
|
|
6
|
+
*/
|
|
7
|
+
export { Comfy, type ComfyOptions } from "./client.js";
|
|
8
|
+
export { Asset, AssetFactory } from "./assets.js";
|
|
9
|
+
export { Workflow, WorkflowFactory, type WorkflowGraph } from "./workflows.js";
|
|
10
|
+
export { Job, JobFactory } from "./jobs.js";
|
|
11
|
+
export { Output } from "./outputs.js";
|
|
12
|
+
export type { ComfyEvent, Log, OutputReady, Preview, Progress, StatusChange } from "./events.js";
|
|
13
|
+
export { BlobNotFound, ComfyError, Forbidden, HashMismatch, IdempotencyKeyReuse, InsufficientCredits, InvalidWorkflow, JobFailed, MissingAsset, NotFound, QueueFull, Unauthorized, WorkflowFormatUi, } from "./exceptions.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sdk` — the idiomatic, hand-written layer integrators import (the
|
|
3
|
+
* package's default export surface, re-exported from the package root).
|
|
4
|
+
* Mirrors `comfy_sdk`'s `__init__.py` in the Python SDK, minus the
|
|
5
|
+
* sync/async duplication (JS is async-native).
|
|
6
|
+
*/
|
|
7
|
+
export { Comfy } from "./client.js";
|
|
8
|
+
export { Asset, AssetFactory } from "./assets.js";
|
|
9
|
+
export { Workflow, WorkflowFactory } from "./workflows.js";
|
|
10
|
+
export { Job, JobFactory } from "./jobs.js";
|
|
11
|
+
export { Output } from "./outputs.js";
|
|
12
|
+
export { BlobNotFound, ComfyError, Forbidden, HashMismatch, IdempotencyKeyReuse, InsufficientCredits, InvalidWorkflow, JobFailed, MissingAsset, NotFound, QueueFull, Unauthorized, WorkflowFormatUi, } from "./exceptions.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job handles — the resumable, poll-authoritative core of the SDK.
|
|
3
|
+
*
|
|
4
|
+
* A {@link Job} is rehydratable purely from its ID. `wait` polls
|
|
5
|
+
* `GET /api/v2/jobs/{id}` with adaptive backoff as the source of truth for
|
|
6
|
+
* terminal status and outputs, so a stream that is throttled, dropped, or
|
|
7
|
+
* permanently unavailable never stalls completion. `events` is the live SSE
|
|
8
|
+
* stream on top: typed, auto-reconnecting (no replay — the stream carries
|
|
9
|
+
* no cursor), with the poll path as its backstop. Mirrors `comfy_sdk.jobs`
|
|
10
|
+
* in the Python SDK — one async class since JS is async-native.
|
|
11
|
+
*/
|
|
12
|
+
import type { ComfyLow, Job as LowJob } from "../low/index.js";
|
|
13
|
+
import { type ComfyEvent } from "./events.js";
|
|
14
|
+
import { Output } from "./outputs.js";
|
|
15
|
+
export declare class Job {
|
|
16
|
+
private readonly low;
|
|
17
|
+
private model;
|
|
18
|
+
constructor(low: ComfyLow, model: LowJob);
|
|
19
|
+
get id(): string;
|
|
20
|
+
get status(): string;
|
|
21
|
+
get outputs(): Output[];
|
|
22
|
+
get error(): LowJob["error"];
|
|
23
|
+
getOutputs(nodeId: string): Output[];
|
|
24
|
+
private bindOutput;
|
|
25
|
+
/** Poll `GET /api/v2/jobs/{id}` once and adopt the fresh state. */
|
|
26
|
+
refresh(signal?: AbortSignal): Promise<this>;
|
|
27
|
+
/** Poll to a terminal state (adaptive backoff). Rejects with a
|
|
28
|
+
* `TimeoutError` if `timeoutMs` elapses first, or immediately if `signal`
|
|
29
|
+
* aborts — the abort interrupts the backoff wait itself, not just the
|
|
30
|
+
* in-flight poll request. */
|
|
31
|
+
wait(timeoutMs?: number, signal?: AbortSignal): Promise<this>;
|
|
32
|
+
/** Wait for terminal, then throw `JobFailed` unless it succeeded. */
|
|
33
|
+
result(signal?: AbortSignal): Promise<this>;
|
|
34
|
+
cancel(signal?: AbortSignal): Promise<this>;
|
|
35
|
+
/**
|
|
36
|
+
* Typed live event iterator. Auto-reconnects with no replay; falls back
|
|
37
|
+
* to polling to detect terminal status if the stream ends early. An
|
|
38
|
+
* aborted `signal` stops both the current SSE connection/poll and the
|
|
39
|
+
* pause between reconnect attempts.
|
|
40
|
+
*/
|
|
41
|
+
events(signal?: AbortSignal): AsyncGenerator<ComfyEvent, void, void>;
|
|
42
|
+
}
|
|
43
|
+
export declare class JobFactory {
|
|
44
|
+
private readonly low;
|
|
45
|
+
constructor(low: ComfyLow);
|
|
46
|
+
get(jobId: string): Promise<Job>;
|
|
47
|
+
}
|
package/dist/sdk/jobs.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job handles — the resumable, poll-authoritative core of the SDK.
|
|
3
|
+
*
|
|
4
|
+
* A {@link Job} is rehydratable purely from its ID. `wait` polls
|
|
5
|
+
* `GET /api/v2/jobs/{id}` with adaptive backoff as the source of truth for
|
|
6
|
+
* terminal status and outputs, so a stream that is throttled, dropped, or
|
|
7
|
+
* permanently unavailable never stalls completion. `events` is the live SSE
|
|
8
|
+
* stream on top: typed, auto-reconnecting (no replay — the stream carries
|
|
9
|
+
* no cursor), with the poll path as its backstop. Mirrors `comfy_sdk.jobs`
|
|
10
|
+
* in the Python SDK — one async class since JS is async-native.
|
|
11
|
+
*/
|
|
12
|
+
import { abortableSleep } from "./abortable-sleep.js";
|
|
13
|
+
import { backoffSchedule, isTerminal, SUCCESS } from "./core.js";
|
|
14
|
+
import { eventFromRaw } from "./events.js";
|
|
15
|
+
import { JobFailed, translate } from "./exceptions.js";
|
|
16
|
+
import { Output } from "./outputs.js";
|
|
17
|
+
// Pause before reconnecting an SSE stream that dropped mid-job, without a
|
|
18
|
+
// terminal frame having been seen.
|
|
19
|
+
const RECONNECT_PAUSE_MS = 100;
|
|
20
|
+
export class Job {
|
|
21
|
+
low;
|
|
22
|
+
model;
|
|
23
|
+
constructor(low, model) {
|
|
24
|
+
this.low = low;
|
|
25
|
+
this.model = model;
|
|
26
|
+
}
|
|
27
|
+
get id() {
|
|
28
|
+
return this.model.id;
|
|
29
|
+
}
|
|
30
|
+
get status() {
|
|
31
|
+
return this.model.status;
|
|
32
|
+
}
|
|
33
|
+
get outputs() {
|
|
34
|
+
return this.model.outputs.map((o) => this.bindOutput(o));
|
|
35
|
+
}
|
|
36
|
+
get error() {
|
|
37
|
+
return this.model.error;
|
|
38
|
+
}
|
|
39
|
+
getOutputs(nodeId) {
|
|
40
|
+
return this.model.outputs.filter((o) => o.node_id === nodeId).map((o) => this.bindOutput(o));
|
|
41
|
+
}
|
|
42
|
+
bindOutput(model) {
|
|
43
|
+
return new Output(model, this.low);
|
|
44
|
+
}
|
|
45
|
+
/** Poll `GET /api/v2/jobs/{id}` once and adopt the fresh state. */
|
|
46
|
+
async refresh(signal) {
|
|
47
|
+
this.model = await translate(() => this.low.getJob(this.model.urls.self || this.model.id, { signal }));
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
/** Poll to a terminal state (adaptive backoff). Rejects with a
|
|
51
|
+
* `TimeoutError` if `timeoutMs` elapses first, or immediately if `signal`
|
|
52
|
+
* aborts — the abort interrupts the backoff wait itself, not just the
|
|
53
|
+
* in-flight poll request. */
|
|
54
|
+
async wait(timeoutMs, signal) {
|
|
55
|
+
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
56
|
+
const backoff = backoffSchedule();
|
|
57
|
+
for (;;) {
|
|
58
|
+
await this.refresh(signal);
|
|
59
|
+
if (isTerminal(this.status))
|
|
60
|
+
return this;
|
|
61
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
62
|
+
throw new Error(`job ${this.id} not terminal after ${timeoutMs}ms (status=${this.status})`);
|
|
63
|
+
}
|
|
64
|
+
await abortableSleep(backoff.next().value, signal);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Wait for terminal, then throw `JobFailed` unless it succeeded. */
|
|
68
|
+
async result(signal) {
|
|
69
|
+
await this.wait(undefined, signal);
|
|
70
|
+
if (this.status !== SUCCESS) {
|
|
71
|
+
throw new JobFailed(`job ${this.id} ended ${this.status}`, { error: this.model.error });
|
|
72
|
+
}
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
async cancel(signal) {
|
|
76
|
+
this.model = await translate(() => this.low.cancelJob(this.model.urls.cancel || this.model.id, { signal }));
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Typed live event iterator. Auto-reconnects with no replay; falls back
|
|
81
|
+
* to polling to detect terminal status if the stream ends early. An
|
|
82
|
+
* aborted `signal` stops both the current SSE connection/poll and the
|
|
83
|
+
* pause between reconnect attempts.
|
|
84
|
+
*/
|
|
85
|
+
async *events(signal) {
|
|
86
|
+
const eventsUrl = this.model.urls.events || this.model.id;
|
|
87
|
+
// Progress is monotonic across the whole stream, reconnects included: a
|
|
88
|
+
// frame whose value regresses (e.g. a lower value replayed by the server
|
|
89
|
+
// after a mid-stream drop) is suppressed so a consumer's progress never
|
|
90
|
+
// goes backwards.
|
|
91
|
+
let lastProgress = Number.NEGATIVE_INFINITY;
|
|
92
|
+
for (;;) {
|
|
93
|
+
let terminalSeen = false;
|
|
94
|
+
try {
|
|
95
|
+
for await (const raw of this.low.getJobEvents(eventsUrl, { signal })) {
|
|
96
|
+
const event = eventFromRaw(raw, (data) => this.bindOutput(data));
|
|
97
|
+
if (event === null)
|
|
98
|
+
continue;
|
|
99
|
+
if (event.kind === "progress") {
|
|
100
|
+
if (event.value < lastProgress)
|
|
101
|
+
continue;
|
|
102
|
+
lastProgress = event.value;
|
|
103
|
+
}
|
|
104
|
+
if (event.kind === "statusChange" && isTerminal(event.status)) {
|
|
105
|
+
terminalSeen = true;
|
|
106
|
+
yield event;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
yield event;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (exc) {
|
|
113
|
+
// A caller abort must propagate (and stop the loop), not be
|
|
114
|
+
// swallowed as an ordinary mid-stream drop.
|
|
115
|
+
if (signal?.aborted)
|
|
116
|
+
throw exc;
|
|
117
|
+
// Connection dropped mid-stream — reconnect below.
|
|
118
|
+
}
|
|
119
|
+
if (terminalSeen)
|
|
120
|
+
return;
|
|
121
|
+
// Stream ended without a terminal frame. Poll the authoritative
|
|
122
|
+
// state: stop if already terminal, else reconnect for fresh frames.
|
|
123
|
+
await this.refresh(signal);
|
|
124
|
+
if (isTerminal(this.status)) {
|
|
125
|
+
const statusChange = {
|
|
126
|
+
kind: "statusChange",
|
|
127
|
+
status: this.status,
|
|
128
|
+
queuePosition: null,
|
|
129
|
+
};
|
|
130
|
+
yield statusChange;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await abortableSleep(RECONNECT_PAUSE_MS, signal);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export class JobFactory {
|
|
138
|
+
low;
|
|
139
|
+
constructor(low) {
|
|
140
|
+
this.low = low;
|
|
141
|
+
}
|
|
142
|
+
async get(jobId) {
|
|
143
|
+
return new Job(this.low, await translate(() => this.low.getJob(jobId)));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output handles — typed, range-aware download over an asset id.
|
|
3
|
+
*
|
|
4
|
+
* An output is an asset: the bytes are retrievable via `getAssetContent`
|
|
5
|
+
* (which serves directly or redirects to a signed URL) for as long as the
|
|
6
|
+
* job is retained. `toFile` streams to disk; `toBytes` buffers. Mirrors
|
|
7
|
+
* `comfy_sdk.outputs` in the Python SDK.
|
|
8
|
+
*/
|
|
9
|
+
import type { ComfyLow, Output as LowOutput } from "../low/index.js";
|
|
10
|
+
export declare class Output {
|
|
11
|
+
private readonly model;
|
|
12
|
+
private readonly low;
|
|
13
|
+
constructor(model: LowOutput, low: ComfyLow);
|
|
14
|
+
get nodeId(): string;
|
|
15
|
+
get name(): string;
|
|
16
|
+
get type(): LowOutput["type"];
|
|
17
|
+
get id(): string;
|
|
18
|
+
get sizeBytes(): number;
|
|
19
|
+
get contentType(): string;
|
|
20
|
+
toFile(path: string, options?: {
|
|
21
|
+
range?: readonly [number, number];
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
toBytes(options?: {
|
|
24
|
+
range?: readonly [number, number];
|
|
25
|
+
}): Promise<Uint8Array>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output handles — typed, range-aware download over an asset id.
|
|
3
|
+
*
|
|
4
|
+
* An output is an asset: the bytes are retrievable via `getAssetContent`
|
|
5
|
+
* (which serves directly or redirects to a signed URL) for as long as the
|
|
6
|
+
* job is retained. `toFile` streams to disk; `toBytes` buffers. Mirrors
|
|
7
|
+
* `comfy_sdk.outputs` in the Python SDK.
|
|
8
|
+
*/
|
|
9
|
+
import { createWriteStream } from "node:fs";
|
|
10
|
+
import { Readable } from "node:stream";
|
|
11
|
+
import { pipeline } from "node:stream/promises";
|
|
12
|
+
export class Output {
|
|
13
|
+
model;
|
|
14
|
+
low;
|
|
15
|
+
constructor(model, low) {
|
|
16
|
+
this.model = model;
|
|
17
|
+
this.low = low;
|
|
18
|
+
}
|
|
19
|
+
get nodeId() {
|
|
20
|
+
return this.model.node_id;
|
|
21
|
+
}
|
|
22
|
+
get name() {
|
|
23
|
+
return this.model.name;
|
|
24
|
+
}
|
|
25
|
+
get type() {
|
|
26
|
+
return this.model.type;
|
|
27
|
+
}
|
|
28
|
+
get id() {
|
|
29
|
+
return this.model.id;
|
|
30
|
+
}
|
|
31
|
+
get sizeBytes() {
|
|
32
|
+
return this.model.size_bytes;
|
|
33
|
+
}
|
|
34
|
+
get contentType() {
|
|
35
|
+
return this.model.content_type;
|
|
36
|
+
}
|
|
37
|
+
async toFile(path, options = {}) {
|
|
38
|
+
const response = await this.low.getAssetContent(this.model.id, { range: options.range });
|
|
39
|
+
if (!response.body)
|
|
40
|
+
throw new Error(`empty response body for asset ${this.model.id}`);
|
|
41
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(path));
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
async toBytes(options = {}) {
|
|
45
|
+
const response = await this.low.getAssetContent(this.model.id, { range: options.range });
|
|
46
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow construction and mutation.
|
|
3
|
+
*
|
|
4
|
+
* A {@link Workflow} is a thin, local wrapper over the raw API-format
|
|
5
|
+
* graph. The graph stays a freely-mutable object (`wf.json`); `setInput` is
|
|
6
|
+
* sugar for `wf.json[node].inputs[field] = value` that also accepts an
|
|
7
|
+
* asset handle (substituted into a `core/ASSET` object at submit time).
|
|
8
|
+
* Construction does no network I/O in v1. Mirrors `comfy_sdk.workflows` in
|
|
9
|
+
* the Python SDK.
|
|
10
|
+
*/
|
|
11
|
+
export type WorkflowGraph = Record<string, unknown>;
|
|
12
|
+
export declare class Workflow {
|
|
13
|
+
json: WorkflowGraph;
|
|
14
|
+
constructor(graph: WorkflowGraph);
|
|
15
|
+
/**
|
|
16
|
+
* Set `node.inputs.field`. `value` may be a plain JSON value or an asset
|
|
17
|
+
* handle; handles are substituted into `core/ASSET` objects when the
|
|
18
|
+
* workflow is submitted.
|
|
19
|
+
*/
|
|
20
|
+
setInput(nodeId: string, field: string, value: unknown): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `client.workflows` — alternative constructors for {@link Workflow}.
|
|
24
|
+
* Namespaced on the client (rather than free-standing) because
|
|
25
|
+
* construction is expected to become client-bound once server-side
|
|
26
|
+
* subgraphs land; in v1 it is purely local.
|
|
27
|
+
*/
|
|
28
|
+
export declare class WorkflowFactory {
|
|
29
|
+
fromFile(path: string): Promise<Workflow>;
|
|
30
|
+
fromJson(graph: WorkflowGraph): Workflow;
|
|
31
|
+
fromString(text: string): Workflow;
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow construction and mutation.
|
|
3
|
+
*
|
|
4
|
+
* A {@link Workflow} is a thin, local wrapper over the raw API-format
|
|
5
|
+
* graph. The graph stays a freely-mutable object (`wf.json`); `setInput` is
|
|
6
|
+
* sugar for `wf.json[node].inputs[field] = value` that also accepts an
|
|
7
|
+
* asset handle (substituted into a `core/ASSET` object at submit time).
|
|
8
|
+
* Construction does no network I/O in v1. Mirrors `comfy_sdk.workflows` in
|
|
9
|
+
* the Python SDK.
|
|
10
|
+
*/
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
export class Workflow {
|
|
13
|
+
json;
|
|
14
|
+
constructor(graph) {
|
|
15
|
+
this.json = graph;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set `node.inputs.field`. `value` may be a plain JSON value or an asset
|
|
19
|
+
* handle; handles are substituted into `core/ASSET` objects when the
|
|
20
|
+
* workflow is submitted.
|
|
21
|
+
*/
|
|
22
|
+
setInput(nodeId, field, value) {
|
|
23
|
+
const node = (this.json[nodeId] ??= {});
|
|
24
|
+
const inputs = (node.inputs ??= {});
|
|
25
|
+
inputs[field] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `client.workflows` — alternative constructors for {@link Workflow}.
|
|
30
|
+
* Namespaced on the client (rather than free-standing) because
|
|
31
|
+
* construction is expected to become client-bound once server-side
|
|
32
|
+
* subgraphs land; in v1 it is purely local.
|
|
33
|
+
*/
|
|
34
|
+
export class WorkflowFactory {
|
|
35
|
+
async fromFile(path) {
|
|
36
|
+
const text = await readFile(path, "utf-8");
|
|
37
|
+
return new Workflow(JSON.parse(text));
|
|
38
|
+
}
|
|
39
|
+
fromJson(graph) {
|
|
40
|
+
return new Workflow(graph);
|
|
41
|
+
}
|
|
42
|
+
fromString(text) {
|
|
43
|
+
return new Workflow(JSON.parse(text));
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@comfyorg/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "TypeScript SDK for running ComfyUI workflows via the Comfy API v2 (self-hosted, Comfy Cloud, serverless).",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./low": {
|
|
18
|
+
"types": "./dist/low/index.d.ts",
|
|
19
|
+
"default": "./dist/low/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"generate": "openapi-ts",
|
|
25
|
+
"lint": "oxlint .",
|
|
26
|
+
"format": "oxfmt --write .",
|
|
27
|
+
"format:check": "oxfmt --check .",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
31
|
+
"check:spec-drift": "node scripts/check-spec-drift.mjs"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"eventsource-parser": "^3.0.0",
|
|
35
|
+
"hash-wasm": "^4.11.0",
|
|
36
|
+
"zod": "^4.4.3"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@hey-api/openapi-ts": "0.99.0",
|
|
40
|
+
"@types/node": "^22.10.0",
|
|
41
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
42
|
+
"oxfmt": "^0.45.0",
|
|
43
|
+
"oxlint": "^1.62.0",
|
|
44
|
+
"typescript": "^5.8.3",
|
|
45
|
+
"vitest": "^4.1.0",
|
|
46
|
+
"yaml": "^2.9.0"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22"
|
|
50
|
+
},
|
|
51
|
+
"packageManager": "pnpm@11.13.0"
|
|
52
|
+
}
|