@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 • 137 kB
Source Map (JSON)
{"version":3,"file":"main.modern.mjs","sources":["../../../node_modules/.pnpm/eventemitter3@5.0.1/node_modules/eventemitter3/index.js","../src/utils/misc.ts","../src/utils/Errors.ts","../src/client/WsClient.ts","../src/utils/CachedFn.ts","../src/builtins.ts","../src/client/Client.ts","../src/utils/Disposable.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/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;\n","export namespace Errors {\n export 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}\n","import { EventEmitter } from \"eventemitter3\";\nimport { ComfyUIClientEvents } from \"./ws.types\";\nimport { uuidv4 } from \"../utils/misc\";\nimport { Errors } from \"../utils/Errors\";\nimport { IComfyApiConfig } from \"./types\";\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 loadImageData(buf: ArrayBuffer) {\n const view = new DataView(buf);\n const eventType = view.getUint32(0);\n const imageType = view.getUint32(1);\n\n if (eventType !== 1) {\n throw new Error(`Unknown binary websocket message of type ${eventType}`);\n }\n\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 return { image, mime };\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 Error(\"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 /**\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 if (this.closed) {\n throw new Error(\"Client is closed\");\n }\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 Errors.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 Error(\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 isImageMessage = (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\", (event) => {\n this.events.emit(\"message\", event);\n\n if (isImageMessage(event)) {\n const image = WsClient.loadImageData(event.data);\n this.events.emit(\"image_data\", image);\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 Error(\"Polling connection timed out\"));\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 Error(\"WebSocket connection timed out\"));\n }, timeout_ms);\n this.once(\"connected\", () => {\n resolve(true);\n clearTimeout(timer);\n });\n });\n }\n throw new Error(\"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","export type CachedFnOptions = {\r\n expire_time?: number;\r\n enabled?: boolean;\r\n};\r\n\r\nclass GlobalCacheHub {\r\n static KEY = \"__COMFY_UI_CLIENT_CACHE__\";\r\n protected _cached: Map<string, { result: any; expire: number }>;\r\n\r\n constructor() {\r\n this._cached = (globalThis as any)[GlobalCacheHub.KEY] || new Map();\r\n (globalThis as any)[GlobalCacheHub.KEY] = this._cached;\r\n }\r\n\r\n clear() {\r\n this._cached.clear();\r\n }\r\n\r\n get(key: string) {\r\n return this._cached.get(key);\r\n }\r\n\r\n set(key: string, value: { result: any; expire: number }) {\r\n this._cached.set(key, value);\r\n }\r\n}\r\n\r\nexport class CachedFn {\r\n static _defaultExpire: number = 60 * 1000;\r\n\r\n protected expire_time_ms: number;\r\n protected enabled: boolean;\r\n\r\n protected _cached = new GlobalCacheHub();\r\n\r\n protected cache_ns: string = \"\";\r\n\r\n constructor(ns: string, options?: CachedFnOptions) {\r\n this.expire_time_ms = options?.expire_time ?? CachedFn._defaultExpire;\r\n this.enabled = options?.enabled ?? true;\r\n this.cache_ns = ns;\r\n }\r\n\r\n public reset() {\r\n this._cached.clear();\r\n }\r\n\r\n private _hashArgs(args: any[]): string {\r\n try {\r\n return JSON.stringify(args);\r\n } catch (error) {\r\n return args.toString();\r\n }\r\n }\r\n\r\n public warp<ARGS extends any[], RET>(\r\n key: string,\r\n fn: (...args: ARGS) => RET\r\n ): (...args: ARGS) => RET {\r\n if (!this.enabled) {\r\n return fn;\r\n }\r\n return (...args: ARGS) => {\r\n const now = Date.now();\r\n const argsHash = this._hashArgs(args);\r\n const cacheKey = `${this.cache_ns}:${key}:${argsHash}`;\r\n const hit_cached = this._cached.get(cacheKey);\r\n\r\n if (hit_cached && hit_cached.expire > now) {\r\n return hit_cached.result;\r\n }\r\n\r\n const result = fn(...args);\r\n this._cached.set(cacheKey, { result, expire: now + this.expire_time_ms });\r\n return result;\r\n };\r\n }\r\n}\r\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 if (isNone(filename) || isNone(subfolder) || type !== \"output\") {\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 { CachedFn } from \"../utils/CachedFn\";\nimport type { Plugin } from \"../plugins/Plugin\";\nimport { WsClient } from \"./WsClient\";\nimport { RESOLVERS } from \"../builtins\";\nimport {\n WorkflowOutputResolver,\n EnqueueOptions,\n IComfyApiConfig,\n} from \"./types\";\nimport { ComfyUIClientResponseTypes } from \"./response.types\";\nimport { WorkflowOutput } from \"../workflow/types\";\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 private _cached_fn: CachedFn;\n\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 const cache_ns = `${config.api_host}`;\n this._cached_fn = new CachedFn(cache_ns, config.cache);\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 const cached = this._cached_fn.warp(\"extensions\", invoke);\n return cached();\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 const cached = this._cached_fn.warp(\"embeddings\", invoke);\n return cached();\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 const cached = this._cached_fn.warp(\"object_info\", invoke);\n return cached();\n }\n\n /**\n * Clears the node object definitions cache\n */\n resetCache() {\n this._cached_fn.reset();\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 // TODO throw Error class\n throw { response: error_data };\n } catch (error) {\n throw { response: error_resp };\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<Record<string, unknown>>;\n Pending: Array<Record<string, unknown>>;\n }> {\n try {\n const res = await this.fetchApi(\"/queue\");\n const data = await res.json();\n return {\n Running: data.queue_running.map((prompt: any) => ({\n prompt,\n remove: { name: \"Cancel\", cb: () => this.interrupt() },\n })),\n Pending: data.queue_pending.map((prompt: any) => ({ prompt })),\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(max_items = 200): Promise<{\n History: Array<{\n // [index, prompt_id, prompt, payload, outputs_node]\n prompt: [number, string, any, any, any];\n outputs: Record<string, unknown>;\n status: {\n status_str: string;\n completed: boolean;\n messages: any[];\n };\n }>;\n }> {\n try {\n const res = await this.fetchApi(`/history?max_items=${max_items}`);\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\n */\n async interrupt() {\n await this.postApi(\"interrupt\", null);\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 Error(\n `Error storing user data file '${file}': ${resp.status} ${error}`,\n );\n }\n }\n\n // ----------------- get status ++ -----------------\n\n /**\n * Retrieves the list of samplers from the node definitions.\n *\n * @return {Promise<string[]>} A promise that resolves to an array of strings representing the sampler names.\n */\n async getSamplers() {\n const node_config = await this.getNodeDefs();\n // find KSampler node\n const node = node_config[\"KSampler\"];\n const sampler_name = node?.input?.required?.[\"sampler_name\"]?.[0] || [];\n return sampler_name as string[];\n }\n\n /**\n * Retrieves the list of schedulers from the node definitions.\n *\n * @return {Promise<string[]>} A promise that resolves to an array of strings representing the scheduler names.\n */\n async getSchedulers() {\n const node_config = await this.getNodeDefs();\n // find Scheduler node\n const node = node_config[\"KSampler\"];\n const scheduler_name = node?.input?.required?.[\"scheduler\"]?.[0] || [];\n return scheduler_name as string[];\n }\n\n /**\n * Retrieves the list of model names from the node definitions.\n *\n * @return {Promise<string[]>} A promise that resolves to an array of strings representing the model names.\n */\n async getSDModels() {\n const node_config = await this.getNodeDefs();\n // find CheckpointLoaderSimple node\n const node = node_config[\"CheckpointLoaderSimple\"];\n const model_name = node?.input?.required?.[\"ckpt_name\"]?.[0] || [];\n return model_name as string[];\n }\n\n /**\n * Retrieves the list of model names from the node definitions.\n *\n * @return {Promise<string[]>} A promise that resolves to an array of strings representing the model names.\n */\n async getCNetModels() {\n const node_config = await this.getNodeDefs();\n // find ControlNetLoader node\n const node = node_config[\"ControlNetLoader\"];\n const model_name = node?.input?.required?.[\"control_net_name\"]?.[0] || [];\n return model_name as string[];\n }\n\n /**\n * Retrieves the list of model names from the node definitions for the UpscaleModelLoader node.\n *\n * @return {Promise<string[]>} A promise that resolves to an array of strings representing the model names.\n */\n async getUpscaleModels() {\n const node_config = await this.getNodeDefs();\n // find UpscaleModelLoader node\n const node = node_config[\"UpscaleModelLoader\"];\n const model_name = node?.input?.required?.[\"model_name\"]?.[0] || [];\n return model_name as string[];\n }\n\n /**\n * Retrieves the list of hypernetwork names from the node definitions.\n *\n * @return {Promise<string[]>} A promise that resolves to an array of strings representing the hypernetwork names.\n */\n async getHyperNetworks() {\n const node_config = await this.getNodeDefs();\n // find HypernetworkLoader node\n const node = node_config[\"HypernetworkLoader\"];\n const model_name = node?.input?.required?.[\"hypernetwork_name\"]?.[0] || [];\n return model_name as string[];\n }\n\n /**\n * Retrieves the list of LoRAs from the node definitions.\n *\n * @return {Promise<string[]>} A promise that resolves to an array of strings representing the LoRAs.\n */\n async getLoRAs() {\n const node_config = await this.getNodeDefs();\n // find LoraLoader node\n const node = node_config[\"LoraLoader\"];\n const model_name = node?.input?.required?.[\"lora_name\"]?.[0] || [];\n return model_name as string[];\n }\n\n /**\n * Retrieves the list of VAE names from the node definitions.\n *\n * @return {Promise<string[]>} A promise that resolves to an array of strings representing the VAE names.\n */\n async getVAEs() {\n const node_config = await this.getNodeDefs();\n // find VAELoader node\n const node = node_config[\"VAELoader\"];\n const model_name = node?.input?.required?.[\"vae_name\"]?.[0] || [];\n return model_name as string[];\n }\n\n // ----------------- Prompt ++ -----------------\n\n /**\n * Retrieves the status of a prompt based on the provided prompt ID.\n *\n * @param {string} prompt_id - The ID of the prompt to check status for.\n * @return {Object} Object containing the running, pending, and done status of the prompt.\n */\n async getPromptStatus(prompt_id: string) {\n const { Running, Pending } = await this.getQueue();\n const running = Running.some(\n (task: any) => task?.prompt?.[1] === prompt_id,\n );\n const pending = Pending.some(\n (task: any) => task?.prompt?.[1] === prompt_id,\n );\n const done = !running && !pending;\n return {\n running,\n pending,\n done,\n };\n }\n\n /**\n * Retrieves the outputs of a prompt with the given ID from the history.\n *\n * @param {string} prompt_id - The ID of the prompt to retrieve the outputs for.\n * @return {Promise<any>} A promise that resolves to the outputs of the prompt.\n * @throws {Error} If the prompt with the given ID is not found in the history or if it failed with a non-\"success\" status.\n */\n async getPromptOutputs(prompt_id: string) {\n const { History: history } = await this.getHistory();\n const item = history.find((item) => item.prompt[1] === prompt_id);\n if (!item) {\n throw new Error(`Prompt [${prompt_id}] not found in history`);\n }\n\n const status = item.status.status_str;\n if (status !== \"success\") {\n throw new Error(`Prompt [${prompt_id}] failed with status: ${status}`);\n }\n\n return item.outputs;\n }\n\n /**\n * Retrieves the result of a prompt with the given ID, resolved using the provided resolver.\n *\n * @param {string} prompt_id - The ID of the prompt to retrieve the result for.\n * @param {WorkflowOutputResolver<T>} resolver - The resolver to use when resolving the prompt result.\n * @return {Promise<WorkflowOutput<T>>} A promise that resolves to the result of the prompt.\n */\n async getPromptResult<T>(\n prompt_id: string,\n resolver: WorkflowOutputResolver<T>,\n ): Promise<WorkflowOutput<T>>;\n async getPromptResult(prompt_id: string): Promise<WorkflowOutput>;\n async getPromptResult(\n prompt_id: string,\n resolver?: any,\n ): Promise<WorkflowOutput> {\n const outputs = await this.getPromptOutputs(prompt_id);\n if (typeof resolver !== \"function\") {\n resolver = RESOLVERS.image;\n }\n return Object.entries(outputs).reduce(\n (acc, [node_id, output]) =>\n resolver(acc, output, {\n client: this,\n prompt_id,\n node_id,\n }),\n {\n images: [],\n prompt_id,\n data: null,\n } as WorkflowOutput,\n );\n }\n\n /**\n * Asynchronously waits for the prompt with the provided ID to be done.\n *\n * @param {string} prompt_id - The ID of the prompt to wait for.\n * @param {number} [polling_ms=1000] - The number of milliseconds to wait between checks.\n * @return {void}\n */\n async waitForPrompt(prompt_id: string, polling_ms = 1000) {\n let prompt_status = await this.getPromptStatus(prompt_id);\n while (!prompt_status.done) {\n await new Promise((resolve) => setTimeout(resolve, polling_ms));\n prompt_status = await this.getPromptStatus(prompt_id);\n }\n }\n\n /**\n * Asynchronously waits for the prompt with the provided ID to be done,\n * using a WebSocket connection to receive updates.\n *\n * @param {string} prompt_id - The ID of the prompt to wait for.\n * @param {WorkflowOutputResolver<T>} resolver - A function to resolve the output of the prompt.\n * @return {Promise<WorkflowOutput<T>>} A promise that resolves with the output of the prompt.\n */\n async waitForPromptWebSocket<T>(\n prompt_id: string,\n resolver: WorkflowOutputResolver<T>,\n ) {\n const output: WorkflowOutput<T> = {\n images: [],\n prompt_id,\n