comfyui-node 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/call-wrapper.d.ts +115 -0
- package/dist/call-wrapper.d.ts.map +1 -0
- package/dist/call-wrapper.js +432 -0
- package/dist/call-wrapper.js.map +1 -0
- package/dist/client.d.ts +233 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +719 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -0
- package/dist/features/abstract.d.ts +15 -0
- package/dist/features/abstract.d.ts.map +1 -0
- package/dist/features/abstract.js +19 -0
- package/dist/features/abstract.js.map +1 -0
- package/dist/features/base.d.ts +9 -0
- package/dist/features/base.d.ts.map +1 -0
- package/dist/features/base.js +15 -0
- package/dist/features/base.js.map +1 -0
- package/dist/features/feature-flags.d.ts +14 -0
- package/dist/features/feature-flags.d.ts.map +1 -0
- package/dist/features/feature-flags.js +27 -0
- package/dist/features/feature-flags.js.map +1 -0
- package/dist/features/file.d.ts +86 -0
- package/dist/features/file.d.ts.map +1 -0
- package/dist/features/file.js +160 -0
- package/dist/features/file.js.map +1 -0
- package/dist/features/history.d.ts +16 -0
- package/dist/features/history.d.ts.map +1 -0
- package/dist/features/history.js +23 -0
- package/dist/features/history.js.map +1 -0
- package/dist/features/index.d.ts +14 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +14 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/manager.d.ts +154 -0
- package/dist/features/manager.d.ts.map +1 -0
- package/dist/features/manager.js +309 -0
- package/dist/features/manager.js.map +1 -0
- package/dist/features/misc.d.ts +17 -0
- package/dist/features/misc.d.ts.map +1 -0
- package/dist/features/misc.js +52 -0
- package/dist/features/misc.js.map +1 -0
- package/dist/features/model.d.ts +39 -0
- package/dist/features/model.d.ts.map +1 -0
- package/dist/features/model.js +79 -0
- package/dist/features/model.js.map +1 -0
- package/dist/features/monitoring.d.ts +90 -0
- package/dist/features/monitoring.d.ts.map +1 -0
- package/dist/features/monitoring.js +129 -0
- package/dist/features/monitoring.js.map +1 -0
- package/dist/features/node.d.ts +42 -0
- package/dist/features/node.d.ts.map +1 -0
- package/dist/features/node.js +68 -0
- package/dist/features/node.js.map +1 -0
- package/dist/features/queue.d.ts +23 -0
- package/dist/features/queue.d.ts.map +1 -0
- package/dist/features/queue.js +68 -0
- package/dist/features/queue.js.map +1 -0
- package/dist/features/system.d.ts +21 -0
- package/dist/features/system.d.ts.map +1 -0
- package/dist/features/system.js +45 -0
- package/dist/features/system.js.map +1 -0
- package/dist/features/terminal.d.ts +25 -0
- package/dist/features/terminal.d.ts.map +1 -0
- package/dist/features/terminal.js +32 -0
- package/dist/features/terminal.js.map +1 -0
- package/dist/features/user.d.ts +42 -0
- package/dist/features/user.d.ts.map +1 -0
- package/dist/features/user.js +76 -0
- package/dist/features/user.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/pool.d.ts +171 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +467 -0
- package/dist/pool.js.map +1 -0
- package/dist/prompt-builder.d.ts +131 -0
- package/dist/prompt-builder.d.ts.map +1 -0
- package/dist/prompt-builder.js +266 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/tools.d.ts +10 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +16 -0
- package/dist/tools.js.map +1 -0
- package/dist/typed-event-target.d.ts +7 -0
- package/dist/typed-event-target.d.ts.map +1 -0
- package/dist/typed-event-target.js +19 -0
- package/dist/typed-event-target.js.map +1 -0
- package/dist/types/api.d.ts +212 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +16 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/error.d.ts +72 -0
- package/dist/types/error.d.ts.map +1 -0
- package/dist/types/error.js +75 -0
- package/dist/types/error.js.map +1 -0
- package/dist/types/event.d.ts +164 -0
- package/dist/types/event.d.ts.map +1 -0
- package/dist/types/event.js +2 -0
- package/dist/types/event.js.map +1 -0
- package/dist/types/manager.d.ts +157 -0
- package/dist/types/manager.d.ts.map +1 -0
- package/dist/types/manager.js +41 -0
- package/dist/types/manager.js.map +1 -0
- package/dist/types/sampler.d.ts +3 -0
- package/dist/types/sampler.d.ts.map +1 -0
- package/dist/types/sampler.js +2 -0
- package/dist/types/sampler.js.map +1 -0
- package/dist/types/tool.d.ts +10 -0
- package/dist/types/tool.d.ts.map +1 -0
- package/dist/types/tool.js +2 -0
- package/dist/types/tool.js.map +1 -0
- package/dist/utils/response-error.d.ts +4 -0
- package/dist/utils/response-error.d.ts.map +1 -0
- package/dist/utils/response-error.js +62 -0
- package/dist/utils/response-error.js.map +1 -0
- package/dist/utils/ws-reconnect.d.ts +29 -0
- package/dist/utils/ws-reconnect.d.ts.map +1 -0
- package/dist/utils/ws-reconnect.js +91 -0
- package/dist/utils/ws-reconnect.js.map +1 -0
- package/package.json +71 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
import { TypedEventTarget } from "./typed-event-target.js";
|
|
3
|
+
import { delay } from "./tools.js";
|
|
4
|
+
import { ManagerFeature } from "./features/manager.js";
|
|
5
|
+
import { MonitoringFeature } from "./features/monitoring.js";
|
|
6
|
+
import { QueueFeature } from "./features/queue.js";
|
|
7
|
+
import { HistoryFeature } from "./features/history.js";
|
|
8
|
+
import { SystemFeature } from "./features/system.js";
|
|
9
|
+
import { NodeFeature } from "./features/node.js";
|
|
10
|
+
import { UserFeature } from "./features/user.js";
|
|
11
|
+
import { FileFeature } from "./features/file.js";
|
|
12
|
+
import { ModelFeature } from "./features/model.js";
|
|
13
|
+
import { TerminalFeature } from "./features/terminal.js";
|
|
14
|
+
import { MiscFeature } from "./features/misc.js";
|
|
15
|
+
import { FeatureFlagsFeature } from "./features/feature-flags.js";
|
|
16
|
+
import { runWebSocketReconnect } from "./utils/ws-reconnect.js";
|
|
17
|
+
/**
|
|
18
|
+
* Primary client for interacting with a ComfyUI server.
|
|
19
|
+
*
|
|
20
|
+
* Responsibilities:
|
|
21
|
+
* - Connection lifecycle (WebSocket + polling fallback)
|
|
22
|
+
* - Authentication header injection
|
|
23
|
+
* - Capability probing / feature support detection
|
|
24
|
+
* - High‑level event fan‑out (progress, status, terminal, etc.)
|
|
25
|
+
* - Aggregation of modular feature namespaces under `ext.*`
|
|
26
|
+
*
|
|
27
|
+
* This class purposefully keeps business logic for specific domains inside feature modules
|
|
28
|
+
* (see files in `src/features/`). Only generic transport & coordination logic lives here.
|
|
29
|
+
*/
|
|
30
|
+
export class ComfyApi extends TypedEventTarget {
|
|
31
|
+
/** Base host (including protocol) e.g. http://localhost:8188 */
|
|
32
|
+
apiHost;
|
|
33
|
+
/** OS type as reported by the server (resolved during init) */
|
|
34
|
+
osType; // assigned during init()
|
|
35
|
+
/** Indicates feature probing + socket establishment completed */
|
|
36
|
+
isReady = false;
|
|
37
|
+
/** Whether to subscribe to terminal log streaming on init */
|
|
38
|
+
listenTerminal = false;
|
|
39
|
+
/** Monotonic timestamp of last socket activity (used for timeout detection) */
|
|
40
|
+
lastActivity = Date.now();
|
|
41
|
+
/** WebSocket inactivity timeout (ms) before attempting reconnection */
|
|
42
|
+
wsTimeout = 10000;
|
|
43
|
+
wsTimer = null;
|
|
44
|
+
_pollingTimer = null;
|
|
45
|
+
/** Host sans protocol (used to compose ws:// / wss:// URL) */
|
|
46
|
+
apiBase;
|
|
47
|
+
clientId;
|
|
48
|
+
socket = null;
|
|
49
|
+
listeners = [];
|
|
50
|
+
credentials = null;
|
|
51
|
+
/** Modular feature namespaces (tree intentionally flat & dependency‑free) */
|
|
52
|
+
ext = {
|
|
53
|
+
/** ComfyUI-Manager extension integration */
|
|
54
|
+
manager: new ManagerFeature(this),
|
|
55
|
+
/** Crystools monitor / system resource streaming */
|
|
56
|
+
monitor: new MonitoringFeature(this),
|
|
57
|
+
/** Prompt queue submission / control */
|
|
58
|
+
queue: new QueueFeature(this),
|
|
59
|
+
/** Execution history lookups */
|
|
60
|
+
history: new HistoryFeature(this),
|
|
61
|
+
/** System stats & memory free */
|
|
62
|
+
system: new SystemFeature(this),
|
|
63
|
+
/** Node defs + sampler / checkpoint / lora helpers */
|
|
64
|
+
node: new NodeFeature(this),
|
|
65
|
+
/** User CRUD & settings */
|
|
66
|
+
user: new UserFeature(this),
|
|
67
|
+
/** File uploads, image helpers & user data file operations */
|
|
68
|
+
file: new FileFeature(this),
|
|
69
|
+
/** Experimental model browsing / preview */
|
|
70
|
+
model: new ModelFeature(this),
|
|
71
|
+
/** Terminal log retrieval & streaming toggle */
|
|
72
|
+
terminal: new TerminalFeature(this),
|
|
73
|
+
/** Misc endpoints (extensions list, embeddings) */
|
|
74
|
+
misc: new MiscFeature(this),
|
|
75
|
+
/** Server advertised feature flags */
|
|
76
|
+
featureFlags: new FeatureFlagsFeature(this)
|
|
77
|
+
};
|
|
78
|
+
/** Helper type guard shaping expected feature API */
|
|
79
|
+
asFeature(obj) {
|
|
80
|
+
return obj;
|
|
81
|
+
}
|
|
82
|
+
static generateId() {
|
|
83
|
+
return "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
84
|
+
const r = (Math.random() * 16) | 0;
|
|
85
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
86
|
+
return v.toString(16);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
on(type, callback, options) {
|
|
90
|
+
this.log("on", "Add listener", { type, callback, options });
|
|
91
|
+
super.on(type, callback, options);
|
|
92
|
+
this.listeners.push({ event: type, handler: callback, options });
|
|
93
|
+
return () => this.off(type, callback, options);
|
|
94
|
+
}
|
|
95
|
+
off(type, callback, options) {
|
|
96
|
+
this.log("off", "Remove listener", { type, callback, options });
|
|
97
|
+
this.listeners = this.listeners.filter((l) => !(l.event === type && l.handler === callback));
|
|
98
|
+
super.off(type, callback, options);
|
|
99
|
+
}
|
|
100
|
+
removeAllListeners() {
|
|
101
|
+
this.log("removeAllListeners", "Triggered");
|
|
102
|
+
this.listeners.forEach((listener) => {
|
|
103
|
+
super.off(listener.event, listener.handler, listener.options);
|
|
104
|
+
});
|
|
105
|
+
this.listeners = [];
|
|
106
|
+
}
|
|
107
|
+
get id() {
|
|
108
|
+
return this.clientId ?? this.apiBase;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Retrieves the available features of the client.
|
|
112
|
+
*
|
|
113
|
+
* @returns An object containing the available features, where each feature is a key-value pair.
|
|
114
|
+
*/
|
|
115
|
+
get availableFeatures() {
|
|
116
|
+
return Object.keys(this.ext).reduce((acc, key) => {
|
|
117
|
+
const feat = this.asFeature(this.ext[key]);
|
|
118
|
+
return { ...acc, [key]: !!feat.isSupported };
|
|
119
|
+
}, {});
|
|
120
|
+
}
|
|
121
|
+
constructor(host, clientId = ComfyApi.generateId(), opts) {
|
|
122
|
+
super();
|
|
123
|
+
this.apiHost = host;
|
|
124
|
+
this.apiBase = host.split("://")[1];
|
|
125
|
+
this.clientId = clientId;
|
|
126
|
+
if (opts?.credentials) {
|
|
127
|
+
this.credentials = opts?.credentials;
|
|
128
|
+
this.testCredentials();
|
|
129
|
+
}
|
|
130
|
+
if (opts?.wsTimeout) {
|
|
131
|
+
this.wsTimeout = opts.wsTimeout;
|
|
132
|
+
}
|
|
133
|
+
if (opts?.listenTerminal) {
|
|
134
|
+
this.listenTerminal = opts.listenTerminal;
|
|
135
|
+
}
|
|
136
|
+
if (opts?.reconnect) {
|
|
137
|
+
this._reconnect = { ...opts.reconnect };
|
|
138
|
+
}
|
|
139
|
+
this.log("constructor", "Initialized", {
|
|
140
|
+
host,
|
|
141
|
+
clientId,
|
|
142
|
+
opts
|
|
143
|
+
});
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Destroys the client instance.
|
|
148
|
+
* Ensures all connections, timers and event listeners are properly closed.
|
|
149
|
+
*/
|
|
150
|
+
destroy() {
|
|
151
|
+
this.log("destroy", "Destroying client...");
|
|
152
|
+
// Cleanup flag to prevent re-entry
|
|
153
|
+
if (this._destroyed) {
|
|
154
|
+
this.log("destroy", "Client already destroyed");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this._destroyed = true;
|
|
158
|
+
// Clean up WebSocket timer
|
|
159
|
+
if (this.wsTimer) {
|
|
160
|
+
clearInterval(this.wsTimer);
|
|
161
|
+
this.wsTimer = null;
|
|
162
|
+
}
|
|
163
|
+
// Clean up polling timer if exists
|
|
164
|
+
if (this._pollingTimer) {
|
|
165
|
+
clearInterval(this._pollingTimer);
|
|
166
|
+
this._pollingTimer = null;
|
|
167
|
+
}
|
|
168
|
+
// Clean up socket event handlers and force close WebSocket
|
|
169
|
+
if (this.socket) {
|
|
170
|
+
try {
|
|
171
|
+
// Remove all event handlers
|
|
172
|
+
this.socket.onclose = null;
|
|
173
|
+
this.socket.onerror = null;
|
|
174
|
+
this.socket.onmessage = null;
|
|
175
|
+
this.socket.onopen = null;
|
|
176
|
+
// Forcefully close the WebSocket
|
|
177
|
+
if (this.socket.readyState === WebSocket.OPEN ||
|
|
178
|
+
this.socket.readyState === WebSocket.CONNECTING) {
|
|
179
|
+
this.socket.close();
|
|
180
|
+
}
|
|
181
|
+
// Terminate the WebSocket connection
|
|
182
|
+
this.socket.terminate();
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
this.log("destroy", "Error while closing WebSocket", e);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Destroy all extensions
|
|
189
|
+
for (const ext in this.ext) {
|
|
190
|
+
try {
|
|
191
|
+
const feat = this.asFeature(this.ext[ext]);
|
|
192
|
+
feat.destroy?.();
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
this.log("destroy", `Error destroying extension ${ext}`, e);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Make sure socket is closed
|
|
199
|
+
try {
|
|
200
|
+
this.socket?.close();
|
|
201
|
+
this.socket = null;
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
this.log("destroy", "Error closing socket", e);
|
|
205
|
+
}
|
|
206
|
+
// Remove all event listeners
|
|
207
|
+
this.removeAllListeners();
|
|
208
|
+
this.log("destroy", "Client destroyed completely");
|
|
209
|
+
}
|
|
210
|
+
log(fnName, message, data) {
|
|
211
|
+
this.dispatchEvent(new CustomEvent("log", { detail: { fnName, message, data } }));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Build full API URL (made public for feature modules)
|
|
215
|
+
*/
|
|
216
|
+
apiURL(route) {
|
|
217
|
+
return `${this.apiHost}${route}`;
|
|
218
|
+
}
|
|
219
|
+
getCredentialHeaders() {
|
|
220
|
+
if (!this.credentials)
|
|
221
|
+
return {};
|
|
222
|
+
switch (this.credentials?.type) {
|
|
223
|
+
case "basic":
|
|
224
|
+
return {
|
|
225
|
+
Authorization: `Basic ${btoa(`${this.credentials.username}:${this.credentials.password}`)}`
|
|
226
|
+
};
|
|
227
|
+
case "bearer_token":
|
|
228
|
+
return {
|
|
229
|
+
Authorization: `Bearer ${this.credentials.token}`
|
|
230
|
+
};
|
|
231
|
+
case "custom":
|
|
232
|
+
return this.credentials.headers;
|
|
233
|
+
default:
|
|
234
|
+
return {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async testCredentials() {
|
|
238
|
+
try {
|
|
239
|
+
if (!this.credentials)
|
|
240
|
+
return false;
|
|
241
|
+
await this.pollStatus(2000);
|
|
242
|
+
this.dispatchEvent(new CustomEvent("auth_success"));
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
this.log("testCredentials", "Failed", e);
|
|
247
|
+
if (e instanceof Response) {
|
|
248
|
+
if (e.status === 401) {
|
|
249
|
+
this.dispatchEvent(new CustomEvent("auth_error", { detail: e }));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this.dispatchEvent(new CustomEvent("connection_error", { detail: e }));
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async testFeatures() {
|
|
258
|
+
const extensions = Object.values(this.ext).map((e) => this.asFeature(e));
|
|
259
|
+
await Promise.all(extensions.map((ext) => ext.checkSupported?.()));
|
|
260
|
+
/**
|
|
261
|
+
* Mark the client is ready to use the API.
|
|
262
|
+
*/
|
|
263
|
+
this.isReady = true;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Fetches data from the API.
|
|
267
|
+
*
|
|
268
|
+
* @param route - The route to fetch data from.
|
|
269
|
+
* @param options - The options for the fetch request.
|
|
270
|
+
* @returns A promise that resolves to the response from the API.
|
|
271
|
+
*/
|
|
272
|
+
async fetchApi(route, options) {
|
|
273
|
+
if (!options) {
|
|
274
|
+
options = {};
|
|
275
|
+
}
|
|
276
|
+
options.headers = {
|
|
277
|
+
...this.getCredentialHeaders()
|
|
278
|
+
};
|
|
279
|
+
options.mode = "cors";
|
|
280
|
+
return fetch(this.apiURL(route), options);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Polls the status for colab and other things that don't support websockets.
|
|
284
|
+
* @returns {Promise<QueueStatus>} The status information.
|
|
285
|
+
*/
|
|
286
|
+
async pollStatus(timeout = 1000) {
|
|
287
|
+
const controller = new AbortController();
|
|
288
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
289
|
+
try {
|
|
290
|
+
const response = await this.fetchApi("/prompt", {
|
|
291
|
+
signal: controller.signal
|
|
292
|
+
});
|
|
293
|
+
if (response.status === 200) {
|
|
294
|
+
return response.json();
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
throw response;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
this.log("pollStatus", "Failed", error);
|
|
302
|
+
if (error.name === "AbortError") {
|
|
303
|
+
throw new Error("Request timed out");
|
|
304
|
+
}
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
clearTimeout(timeoutId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Queues a prompt for processing.
|
|
313
|
+
* @param {number} number The index at which to queue the prompt. using NULL will append to the end of the queue.
|
|
314
|
+
* @param {object} workflow Additional workflow data.
|
|
315
|
+
* @returns {Promise<QueuePromptResponse>} The response from the API.
|
|
316
|
+
*/
|
|
317
|
+
// Deprecated queuePrompt / appendPrompt wrappers removed. Use feature: api.ext.queue.*
|
|
318
|
+
/**
|
|
319
|
+
* Fetch raw queue status snapshot (lightweight helper not yet moved into a feature wrapper).
|
|
320
|
+
*/
|
|
321
|
+
async getQueue() {
|
|
322
|
+
// Direct call (no feature wrapper yet for queue status)
|
|
323
|
+
const response = await this.fetchApi("/queue");
|
|
324
|
+
return response.json();
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Hint the server to unload models / free memory (maps to `/free`).
|
|
328
|
+
* Returns false if request fails (does not throw to simplify caller ergonomics).
|
|
329
|
+
*/
|
|
330
|
+
async freeMemory(unloadModels, freeMemory) {
|
|
331
|
+
const payload = {
|
|
332
|
+
unload_models: unloadModels,
|
|
333
|
+
free_memory: freeMemory
|
|
334
|
+
};
|
|
335
|
+
try {
|
|
336
|
+
const response = await this.fetchApi("/free", {
|
|
337
|
+
method: "POST",
|
|
338
|
+
headers: {
|
|
339
|
+
"Content-Type": "application/json"
|
|
340
|
+
},
|
|
341
|
+
body: JSON.stringify(payload)
|
|
342
|
+
});
|
|
343
|
+
// Check if the response is successful
|
|
344
|
+
if (!response.ok) {
|
|
345
|
+
this.log("freeMemory", "Free memory failed", response);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
// Return the response object
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
this.log("freeMemory", "Free memory failed", error);
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Initialize: ping server with retries, probe features, establish WebSocket, optionally subscribe to terminal logs.
|
|
358
|
+
* Resolves with the client instance when ready; throws on unrecoverable connection failure.
|
|
359
|
+
*/
|
|
360
|
+
async init(maxTries = 10, delayTime = 1000) {
|
|
361
|
+
try {
|
|
362
|
+
// Wait for ping to succeed
|
|
363
|
+
await this.pingSuccess(maxTries, delayTime);
|
|
364
|
+
// Get system OS type on initialization
|
|
365
|
+
// Use feature namespace directly to avoid triggering deprecated shim
|
|
366
|
+
try {
|
|
367
|
+
const sys = await this.ext.system.getSystemStats();
|
|
368
|
+
this.osType = sys.system.os;
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
console.warn("Failed to get OS type during init:", e);
|
|
372
|
+
this.osType = "Unknown";
|
|
373
|
+
}
|
|
374
|
+
// Test features on initialization
|
|
375
|
+
await this.testFeatures();
|
|
376
|
+
// Create WebSocket connection on initialization
|
|
377
|
+
this.createSocket();
|
|
378
|
+
// Set terminal subscription on initialization (use feature namespace to avoid deprecated shim)
|
|
379
|
+
if (this.listenTerminal) {
|
|
380
|
+
try {
|
|
381
|
+
await this.ext.terminal.setTerminalSubscription(true);
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
console.warn("Failed to set terminal subscription during init:", e);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Mark as ready
|
|
388
|
+
this.isReady = true;
|
|
389
|
+
return this;
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
this.log("init", "Failed", e);
|
|
393
|
+
this.dispatchEvent(new CustomEvent("connection_error", { detail: e }));
|
|
394
|
+
throw e; // Propagate the error
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async pingSuccess(maxTries = 10, delayTime = 1000) {
|
|
398
|
+
let tries = 0;
|
|
399
|
+
let ping = await this.ping();
|
|
400
|
+
while (!ping.status) {
|
|
401
|
+
if (tries > maxTries) {
|
|
402
|
+
throw new Error("Can't connect to the server");
|
|
403
|
+
}
|
|
404
|
+
await delay(delayTime); // Wait for 1s before trying again
|
|
405
|
+
ping = await this.ping();
|
|
406
|
+
tries++;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** Await until feature probing + socket creation finished. */
|
|
410
|
+
async waitForReady() {
|
|
411
|
+
while (!this.isReady) {
|
|
412
|
+
await delay(100);
|
|
413
|
+
}
|
|
414
|
+
return this;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Sends a ping request to the server and returns a boolean indicating whether the server is reachable.
|
|
418
|
+
* @returns A promise that resolves to `true` if the server is reachable, or `false` otherwise.
|
|
419
|
+
*/
|
|
420
|
+
async ping() {
|
|
421
|
+
const start = performance.now();
|
|
422
|
+
return this.pollStatus(5000)
|
|
423
|
+
.then(() => {
|
|
424
|
+
return { status: true, time: performance.now() - start };
|
|
425
|
+
})
|
|
426
|
+
.catch((error) => {
|
|
427
|
+
this.log("ping", "Can't connect to the server", error);
|
|
428
|
+
return { status: false };
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Attempt WebSocket reconnection with exponential backoff + jitter.
|
|
433
|
+
* Falls back to a bounded number of attempts then emits `reconnection_failed`.
|
|
434
|
+
*/
|
|
435
|
+
async reconnectWs(triggerEvent) {
|
|
436
|
+
if (this._reconnectController) {
|
|
437
|
+
// Avoid stacking multiple controllers concurrently
|
|
438
|
+
try {
|
|
439
|
+
this._reconnectController.abort();
|
|
440
|
+
}
|
|
441
|
+
catch { }
|
|
442
|
+
}
|
|
443
|
+
this._reconnectController = runWebSocketReconnect(this, () => this.createSocket(true), {
|
|
444
|
+
triggerEvents: !!triggerEvent,
|
|
445
|
+
maxAttempts: this._reconnect?.maxAttempts,
|
|
446
|
+
baseDelayMs: this._reconnect?.baseDelayMs,
|
|
447
|
+
maxDelayMs: this._reconnect?.maxDelayMs,
|
|
448
|
+
strategy: this._reconnect?.strategy,
|
|
449
|
+
jitterPercent: this._reconnect?.jitterPercent,
|
|
450
|
+
customDelayFn: this._reconnect?.customDelayFn
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/** Abort any in-flight reconnection loop (no-op if none active). */
|
|
454
|
+
abortReconnect() {
|
|
455
|
+
try {
|
|
456
|
+
this._reconnectController?.abort();
|
|
457
|
+
}
|
|
458
|
+
catch { }
|
|
459
|
+
}
|
|
460
|
+
resetLastActivity() {
|
|
461
|
+
this.lastActivity = Date.now();
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Establish a WebSocket connection for real‑time events; installs polling fallback on failure.
|
|
465
|
+
* @param isReconnect internal flag indicating this creation follows a reconnect attempt
|
|
466
|
+
*/
|
|
467
|
+
createSocket(isReconnect = false) {
|
|
468
|
+
let reconnecting = false;
|
|
469
|
+
let usePolling = false;
|
|
470
|
+
if (this.socket) {
|
|
471
|
+
this.log("socket", "Socket already exists, skipping creation.");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const headers = {
|
|
475
|
+
...this.getCredentialHeaders()
|
|
476
|
+
};
|
|
477
|
+
const existingSession = `?clientId=${this.clientId}`;
|
|
478
|
+
const wsUrl = `ws${this.apiHost.includes("https:") ? "s" : ""}://${this.apiBase}/ws${existingSession}`;
|
|
479
|
+
// Try to create WebSocket connection
|
|
480
|
+
try {
|
|
481
|
+
this.socket = new WebSocket(wsUrl, {
|
|
482
|
+
headers: headers
|
|
483
|
+
});
|
|
484
|
+
this.socket.onclose = () => {
|
|
485
|
+
if (reconnecting || isReconnect)
|
|
486
|
+
return;
|
|
487
|
+
reconnecting = true;
|
|
488
|
+
this.log("socket", "Socket closed -> Reconnecting");
|
|
489
|
+
this.reconnectWs(true);
|
|
490
|
+
};
|
|
491
|
+
this.socket.onopen = () => {
|
|
492
|
+
this.resetLastActivity();
|
|
493
|
+
reconnecting = false;
|
|
494
|
+
usePolling = false; // Reset polling flag if we have an open connection
|
|
495
|
+
this.log("socket", "Socket opened");
|
|
496
|
+
if (isReconnect) {
|
|
497
|
+
this.dispatchEvent(new CustomEvent("reconnected"));
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
this.dispatchEvent(new CustomEvent("connected"));
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
this.log("socket", "WebSocket creation failed, falling back to polling", error);
|
|
506
|
+
this.socket = null;
|
|
507
|
+
usePolling = true;
|
|
508
|
+
this.dispatchEvent(new CustomEvent("websocket_unavailable", { detail: error }));
|
|
509
|
+
// Set up polling mechanism
|
|
510
|
+
this.setupPollingFallback();
|
|
511
|
+
}
|
|
512
|
+
// Only continue with WebSocket setup if creation was successful
|
|
513
|
+
if (this.socket) {
|
|
514
|
+
this.socket.onmessage = (event) => {
|
|
515
|
+
this.resetLastActivity();
|
|
516
|
+
try {
|
|
517
|
+
if (event.data instanceof Buffer) {
|
|
518
|
+
const buffer = event.data;
|
|
519
|
+
const view = new DataView(buffer.buffer);
|
|
520
|
+
const eventType = view.getUint32(0);
|
|
521
|
+
switch (eventType) {
|
|
522
|
+
case 1:
|
|
523
|
+
const imageType = view.getUint32(0);
|
|
524
|
+
let imageMime;
|
|
525
|
+
switch (imageType) {
|
|
526
|
+
case 1:
|
|
527
|
+
default:
|
|
528
|
+
imageMime = "image/jpeg";
|
|
529
|
+
break;
|
|
530
|
+
case 2:
|
|
531
|
+
imageMime = "image/png";
|
|
532
|
+
}
|
|
533
|
+
const imageBlob = new Blob([buffer.slice(8)], {
|
|
534
|
+
type: imageMime
|
|
535
|
+
});
|
|
536
|
+
this.dispatchEvent(new CustomEvent("b_preview", { detail: imageBlob }));
|
|
537
|
+
break;
|
|
538
|
+
default:
|
|
539
|
+
throw new Error(`Unknown binary websocket message of type ${eventType}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else if (typeof event.data === "string") {
|
|
543
|
+
const msg = JSON.parse(event.data);
|
|
544
|
+
if (!msg.data || !msg.type)
|
|
545
|
+
return;
|
|
546
|
+
this.dispatchEvent(new CustomEvent("all", { detail: msg }));
|
|
547
|
+
if (msg.type === "logs") {
|
|
548
|
+
this.dispatchEvent(new CustomEvent("terminal", { detail: msg.data.entries?.[0] || null }));
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
|
|
552
|
+
}
|
|
553
|
+
if (msg.data.sid) {
|
|
554
|
+
this.clientId = msg.data.sid;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
this.log("socket", "Unhandled message", event);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
this.log("socket", "Unhandled message", { event, error });
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
this.socket.onerror = (e) => {
|
|
566
|
+
this.log("socket", "Socket error", e);
|
|
567
|
+
// If this is the first error and we're not already in reconnect mode
|
|
568
|
+
if (!reconnecting && !usePolling) {
|
|
569
|
+
usePolling = true;
|
|
570
|
+
this.log("socket", "WebSocket error, will try polling as fallback");
|
|
571
|
+
this.setupPollingFallback();
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
if (!isReconnect) {
|
|
575
|
+
this.wsTimer = setInterval(() => {
|
|
576
|
+
if (reconnecting)
|
|
577
|
+
return;
|
|
578
|
+
if (Date.now() - this.lastActivity > this.wsTimeout) {
|
|
579
|
+
reconnecting = true;
|
|
580
|
+
this.log("socket", "Connection timed out, reconnecting...");
|
|
581
|
+
this.reconnectWs(true);
|
|
582
|
+
}
|
|
583
|
+
}, this.wsTimeout / 2);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Install a 2s interval polling loop to replicate essential status events when WebSocket is unavailable.
|
|
589
|
+
* Stops automatically once a socket connection is restored.
|
|
590
|
+
*/
|
|
591
|
+
setupPollingFallback() {
|
|
592
|
+
this.log("socket", "Setting up polling fallback mechanism");
|
|
593
|
+
// Clear any existing polling timer
|
|
594
|
+
if (this._pollingTimer) {
|
|
595
|
+
try {
|
|
596
|
+
clearInterval(this._pollingTimer);
|
|
597
|
+
this._pollingTimer = null;
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
this.log("socket", "Error clearing polling timer", e);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Poll every 2 seconds
|
|
604
|
+
const POLLING_INTERVAL = 2000;
|
|
605
|
+
const pollFn = async () => {
|
|
606
|
+
try {
|
|
607
|
+
// Poll execution status
|
|
608
|
+
const status = await this.pollStatus();
|
|
609
|
+
// Simulate an event dispatch similar to WebSocket
|
|
610
|
+
this.dispatchEvent(new CustomEvent("status", { detail: status }));
|
|
611
|
+
// Reset activity timestamp to prevent timeout
|
|
612
|
+
this.resetLastActivity();
|
|
613
|
+
// Try to re-establish WebSocket connection periodically
|
|
614
|
+
if (!this.socket || !this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
615
|
+
this.log("socket", "Attempting to restore WebSocket connection");
|
|
616
|
+
try {
|
|
617
|
+
this.createSocket(true);
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
// Continue with polling if WebSocket creation fails
|
|
621
|
+
this.log("socket", "WebSocket still unavailable, continuing with polling", error);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// WebSocket is back, we can stop polling
|
|
626
|
+
this.log("socket", "WebSocket connection restored, stopping polling");
|
|
627
|
+
if (this._pollingTimer) {
|
|
628
|
+
clearInterval(this._pollingTimer);
|
|
629
|
+
this._pollingTimer = null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
this.log("socket", "Polling error", error);
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
// Using setInterval and casting to the expected type
|
|
638
|
+
this._pollingTimer = setInterval(pollFn, POLLING_INTERVAL);
|
|
639
|
+
this.log("socket", `Polling started with interval of ${POLLING_INTERVAL}ms`);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Retrieves a list of all available model folders.
|
|
643
|
+
* @experimental API that may change in future versions
|
|
644
|
+
* @returns A promise that resolves to an array of ModelFolder objects.
|
|
645
|
+
*/
|
|
646
|
+
async getModelFolders() {
|
|
647
|
+
try {
|
|
648
|
+
const response = await this.fetchApi("/experiment/models");
|
|
649
|
+
if (!response.ok) {
|
|
650
|
+
this.log("getModelFolders", "Failed to fetch model folders", response);
|
|
651
|
+
throw new Error(`Failed to fetch model folders: ${response.status} ${response.statusText}`);
|
|
652
|
+
}
|
|
653
|
+
return response.json();
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
this.log("getModelFolders", "Error fetching model folders", error);
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Retrieves a list of all model files in a specific folder.
|
|
662
|
+
* @experimental API that may change in future versions
|
|
663
|
+
* @param folder - The name of the model folder.
|
|
664
|
+
* @returns A promise that resolves to an array of ModelFile objects.
|
|
665
|
+
*/
|
|
666
|
+
async getModelFiles(folder) {
|
|
667
|
+
try {
|
|
668
|
+
const response = await this.fetchApi(`/experiment/models/${encodeURIComponent(folder)}`);
|
|
669
|
+
if (!response.ok) {
|
|
670
|
+
this.log("getModelFiles", "Failed to fetch model files", { folder, response });
|
|
671
|
+
throw new Error(`Failed to fetch model files: ${response.status} ${response.statusText}`);
|
|
672
|
+
}
|
|
673
|
+
return response.json();
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
this.log("getModelFiles", "Error fetching model files", { folder, error });
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Retrieves a preview image for a specific model file.
|
|
682
|
+
* @experimental API that may change in future versions
|
|
683
|
+
* @param folder - The name of the model folder.
|
|
684
|
+
* @param pathIndex - The index of the folder path where the file is stored.
|
|
685
|
+
* @param filename - The name of the model file.
|
|
686
|
+
* @returns A promise that resolves to a ModelPreviewResponse object containing the preview image data.
|
|
687
|
+
*/
|
|
688
|
+
async getModelPreview(folder, pathIndex, filename) {
|
|
689
|
+
try {
|
|
690
|
+
const response = await this.fetchApi(`/experiment/models/preview/${encodeURIComponent(folder)}/${pathIndex}/${encodeURIComponent(filename)}`);
|
|
691
|
+
if (!response.ok) {
|
|
692
|
+
this.log("getModelPreview", "Failed to fetch model preview", { folder, pathIndex, filename, response });
|
|
693
|
+
throw new Error(`Failed to fetch model preview: ${response.status} ${response.statusText}`);
|
|
694
|
+
}
|
|
695
|
+
const contentType = response.headers.get("content-type") || "image/webp";
|
|
696
|
+
const body = await response.arrayBuffer();
|
|
697
|
+
return {
|
|
698
|
+
body,
|
|
699
|
+
contentType
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
this.log("getModelPreview", "Error fetching model preview", { folder, pathIndex, filename, error });
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Creates a URL for a model preview image.
|
|
709
|
+
* @experimental API that may change in future versions
|
|
710
|
+
* @param folder - The name of the model folder.
|
|
711
|
+
* @param pathIndex - The index of the folder path where the file is stored.
|
|
712
|
+
* @param filename - The name of the model file.
|
|
713
|
+
* @returns The URL string for the model preview.
|
|
714
|
+
*/
|
|
715
|
+
getModelPreviewUrl(folder, pathIndex, filename) {
|
|
716
|
+
return this.apiURL(`/experiment/models/preview/${encodeURIComponent(folder)}/${pathIndex}/${encodeURIComponent(filename)}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
//# sourceMappingURL=client.js.map
|