api.js 6.19 KB
Newer Older
pythongosssss's avatar
pythongosssss committed
1
class ComfyApi extends EventTarget {
2
3
	#registered = new Set();

pythongosssss's avatar
pythongosssss committed
4
	constructor() {
pythongosssss's avatar
pythongosssss committed
5
		super();
pythongosssss's avatar
pythongosssss committed
6
7
	}

8
9
10
11
12
	addEventListener(type, callback, options) {
		super.addEventListener(type, callback, options);
		this.#registered.add(type);
	}

pythongosssss's avatar
pythongosssss committed
13
14
15
	/**
	 * Poll status  for colab and other things that don't support websockets.
	 */
pythongosssss's avatar
pythongosssss committed
16
17
18
19
20
21
22
23
24
25
26
27
	#pollQueue() {
		setInterval(async () => {
			try {
				const resp = await fetch("/prompt");
				const status = await resp.json();
				this.dispatchEvent(new CustomEvent("status", { detail: status }));
			} catch (error) {
				this.dispatchEvent(new CustomEvent("status", { detail: null }));
			}
		}, 1000);
	}

pythongosssss's avatar
pythongosssss committed
28
29
30
31
	/**
	 * Creates and connects a WebSocket for realtime updates
	 * @param {boolean} isReconnect If the socket is connection is a reconnect attempt
	 */
pythongosssss's avatar
pythongosssss committed
32
33
34
35
36
37
	#createSocket(isReconnect) {
		if (this.socket) {
			return;
		}

		let opened = false;
38
		let existingSession = window.name;
39
40
41
42
43
44
		if (existingSession) {
			existingSession = "?clientId=" + existingSession;
		}
		this.socket = new WebSocket(
			`ws${window.location.protocol === "https:" ? "s" : ""}://${location.host}/ws${existingSession}`
		);
pythongosssss's avatar
pythongosssss committed
45
46
47
48
49
50
51
52
53
54

		this.socket.addEventListener("open", () => {
			opened = true;
			if (isReconnect) {
				this.dispatchEvent(new CustomEvent("reconnected"));
			}
		});

		this.socket.addEventListener("error", () => {
			if (this.socket) this.socket.close();
pythongosssss's avatar
pythongosssss committed
55
			if (!isReconnect && !opened) {
56
57
				this.#pollQueue();
			}
pythongosssss's avatar
pythongosssss committed
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
		});

		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 {
				const msg = JSON.parse(event.data);
				switch (msg.type) {
					case "status":
						if (msg.data.sid) {
							this.clientId = msg.data.sid;
78
							window.name = this.clientId;
pythongosssss's avatar
pythongosssss committed
79
80
81
82
83
84
85
86
87
88
89
90
91
						}
						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.node }));
						break;
					case "executed":
						this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
						break;
					default:
92
93
94
95
96
						if (this.#registered.has(msg.type)) {
							this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
						} else {
							throw new Error("Unknown message type");
						}
pythongosssss's avatar
pythongosssss committed
97
98
99
100
101
102
103
				}
			} catch (error) {
				console.warn("Unhandled message:", event.data);
			}
		});
	}

pythongosssss's avatar
pythongosssss committed
104
105
106
	/**
	 * Initialises sockets and realtime updates
	 */
pythongosssss's avatar
pythongosssss committed
107
108
109
	init() {
		this.#createSocket();
	}
pythongosssss's avatar
pythongosssss committed
110

111
112
113
114
115
116
117
118
119
	/**
	 * Gets a list of extension urls
	 * @returns An array of script urls to import
	 */
	async getExtensions() {
		const resp = await fetch("/extensions", { cache: "no-store" });
		return await resp.json();
	}

120
121
122
123
124
125
126
127
128
	/**
	 * Gets a list of embedding names
	 * @returns An array of script urls to import
	 */
	async getEmbeddings() {
		const resp = await fetch("/embeddings", { cache: "no-store" });
		return await resp.json();
	}

