UNPKG

@stable-canvas/comfyui-client

Version:

api Client for ComfyUI that supports both NodeJS and Browser environments. It provides full support for all RESTful / WebSocket APIs.

1 lines 160 kB
{"version":3,"file":"main.modern.mjs","sources":["../../../node_modules/.pnpm/eventemitter3@5.0.1/node_modules/eventemitter3/index.js","../src/utils/misc.ts","../src/client/errors.ts","../src/client/WsClient.ts","../src/builtins.ts","../src/workflow/errors.ts","../src/utils/Disposable.ts","../src/client/Client.ts","../src/workflow/InvokedWorkflow.ts","../src/workflow/Workflow.ts","../src/plugins/Plugin.ts","../src/pipeline/types.ts","../src/plugins/LoginAuthPlugin.ts","../src/pipeline/base.ts","../src/pipeline/efficient.ts","../src/utils/arrayBuffer.ts","../src/utils/tools.ts","../src/main.ts"],"sourcesContent":["'use strict';\n\nvar has = Object.prototype.hasOwnProperty\n , prefix = '~';\n\n/**\n * Constructor to create a storage for our `EE` objects.\n * An `Events` instance is a plain object whose properties are event names.\n *\n * @constructor\n * @private\n */\nfunction Events() {}\n\n//\n// We try to not inherit from `Object.prototype`. In some engines creating an\n// instance in this way is faster than calling `Object.create(null)` directly.\n// If `Object.create(null)` is not supported we prefix the event names with a\n// character to make sure that the built-in object properties are not\n// overridden or used as an attack vector.\n//\nif (Object.create) {\n Events.prototype = Object.create(null);\n\n //\n // This hack is needed because the `__proto__` property is still inherited in\n // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.\n //\n if (!new Events().__proto__) prefix = false;\n}\n\n/**\n * Representation of a single event listener.\n *\n * @param {Function} fn The listener function.\n * @param {*} context The context to invoke the listener with.\n * @param {Boolean} [once=false] Specify if the listener is a one-time listener.\n * @constructor\n * @private\n */\nfunction EE(fn, context, once) {\n this.fn = fn;\n this.context = context;\n this.once = once || false;\n}\n\n/**\n * Add a listener for a given event.\n *\n * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} context The context to invoke the listener with.\n * @param {Boolean} once Specify if the listener is a one-time listener.\n * @returns {EventEmitter}\n * @private\n */\nfunction addListener(emitter, event, fn, context, once) {\n if (typeof fn !== 'function') {\n throw new TypeError('The listener must be a function');\n }\n\n var listener = new EE(fn, context || emitter, once)\n , evt = prefix ? prefix + event : event;\n\n if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;\n else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);\n else emitter._events[evt] = [emitter._events[evt], listener];\n\n return emitter;\n}\n\n/**\n * Clear event by name.\n *\n * @param {EventEmitter} emitter Reference to the `EventEmitter` instance.\n * @param {(String|Symbol)} evt The Event name.\n * @private\n */\nfunction clearEvent(emitter, evt) {\n if (--emitter._eventsCount === 0) emitter._events = new Events();\n else delete emitter._events[evt];\n}\n\n/**\n * Minimal `EventEmitter` interface that is molded against the Node.js\n * `EventEmitter` interface.\n *\n * @constructor\n * @public\n */\nfunction EventEmitter() {\n this._events = new Events();\n this._eventsCount = 0;\n}\n\n/**\n * Return an array listing the events for which the emitter has registered\n * listeners.\n *\n * @returns {Array}\n * @public\n */\nEventEmitter.prototype.eventNames = function eventNames() {\n var names = []\n , events\n , name;\n\n if (this._eventsCount === 0) return names;\n\n for (name in (events = this._events)) {\n if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);\n }\n\n if (Object.getOwnPropertySymbols) {\n return names.concat(Object.getOwnPropertySymbols(events));\n }\n\n return names;\n};\n\n/**\n * Return the listeners registered for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Array} The registered listeners.\n * @public\n */\nEventEmitter.prototype.listeners = function listeners(event) {\n var evt = prefix ? prefix + event : event\n , handlers = this._events[evt];\n\n if (!handlers) return [];\n if (handlers.fn) return [handlers.fn];\n\n for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) {\n ee[i] = handlers[i].fn;\n }\n\n return ee;\n};\n\n/**\n * Return the number of listeners listening to a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Number} The number of listeners.\n * @public\n */\nEventEmitter.prototype.listenerCount = function listenerCount(event) {\n var evt = prefix ? prefix + event : event\n , listeners = this._events[evt];\n\n if (!listeners) return 0;\n if (listeners.fn) return 1;\n return listeners.length;\n};\n\n/**\n * Calls each of the listeners registered for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @returns {Boolean} `true` if the event had listeners, else `false`.\n * @public\n */\nEventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {\n var evt = prefix ? prefix + event : event;\n\n if (!this._events[evt]) return false;\n\n var listeners = this._events[evt]\n , len = arguments.length\n , args\n , i;\n\n if (listeners.fn) {\n if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);\n\n switch (len) {\n case 1: return listeners.fn.call(listeners.context), true;\n case 2: return listeners.fn.call(listeners.context, a1), true;\n case 3: return listeners.fn.call(listeners.context, a1, a2), true;\n case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;\n case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;\n case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;\n }\n\n for (i = 1, args = new Array(len -1); i < len; i++) {\n args[i - 1] = arguments[i];\n }\n\n listeners.fn.apply(listeners.context, args);\n } else {\n var length = listeners.length\n , j;\n\n for (i = 0; i < length; i++) {\n if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);\n\n switch (len) {\n case 1: listeners[i].fn.call(listeners[i].context); break;\n case 2: listeners[i].fn.call(listeners[i].context, a1); break;\n case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;\n case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;\n default:\n if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {\n args[j - 1] = arguments[j];\n }\n\n listeners[i].fn.apply(listeners[i].context, args);\n }\n }\n }\n\n return true;\n};\n\n/**\n * Add a listener for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} [context=this] The context to invoke the listener with.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.on = function on(event, fn, context) {\n return addListener(this, event, fn, context, false);\n};\n\n/**\n * Add a one-time listener for a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn The listener function.\n * @param {*} [context=this] The context to invoke the listener with.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.once = function once(event, fn, context) {\n return addListener(this, event, fn, context, true);\n};\n\n/**\n * Remove the listeners of a given event.\n *\n * @param {(String|Symbol)} event The event name.\n * @param {Function} fn Only remove the listeners that match this function.\n * @param {*} context Only remove the listeners that have this context.\n * @param {Boolean} once Only remove one-time listeners.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {\n var evt = prefix ? prefix + event : event;\n\n if (!this._events[evt]) return this;\n if (!fn) {\n clearEvent(this, evt);\n return this;\n }\n\n var listeners = this._events[evt];\n\n if (listeners.fn) {\n if (\n listeners.fn === fn &&\n (!once || listeners.once) &&\n (!context || listeners.context === context)\n ) {\n clearEvent(this, evt);\n }\n } else {\n for (var i = 0, events = [], length = listeners.length; i < length; i++) {\n if (\n listeners[i].fn !== fn ||\n (once && !listeners[i].once) ||\n (context && listeners[i].context !== context)\n ) {\n events.push(listeners[i]);\n }\n }\n\n //\n // Reset the array, or remove it completely if we have no more listeners.\n //\n if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;\n else clearEvent(this, evt);\n }\n\n return this;\n};\n\n/**\n * Remove all listeners, or those of the specified event.\n *\n * @param {(String|Symbol)} [event] The event name.\n * @returns {EventEmitter} `this`.\n * @public\n */\nEventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {\n var evt;\n\n if (event) {\n evt = prefix ? prefix + event : event;\n if (this._events[evt]) clearEvent(this, evt);\n } else {\n this._events = new Events();\n this._eventsCount = 0;\n }\n\n return this;\n};\n\n//\n// Alias methods names because people roll like that.\n//\nEventEmitter.prototype.off = EventEmitter.prototype.removeListener;\nEventEmitter.prototype.addListener = EventEmitter.prototype.on;\n\n//\n// Expose the prefix.\n//\nEventEmitter.prefixed = prefix;\n\n//\n// Allow `EventEmitter` to be imported as module namespace.\n//\nEventEmitter.EventEmitter = EventEmitter;\n\n//\n// Expose the module.\n//\nif ('undefined' !== typeof module) {\n module.exports = EventEmitter;\n}\n","export const uuidv4 = () =>\n \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0,\n v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n\nexport const isNone = (x: any): x is null | undefined =>\n x === null || x === undefined;\nexport const debounce = <F extends (...args: any[]) => any>(\n fn: F,\n wait_ms: number,\n): ((...args: Parameters<F>) => void) => {\n let timeout: any = null;\n return (...args: Parameters<F>): void => {\n if (timeout) clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), wait_ms);\n };\n};\n","import { ComfyUIClientResponseTypes } from \"./response.types\";\n\nexport class ClientError extends Error {\n constructor(message: string) {\n super(message);\n }\n}\n\n// 等待 prompt 执行超时\nexport class PromptTimeoutError extends ClientError {\n constructor(prompt_id: string, timeout_ms: number) {\n super(`Prompt ${prompt_id} timed out after ${timeout_ms} ms`);\n }\n}\n\n// enqueue prompt 错误\nexport class PromptEnqueueError extends ClientError {\n constructor(resp: ComfyUIClientResponseTypes.QueuePromptError) {\n const { error, node_errors } = resp;\n const message =\n typeof error === \"string\" ? error : error.errors?.[0].message;\n const details_message = node_errors ? JSON.stringify(node_errors) : \"\";\n super(`Failed to enqueue prompt: ${message}, details: ${details_message}`);\n }\n}\n\n// client enqueue 参数错误\nexport class ClientEnqueueError extends ClientError {\n constructor(err_msg: string) {\n super(`Client enqueue error: ${err_msg}`);\n }\n}\n\n// prompt_id 错误,没有找到对应的 prompt 在 history 中\nexport class PromptNotFoundError extends ClientError {\n constructor(prompt_id: string) {\n super(`Prompt [${prompt_id}] not found in history`);\n }\n}\n\n// task 数据类型错误,可能是 comfyui api 版本变动\nexport class TaskDataTypeError extends ClientError {\n constructor(task_data: any) {\n // 此数据类型与预期不符,请尝试更新 comfyui version\n super(\n `Task data type error, please try updating comfyui version: ${JSON.stringify(task_data)}`,\n );\n }\n}\n// prompt 执行失败,状态不是 success\nexport class PromptExecutionFailedError extends ClientError {\n constructor(prompt_id: string, status: string) {\n super(`Prompt ${prompt_id} execution failed with status: ${status}`);\n }\n}\n\n// client 请求执行失败\nexport class ClientRequestError extends ClientError {\n constructor(err_msg: string) {\n super(`Client request error: ${err_msg}`);\n }\n}\n\n// client polling timeout\nexport class PollingTimeoutError extends ClientError {\n constructor(timeout_ms: number) {\n super(`Polling timed out after ${timeout_ms} ms`);\n }\n}\n\n// client web socket timeout\nexport class WebSocketTimeoutError extends ClientError {\n constructor(timeout_ms: number) {\n super(`WebSocket timed out after ${timeout_ms} ms`);\n }\n}\n\n// connect 参数错误\nexport class ConnectError extends ClientError {\n constructor(err_msg: string) {\n super(`Connect error: ${err_msg}`);\n }\n}\n\n// 解析 ws 数据错误\nexport class WebSocketParseError extends ClientError {\n constructor(err_msg: string) {\n super(`WebSocket parse error: ${err_msg}`);\n }\n}\n\n// http errors\nexport class HttpError extends Error {\n status: number;\n json: any;\n\n constructor(message: string, status: number, json?: any) {\n super(message);\n this.name = \"HttpError\";\n this.status = status;\n this.json = json;\n }\n}\n","import { EventEmitter } from \"eventemitter3\";\nimport { ComfyUIClientEvents } from \"./ws.types\";\nimport { uuidv4 } from \"../utils/misc\";\nimport { IComfyApiConfig } from \"./types\";\nimport {\n ConnectError,\n HttpError,\n PollingTimeoutError,\n WebSocketParseError,\n WebSocketTimeoutError,\n} from \"./errors\";\n\n/**\n * A client for interacting with the ComfyUI API server using WebSockets.\n *\n * NOTE: CORS policy: Request header field comfy-user is not allowed by Access-Control-Allow-Headers in preflight response. Please config.use empty string in browser.\n *\n * @example\n * ```typescript\n * const client = new WsClient({\n * api_host: \"YOUR_API_HOST\"\n * });\n *\n * // Connect to the server\n * client.connect();\n *\n * // Listen for status updates\n * client.on(\"status\", (status) => {\n * console.log(\"Status:\", status);\n * });\n *\n * // when done, close the client\n * client.close();\n */\nexport class WsClient {\n static DEFAULT_API_HOST = \"127.0.0.1:8188\";\n static DEFAULT_API_BASE = \"\";\n static DEFAULT_USER = \"\";\n static IS_BROWSER = typeof window !== \"undefined\";\n\n static readBinaryData(buf: ArrayBuffer) {\n const view = new DataView(buf);\n const eventType = view.getUint32(0);\n const imageType = view.getUint32(1);\n\n switch (eventType) {\n case 3: {\n const decoder = new TextDecoder();\n const data = buf.slice(4);\n const nodeIdLength = view.getUint32(4);\n return [\n {\n type: \"progress_text\",\n data: {\n nodeId: decoder.decode(data.slice(4, 4 + nodeIdLength)),\n text: decoder.decode(data.slice(4 + nodeIdLength)),\n },\n },\n ] as const;\n }\n\n case 1: {\n const mimeTypes = {\n 1: \"image/jpeg\",\n 2: \"image/png\",\n } as any;\n\n const mime = mimeTypes[imageType] || \"image/png\";\n const image = buf.slice(8);\n\n const imageBlob = new Blob([image], {\n type: mime,\n });\n\n return [\n {\n type: \"b_preview\",\n data: imageBlob,\n },\n ] as const;\n }\n\n case 4: {\n // PREVIEW_IMAGE_WITH_METADATA\n const decoder4 = new TextDecoder();\n const metadataLength = view.getUint32(4);\n const metadataBytes = buf.slice(8, 8 + metadataLength);\n const metadata = JSON.parse(decoder4.decode(metadataBytes));\n const imageData4 = buf.slice(8 + metadataLength);\n\n let imageMime4 = metadata.image_type;\n\n const imageBlob4 = new Blob([imageData4], {\n type: imageMime4,\n });\n\n return [\n {\n type: \"b_preview_with_metadata\",\n data: {\n blob: imageBlob4,\n nodeId: metadata.node_id,\n displayNodeId: metadata.display_node_id,\n parentNodeId: metadata.parent_node_id,\n realNodeId: metadata.real_node_id,\n promptId: metadata.prompt_id,\n },\n },\n {\n type: \"b_preview\",\n data: imageBlob4,\n },\n ] as const;\n }\n default:\n throw new WebSocketParseError(\n `Unknown binary websocket message of type ${eventType}`,\n );\n }\n }\n\n api_host: string;\n api_base: string;\n clientId?: string;\n socket?: WebSocket | null;\n WebSocket: typeof WebSocket;\n ssl: boolean;\n user: string;\n fetch: typeof fetch;\n\n events: EventEmitter<ComfyUIClientEvents & Record<string & {}, any>> =\n new EventEmitter();\n\n protected socket_callbacks: Record<string, any> = {};\n\n get registered() {\n return this.events.eventNames();\n }\n\n constructor(config: IComfyApiConfig) {\n this.api_host = config.api_host ?? WsClient.DEFAULT_API_HOST;\n this.api_base = config.api_base ?? WsClient.DEFAULT_API_BASE;\n this.clientId = config.clientId ?? uuidv4();\n this.WebSocket = config.WebSocket ?? globalThis.WebSocket;\n this.ssl = config.ssl ?? false;\n this.user = config.user ?? WsClient.DEFAULT_USER;\n if (!globalThis.fetch) {\n throw new ConnectError(\"fetch is not defined\");\n }\n this.fetch = config.fetch ?? globalThis.fetch.bind(globalThis);\n\n if (!this.WebSocket) {\n console.warn(\"No WebSocket implementation available, WebSocket disabled\");\n }\n }\n\n /**\n * Returns the headers for the API request.\n *\n * @param {RequestInit} [options] - (Optional) Additional options for the request.\n * @return {HeadersInit} The headers for the API request.\n */\n apiHeaders(options?: RequestInit) {\n const headers: HeadersInit = {\n ...(this.user\n ? {\n \"Comfy-User\": this.user,\n }\n : {}),\n // \"User-Agent\": `ComfyUIClient/${version}`,\n Accept: \"*/*\",\n ...(options?.headers ?? {}),\n };\n return headers;\n }\n\n /**\n * Generates the URL for the API endpoint based on the provided route.\n *\n * @param {string} route - The route for the API endpoint.\n * @return {string} The generated URL for the API endpoint.\n */\n apiURL(route: string): string {\n const url = new URL(`http${this.ssl ? \"s\" : \"\"}://${this.api_host}`);\n let [pathname, query] = (this.api_base + route).split(\"?\");\n url.pathname = pathname;\n url.pathname = url.pathname.replace(/\\/+/g, \"/\");\n if (query) {\n url.search = query;\n }\n if (this.clientId) {\n url.searchParams.set(\"clientId\", this.clientId);\n }\n return url.toString();\n }\n\n internalURL(route: string): string {\n return this.apiURL(`/internal${route}`);\n }\n\n /**\n * Generates a URL for viewing a specific file with the given filename, subfolder, and type.\n *\n * @param {string} filename - The name of the file to view.\n * @param {string} subfolder - The subfolder where the file is located.\n * @param {string} type - The type of the file.\n * @return {string} The URL for viewing the file.\n */\n viewURL(filename: string, subfolder: string, type: string): string {\n const query = new URLSearchParams({\n filename,\n subfolder,\n type,\n }).toString();\n return `http${this.ssl ? \"s\" : \"\"}://${this.api_host}${\n this.api_base\n }/view?${query}`;\n }\n\n /**\n * Generates the WebSocket URL based on the current API host and SSL configuration.\n *\n * @return {string} The generated WebSocket URL.\n */\n wsURL(): string {\n const url = new URL(`ws${this.ssl ? \"s\" : \"\"}://${this.api_host}`);\n url.pathname = \"/ws\";\n if (this.clientId) {\n url.searchParams.set(\"clientId\", this.clientId);\n }\n return url.toString();\n }\n\n /**\n * Fetches API data based on the provided route and options.\n *\n * NOTE: CORS policy: Request header field comfy-user is not allowed by Access-Control-Allow-Headers in preflight response. Please use empty string in browser.\n *\n * @param {string} route - The route for the API request.\n * @param {RequestInit} [options] - (Optional) Additional options for the request.\n * @return {Promise<Response>} A promise that resolves to the API response.\n */\n async fetchApi(route: string, options?: RequestInit): Promise<Response> {\n const url = this.apiURL(route);\n const res = await this.fetch(url, {\n ...options,\n headers: this.apiHeaders(options),\n });\n const { status, statusText } = res;\n\n if (status < 200 || status >= 400) {\n throw new HttpError(\n `Endpoint Bad Request (${status} ${statusText}): ${url}`,\n status,\n await res.json(),\n );\n }\n\n return res;\n }\n\n /**\n * Adds an event listener for the specified event type.\n *\n * @param {keyof ComfyUIClientEvents | (string & {})} type - The type of event to listen for.\n * @param {(...args: any) => void} callback - The callback function to be executed when the event is triggered.\n * @param {any} options - (Optional) Additional options for the event listener.\n * @return {() => void} A function that removes the event listener when called.\n */\n addEventListener<T extends EventEmitter.EventNames<ComfyUIClientEvents>>(\n type: T,\n callback: EventEmitter.EventListener<ComfyUIClientEvents, T>,\n options?: any,\n ) {\n this.events.on(type as any, callback as any, options);\n\n return () => {\n this.events.off(type as any, callback as any);\n };\n }\n\n /**\n * Adds an event listener for the specified event type.\n *\n * @param {keyof ComfyUIClientEvents | (string & {})} type - The type of event to listen for.\n * @param {(...args: any) => void} callback - The callback function to be executed when the event is triggered.\n * @param {any} options - (Optional) Additional options for the event listener.\n * @return {() => void} A function that removes the event listener when called.\n */\n on<T extends EventEmitter.EventNames<ComfyUIClientEvents>>(\n type: T,\n callback: EventEmitter.EventListener<ComfyUIClientEvents, T>,\n options?: any,\n ) {\n return this.addEventListener(type, callback, options);\n }\n\n /**\n * Adds an event listener for the specified event type.\n *\n * @param {keyof ComfyUIClientEvents | (string & {})} type - The type of event to listen for.\n * @param {(...args: any) => void} callback - The callback function to be executed when the event is triggered.\n * @param {any} options - (Optional) Additional options for the event listener.\n * @return {() => void} A function that removes the event listener when called.\n */\n once<T extends EventEmitter.EventNames<ComfyUIClientEvents>>(\n type: T,\n callback: EventEmitter.EventListener<ComfyUIClientEvents, T>,\n options?: any,\n ) {\n this.events.once(type as any, callback as any, options);\n\n return () => {\n this.events.off(type as any, callback as any);\n };\n }\n\n protected _polling_timer: any = null;\n protected _polling_interval = 1000;\n /**\n * Poll status for colab and other things that don't support websockets.\n */\n private startPollingQueue() {\n if (this._polling_timer) {\n return;\n }\n // FIXME: 优化点\n // 这里不需要一直 polling ,只有有任务的时候才需要 polling\n this._polling_timer = setInterval(async () => {\n try {\n const resp = await this.fetchApi(\"/prompt\");\n const status = await resp.json();\n this.events.emit(\"status\", status);\n } catch (error) {\n this.events.emit(\"status\", null);\n }\n }, this._polling_interval);\n }\n\n protected addSocketCallback<K extends keyof WebSocketEventMap>(\n socket: WebSocket,\n type: K,\n listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,\n options?: boolean | AddEventListenerOptions,\n ) {\n this.socket_callbacks[type] = listener;\n socket.addEventListener(type, listener, options);\n return () => {\n delete this.socket_callbacks[type];\n socket.removeEventListener(type, listener, options);\n };\n }\n\n /**\n * Removes all event listeners from the given WebSocket and clears the socket_callbacks object.\n */\n protected removeSocketCallbacks() {\n if (this.socket) {\n for (const type in this.socket_callbacks) {\n const listener = this.socket_callbacks[type];\n this.socket.removeEventListener(type, listener);\n }\n }\n this.socket_callbacks = {};\n }\n\n /**\n * Creates and connects a WebSocket for realtime updates\n * @param {boolean} isReconnect If the socket is connection is a reconnect attempt\n */\n private createSocket(isReconnect = false) {\n if (this.socket) {\n return;\n }\n if (!this.WebSocket) {\n throw new ConnectError(\n \"WebSocket is not defined, please provide a WebSocket implementation\",\n );\n }\n if (this.closed) {\n return;\n }\n\n let opened = false;\n\n this.socket = new this.WebSocket(this.wsURL());\n this.socket.binaryType = \"arraybuffer\";\n\n this.addSocketCallback(this.socket, \"open\", () => {\n opened = true;\n if (isReconnect) {\n this.events.emit(\"reconnected\");\n } else {\n this.events.emit(\"connected\");\n }\n });\n\n this.addSocketCallback(this.socket, \"error\", (ev: Event) => {\n // Expose websocket errors as unhandled events\n // Allows for catching 404 and other network errors\n const err = ev as ErrorEvent;\n const is404Error = err.message?.includes(\"404\");\n\n this.events.emit(\"connection_error\", {\n type: \"404\",\n message: err.message,\n });\n\n if (this.socket) this.socket.close();\n\n if (!is404Error && !isReconnect && !opened) {\n this.startPollingQueue();\n }\n });\n\n this.addSocketCallback(this.socket, \"close\", () => {\n setTimeout(() => {\n this.socket = null;\n this.createSocket(true);\n }, 300);\n if (opened) {\n this.events.emit(\"status\", null);\n this.events.emit(\"reconnecting\");\n }\n });\n\n const isBinaryData = (event: MessageEvent) => {\n if (typeof event.data === \"string\") {\n return false;\n }\n if (ArrayBuffer && event.data instanceof ArrayBuffer) {\n return true;\n }\n if (Buffer && Buffer.isBuffer(event.data)) {\n return true;\n }\n return false;\n };\n\n this.addSocketCallback(this.socket, \"message\", async (event) => {\n this.events.emit(\"message\", event);\n\n if (isBinaryData(event)) {\n const binaryEvents = WsClient.readBinaryData(event.data);\n for (const ev of binaryEvents) {\n switch (ev.type) {\n case \"b_preview\": {\n // TODO add types\n this.events.emit(\"b_preview\", ev.data);\n this.events.emit(\"image_data\", {\n image: await ev.data.arrayBuffer(),\n mime: ev.data.type,\n });\n break;\n }\n case \"b_preview_with_metadata\": {\n // TODO add types\n this.events.emit(\"b_preview_with_metadata\", ev.data);\n this.events.emit(\"image_data\", {\n image: await ev.data.blob.arrayBuffer(),\n mime: ev.data.blob.type,\n });\n break;\n }\n case \"progress_text\": {\n // TODO add types\n this.events.emit(\"progress_text\", ev.data);\n break;\n }\n default:\n return;\n }\n }\n } else {\n const msg = JSON.parse(event.data);\n\n switch (msg.type) {\n case \"status\":\n if (msg.data.sid) {\n this.clientId = msg.data.sid;\n }\n this.events.emit(\"status\", msg.data.status);\n break;\n case \"progress\":\n this.events.emit(\"progress\", msg.data);\n break;\n case \"executing\":\n this.events.emit(\"executing\", msg.data);\n break;\n case \"executed\":\n this.events.emit(\"executed\", msg.data);\n break;\n case \"execution_start\":\n this.events.emit(\"execution_start\", msg.data);\n break;\n case \"execution_error\":\n this.events.emit(\"execution_error\", msg.data);\n break;\n case \"execution_cached\":\n this.events.emit(\"execution_cached\", msg.data);\n break;\n case \"execution_interrupted\":\n this.events.emit(\"execution_interrupted\", msg.data);\n break;\n default:\n this.events.emit(msg.type, msg.data);\n break;\n }\n\n const is_unhandled_message =\n msg.type !== \"message\" &&\n this.registered.includes(msg.type) === false;\n if (is_unhandled_message) {\n this.events.emit(\"unhandled\", msg);\n }\n }\n });\n }\n\n /**\n * Initializes sockets and realtime updates\n *\n * @deprecated move to client.connect()\n */\n init() {\n this.createSocket();\n }\n\n closed = false;\n /**\n * Closes the WebSocket connection and cleans up event listeners\n */\n close() {\n if (this.closed) {\n return;\n }\n this.closed = true;\n this.events.emit(\"close\");\n\n this.disconnect();\n this.events.removeAllListeners();\n }\n\n /**\n * Connects to the WebSocket server by creating a new socket connection.\n *\n * @param {Object} options - The options for connecting to the server.\n * @param {Object} options.polling - The options for polling.\n * @param {boolean} options.polling.enabled - Whether polling is enabled.\n * @param {number} [options.polling.interval] - The interval for polling.\n * @param {Object} options.websocket - The options for the WebSocket connection.\n * @param {boolean} options.websocket.enabled - Whether the WebSocket connection is enabled.\n * @param {number} [options.timeout_ms] - The timeout for the connection in milliseconds.\n * @return {Promise<boolean>} - A promise that resolves to true if the connection was successful, false otherwise.\n */\n connect({\n polling = {\n enabled: false,\n },\n websocket = {\n enabled: true,\n },\n timeout_ms = 15 * 1000,\n }: {\n polling?: {\n enabled: boolean;\n interval?: number;\n };\n websocket?: {\n enabled: boolean;\n };\n timeout_ms?: number;\n } = {}) {\n if (polling?.enabled) {\n this._polling_interval = polling.interval ?? this._polling_interval;\n this.startPollingQueue();\n return new Promise(async (resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new PollingTimeoutError(timeout_ms));\n }, timeout_ms);\n // ping to ok or fail\n const resp = await this.fetchApi(\"/system_stats\");\n resolve(resp.ok && resp.status === 200);\n clearTimeout(timer);\n });\n }\n if (websocket?.enabled) {\n this.createSocket();\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new WebSocketTimeoutError(timeout_ms));\n }, timeout_ms);\n this.once(\"connected\", () => {\n resolve(true);\n clearTimeout(timer);\n });\n });\n }\n throw new ConnectError(\"You must enable either polling or websocket\");\n }\n\n /**\n * Disconnects the WebSocket connection and cleans up event listeners.\n */\n disconnect() {\n if (!this.socket) {\n process.nextTick(this._disconnectPolling.bind(this));\n } else {\n this._disconnectSocket();\n }\n this._disconnectPolling();\n }\n\n /**\n * Disconnects the WebSocket connection and cleans up event listeners.\n *\n * @return {void} This function does not return anything.\n */\n _disconnectSocket() {\n const { socket } = this;\n if (!socket) return;\n this.socket = null;\n try {\n if (socket.readyState === socket.OPEN) {\n socket.close(1000, \"Client closed\");\n }\n } catch (error) {\n // pass\n }\n this.removeSocketCallbacks();\n if (\"removeAllListeners\" in socket) {\n (socket.removeAllListeners as any)?.();\n }\n }\n\n /**\n * Disconnects the polling timer and sets it to null.\n *\n * @return {void}\n */\n _disconnectPolling() {\n if (this._polling_timer !== null) {\n clearInterval(this._polling_timer);\n this._polling_timer = null;\n }\n }\n}\n","import { WorkflowOutputResolver } from \"./client/types\";\nimport { isNone } from \"./utils/misc\";\nimport { WorkflowOutput } from \"./workflow/types\";\n\nexport const RESOLVERS = {\n image: ((acc, output, { client }) => {\n if (output === null || output === undefined) {\n return acc;\n }\n\n const output_images: {\n filename?: string;\n subfolder?: string;\n type: string;\n }[] = (output?.images || []) as any;\n\n const images_url = output_images\n .map((image) => {\n const { filename, subfolder, type } = image;\n const type_should_be = [\n // SaveImage node output as \"output\"\n \"output\",\n // PreviewImage node output as \"temp\"\n \"temp\",\n ];\n if (\n isNone(filename) ||\n isNone(subfolder) ||\n !type_should_be.includes(type)\n ) {\n return null;\n }\n return client.viewURL(filename, subfolder, type);\n })\n .filter(Boolean) as string[];\n\n const images = images_url.map((image) => ({\n type: \"url\" as const,\n data: image,\n }));\n return {\n ...acc,\n images: [...acc.images, ...images],\n };\n }) as WorkflowOutputResolver<WorkflowOutput>,\n};\n","import { ComfyUiWsTypes } from \"../client/ws.types\";\nimport { InvokedWorkflow } from \"./InvokedWorkflow\";\nimport { IWorkflow } from \"./types\";\nimport { Workflow } from \"./Workflow\";\n\nexport class WorkflowError extends Error {\n constructor(\n message: string,\n readonly task_id: string,\n readonly workflow?: IWorkflow,\n readonly invoked?: InvokedWorkflow,\n ) {\n super(message);\n }\n}\n\nexport class WorkflowExecutionError extends WorkflowError {\n constructor(\n readonly payload: ComfyUiWsTypes.Messages.ExecutionError,\n task_id: string,\n workflow?: IWorkflow,\n invoked?: InvokedWorkflow<any>,\n ) {\n const { exception_message, exception_type } = payload;\n super(\n `Execution error: ${exception_message} (${exception_type})`,\n task_id,\n workflow,\n invoked,\n );\n }\n}\n\nexport class ClientConnectionError extends Error {\n constructor(type: string, message: string) {\n super(`Client connection error (${type}): ${message}`);\n }\n}\n\n// --- guard errors ---\nexport class WorkflowGuardError extends Error {\n readonly workflow: IWorkflow;\n\n constructor(\n message: string,\n readonly invoked: InvokedWorkflow<any>,\n ) {\n super(message);\n this.workflow = invoked.workflow;\n }\n}\n\n// \"This workflow is already enqueued\"\nexport class WorkflowEnqueuedError extends WorkflowGuardError {\n constructor(invoked: InvokedWorkflow<any>) {\n super(\"This workflow is already enqueued\", invoked);\n }\n}\n\n// 工作流没有被 enqueued,或者执行状态还未更新,缺少 task_id\nexport class WorkflowTaskIdError extends WorkflowGuardError {\n constructor(invoked: InvokedWorkflow<any>) {\n super(\n \"This workflow is not enqueued and the execution status cannot be interrupted\",\n invoked,\n );\n }\n}\n\n// 工作流已经 disposed 或者已经 结束\nexport class WorkflowDoneError extends WorkflowGuardError {\n constructor(invoked: InvokedWorkflow<any>) {\n const { is_done, is_disposed } = invoked;\n const message = is_done\n ? \"This workflow has already finished\"\n : \"This workflow has been disposed\";\n super(message, invoked);\n }\n}\n\n// ws 没有连接\nexport class WorkflowWsError extends WorkflowGuardError {\n constructor(invoked: InvokedWorkflow<any>) {\n super(\"WebSocket is not connected\", invoked);\n }\n}\n\n// 无法获取 任务状态\nexport class WorkflowTaskStatusError extends WorkflowGuardError {\n constructor(invoked: InvokedWorkflow<any>) {\n const { task_id } = invoked;\n super(\n task_id\n ? `Cannot get task status for task ${task_id}`\n : \"Cannot get task status\",\n invoked,\n );\n }\n}\n\n// 执行被 interrupted\nexport class WorkflowInterruptedError extends WorkflowGuardError {\n constructor(invoked: InvokedWorkflow<any>) {\n super(\"Execution has been interrupted\", invoked);\n }\n}\n\n// 参数错误\nexport class WorkflowArgumentError extends WorkflowGuardError {\n constructor(invoked: InvokedWorkflow<any>, message: string) {\n super(message, invoked);\n }\n}\n","export class Disposable {\n protected _disposed = false;\n protected _disposed_cbs = [] as any[];\n public dispose() {\n if (this._disposed) {\n return;\n }\n this._disposed = true;\n\n this._disposed_cbs.forEach((cb) => {\n if (typeof cb === \"function\") {\n cb();\n }\n });\n }\n public _connect(cb: () => void) {\n if (this._disposed) {\n cb();\n return;\n }\n this._disposed_cbs.push(cb);\n }\n}\n","import type { Plugin } from \"../plugins/Plugin\";\nimport { WsClient } from \"./WsClient\";\nimport { RESOLVERS } from \"../builtins\";\nimport {\n WorkflowOutputResolver,\n EnqueueOptions,\n IComfyApiConfig,\n PromptBody,\n PromptQueueItem,\n ModelFolderInfo,\n ModelFile,\n LogsRawResponse,\n} from \"./types\";\nimport { ComfyUIClientResponseTypes } from \"./response.types\";\nimport { WorkflowOutput } from \"../workflow/types\";\nimport { WorkflowExecutionError } from \"../workflow/errors\";\nimport {\n ClientEnqueueError,\n ClientRequestError,\n PromptEnqueueError,\n PromptExecutionFailedError,\n PromptNotFoundError,\n PromptTimeoutError,\n TaskDataTypeError,\n} from \"./errors\";\nimport { Disposable } from \"../utils/Disposable\";\n\n/**\n * The Client class provides a high-level interface for interacting with the ComfyUI API.\n *\n * @extends WsClient\n *\n * @example\n * ```typescript\n * const client = new Client({\n * api_host: \"YOUR_API_HOST\",\n * clientId: \"YOUR_CLIENT_ID\",\n * });\n *\n * const extensions = await client.getEmbeddings();\n * console.log(extensions);\n * ```\n */\nexport class Client extends WsClient {\n // NOTE: useless ... just for debug\n private _plugins = [] as Plugin[];\n\n constructor(\n config: Omit<IComfyApiConfig, \"fetch\" | \"WebSocket\"> & {\n // NOTE: This is written to reduce type issues... because sometimes `as any` is unavoidable\n fetch?: any;\n WebSocket?: any;\n },\n ) {\n super(config);\n }\n\n /**\n * Use a plugin by calling its install method on this instance.\n *\n * @param {Plugin} plugin - The plugin to install.\n */\n use(plugin: Plugin) {\n plugin.install(this);\n this._plugins.push(plugin);\n }\n\n /**\n * Gets a list of extension urls\n * @returns An array of script urls to import\n */\n async getExtensions(): Promise<string[]> {\n const invoke = async () => {\n const resp = await this.fetchApi(\"/extensions\", { cache: \"no-store\" });\n return await resp.json();\n };\n return invoke();\n }\n\n /**\n * Gets a list of embedding names\n * @returns An array of script urls to import\n */\n async getEmbeddings(): Promise<string[]> {\n const invoke = async () => {\n const resp = await this.fetchApi(\"/embeddings\", { cache: \"no-store\" });\n return await resp.json();\n };\n return invoke();\n }\n\n /**\n * Loads node object definitions for the graph\n * @returns {Promise<ComfyUIClientResponseTypes.ObjectInfo>} The object info for the graph\n */\n async getNodeDefs(): Promise<ComfyUIClientResponseTypes.ObjectInfo> {\n const invoke = async () => {\n const resp = await this.fetchApi(\"/object_info\", { cache: \"no-store\" });\n const node_defs = await resp.json();\n return node_defs;\n };\n return invoke();\n }\n\n /**\n *\n * @param {number} queue_index The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue\n * @param {Object} options\n * @param {Object} options.prompt The prompt to queue\n * @param {Object} options.workflow This png info to be added to resulting image\n * @returns {Promise<ComfyUIClientResponseTypes.QueuePrompt>} The response from the server\n */\n async queuePrompt(\n queue_index: number,\n { prompt, workflow }: { prompt: any; workflow: any },\n ): Promise<ComfyUIClientResponseTypes.QueuePrompt> {\n const body: Record<string, unknown> = {\n client_id: this.clientId,\n prompt,\n extra_data: { extra_pnginfo: { workflow } },\n };\n\n if (queue_index === -1) {\n body.front = true;\n } else if (queue_index !== 0) {\n body.number = queue_index;\n }\n\n const res = await this.fetchApi(\"/prompt\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (res.status !== 200) {\n const error_resp = await res.text();\n try {\n const error_data = JSON.parse(error_resp);\n throw new PromptEnqueueError(error_data);\n } catch (error) {\n throw new PromptEnqueueError({ error: error_resp, node_errors: {} });\n }\n }\n\n return await res.json();\n }\n\n /**\n * Loads a list of items (queue or history)\n * @param {\"queue\" | \"history\"} type The type of items to load, queue or history\n * @returns The items of the specified type grouped by their status\n */\n async getItems(type: \"history\"): ReturnType<Client[\"getHistory\"]>;\n async getItems(type: \"queue\"): ReturnType<Client[\"getQueue\"]>;\n async getItems(type: \"queue\" | \"history\"): Promise<any> {\n if (type === \"queue\") {\n return this.getQueue();\n }\n return this.getHistory();\n }\n\n /**\n * Gets the current state of the queue\n * @returns The currently running and queued items\n */\n async getQueue(): Promise<{\n Running: Array<PromptBody>;\n Pending: Array<PromptBody>;\n }> {\n try {\n const res = await this.fetchApi(\"/queue\");\n const data = await res.json();\n return {\n Running: data.queue_running,\n Pending: data.queue_pending,\n };\n } catch (error) {\n console.error(error);\n return { Running: [], Pending: [] };\n }\n }\n\n /**\n * Gets the prompt execution history\n * @returns Prompt history including node outputs\n */\n async getHistory(\n max_items = 200,\n options?: {\n offset?: number;\n },\n ): Promise<{\n History: Array<PromptQueueItem>;\n }> {\n const { offset } = options || {};\n // Validate offset parameter\n if (offset !== undefined && (offset < 0 || !Number.isInteger(offset))) {\n throw new Error(\n `Invalid offset parameter: ${offset}. Must be a non-negative integer.`,\n );\n }\n const params = new URLSearchParams({ max_items: max_items.toString() });\n if (offset !== undefined) {\n params.set(\"offset\", offset.toString());\n }\n try {\n const res = await this.fetchApi(`/history?${params.toString()}`);\n return { History: Object.values(await res.json()) };\n } catch (error) {\n console.error(error);\n return { History: [] };\n }\n }\n\n /**\n * Gets system & device stats\n * @returns {ComfyUIClientResponseTypes.SystemStatsRoot} System stats such as python version, OS, per device info\n */\n\n async getSystemStats(): Promise<ComfyUIClientResponseTypes.SystemStatsRoot> {\n const res = await this.fetchApi(\"/system_stats\");\n return res.json();\n }\n\n /**\n * Sends a POST request to the API\n * @param {string} type The endpoint to post to\n * @param {any} body Optional POST data\n */\n private async postApi(type: string, body: any) {\n await this.fetchApi(\"/\" + type, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n }\n\n /**\n * Deletes an item from the specified list\n * @param {\"queue\" | \"history\"} type The type of item to delete, queue or history\n * @param {any} id The id of the item to delete\n */\n async deleteItem(type: \"queue\" | \"history\", id: any) {\n await this.postApi(type, { delete: [id] });\n }\n\n /**\n * Clears the specified list\n * @param {\"queue\" | \"history\"} type The type of list to clear, queue or history\n */\n async clearItems(type: \"queue\" | \"history\") {\n await this.postApi(type, { clear: true });\n }\n\n /**\n * Interrupts the execution of the running prompt. If runningPromptId is provided,\n * it is included in the payload as a helpful hint to the backend.\n * @param {string | null} [runningPromptId] Optional Running Prompt ID to interrupt\n */\n async interrupt(runningPromptId: string | null = null) {\n await this.postApi(\n \"interrupt\",\n runningPromptId ? { prompt_id: runningPromptId } : undefined,\n );\n }\n\n /**\n * Free up memory by unloading models and freeing memory\n */\n async free(params?: { unload_models?: boolean; free_memory?: boolean }) {\n await this.postApi(\"free\", params);\n }\n\n /**\n * Gets user configuration data and where data should be stored\n * @returns { Promise<{ storage: \"server\" | \"browser\", users?: Promise<string, unknown>, migrated?: boolean }> }\n */\n async getUserConfig() {\n return (await this.fetchApi(\"/users\")).json();\n }\n\n /**\n * Creates a new user\n * @param { string } username\n * @returns The fetch response\n */\n async createUser(username: string): Promise<Response> {\n return this.fetchApi(\"/users\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ username }),\n });\n }\n\n /**\n * Gets all setting values for the current user\n * @returns { Promise<string, unknown> } A dictionary of id -> value\n */\n async getSettings(): Promise<Record<string, unknown>> {\n return (await this.fetchApi(\"/settings\")).json();\n }\n\n /**\n * Gets a setting for the current user\n * @param { string } id The id of the setting to fetch\n * @returns { Promise<unknown> } The setting value\n */\n async getSetting(id: string): Promise<unknown> {\n return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json();\n }\n\n /**\n * Stores a dictionary of settings for the current user\n * @param { Record<string, unknown> } settings Dictionary of setting id -> value to save\n * @returns { Promise<void> }\n */\n async storeSettings(settings: Record<string, unknown>): Promise<Response> {\n return this.fetchApi(`/settings`, {\n method: \"POST\",\n body: JSON.stringify(settings),\n });\n }\n\n /**\n * Stores a setting for the current user\n * @param { string } id The id of the setting to update\n * @param { unknown } value The value of the setting\n * @returns { Promise<void> }\n */\n async storeSetting(id: string, value: unknown): Promise<Response> {\n return this.fetchApi(`/settings/${encodeURIComponent(id)}`, {\n method: \"POST\",\n body: JSON.stringify(value),\n });\n }\n\n /**\n * Gets a user data file for the current user\n * @param { string } file The name of the userdata file to load\n * @param { RequestInit } [options]\n * @returns { Promise<unknown> } The fetch response object\n */\n async getUserData(file: string, options?: RequestInit): Promise<Response> {\n return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options);\n }\n\n /**\n * Stores a user data file for the current user\n * @param { string } file The name of the userdata file to save\n * @param { any } data The data to save to the file\n * @param { RequestInit & { stringify?: boolean, throwOnError?: boolean } } [options]\n * @returns { Promise<void> }\n */\n async storeUserData(\n file: string,\n data: any,\n options?: RequestInit & { stringify?: boolean; throwOnError?: boolean },\n ): Promise<void> {\n const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {\n method: \"POST\",\n body: options?.stringify ? JSON.stringify(data) : data,\n ...options,\n });\n if (resp.status !== 200) {\n const error = await resp.text();\n throw new ClientRequestError(\n `Error storing user data file '${file}': ${resp.status} ${error}`,\n );\n }\n }\n\n // ----------------- experiment apis -----------------\n\n /**\n * Gets a list of model folder keys (eg ['checkpoints', 'loras', ...])\n * @returns The list o