mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2025-09-15 05:57:57 +00:00
Use new TS frontend uncompressed (#4379)
* Swap frontend uncompressed * Add uncompressed files
This commit is contained in:
484
web/scripts/api.js
vendored
484
web/scripts/api.js
vendored
@@ -1,482 +1,2 @@
|
||||
class ComfyApi extends EventTarget {
|
||||
#registered = new Set();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.api_host = location.host;
|
||||
this.api_base = location.pathname.split('/').slice(0, -1).join('/');
|
||||
this.initialClientId = sessionStorage.getItem("clientId");
|
||||
}
|
||||
|
||||
apiURL(route) {
|
||||
return this.api_base + route;
|
||||
}
|
||||
|
||||
fetchApi(route, options) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
if (!options.headers) {
|
||||
options.headers = {};
|
||||
}
|
||||
options.headers["Comfy-User"] = this.user;
|
||||
return fetch(this.apiURL(route), options);
|
||||
}
|
||||
|
||||
addEventListener(type, callback, options) {
|
||||
super.addEventListener(type, callback, options);
|
||||
this.#registered.add(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll status for colab and other things that don't support websockets.
|
||||
*/
|
||||
#pollQueue() {
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const resp = await this.fetchApi("/prompt");
|
||||
const status = await resp.json();
|
||||
this.dispatchEvent(new CustomEvent("status", { detail: status }));
|
||||
} catch (error) {
|
||||
this.dispatchEvent(new CustomEvent("status", { detail: null }));
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and connects a WebSocket for realtime updates
|
||||
* @param {boolean} isReconnect If the socket is connection is a reconnect attempt
|
||||
*/
|
||||
#createSocket(isReconnect) {
|
||||
if (this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
let opened = false;
|
||||
let existingSession = window.name;
|
||||
if (existingSession) {
|
||||
existingSession = "?clientId=" + existingSession;
|
||||
}
|
||||
this.socket = new WebSocket(
|
||||
`ws${window.location.protocol === "https:" ? "s" : ""}://${this.api_host}${this.api_base}/ws${existingSession}`
|
||||
);
|
||||
this.socket.binaryType = "arraybuffer";
|
||||
|
||||
this.socket.addEventListener("open", () => {
|
||||
opened = true;
|
||||
if (isReconnect) {
|
||||
this.dispatchEvent(new CustomEvent("reconnected"));
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.addEventListener("error", () => {
|
||||
if (this.socket) this.socket.close();
|
||||
if (!isReconnect && !opened) {
|
||||
this.#pollQueue();
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.addEventListener("close", () => {
|
||||
setTimeout(() => {
|
||||
this.socket = null;
|
||||
this.#createSocket(true);
|
||||
}, 300);
|
||||
if (opened) {
|
||||
this.dispatchEvent(new CustomEvent("status", { detail: null }));
|
||||
this.dispatchEvent(new CustomEvent("reconnecting"));
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.addEventListener("message", (event) => {
|
||||
try {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
const view = new DataView(event.data);
|
||||
const eventType = view.getUint32(0);
|
||||
const buffer = event.data.slice(4);
|
||||
switch (eventType) {
|
||||
case 1:
|
||||
const view2 = new DataView(event.data);
|
||||
const imageType = view2.getUint32(0)
|
||||
let imageMime
|
||||
switch (imageType) {
|
||||
case 1:
|
||||
default:
|
||||
imageMime = "image/jpeg";
|
||||
break;
|
||||
case 2:
|
||||
imageMime = "image/png"
|
||||
}
|
||||
const imageBlob = new Blob([buffer.slice(4)], { type: imageMime });
|
||||
this.dispatchEvent(new CustomEvent("b_preview", { detail: imageBlob }));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown binary websocket message of type ${eventType}`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const msg = JSON.parse(event.data);
|
||||
switch (msg.type) {
|
||||
case "status":
|
||||
if (msg.data.sid) {
|
||||
this.clientId = msg.data.sid;
|
||||
window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
|
||||
sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
|
||||
break;
|
||||
case "progress":
|
||||
this.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
|
||||
break;
|
||||
case "executing":
|
||||
this.dispatchEvent(new CustomEvent("executing", { detail: msg.data.display_node }));
|
||||
break;
|
||||
case "executed":
|
||||
this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
|
||||
break;
|
||||
case "execution_start":
|
||||
this.dispatchEvent(new CustomEvent("execution_start", { detail: msg.data }));
|
||||
break;
|
||||
case "execution_success":
|
||||
this.dispatchEvent(new CustomEvent("execution_success", { detail: msg.data }));
|
||||
break;
|
||||
case "execution_error":
|
||||
this.dispatchEvent(new CustomEvent("execution_error", { detail: msg.data }));
|
||||
break;
|
||||
case "execution_cached":
|
||||
this.dispatchEvent(new CustomEvent("execution_cached", { detail: msg.data }));
|
||||
break;
|
||||
default:
|
||||
if (this.#registered.has(msg.type)) {
|
||||
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
|
||||
} else {
|
||||
throw new Error(`Unknown message type ${msg.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Unhandled message:", event.data, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises sockets and realtime updates
|
||||
*/
|
||||
init() {
|
||||
this.#createSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of extension urls
|
||||
* @returns An array of script urls to import
|
||||
*/
|
||||
async getExtensions() {
|
||||
const resp = await this.fetchApi("/extensions", { cache: "no-store" });
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of embedding names
|
||||
* @returns An array of script urls to import
|
||||
*/
|
||||
async getEmbeddings() {
|
||||
const resp = await this.fetchApi("/embeddings", { cache: "no-store" });
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads node object definitions for the graph
|
||||
* @returns The node definitions
|
||||
*/
|
||||
async getNodeDefs() {
|
||||
const resp = await this.fetchApi("/object_info", { cache: "no-store" });
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} number The index at which to queue the prompt, passing -1 will insert the prompt at the front of the queue
|
||||
* @param {object} prompt The prompt data to queue
|
||||
*/
|
||||
async queuePrompt(number, { output, workflow }) {
|
||||
const body = {
|
||||
client_id: this.clientId,
|
||||
prompt: output,
|
||||
extra_data: { extra_pnginfo: { workflow } },
|
||||
};
|
||||
|
||||
if (number === -1) {
|
||||
body.front = true;
|
||||
} else if (number != 0) {
|
||||
body.number = number;
|
||||
}
|
||||
|
||||
const res = await this.fetchApi("/prompt", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw {
|
||||
response: await res.json(),
|
||||
};
|
||||
}
|
||||
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a list of items (queue or history)
|
||||
* @param {string} type The type of items to load, queue or history
|
||||
* @returns The items of the specified type grouped by their status
|
||||
*/
|
||||
async getItems(type) {
|
||||
if (type === "queue") {
|
||||
return this.getQueue();
|
||||
}
|
||||
return this.getHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current state of the queue
|
||||
* @returns The currently running and queued items
|
||||
*/
|
||||
async getQueue() {
|
||||
try {
|
||||
const res = await this.fetchApi("/queue");
|
||||
const data = await res.json();
|
||||
return {
|
||||
// Running action uses a different endpoint for cancelling
|
||||
Running: data.queue_running.map((prompt) => ({
|
||||
prompt,
|
||||
remove: { name: "Cancel", cb: () => api.interrupt() },
|
||||
})),
|
||||
Pending: data.queue_pending.map((prompt) => ({ prompt })),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { Running: [], Pending: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the prompt execution history
|
||||
* @returns Prompt history including node outputs
|
||||
*/
|
||||
async getHistory(max_items=200) {
|
||||
try {
|
||||
const res = await this.fetchApi(`/history?max_items=${max_items}`);
|
||||
return { History: Object.values(await res.json()) };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { History: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets system & device stats
|
||||
* @returns System stats such as python version, OS, per device info
|
||||
*/
|
||||
async getSystemStats() {
|
||||
const res = await this.fetchApi("/system_stats");
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a POST request to the API
|
||||
* @param {*} type The endpoint to post to
|
||||
* @param {*} body Optional POST data
|
||||
*/
|
||||
async #postItem(type, body) {
|
||||
try {
|
||||
await this.fetchApi("/" + type, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an item from the specified list
|
||||
* @param {string} type The type of item to delete, queue or history
|
||||
* @param {number} id The id of the item to delete
|
||||
*/
|
||||
async deleteItem(type, id) {
|
||||
await this.#postItem(type, { delete: [id] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the specified list
|
||||
* @param {string} type The type of list to clear, queue or history
|
||||
*/
|
||||
async clearItems(type) {
|
||||
await this.#postItem(type, { clear: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrupts the execution of the running prompt
|
||||
*/
|
||||
async interrupt() {
|
||||
await this.#postItem("interrupt", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets user configuration data and where data should be stored
|
||||
* @returns { Promise<{ storage: "server" | "browser", users?: Promise<string, unknown>, migrated?: boolean }> }
|
||||
*/
|
||||
async getUserConfig() {
|
||||
return (await this.fetchApi("/users")).json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
* @param { string } username
|
||||
* @returns The fetch response
|
||||
*/
|
||||
createUser(username) {
|
||||
return this.fetchApi("/users", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all setting values for the current user
|
||||
* @returns { Promise<string, unknown> } A dictionary of id -> value
|
||||
*/
|
||||
async getSettings() {
|
||||
return (await this.fetchApi("/settings")).json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a setting for the current user
|
||||
* @param { string } id The id of the setting to fetch
|
||||
* @returns { Promise<unknown> } The setting value
|
||||
*/
|
||||
async getSetting(id) {
|
||||
return (await this.fetchApi(`/settings/${encodeURIComponent(id)}`)).json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a dictionary of settings for the current user
|
||||
* @param { Record<string, unknown> } settings Dictionary of setting id -> value to save
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
async storeSettings(settings) {
|
||||
return this.fetchApi(`/settings`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a setting for the current user
|
||||
* @param { string } id The id of the setting to update
|
||||
* @param { unknown } value The value of the setting
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
async storeSetting(id, value) {
|
||||
return this.fetchApi(`/settings/${encodeURIComponent(id)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(value)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user data file for the current user
|
||||
* @param { string } file The name of the userdata file to load
|
||||
* @param { RequestInit } [options]
|
||||
* @returns { Promise<Response> } The fetch response object
|
||||
*/
|
||||
async getUserData(file, options) {
|
||||
return this.fetchApi(`/userdata/${encodeURIComponent(file)}`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a user data file for the current user
|
||||
* @param { string } file The name of the userdata file to save
|
||||
* @param { unknown } data The data to save to the file
|
||||
* @param { RequestInit & { overwrite?: boolean, stringify?: boolean, throwOnError?: boolean } } [options]
|
||||
* @returns { Promise<Response> }
|
||||
*/
|
||||
async storeUserData(file, data, options = { overwrite: true, stringify: true, throwOnError: true }) {
|
||||
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}?overwrite=${options?.overwrite}`, {
|
||||
method: "POST",
|
||||
body: options?.stringify ? JSON.stringify(data) : data,
|
||||
...options,
|
||||
});
|
||||
if (resp.status !== 200 && options?.throwOnError !== false) {
|
||||
throw new Error(`Error storing user data file '${file}': ${resp.status} ${(await resp).statusText}`);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user data file for the current user
|
||||
* @param { string } file The name of the userdata file to delete
|
||||
*/
|
||||
async deleteUserData(file) {
|
||||
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(file)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (resp.status !== 204) {
|
||||
throw new Error(`Error removing user data file '${file}': ${resp.status} ${(resp).statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a user data file for the current user
|
||||
* @param { string } source The userdata file to move
|
||||
* @param { string } dest The destination for the file
|
||||
*/
|
||||
async moveUserData(source, dest, options = { overwrite: false }) {
|
||||
const resp = await this.fetchApi(`/userdata/${encodeURIComponent(source)}/move/${encodeURIComponent(dest)}?overwrite=${options?.overwrite}`, {
|
||||
method: "POST",
|
||||
});
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* Lists user data files for the current user
|
||||
* @param { string } dir The directory in which to list files
|
||||
* @param { boolean } [recurse] If the listing should be recursive
|
||||
* @param { true } [split] If the paths should be split based on the os path separator
|
||||
* @returns { Promise<string[][]>> } The list of split file paths in the format [fullPath, ...splitPath]
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* Lists user data files for the current user
|
||||
* @param { string } dir The directory in which to list files
|
||||
* @param { boolean } [recurse] If the listing should be recursive
|
||||
* @param { false | undefined } [split] If the paths should be split based on the os path separator
|
||||
* @returns { Promise<string[]>> } The list of files
|
||||
*/
|
||||
async listUserData(dir, recurse, split) {
|
||||
const resp = await this.fetchApi(
|
||||
`/userdata?${new URLSearchParams({
|
||||
recurse,
|
||||
dir,
|
||||
split,
|
||||
})}`
|
||||
);
|
||||
if (resp.status === 404) return [];
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`Error getting user data list '${dir}': ${resp.status} ${resp.statusText}`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ComfyApi();
|
||||
// Shim for scripts\api.ts
|
||||
export const api = window.comfyAPI.api.api;
|
||||
|
Reference in New Issue
Block a user