@saintno/comfyui-sdk
Version:
SDK for ComfyUI
11 lines • 142 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/socket.ts", "../src/contansts.ts", "../src/tools.ts", "../src/features/abstract.ts", "../src/features/manager.ts", "../src/features/monitoring.ts", "../src/client.ts", "../src/types/error.ts", "../src/call-wrapper.ts", "../src/pool.ts", "../src/prompt-builder.ts", "../index.ts"],
"sourcesContent": [
"// src/WebSocketClient.ts\n\nimport WebSocketLib from \"ws\";\n\n// Define WebSocketInterface to allow for custom implementation\nexport interface WebSocketInterface {\n new (url: string, protocols?: string | string[]): WebSocket;\n new (url: string, options?: any): WebSocket;\n CONNECTING: number;\n OPEN: number;\n CLOSING: number;\n CLOSED: number;\n}\n\n// Default WebSocket implementation based on environment\nlet DefaultWebSocketImpl: WebSocketInterface;\n\nif (typeof window !== \"undefined\" && window.WebSocket) {\n // In a browser environment\n DefaultWebSocketImpl = window.WebSocket;\n} else {\n // In a Node.js environment\n DefaultWebSocketImpl = WebSocketLib as any;\n}\n\nexport interface WebSocketClientOptions {\n headers?: { [key: string]: string };\n customWebSocketImpl?: WebSocketInterface;\n}\n\nexport class WebSocketClient {\n private socket: WebSocket;\n private readonly webSocketImpl: WebSocketInterface;\n\n constructor(url: string, options: WebSocketClientOptions = {}) {\n const { headers, customWebSocketImpl } = options;\n \n // Use custom WebSocket implementation if provided, otherwise use default\n this.webSocketImpl = customWebSocketImpl || DefaultWebSocketImpl;\n\n try {\n if (typeof window !== \"undefined\" && window.WebSocket) {\n // Browser environment - WebSocket does not support custom headers\n this.socket = new this.webSocketImpl(url);\n } else {\n // Node.js environment - using ws package, which supports custom headers\n const WebSocketConstructor = this.webSocketImpl as any;\n this.socket = new WebSocketConstructor(url, { headers });\n }\n } catch (error) {\n console.error(\"WebSocket initialization failed:\", error);\n throw new Error(`WebSocket initialization failed: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n return this;\n }\n\n get client() {\n return this.socket;\n }\n\n public send(message: string) {\n if (this.socket && this.socket.readyState === this.webSocketImpl.OPEN) {\n this.socket.send(message);\n } else {\n console.error(\"WebSocket is not open or available\");\n }\n }\n\n public close() {\n if (this.socket) {\n this.socket.close();\n }\n }\n}\n",
"export const LOAD_CHECKPOINTS_EXTENSION = \"CheckpointLoaderSimple\";\nexport const LOAD_LORAS_EXTENSION = \"LoraLoader\";\nexport const LOAD_KSAMPLER_EXTENSION = \"KSampler\";\n",
"export const randomInt = (min: number, max: number) => {\n return Math.floor(Math.random() * (max - min + 1) + min);\n};\n\nexport const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nexport const seed = () => randomInt(10000000000, 999999999999);\n\n/**\n * Encode POSIX path to NT path\n *\n * For example: `SDXL/realvisxlV40` -> `SDXL\\\\realvisxlV40`\n *\n * Useful for loading model with Windows's ComfyUI Client\n */\nexport const encodeNTPath = (path: string) => {\n return path.replace(/\\//g, \"\\\\\");\n};\n\nexport const encodePosixPath = (path: string) => {\n return path.replace(/\\\\/g, \"/\");\n};\n",
"import { ComfyApi } from \"src/client\";\n\nexport abstract class AbstractFeature extends EventTarget {\n protected client: ComfyApi;\n protected supported = false;\n\n constructor(client: ComfyApi) {\n super();\n this.client = client;\n }\n\n get isSupported() {\n return this.supported;\n }\n\n public on(type: string, callback: (event: any) => void, options?: AddEventListenerOptions | boolean) {\n this.addEventListener(type, callback as any, options);\n return () => this.off(type, callback);\n }\n\n public off(type: string, callback: (event: any) => void, options?: EventListenerOptions | boolean): void {\n this.removeEventListener(type, callback as any, options);\n }\n\n abstract destroy(): void;\n\n /**\n * Check if this feature is supported by the current client\n */\n abstract checkSupported(): Promise<boolean>;\n}\n",
"import {\n TDefaultUI,\n TExtensionNodeItem,\n EExtensionUpdateCheckResult,\n EUpdateResult,\n IExtensionInfo,\n TPreviewMethod,\n IInstallExtensionRequest,\n EInstallType,\n IExtensionUninstallRequest,\n IExtensionUpdateRequest,\n IExtensionActiveRequest,\n IModelInstallRequest,\n INodeMapItem\n} from \"src/types/manager\";\nimport { AbstractFeature } from \"./abstract\";\n\nexport interface FetchOptions extends RequestInit {\n headers?: {\n [key: string]: string;\n };\n}\n\nexport class ManagerFeature extends AbstractFeature {\n async checkSupported() {\n const data = await this.getVersion().catch(() => false);\n if (data !== false) {\n this.supported = true;\n }\n return this.supported;\n }\n\n public destroy(): void {\n this.supported = false;\n }\n\n private async fetchApi(path: string, options?: FetchOptions) {\n if (!this.supported) {\n return false;\n }\n return this.client.fetchApi(path, options);\n }\n\n /**\n * Set the default state to be displayed in the main menu when the browser starts.\n *\n * We use this api to checking if the manager feature is supported.\n *\n * Default will return the current state.\n * @deprecated Not working anymore\n */\n async defaultUi(setUi?: TDefaultUI): Promise<boolean> {\n return true;\n }\n\n async getVersion(): Promise<string> {\n const callURL = \"/manager/version\";\n const data = await this.client.fetchApi(callURL);\n if (data && data.ok) {\n return data.text() as Promise<string>;\n }\n throw new Error(\"Failed to get version\", { cause: data });\n }\n\n /**\n * Retrieves a list of extension's nodes based on the specified mode.\n *\n * Usefull to find the node suitable for the current workflow.\n *\n * @param mode - The mode to determine the source of the nodes. Defaults to \"local\".\n * @returns A promise that resolves to an array of extension nodes.\n * @throws An error if the retrieval fails.\n */\n async getNodeMapList(mode: \"local\" | \"nickname\" = \"local\"): Promise<Array<INodeMapItem>> {\n const listNodes: TExtensionNodeItem[] = [];\n const data = await this.fetchApi(`/customnode/getmappings?mode=${mode}`);\n if (data && data.ok) {\n const nodes: { [key: string]: [string[], any] } = await data.json();\n for (const url in nodes) {\n const [nodeNames, nodeData] = nodes[url];\n listNodes.push({\n url,\n nodeNames,\n title_aux: nodeData.title_aux,\n title: nodeData.title,\n author: nodeData.author,\n nickname: nodeData.nickname,\n description: nodeData.description\n });\n }\n return listNodes;\n }\n throw new Error(\"Failed to get node map list\", { cause: data });\n }\n\n /**\n * Checks for extension updates.\n *\n * @param mode - The mode to use for checking updates. Defaults to \"local\".\n * @returns The result of the extension update check.\n */\n async checkExtensionUpdate(mode: \"local\" | \"cache\" = \"local\") {\n const data = await this.fetchApi(`/customnode/fetch_updates?mode=${mode}`);\n if (data && data.ok) {\n if (data.status === 201) {\n return EExtensionUpdateCheckResult.UPDATE_AVAILABLE;\n }\n return EExtensionUpdateCheckResult.NO_UPDATE;\n }\n return EExtensionUpdateCheckResult.FAILED;\n }\n\n /**\n * Updates all extensions.\n * @param mode - The update mode. Can be \"local\" or \"cache\". Defaults to \"local\".\n * @returns An object representing the result of the extension update.\n */\n async updataAllExtensions(mode: \"local\" | \"cache\" = \"local\") {\n const data = await this.fetchApi(`/customnode/update_all?mode=${mode}`);\n if (data && data.ok) {\n if (data.status === 200) {\n return { type: EUpdateResult.UNCHANGED };\n }\n return {\n type: EUpdateResult.SUCCESS,\n data: (await data.json()) as { updated: number; failed: number }\n } as const;\n }\n return { type: EUpdateResult.FAILED };\n }\n\n /**\n * Updates the ComfyUI.\n *\n * @returns The result of the update operation.\n */\n async updateComfyUI() {\n const data = await this.fetchApi(\"/comfyui_manager/update_comfyui\");\n if (data) {\n switch (data.status) {\n case 200:\n return EUpdateResult.UNCHANGED;\n case 201:\n return EUpdateResult.SUCCESS;\n default:\n return EUpdateResult.FAILED;\n }\n }\n return EUpdateResult.FAILED;\n }\n\n /**\n * Retrieves the list of extensions.\n *\n * @param mode - The mode to retrieve the extensions from. Can be \"local\" or \"cache\". Defaults to \"local\".\n * @param skipUpdate - Indicates whether to skip updating the extensions. Defaults to true.\n * @returns A promise that resolves to an object containing the channel and custom nodes, or false if the retrieval fails.\n * @throws An error if the retrieval fails.\n */\n async getExtensionList(\n mode: \"local\" | \"cache\" = \"local\",\n skipUpdate: boolean = true\n ): Promise<\n | {\n channel: \"local\" | \"default\";\n custom_nodes: IExtensionInfo[];\n }\n | false\n > {\n const data = await this.fetchApi(`/customnode/getlist?mode=${mode}&skip_update=${skipUpdate}`);\n if (data && data.ok) {\n return data.json();\n }\n throw new Error(\"Failed to get extension list\", { cause: data });\n }\n\n /**\n * Reboots the instance.\n *\n * @returns A promise that resolves to `true` if the instance was successfully rebooted, or `false` otherwise.\n */\n async rebootInstance() {\n const data = await this.fetchApi(\"/manager/reboot\").catch((e) => {\n return true;\n });\n if (data !== true) return false;\n return true;\n }\n\n /**\n * Return the current preview method. Will set to `mode` if provided.\n *\n * @param mode - The preview method mode.\n * @returns The result of the preview method.\n * @throws An error if the preview method fails to set.\n */\n async previewMethod(mode?: TPreviewMethod): Promise<TPreviewMethod | undefined> {\n let callURL = \"/manager/preview_method\";\n if (mode) {\n callURL += `?value=${mode}`;\n }\n const data = await this.fetchApi(callURL);\n if (data && data.ok) {\n const result = await data.text();\n if (!result) return mode;\n return result as TPreviewMethod;\n }\n throw new Error(\"Failed to set preview method\", { cause: data });\n }\n\n /**\n * Installs an extension based on the provided configuration.\n *\n * @param config - The configuration for the extension installation.\n * @returns A boolean indicating whether the installation was successful.\n * @throws An error if the installation fails.\n */\n async installExtension(config: IInstallExtensionRequest) {\n const data = await this.fetchApi(\"/customnode/install\", {\n method: \"POST\",\n body: JSON.stringify(config)\n });\n if (data && data.ok) {\n return true;\n }\n throw new Error(\"Failed to install extension\", { cause: data });\n }\n\n /**\n * Try to fix installation of an extension by re-install it again with fixes.\n *\n * @param config - The configuration object for fixing the extension.\n * @returns A boolean indicating whether the extension was fixed successfully.\n * @throws An error if the fix fails.\n */\n async fixInstallExtension(\n config: Omit<IInstallExtensionRequest, \"js_path\" | \"install_type\"> & {\n install_type: EInstallType.GIT_CLONE;\n }\n ) {\n const data = await this.fetchApi(\"/customnode/fix\", {\n method: \"POST\",\n body: JSON.stringify(config)\n });\n if (data && data.ok) {\n return true;\n }\n throw new Error(\"Failed to fix extension installation\", { cause: data });\n }\n\n /**\n * Install an extension from a Git URL.\n *\n * @param url - The URL of the Git repository.\n * @returns A boolean indicating whether the installation was successful.\n * @throws An error if the installation fails.\n */\n async installExtensionFromGit(url: string) {\n const data = await this.fetchApi(\"/customnode/install/git_url\", {\n method: \"POST\",\n body: url\n });\n if (data && data.ok) {\n return true;\n }\n throw new Error(\"Failed to install extension from git\", { cause: data });\n }\n\n /**\n * Installs pip packages.\n *\n * @param packages - An array of packages to install.\n * @returns A boolean indicating whether the installation was successful.\n * @throws An error if the installation fails.\n */\n async installPipPackages(packages: string[]) {\n const data = await this.fetchApi(\"/customnode/install/pip\", {\n method: \"POST\",\n body: packages.join(\" \")\n });\n if (data && data.ok) {\n return true;\n }\n throw new Error(\"Failed to install pip's packages\", { cause: data });\n }\n\n /**\n * Uninstalls an extension.\n *\n * @param config - The configuration for uninstalling the extension.\n * @returns A boolean indicating whether the uninstallation was successful.\n * @throws An error if the uninstallation fails.\n */\n async uninstallExtension(config: IExtensionUninstallRequest) {\n const data = await this.fetchApi(\"/customnode/uninstall\", {\n method: \"POST\",\n body: JSON.stringify(config)\n });\n if (data && data.ok) {\n return true;\n }\n throw new Error(\"Failed to uninstall extension\", { cause: data });\n }\n\n /**\n * Updates the extension with the provided configuration. Only work with git-clone method\n *\n * @param config - The configuration object for the extension update.\n * @returns A boolean indicating whether the extension update was successful.\n * @throws An error if the extension update fails.\n */\n async updateExtension(config: IExtensionUpdateRequest) {\n const data = await this.fetchApi(\"/customnode/update\", {\n method: \"POST\",\n body: JSON.stringify(config)\n });\n if (data && data.ok) {\n return true;\n }\n throw new Error(\"Failed to update extension\", { cause: data });\n }\n\n /**\n * Set the activation of extension.\n *\n * @param config - The configuration for the active extension.\n * @returns A boolean indicating whether the active extension was set successfully.\n * @throws An error if setting the active extension fails.\n */\n async setActiveExtension(config: IExtensionActiveRequest) {\n const data = await this.fetchApi(\"/customnode/toggle_active\", {\n method: \"POST\",\n body: JSON.stringify(config)\n });\n if (data && data.ok) {\n return true;\n }\n throw new Error(\"Failed to set active extension\", { cause: data });\n }\n\n /**\n * Install a model from given info.\n *\n * @param info - The model installation request information.\n * @returns A boolean indicating whether the model installation was successful.\n * @throws An error if the model installation fails.\n */\n async installModel(info: IModelInstallRequest) {\n const data = await this.fetchApi(\"/model/install\", {\n method: \"POST\",\n body: JSON.stringify(info)\n });\n if (data && data.ok) {\n return true;\n }\n throw new Error(\"Failed to install model\", { cause: data });\n }\n}\n",
"import { AbstractFeature } from \"./abstract\";\nimport { FetchOptions } from \"./manager\";\n\nconst SYSTEM_MONITOR_EXTENSION = encodeURIComponent(\"Primitive boolean [Crystools]\");\n\nexport type TMonitorEvent = {\n cpu_utilization: number;\n ram_total: number;\n ram_used: number;\n ram_used_percent: number;\n hdd_total: number;\n hdd_used: number;\n hdd_used_percent: number;\n device_type: \"cuda\";\n gpus: Array<{\n gpu_utilization: number;\n gpu_temperature: number;\n vram_total: number;\n vram_used: number;\n vram_used_percent: number;\n }>;\n};\n\nexport type TMonitorEventMap = {\n system_monitor: CustomEvent<TMonitorEvent>;\n};\n\nexport class MonitoringFeature extends AbstractFeature {\n private resources?: TMonitorEvent;\n private listeners: {\n event: keyof TMonitorEventMap;\n options?: AddEventListenerOptions | boolean;\n handler: (event: TMonitorEventMap[keyof TMonitorEventMap]) => void;\n }[] = [];\n private binded = false;\n\n async checkSupported() {\n const data = await this.client.getNodeDefs(SYSTEM_MONITOR_EXTENSION);\n if (data) {\n this.supported = true;\n this.bind();\n }\n return this.supported;\n }\n\n public destroy(): void {\n this.listeners.forEach((listener) => {\n this.off(listener.event, listener.handler, listener.options);\n });\n this.listeners = [];\n }\n\n private async fetchApi(path: string, options?: FetchOptions) {\n if (!this.supported) {\n return false;\n }\n return this.client.fetchApi(path, options);\n }\n\n public on<K extends keyof TMonitorEventMap>(\n type: K,\n callback: (event: TMonitorEventMap[K]) => void,\n options?: AddEventListenerOptions | boolean\n ) {\n this.addEventListener(type, callback as any, options);\n this.listeners.push({ event: type, options, handler: callback });\n return () => this.off(type, callback);\n }\n\n public off<K extends keyof TMonitorEventMap>(\n type: K,\n callback: (event: TMonitorEventMap[K]) => void,\n options?: EventListenerOptions | boolean\n ): void {\n this.removeEventListener(type, callback as any, options);\n this.listeners = this.listeners.filter((listener) => listener.event !== type && listener.handler !== callback);\n }\n\n /**\n * Gets the monitor data.\n *\n * @returns The monitor data if supported, otherwise false.\n */\n get monitorData() {\n if (!this.supported) {\n return false;\n }\n return this.resources;\n }\n\n /**\n * Sets the monitor configuration.\n */\n async setConfig(\n config?: Partial<{\n /**\n * Refresh per second (Default 0.5)\n */\n rate: number;\n /**\n * Switch to enable/disable CPU monitoring\n */\n switchCPU: boolean;\n /**\n * Switch to enable/disable GPU monitoring\n */\n switchHDD: boolean;\n /**\n * Switch to enable/disable RAM monitoring\n */\n switchRAM: boolean;\n /**\n * Path of HDD to monitor HDD usage (use getHddList to get the pick-able list)\n */\n whichHDD: string;\n }>\n ) {\n if (!this.supported) {\n return false;\n }\n return this.fetchApi(`/api/crystools/monitor`, {\n method: \"PATCH\",\n body: JSON.stringify(config)\n });\n }\n\n /**\n * Switches the monitor on or off.\n */\n async switch(active: boolean) {\n if (!this.supported) {\n return false;\n }\n return this.fetchApi(`/api/crystools/monitor/switch`, {\n method: \"POST\",\n body: JSON.stringify({ monitor: active })\n });\n }\n\n /**\n * Gets the list of HDDs.\n */\n async getHddList(): Promise<null | Array<string>> {\n if (!this.supported) {\n return null;\n }\n const data = await this.fetchApi(`/api/crystools/monitor/HDD`);\n if (data) {\n return data.json();\n }\n return null;\n }\n\n /**\n * Gets the list of GPUs.\n */\n async getGpuList(): Promise<null | Array<{ index: number; name: string }>> {\n if (!this.supported) {\n return null;\n }\n const data = await this.fetchApi(`/api/crystools/monitor/GPU`);\n if (data) {\n return data.json();\n }\n return null;\n }\n\n /**\n * Config gpu monitoring\n * @param index Index of the GPU\n * @param config Configuration of monitoring, set to `true` to enable monitoring\n */\n async setGpuConfig(index: number, config: Partial<{ utilization: boolean; vram: boolean; temperature: boolean }>) {\n if (!this.supported) {\n return false;\n }\n return this.fetchApi(`/api/crystools/monitor/GPU/${index}`, {\n method: \"PATCH\",\n body: JSON.stringify(config)\n });\n }\n\n private bind() {\n if (this.binded) {\n return;\n } else {\n this.binded = true;\n }\n this.client.on(\"all\", (ev) => {\n const msg = ev.detail;\n if (msg.type === \"crystools.monitor\") {\n this.resources = msg.data;\n this.dispatchEvent(new CustomEvent(\"system_monitor\", { detail: msg.data }));\n }\n });\n }\n}\n",
"import { WebSocketClient } from \"./socket\";\n\nimport {\n BasicCredentials,\n BearerTokenCredentials,\n CustomCredentials,\n HistoryEntry,\n HistoryResponse,\n ImageInfo,\n ModelFile,\n ModelFolder,\n ModelPreviewResponse,\n NodeDefsResponse,\n OSType,\n QueuePromptResponse,\n QueueResponse,\n QueueStatus,\n SystemStatsResponse\n} from \"./types/api\";\n\nimport { LOAD_CHECKPOINTS_EXTENSION, LOAD_KSAMPLER_EXTENSION, LOAD_LORAS_EXTENSION } from \"./contansts\";\nimport { TComfyAPIEventMap } from \"./types/event\";\nimport { delay } from \"./tools\";\nimport { ManagerFeature } from \"./features/manager\";\nimport { MonitoringFeature } from \"./features/monitoring\";\n\ninterface FetchOptions extends RequestInit {\n headers?: {\n [key: string]: string;\n };\n}\n\nexport class ComfyApi extends EventTarget {\n public apiHost: string;\n public osType: OSType;\n public isReady: boolean = false;\n public listenTerminal: boolean = false;\n public lastActivity: number = Date.now();\n\n private wsTimeout: number = 10000;\n private wsTimer: Timer | null = null;\n private _pollingTimer: NodeJS.Timeout | number | null = null;\n\n private apiBase: string;\n private clientId: string | null;\n private socket: WebSocketClient | null = null;\n private listeners: {\n event: keyof TComfyAPIEventMap;\n options?: AddEventListenerOptions | boolean;\n handler: (event: TComfyAPIEventMap[keyof TComfyAPIEventMap]) => void;\n }[] = [];\n private credentials: BasicCredentials | BearerTokenCredentials | CustomCredentials | null = null;\n\n public ext = {\n /**\n * Interact with ComfyUI-Manager Extension\n */\n manager: new ManagerFeature(this),\n /**\n * Interact with ComfyUI-Crystools Extension for track system resouces\n */\n monitor: new MonitoringFeature(this)\n };\n\n static generateId(): string {\n return \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\".replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n }\n\n public on<K extends keyof TComfyAPIEventMap>(\n type: K,\n callback: (event: TComfyAPIEventMap[K]) => void,\n options?: AddEventListenerOptions | boolean\n ) {\n this.log(\"on\", \"Add listener\", { type, callback, options });\n this.addEventListener(type, callback as any, options);\n this.listeners.push({ event: type, handler: callback, options });\n const clr = () => this.off(type, callback, options);\n return clr;\n }\n\n public off<K extends keyof TComfyAPIEventMap>(\n type: K,\n callback: (event: TComfyAPIEventMap[K]) => void,\n options?: EventListenerOptions | boolean\n ): void {\n this.log(\"off\", \"Remove listener\", { type, callback, options });\n this.listeners = this.listeners.filter((listener) => listener.event !== type && listener.handler !== callback);\n this.removeEventListener(type, callback as any, options);\n }\n\n public removeAllListeners() {\n this.log(\"removeAllListeners\", \"Triggered\");\n this.listeners.forEach((listener) => {\n this.removeEventListener(listener.event, listener.handler, listener.options);\n });\n this.listeners = [];\n }\n\n get id(): string {\n return this.clientId ?? this.apiBase;\n }\n\n /**\n * Retrieves the available features of the client.\n *\n * @returns An object containing the available features, where each feature is a key-value pair.\n */\n get availableFeatures() {\n return Object.keys(this.ext).reduce(\n (acc, key) => ({\n ...acc,\n [key]: this.ext[key as keyof typeof this.ext].isSupported\n }),\n {}\n );\n }\n\n constructor(\n host: string,\n clientId: string = ComfyApi.generateId(),\n opts?: {\n /**\n * Do not fallback to HTTP if WebSocket is not available.\n * This will retry to connect to WebSocket on error.\n */\n forceWs?: boolean;\n /**\n * Timeout for WebSocket connection.\n * Default is 10000ms.\n */\n wsTimeout?: number;\n /**\n * Listen to terminal logs from the server. Default (false)\n */\n listenTerminal?: boolean;\n credentials?: BasicCredentials | BearerTokenCredentials | CustomCredentials;\n }\n ) {\n super();\n this.apiHost = host;\n this.apiBase = host.split(\"://\")[1];\n this.clientId = clientId;\n if (opts?.credentials) {\n this.credentials = opts?.credentials;\n this.testCredentials();\n }\n if (opts?.wsTimeout) {\n this.wsTimeout = opts.wsTimeout;\n }\n if (opts?.listenTerminal) {\n this.listenTerminal = opts.listenTerminal;\n }\n this.log(\"constructor\", \"Initialized\", {\n host,\n clientId,\n opts\n });\n return this;\n }\n\n /**\n * Destroys the client instance.\n */\n destroy() {\n this.log(\"destroy\", \"Destroying client...\");\n // Clean up WebSocket timer\n if (this.wsTimer) clearInterval(this.wsTimer);\n // Clean up polling timer if exists\n if (this._pollingTimer) {\n clearInterval(this._pollingTimer as any);\n this._pollingTimer = null;\n }\n // Clean up socket event handlers\n if (this.socket?.client) {\n this.socket.client.onclose = null;\n this.socket.client.onerror = null;\n this.socket.client.onmessage = null;\n this.socket.client.onopen = null;\n this.socket.client.close();\n }\n for (const ext in this.ext) {\n this.ext[ext as keyof typeof this.ext].destroy();\n }\n this.socket?.close();\n this.log(\"destroy\", \"Client destroyed\");\n this.removeAllListeners();\n }\n\n private log(fnName: string, message: string, data?: any) {\n this.dispatchEvent(new CustomEvent(\"log\", { detail: { fnName, message, data } }));\n }\n\n private apiURL(route: string): string {\n return `${this.apiHost}${route}`;\n }\n\n private getCredentialHeaders(): Record<string, string> {\n if (!this.credentials) return {};\n switch (this.credentials?.type) {\n case \"basic\":\n return {\n Authorization: `Basic ${btoa(`${this.credentials.username}:${this.credentials.password}`)}`\n };\n case \"bearer_token\":\n return {\n Authorization: `Bearer ${this.credentials.token}`\n };\n case \"custom\":\n return this.credentials.headers;\n default:\n return {};\n }\n }\n\n private async testCredentials() {\n try {\n if (!this.credentials) return false;\n await this.pollStatus(2000);\n this.dispatchEvent(new CustomEvent(\"auth_success\"));\n return true;\n } catch (e) {\n this.log(\"testCredentials\", \"Failed\", e);\n if (e instanceof Response) {\n if (e.status === 401) {\n this.dispatchEvent(new CustomEvent(\"auth_error\", { detail: e }));\n return;\n }\n }\n this.dispatchEvent(new CustomEvent(\"connection_error\", { detail: e }));\n return false;\n }\n }\n\n private async testFeatures() {\n const exts = Object.values(this.ext);\n await Promise.all(exts.map((ext) => ext.checkSupported()));\n /**\n * Mark the client is ready to use the API.\n */\n this.isReady = true;\n }\n\n /**\n * Fetches data from the API.\n *\n * @param route - The route to fetch data from.\n * @param options - The options for the fetch request.\n * @returns A promise that resolves to the response from the API.\n */\n public async fetchApi(route: string, options?: FetchOptions): Promise<Response> {\n if (!options) {\n options = {};\n }\n options.headers = {\n ...this.getCredentialHeaders()\n };\n options.mode = \"cors\";\n return fetch(this.apiURL(route), options);\n }\n\n /**\n * Polls the status for colab and other things that don't support websockets.\n * @returns {Promise<QueueStatus>} The status information.\n */\n async pollStatus(timeout = 1000): Promise<QueueStatus> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n try {\n const response = await this.fetchApi(\"/prompt\", {\n signal: controller.signal\n });\n if (response.status === 200) {\n return response.json();\n } else {\n throw response;\n }\n } catch (error: any) {\n this.log(\"pollStatus\", \"Failed\", error);\n if (error.name === \"AbortError\") {\n throw new Error(\"Request timed out\");\n }\n throw error;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Queues a prompt for processing.\n * @param {number} number The index at which to queue the prompt. using NULL will append to the end of the queue.\n * @param {object} workflow Additional workflow data.\n * @returns {Promise<QueuePromptResponse>} The response from the API.\n */\n async queuePrompt(number: number | null, workflow: object): Promise<QueuePromptResponse> {\n const body = {\n client_id: this.clientId,\n prompt: workflow\n } as any;\n\n if (number !== null) {\n if (number === -1) {\n body[\"front\"] = true;\n } else if (number !== 0) {\n body[\"number\"] = number;\n }\n }\n\n try {\n const response = await this.fetchApi(\"/prompt\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(body)\n });\n\n if (response.status !== 200) {\n throw {\n response\n };\n }\n\n return response.json();\n } catch (e) {\n this.log(\"queuePrompt\", \"Can't queue prompt\", e);\n throw e.response as Response;\n }\n }\n\n /**\n * Appends a prompt to the workflow queue.\n *\n * @param {object} workflow Additional workflow data.\n * @returns {Promise<QueuePromptResponse>} The response from the API.\n */\n async appendPrompt(workflow: object): Promise<QueuePromptResponse> {\n return this.queuePrompt(null, workflow).catch((e) => {\n this.dispatchEvent(new CustomEvent(\"queue_error\"));\n throw e;\n });\n }\n\n /**\n * Retrieves the current state of the queue.\n * @returns {Promise<QueueResponse>} The queue state.\n */\n async getQueue(): Promise<QueueResponse> {\n const response = await this.fetchApi(\"/queue\");\n return response.json();\n }\n\n /**\n * Retrieves the prompt execution history.\n * @param {number} [maxItems=200] The maximum number of items to retrieve.\n * @returns {Promise<HistoryResponse>} The prompt execution history.\n */\n async getHistories(maxItems: number = 200): Promise<HistoryResponse> {\n const response = await this.fetchApi(`/history?max_items=${maxItems}`);\n return response.json();\n }\n\n /**\n * Retrieves the history entry for a given prompt ID.\n * @param promptId - The ID of the prompt.\n * @returns A Promise that resolves to the HistoryEntry object.\n */\n async getHistory(promptId: string): Promise<HistoryEntry | undefined> {\n const response = await this.fetchApi(`/history/${promptId}`);\n const history: HistoryResponse = await response.json();\n return history[promptId];\n }\n\n /**\n * Retrieves system and device stats.\n * @returns {Promise<SystemStatsResponse>} The system stats.\n */\n async getSystemStats(): Promise<SystemStatsResponse> {\n const response = await this.fetchApi(\"/system_stats\");\n return response.json();\n }\n\n /**\n * Retrieves the terminal logs from the server.\n */\n async getTerminalLogs(): Promise<{\n entries: Array<{ t: string; m: string }>;\n size: { cols: number; rows: number };\n }> {\n const response = await this.fetchApi(\"/internal/logs/raw\");\n return response.json();\n }\n\n /**\n * Sets the terminal subscription status.\n * Enable will subscribe to terminal logs from websocket.\n */\n async setTerminalSubscription(subscribe: boolean) {\n // Set the terminal subscription status again if call again\n this.listenTerminal = subscribe;\n // Send the request to the server\n await this.fetchApi(\"/internal/logs/subscribe\", {\n method: \"PATCH\",\n headers: {\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n clientId: this.clientId,\n enabled: subscribe\n })\n });\n }\n\n /**\n * Retrieves a list of extension URLs.\n * @returns {Promise<string[]>} A list of extension URLs.\n */\n async getExtensions(): Promise<string[]> {\n const response = await this.fetchApi(\"/extensions\");\n return response.json();\n }\n\n /**\n * Retrieves a list of embedding names.\n * @returns {Promise<string[]>} A list of embedding names.\n */\n async getEmbeddings(): Promise<string[]> {\n const response = await this.fetchApi(\"/embeddings\");\n return response.json();\n }\n\n /**\n * Retrieves the checkpoints from the server.\n * @returns A promise that resolves to an array of strings representing the checkpoints.\n */\n async getCheckpoints(): Promise<string[]> {\n const nodeInfo = await this.getNodeDefs(LOAD_CHECKPOINTS_EXTENSION);\n if (!nodeInfo) return [];\n const output = nodeInfo[LOAD_CHECKPOINTS_EXTENSION].input.required?.ckpt_name?.[0];\n if (!output) return [];\n return output as string[];\n }\n\n /**\n * Retrieves the Loras from the node definitions.\n * @returns A Promise that resolves to an array of strings representing the Loras.\n */\n async getLoras(): Promise<string[]> {\n const nodeInfo = await this.getNodeDefs(LOAD_LORAS_EXTENSION);\n if (!nodeInfo) return [];\n const output = nodeInfo[LOAD_LORAS_EXTENSION].input.required?.lora_name?.[0];\n if (!output) return [];\n return output as string[];\n }\n\n /**\n * Retrieves the sampler information.\n * @returns An object containing the sampler and scheduler information.\n */\n async getSamplerInfo() {\n const nodeInfo = await this.getNodeDefs(LOAD_KSAMPLER_EXTENSION);\n if (!nodeInfo) return {};\n return {\n sampler: nodeInfo[LOAD_KSAMPLER_EXTENSION].input.required.sampler_name ?? [],\n scheduler: nodeInfo[LOAD_KSAMPLER_EXTENSION].input.required.scheduler ?? []\n };\n }\n\n /**\n * Retrieves node object definitions for the graph.\n * @returns {Promise<NodeDefsResponse>} The node definitions.\n */\n async getNodeDefs(nodeName?: string): Promise<NodeDefsResponse | null> {\n const response = await this.fetchApi(`/object_info${nodeName ? `/${nodeName}` : \"\"}`);\n const result = await response.json();\n if (Object.keys(result).length === 0) {\n return null;\n }\n return result;\n }\n\n /**\n * Retrieves user configuration data.\n * @returns {Promise<any>} The user configuration data.\n */\n async getUserConfig(): Promise<any> {\n const response = await this.fetchApi(\"/users\");\n return response.json();\n }\n\n /**\n * Creates a new user.\n * @param {string} username The username of the new user.\n * @returns {Promise<Response>} The response from the API.\n */\n async createUser(username: string): Promise<Response> {\n const response = await this.fetchApi(\"/users\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ username })\n });\n return response;\n }\n\n /**\n * Retrieves all setting values for the current user.\n * @returns {Promise<any>} A dictionary of setting id to value.\n */\n async getSettings(): Promise<any> {\n const response = await this.fetchApi(\"/settings\");\n return response.json();\n }\n\n /**\n * Retrieves a specific setting for the current user.\n * @param {string} id The id of the setting to fetch.\n * @returns {Promise<any>} The setting value.\n */\n async getSetting(id: string): Promise<any> {\n const response = await this.fetchApi(`/settings/${encodeURIComponent(id)}`);\n return response.json();\n }\n\n /**\n * Stores a dictionary of settings for the current user.\n * @param {Record<string, unknown>} settings Dictionary of setting id to value to save.\n * @returns {Promise<void>}\n */\n async storeSettings(settings: Record<string, unknown>): Promise<void> {\n await this.fetchApi(`/settings`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(settings)\n });\n }\n\n /**\n * Stores a specific 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<void> {\n await this.fetchApi(`/settings/${encodeURIComponent(id)}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(value)\n });\n }\n\n /**\n * Uploads an image file to the server.\n * @param file - The image file to upload.\n * @param fileName - The name of the image file.\n * @param override - Optional. Specifies whether to override an existing file with the same name. Default is true.\n * @returns A Promise that resolves to an object containing the image information and the URL of the uploaded image,\n * or false if the upload fails.\n */\n async uploadImage(\n file: Buffer | Blob,\n fileName: string,\n config?: {\n override?: boolean;\n subfolder?: string;\n }\n ): Promise<{ info: ImageInfo; url: string } | false> {\n const formData = new FormData();\n if (file instanceof Buffer) {\n formData.append(\"image\", new Blob([file]), fileName);\n } else {\n formData.append(\"image\", file, fileName);\n }\n formData.append(\"subfolder\", config?.subfolder ?? \"\");\n formData.append(\"overwrite\", config?.override?.toString() ?? \"false\");\n\n try {\n const response = await this.fetchApi(\"/upload/image\", {\n method: \"POST\",\n body: formData\n });\n const imgInfo = await response.json();\n const mapped = { ...imgInfo, filename: imgInfo.name };\n\n // Check if the response is successful\n if (!response.ok) {\n this.log(\"uploadImage\", \"Upload failed\", response);\n return false;\n }\n\n return {\n info: mapped,\n url: this.getPathImage(mapped)\n };\n } catch (e) {\n this.log(\"uploadImage\", \"Upload failed\", e);\n return false;\n }\n }\n\n /**\n * Uploads a mask file to the server.\n *\n * @param file - The mask file to upload, can be a Buffer or Blob.\n * @param originalRef - The original reference information for the file.\n * @returns A Promise that resolves to an object containing the image info and URL if the upload is successful, or false if the upload fails.\n */\n async uploadMask(file: Buffer | Blob, originalRef: ImageInfo): Promise<{ info: ImageInfo; url: string } | false> {\n const formData = new FormData();\n\n // Append the image file to the form data\n if (file instanceof Buffer) {\n formData.append(\"image\", new Blob([file]), \"mask.png\");\n } else {\n formData.append(\"image\", file, \"mask.png\");\n }\n\n // Append the original reference as a JSON string\n formData.append(\"original_ref\", JSON.stringify(originalRef));\n\n try {\n // Send the POST request to the /upload/mask endpoint\n const response = await this.fetchApi(\"/upload/mask\", {\n method: \"POST\",\n body: formData\n });\n\n // Check if the response is successful\n if (!response.ok) {\n this.log(\"uploadMask\", \"Upload failed\", response);\n return false;\n }\n\n const imgInfo = await response.json();\n const mapped = { ...imgInfo, filename: imgInfo.name };\n return {\n info: mapped,\n url: this.getPathImage(mapped)\n };\n } catch (error) {\n this.log(\"uploadMask\", \"Upload failed\", error);\n return false;\n }\n }\n\n /**\n * Frees memory by unloading models and freeing memory.\n *\n * @param unloadModels - A boolean indicating whether to unload models.\n * @param freeMemory - A boolean indicating whether to free memory.\n * @returns A promise that resolves to a boolean indicating whether the memory was successfully freed.\n */\n async freeMemory(unloadModels: boolean, freeMemory: boolean): Promise<boolean> {\n const payload = {\n unload_models: unloadModels,\n free_memory: freeMemory\n };\n\n try {\n const response = await this.fetchApi(\"/free\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(payload)\n });\n\n // Check if the response is successful\n if (!response.ok) {\n this.log(\"freeMemory\", \"Free memory failed\", response);\n return false;\n }\n\n // Return the response object\n return true;\n } catch (error) {\n this.log(\"freeMemory\", \"Free memory failed\", error);\n return false;\n }\n }\n\n /**\n * Returns the path to an image based on the provided image information.\n * @param imageInfo - The information of the image.\n * @returns The path to the image.\n */\n getPathImage(imageInfo: ImageInfo): string {\n return this.apiURL(\n `/view?filename=${imageInfo.filename}&type=${imageInfo.type}&subfolder=${imageInfo.subfolder ?? \"\"}`\n );\n }\n\n /**\n * Get blob of image based on the provided image information. Use when the server have credential.\n */\n async getImage(imageInfo: ImageInfo): Promise<Blob> {\n return this.fetchApi(\n `/view?filename=${imageInfo.filename}&type=${imageInfo.type}&subfolder=${imageInfo.subfolder ?? \"\"}`\n ).then((res) => res.blob());\n }\n\n /**\n * Retrieves a user data file for the current user.\n * @param {string} file The name of the userdata file to load.\n * @returns {Promise<Response>} The fetch response object.\n */\n async getUserData(file: string): Promise<Response> {\n return this.fetchApi(`/userdata/${encodeURIComponent(file)}`);\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 {unknown} data The data to save to the file.\n * @param {RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean }} [options] Additional options for storing the file.\n * @returns {Promise<Response>}\n */\n async storeUserData(\n file: string,\n data: unknown,\n options: RequestInit & {\n overwrite?: boolean;\n stringify?: boolean;\n throwOnError?: boolean;\n } = { overwrite: true, stringify: true, throwOnError: true }\n ): Promise<Response> {\n const response = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options.overwrite}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": options.stringify ? \"application/json\" : \"application/octet-stream\"\n } as any,\n body: options.stringify ? JSON.stringify(data) : (data as any),\n ...options\n });\n\n if (response.status !== 200 && options.throwOnError !== false) {\n this.log(\"storeUserData\", \"Error storing user data file\", response);\n throw new Error(`Error storing user data file '${file}': ${response.status} ${response.statusText}`);\n }\n\n return response;\n }\n\n /**\n * Deletes a user data file for the current user.\n * @param {string} file The name of the userdata file to delete.\n * @returns {Promise<void>}\n */\n async deleteUserData(file: string): Promise<void> {\n const response = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {\n method: \"DELETE\"\n });\n\n if (response.status !== 204) {\n this.log(\"deleteUserData\", \"Error deleting user data file\", response);\n throw new Error(`Error removing user data file '${file}': ${response.status} ${response.statusText}`);\n }\n }\n\n /**\n * Moves a user data file for the current user.\n * @param {string} source The userdata file to move.\n * @param {string} dest The destination for the file.\n * @param {RequestInit & { overwrite?: boolean }} [options] Additional options for moving the file.\n * @returns {Promise<Response>}\n */\n async moveUserData(\n source: string,\n dest: string,\n options: RequestInit & { overwrite?: boolean } = { overwrite: false }\n ): Promise<Response> {\n return this.fetchApi(\n `/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options.overwrite}`,\n {\n method: \"POST\"\n }\n );\n }\n\n /**\n * Lists user data files for the current user.\n * @param {string} dir The directory in which to list files.\n * @param {boolean} [recurse] If the listing should be recursive.\n * @param {boolean} [split] If the paths should be split based on the OS path separator.\n * @returns {Promise<string[]>} The list of files.\n */\n async listUserData(dir: string, recurse?: boolean, split?: boolean): Promise<string[]> {\n const response = await this.fetchApi(\n `/userdata?${new URLSearchParams({\n dir,\n recurse: recurse?.toString() ?? \"\",\n split: split?.toString() ?? \"\"\n })}`\n );\n\n if (response.status === 404) return [];\n if (response.status !== 200) {\n this.log(\"listUserData\", \"Error getting user data list\", response);\n throw new Error(`Error getting user data list '${dir}': ${response.status} ${response.statusText}`);\n }\n\n return response.json();\n }\n\n /**\n * Interrupts the execution of the running prompt.\n * @returns {Promise<void>}\n */\n async interrupt(): Promise<void> {\n await this.fetchApi(\"/interrupt\", {\n method: \"POST\"\n });\n }\n\n /**\n * Initializes the client.\n *\n * @param maxTries - The maximum number of ping tries.\n * @param delayTime - The delay time between ping tries in milliseconds.\n * @returns The initialized client instance.\n */\n init(maxTries = 10, delayTime = 1000) {\n this.pingSuccess(maxTries, delayTime)\n .then(() => {\n /**\n * Get system OS type on initialization.\n */\n this.pullOsType();\n /**\n * Test features on initialization.\n */\n this.testFeatures();\n /**\n * Create WebSocket connection on initialization.\n */\n this.createSocket();\n /**\n * Set terminal subscription on initialization.\n */\n this.setTerminalSubscription(this.listenTerminal);\n })\n .catch((e) => {\n this.log(\"init\", \"Failed\", e);\n this.dispatchEvent(new CustomEvent(\"connection_error\", { detail: e }));\n });\n return this;\n }\n\n private async pingSuccess(maxTries = 10, delayTime = 1000) {\n let tries = 0;\n let ping = await this.ping();\n while (!ping.status) {\n if (tries > maxTries) {\n throw new Error(\"Can't connect to the server\");\n }\n await delay(delayTime); // Wait for 1s before trying again\n ping = await this.ping();\n tries++;\n }\n }\n\n async waitForReady() {\n while (!this.isReady) {\n await delay(100);\n }\n return this;\n }\n\n private async pullOsType() {\n this.getSystemStats().then((data) => {\n this.osType = data.system.os;\n });\n }\n\n /**\n * Sends a ping request to the server and returns a boolean indicating whether the server is reachable.\n * @returns A promise that resolves to `true` if the server is reachable, or `false` otherwise.\n */\n async ping() {\n const start = performance.now();\n return this.pollStatus(5000)\n .then(() => {\n return { status: true, time: performance.now() - start } as const;\n })\n .catch((error) => {\n this.log(\"ping\", \"Can't connect to the server\", error);\n return { status: false } as const;\n });\n }\n\n /**\n * Attempts to reconnect the WebSocket with an exponential backoff strategy\n * @param triggerEvent Whether to trigger disconnect/reconnect events\n */\n public async reconnectWs(triggerEvent?: boolean) {\n if (triggerEvent) {\n this.dispatchEvent(new CustomEvent(\"disconnected\"));\n this.dispatchEvent(new CustomEvent(\"reconnecting\"));\n }\n\n // Maximum number of reconnection attempts\n const MAX_ATTEMPTS = 10;\n // Base delay in milliseconds\n const BASE_DELAY = 1000;\n // Maximum delay between attempts (15 seconds)\n const MAX_DELAY = 15000;\n \n let attempt = 0;\n\n const tryReconnect =