pythongosssss's avatar
pythongosssss committed
129
130
131
132
	/**
	 * Loads node object definitions for the graph
	 * @returns The node definitions
	 */
pythongosssss's avatar
pythongosssss committed
133
134
135
136
137
	async getNodeDefs() {
		const resp = await fetch("object_info", { cache: "no-store" });
		return await resp.json();
	}

pythongosssss's avatar
pythongosssss committed
138
139
140
141
142
	/**
	 *
	 * @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
	 */
pythongosssss's avatar
pythongosssss committed
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
	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 fetch("/prompt", {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
			},
			body: JSON.stringify(body),
		});

		if (res.status !== 200) {
			throw {
166
				response: await res.json(),
pythongosssss's avatar
pythongosssss committed
167
168
169
			};
		}
	}
pythongosssss's avatar
pythongosssss committed
170

pythongosssss's avatar
pythongosssss committed
171
172
173
174
175
	/**
	 * 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
	 */
pythongosssss's avatar
pythongosssss committed
176
177
178
179
180
181
182
	async getItems(type) {
		if (type === "queue") {
			return this.getQueue();
		}
		return this.getHistory();
	}

pythongosssss's avatar
pythongosssss committed
183
184
185
186
	/**
	 * Gets the current state of the queue
	 * @returns The currently running and queued items
	 */
pythongosssss's avatar
pythongosssss committed
187
188
189
190
191
192
	async getQueue() {
		try {
			const res = await fetch("/queue");
			const data = await res.json();
			return {
				// Running action uses a different endpoint for cancelling
pythongosssss's avatar
pythongosssss committed
193
194
195
196
				Running: data.queue_running.map((prompt) => ({
					prompt,
					remove: { name: "Cancel", cb: () => api.interrupt() },
				})),
pythongosssss's avatar
pythongosssss committed
197
198
199
200
201
202
203
204
				Pending: data.queue_pending.map((prompt) => ({ prompt })),
			};
		} catch (error) {
			console.error(error);
			return { Running: [], Pending: [] };
		}
	}

pythongosssss's avatar
pythongosssss committed
205
206
207
208
	/**
	 * Gets the prompt execution history
	 * @returns Prompt history including node outputs
	 */
pythongosssss's avatar
pythongosssss committed
209
210
211
212
213
214
215
216
217
218
	async getHistory() {
		try {
			const res = await fetch("/history");
			return { History: Object.values(await res.json()) };
		} catch (error) {
			console.error(error);
			return { History: [] };
		}
	}

pythongosssss's avatar
pythongosssss committed
219
220
221
222
223
	/**
	 * Sends a POST request to the API
	 * @param {*} type The endpoint to post to
	 * @param {*} body Optional POST data
	 */
pythongosssss's avatar
pythongosssss committed
224
225
226
227
228
229
230
231
232
233
234
235
236
237
	async #postItem(type, body) {
		try {
			await fetch("/" + type, {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				body: body ? JSON.stringify(body) : undefined,
			});
		} catch (error) {
			console.error(error);
		}
	}

pythongosssss's avatar
pythongosssss committed
238
239
240
241
242
	/**
	 * 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
	 */
pythongosssss's avatar
pythongosssss committed
243
244
245
246
	async deleteItem(type, id) {
		await this.#postItem(type, { delete: [id] });
	}

pythongosssss's avatar
pythongosssss committed
247
248
249
250
	/**
	 * Clears the specified list
	 * @param {string} type The type of list to clear, queue or history
	 */
pythongosssss's avatar
pythongosssss committed
251
252
253
254
	async clearItems(type) {
		await this.#postItem(type, { clear: true });
	}

pythongosssss's avatar
pythongosssss committed
255
256
257
	/**
	 * Interrupts the execution of the running prompt
	 */
pythongosssss's avatar
pythongosssss committed
258
259
260
	async interrupt() {
		await this.#postItem("interrupt", null);
	}
pythongosssss's avatar
pythongosssss committed
261
262
263
}

export const api = new ComfyApi();