@sogni-ai/sogni-client 4.0.0-alpha.5 → 4.0.0-alpha.50

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +345 -0
  2. package/README.md +295 -58
  3. package/dist/Account/index.d.ts +18 -16
  4. package/dist/Account/index.js +42 -21
  5. package/dist/Account/index.js.map +1 -1
  6. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.d.ts +66 -0
  7. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +332 -0
  8. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
  9. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.d.ts +28 -0
  10. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +203 -0
  11. package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
  12. package/dist/ApiClient/WebSocketClient/events.d.ts +12 -0
  13. package/dist/ApiClient/WebSocketClient/index.d.ts +2 -2
  14. package/dist/ApiClient/WebSocketClient/index.js +13 -3
  15. package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
  16. package/dist/ApiClient/WebSocketClient/types.d.ts +13 -0
  17. package/dist/ApiClient/index.d.ts +4 -4
  18. package/dist/ApiClient/index.js +23 -4
  19. package/dist/ApiClient/index.js.map +1 -1
  20. package/dist/Projects/Job.d.ts +44 -4
  21. package/dist/Projects/Job.js +83 -16
  22. package/dist/Projects/Job.js.map +1 -1
  23. package/dist/Projects/Project.d.ts +18 -0
  24. package/dist/Projects/Project.js +38 -10
  25. package/dist/Projects/Project.js.map +1 -1
  26. package/dist/Projects/createJobRequestMessage.d.ts +2 -1
  27. package/dist/Projects/createJobRequestMessage.js +173 -14
  28. package/dist/Projects/createJobRequestMessage.js.map +1 -1
  29. package/dist/Projects/index.d.ts +114 -11
  30. package/dist/Projects/index.js +504 -47
  31. package/dist/Projects/index.js.map +1 -1
  32. package/dist/Projects/types/ComfySamplerParams.d.ts +0 -0
  33. package/dist/Projects/types/ComfySamplerParams.js +2 -0
  34. package/dist/Projects/types/ComfySamplerParams.js.map +1 -0
  35. package/dist/Projects/types/EstimationResponse.d.ts +2 -0
  36. package/dist/Projects/types/ModelOptions.d.ts +31 -0
  37. package/dist/Projects/types/ModelOptions.js +56 -0
  38. package/dist/Projects/types/ModelOptions.js.map +1 -0
  39. package/dist/Projects/types/ModelTiersRaw.d.ts +67 -0
  40. package/dist/Projects/types/ModelTiersRaw.js +15 -0
  41. package/dist/Projects/types/ModelTiersRaw.js.map +1 -0
  42. package/dist/Projects/types/events.d.ts +5 -1
  43. package/dist/Projects/types/index.d.ts +201 -42
  44. package/dist/Projects/types/index.js +8 -0
  45. package/dist/Projects/types/index.js.map +1 -1
  46. package/dist/Projects/utils/index.d.ts +20 -0
  47. package/dist/Projects/utils/index.js +82 -0
  48. package/dist/Projects/utils/index.js.map +1 -0
  49. package/dist/Projects/utils/samplers.d.ts +6 -0
  50. package/dist/Projects/utils/samplers.js +39 -0
  51. package/dist/Projects/utils/samplers.js.map +1 -0
  52. package/dist/Projects/utils/scheduler.d.ts +6 -0
  53. package/dist/Projects/utils/scheduler.js +30 -0
  54. package/dist/Projects/utils/scheduler.js.map +1 -0
  55. package/dist/index.d.ts +11 -3
  56. package/dist/index.js +8 -3
  57. package/dist/index.js.map +1 -1
  58. package/dist/lib/AuthManager/TokenAuthManager.js +0 -2
  59. package/dist/lib/AuthManager/TokenAuthManager.js.map +1 -1
  60. package/dist/lib/DataEntity.js +4 -2
  61. package/dist/lib/DataEntity.js.map +1 -1
  62. package/dist/lib/RestClient.js +15 -2
  63. package/dist/lib/RestClient.js.map +1 -1
  64. package/dist/lib/{utils.js → utils/index.js} +1 -1
  65. package/dist/lib/utils/index.js.map +1 -0
  66. package/dist/lib/validation.d.ts +31 -2
  67. package/dist/lib/validation.js +80 -13
  68. package/dist/lib/validation.js.map +1 -1
  69. package/package.json +4 -4
  70. package/src/Account/index.ts +39 -20
  71. package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.ts +426 -0
  72. package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +237 -0
  73. package/src/ApiClient/WebSocketClient/events.ts +14 -0
  74. package/src/ApiClient/WebSocketClient/index.ts +15 -5
  75. package/src/ApiClient/WebSocketClient/types.ts +16 -0
  76. package/src/ApiClient/index.ts +30 -8
  77. package/src/Projects/Job.ts +97 -16
  78. package/src/Projects/Project.ts +46 -13
  79. package/src/Projects/createJobRequestMessage.ts +234 -34
  80. package/src/Projects/index.ts +533 -51
  81. package/src/Projects/types/ComfySamplerParams.ts +0 -0
  82. package/src/Projects/types/EstimationResponse.ts +2 -0
  83. package/src/Projects/types/ModelOptions.ts +92 -0
  84. package/src/Projects/types/ModelTiersRaw.ts +86 -0
  85. package/src/Projects/types/events.ts +6 -0
  86. package/src/Projects/types/index.ts +235 -45
  87. package/src/Projects/utils/index.ts +77 -0
  88. package/src/Projects/utils/samplers.ts +36 -0
  89. package/src/Projects/utils/scheduler.ts +27 -0
  90. package/src/index.ts +36 -9
  91. package/src/lib/AuthManager/TokenAuthManager.ts +0 -2
  92. package/src/lib/DataEntity.ts +4 -2
  93. package/src/lib/RestClient.ts +16 -2
  94. package/src/lib/validation.ts +90 -17
  95. package/dist/Projects/types/SamplerParams.d.ts +0 -15
  96. package/dist/Projects/types/SamplerParams.js +0 -21
  97. package/dist/Projects/types/SamplerParams.js.map +0 -1
  98. package/dist/Projects/types/SchedulerParams.d.ts +0 -13
  99. package/dist/Projects/types/SchedulerParams.js +0 -19
  100. package/dist/Projects/types/SchedulerParams.js.map +0 -1
  101. package/dist/Projects/utils.d.ts +0 -2
  102. package/dist/Projects/utils.js +0 -14
  103. package/dist/Projects/utils.js.map +0 -1
  104. package/dist/lib/utils.js.map +0 -1
  105. package/src/Projects/types/SamplerParams.ts +0 -19
  106. package/src/Projects/types/SchedulerParams.ts +0 -17
  107. package/src/Projects/utils.ts +0 -12
  108. /package/dist/lib/{utils.d.ts → utils/index.d.ts} +0 -0
  109. /package/src/lib/{utils.ts → utils/index.ts} +0 -0
@@ -0,0 +1,237 @@
1
+ import { IWebSocketClient, SupernetType } from '../types';
2
+ import { AuthManager, TokenAuthManager } from '../../../lib/AuthManager';
3
+ import { Logger } from '../../../lib/DefaultLogger';
4
+ import WebSocketClient from '../index';
5
+ import RestClient from '../../../lib/RestClient';
6
+ import { SocketEventMap } from '../events';
7
+ import { MessageType, SocketMessageMap } from '../messages';
8
+ import ChannelCoordinator from './ChannelCoordinator';
9
+
10
+ interface SocketSend<T extends MessageType = MessageType> {
11
+ type: 'socket-send';
12
+ payload: { type: T; data: SocketMessageMap[T] };
13
+ }
14
+
15
+ interface SocketConnect {
16
+ type: 'connect';
17
+ }
18
+
19
+ interface SocketDisconnect {
20
+ type: 'disconnect';
21
+ }
22
+
23
+ interface SwitchNetwork {
24
+ type: 'switchNetwork';
25
+ payload: SupernetType;
26
+ }
27
+
28
+ type Message = SocketConnect | SocketDisconnect | SocketSend | SwitchNetwork;
29
+
30
+ interface EventNotification<T extends keyof SocketEventMap = keyof SocketEventMap> {
31
+ type: 'socket-event';
32
+ payload: { type: T; data: SocketEventMap[T] };
33
+ }
34
+
35
+ interface AuthStateChanged {
36
+ type: 'auth-state-changed';
37
+ payload: boolean;
38
+ }
39
+
40
+ type Notification = EventNotification | AuthStateChanged;
41
+
42
+ type EventInterceptor<T extends keyof SocketEventMap = keyof SocketEventMap> = (
43
+ eventType: T,
44
+ payload: SocketEventMap[T]
45
+ ) => void;
46
+
47
+ class WrappedClient extends WebSocketClient {
48
+ private interceptor: EventInterceptor | undefined = undefined;
49
+ intercept(interceptor: EventInterceptor) {
50
+ this.interceptor = interceptor;
51
+ }
52
+ protected emit<T extends keyof SocketEventMap>(event: T, data: SocketEventMap[T]) {
53
+ super.emit(event, data);
54
+ if (this.interceptor) {
55
+ this.interceptor(event, data);
56
+ }
57
+ }
58
+ }
59
+
60
+ class BrowserWebSocketClient extends RestClient<SocketEventMap> implements IWebSocketClient {
61
+ appId: string;
62
+ baseUrl: string;
63
+ private socketClient: WrappedClient;
64
+ private coordinator: ChannelCoordinator<Message, Notification>;
65
+ private _isConnected = false;
66
+ private _supernetType: SupernetType;
67
+
68
+ constructor(
69
+ baseUrl: string,
70
+ auth: AuthManager,
71
+ appId: string,
72
+ supernetType: SupernetType,
73
+ logger: Logger
74
+ ) {
75
+ const socketClient = new WrappedClient(baseUrl, auth, appId, supernetType, logger);
76
+ super(socketClient.baseUrl, auth, logger);
77
+ this.socketClient = socketClient;
78
+ this.appId = appId;
79
+ this.baseUrl = socketClient.baseUrl;
80
+ this._supernetType = supernetType;
81
+ this.coordinator = new ChannelCoordinator({
82
+ callbacks: {
83
+ onRoleChange: this.handleRoleChange.bind(this),
84
+ onMessage: this.handleMessage.bind(this),
85
+ onNotification: this.handleNotification.bind(this)
86
+ },
87
+ logger
88
+ });
89
+ this.auth.on('updated', this.handleAuthUpdated.bind(this));
90
+ this.socketClient.intercept(this.handleSocketEvent.bind(this));
91
+ }
92
+
93
+ get isConnected() {
94
+ return this.coordinator.isPrimary ? this.socketClient.isConnected : this._isConnected;
95
+ }
96
+
97
+ get supernetType() {
98
+ return this.coordinator.isPrimary ? this.socketClient.supernetType : this._supernetType;
99
+ }
100
+
101
+ async connect(): Promise<void> {
102
+ await this.coordinator.isReady();
103
+ if (this.coordinator.isPrimary) {
104
+ await this.socketClient.connect();
105
+ } else {
106
+ return this.coordinator.sendMessage({
107
+ type: 'connect'
108
+ });
109
+ }
110
+ }
111
+
112
+ async disconnect() {
113
+ await this.coordinator.isReady();
114
+ if (this.coordinator.isPrimary) {
115
+ this.socketClient.disconnect();
116
+ } else {
117
+ this.coordinator.sendMessage({
118
+ type: 'disconnect'
119
+ });
120
+ }
121
+ }
122
+
123
+ async switchNetwork(supernetType: SupernetType): Promise<SupernetType> {
124
+ await this.coordinator.isReady();
125
+ if (this.coordinator.isPrimary) {
126
+ return this.socketClient.switchNetwork(supernetType);
127
+ }
128
+ await this.coordinator.sendMessage({
129
+ type: 'switchNetwork',
130
+ payload: supernetType
131
+ });
132
+ this._supernetType = supernetType;
133
+ return supernetType;
134
+ }
135
+
136
+ async send<T extends MessageType>(messageType: T, data: SocketMessageMap[T]): Promise<void> {
137
+ await this.coordinator.isReady();
138
+ if (this.coordinator.isPrimary) {
139
+ if (!this.socketClient.isConnected) {
140
+ await this.socketClient.connect();
141
+ }
142
+ return this.socketClient.send(messageType, data);
143
+ }
144
+ return this.coordinator.sendMessage({
145
+ type: 'socket-send',
146
+ payload: { type: messageType, data }
147
+ });
148
+ }
149
+
150
+ private async handleMessage(message: Message) {
151
+ this._logger.debug('Received control message', message);
152
+ switch (message.type) {
153
+ case 'socket-send': {
154
+ if (!this.socketClient.isConnected) {
155
+ await this.socketClient.connect();
156
+ }
157
+ return this.socketClient.send(message.payload.type, message.payload.data);
158
+ }
159
+ case 'connect': {
160
+ if (!this.socketClient.isConnected) {
161
+ await this.socketClient.connect();
162
+ }
163
+ return;
164
+ }
165
+ case 'disconnect': {
166
+ if (this.socketClient.isConnected) {
167
+ this.socketClient.disconnect();
168
+ }
169
+ return;
170
+ }
171
+ case 'switchNetwork': {
172
+ await this.switchNetwork(message.payload);
173
+ return;
174
+ }
175
+ default: {
176
+ this._logger.error('Received unknown message type:', message);
177
+ }
178
+ }
179
+ }
180
+
181
+ private async handleNotification(notification: Notification) {
182
+ this._logger.debug('Received notification', notification.type, notification.payload);
183
+ switch (notification.type) {
184
+ case 'socket-event': {
185
+ this.emit(notification.payload.type, notification.payload.data);
186
+ return;
187
+ }
188
+ case 'auth-state-changed': {
189
+ this.handleAuthChanged(notification.payload);
190
+ return;
191
+ }
192
+ default: {
193
+ this._logger.error('Received unknown notification type:', notification);
194
+ }
195
+ }
196
+ }
197
+
198
+ private handleAuthChanged(isAuthenticated: boolean) {
199
+ if (this.auth instanceof TokenAuthManager) {
200
+ throw new Error('TokenAuthManager is not supported in multi client mode');
201
+ }
202
+ if (this.auth.isAuthenticated !== isAuthenticated) {
203
+ if (isAuthenticated) {
204
+ this.auth.authenticate();
205
+ } else {
206
+ this.auth.clear();
207
+ }
208
+ }
209
+ }
210
+
211
+ private handleSocketEvent(eventType: keyof SocketEventMap, payload: any) {
212
+ if (this.coordinator.isPrimary) {
213
+ this.coordinator.notify({
214
+ type: 'socket-event',
215
+ payload: { type: eventType, data: payload }
216
+ });
217
+ this.emit(eventType, payload);
218
+ }
219
+ }
220
+
221
+ private handleAuthUpdated(isAuthenticated: boolean) {
222
+ this.coordinator.notify({
223
+ type: 'auth-state-changed',
224
+ payload: isAuthenticated
225
+ });
226
+ }
227
+
228
+ private handleRoleChange(isPrimary: boolean) {
229
+ if (isPrimary && !this.socketClient.isConnected && this.isConnected) {
230
+ this.socketClient.connect();
231
+ } else if (!isPrimary && this.socketClient.isConnected) {
232
+ this.socketClient.disconnect();
233
+ }
234
+ }
235
+ }
236
+
237
+ export default BrowserWebSocketClient;
@@ -48,6 +48,12 @@ export type JobProgressData = {
48
48
  stepCount: number;
49
49
  };
50
50
 
51
+ export type JobETAData = {
52
+ jobID: string;
53
+ imgID?: string;
54
+ etaSeconds: number;
55
+ };
56
+
51
57
  export type JobResultData = {
52
58
  jobID: string;
53
59
  imgID: string;
@@ -55,6 +61,7 @@ export type JobResultData = {
55
61
  lastSeed: string;
56
62
  userCanceled: boolean;
57
63
  triggeredNSFWFilter: boolean;
64
+ resultUrl?: string;
58
65
  };
59
66
 
60
67
  export type JobStateData =
@@ -121,6 +128,11 @@ export type SocketEventMap = {
121
128
  * @event WebSocketClient#jobProgress - Job progress update
122
129
  */
123
130
  jobProgress: JobProgressData;
131
+ /**
132
+ * @event WebSocketClient#jobETA - Job ETA update (sent every second during inference by ComfyUI workers)
133
+ * Note: Only available for ComfyUI-based workers during video generation
134
+ */
135
+ jobETA: JobETAData;
124
136
  /**
125
137
  * @event WebSocketClient#jobResult - Job result received
126
138
  */
@@ -148,3 +160,5 @@ export type SocketEventMap = {
148
160
 
149
161
  artistCancelConfirmation: ArtistCancelConfirmation;
150
162
  };
163
+
164
+ export type SocketEventName = keyof SocketEventMap;
@@ -1,7 +1,7 @@
1
1
  import { MessageType, SocketMessageMap } from './messages';
2
2
  import { SocketEventMap } from './events';
3
3
  import RestClient from '../../lib/RestClient';
4
- import { SupernetType } from './types';
4
+ import { IWebSocketClient, SupernetType } from './types';
5
5
  import WebSocket, { CloseEvent, ErrorEvent, MessageEvent } from 'isomorphic-ws';
6
6
  import { base64Decode, base64Encode } from '../../lib/base64';
7
7
  import isNodejs from '../../lib/isNodejs';
@@ -13,7 +13,7 @@ const PROTOCOL_VERSION = '3.0.0';
13
13
 
14
14
  const PING_INTERVAL = 15000;
15
15
 
16
- class WebSocketClient extends RestClient<SocketEventMap> {
16
+ class WebSocketClient extends RestClient<SocketEventMap> implements IWebSocketClient {
17
17
  appId: string;
18
18
  baseUrl: string;
19
19
  private socket: WebSocket | null = null;
@@ -86,7 +86,7 @@ class WebSocketClient extends RestClient<SocketEventMap> {
86
86
  socket.onmessage = null;
87
87
  socket.onopen = null;
88
88
  this.stopPing();
89
- socket.close();
89
+ socket.close(1000, 'Client disconnected');
90
90
  }
91
91
 
92
92
  private startPing(socket: WebSocket) {
@@ -148,9 +148,16 @@ class WebSocketClient extends RestClient<SocketEventMap> {
148
148
  }
149
149
 
150
150
  private handleClose(e: CloseEvent) {
151
- if (e.target === this.socket) {
151
+ const socket = e.target;
152
+ socket.onerror = null;
153
+ socket.onmessage = null;
154
+ socket.onopen = null;
155
+ if (socket === this.socket || !this.socket) {
152
156
  this._logger.info('WebSocket disconnected, cleanup', e);
153
- this.disconnect();
157
+ if (socket === this.socket) {
158
+ this.stopPing();
159
+ this.socket = null;
160
+ }
154
161
  this.emit('disconnected', {
155
162
  code: e.code,
156
163
  reason: e.reason
@@ -193,6 +200,9 @@ class WebSocketClient extends RestClient<SocketEventMap> {
193
200
  }
194
201
 
195
202
  async send<T extends MessageType>(messageType: T, data: SocketMessageMap[T]) {
203
+ if (!this.isConnected) {
204
+ await this.connect();
205
+ }
196
206
  await this.waitForConnection();
197
207
  this._logger.debug('WebSocket send:', messageType, data);
198
208
  this.socket!.send(
@@ -1 +1,17 @@
1
+ import { MessageType, SocketMessageMap } from './messages';
2
+ import RestClient from '../../lib/RestClient';
3
+ import { SocketEventMap } from './events';
4
+
1
5
  export type SupernetType = 'relaxed' | 'fast';
6
+
7
+ export interface IWebSocketClient extends RestClient<SocketEventMap> {
8
+ appId: string;
9
+ baseUrl: string;
10
+ isConnected: boolean;
11
+ supernetType: SupernetType;
12
+
13
+ connect(): Promise<void>;
14
+ disconnect(): void;
15
+ send<T extends MessageType>(messageType: T, data: SocketMessageMap[T]): Promise<void>;
16
+ switchNetwork(supernetType: SupernetType): Promise<SupernetType>;
17
+ }
@@ -3,12 +3,14 @@ import WebSocketClient from './WebSocketClient';
3
3
  import TypedEventEmitter from '../lib/TypedEventEmitter';
4
4
  import { ApiClientEvents } from './events';
5
5
  import { ServerConnectData, ServerDisconnectData } from './WebSocketClient/events';
6
- import { isNotRecoverable } from './WebSocketClient/ErrorCode';
6
+ import { ErrorCode, isNotRecoverable } from './WebSocketClient/ErrorCode';
7
7
  import { JSONValue } from '../types/json';
8
- import { SupernetType } from './WebSocketClient/types';
8
+ import { IWebSocketClient, SupernetType } from './WebSocketClient/types';
9
9
  import { Logger } from '../lib/DefaultLogger';
10
10
  import CookieAuthManager from '../lib/AuthManager/CookieAuthManager';
11
11
  import { AuthManager, TokenAuthManager } from '../lib/AuthManager';
12
+ import isNodejs from '../lib/isNodejs';
13
+ import BrowserWebSocketClient from './WebSocketClient/BrowserWebSocketClient';
12
14
 
13
15
  const WS_RECONNECT_ATTEMPTS = 5;
14
16
 
@@ -42,13 +44,14 @@ export interface ApiClientOptions {
42
44
  logger: Logger;
43
45
  authType: 'token' | 'cookies';
44
46
  disableSocket?: boolean;
47
+ multiInstance?: boolean;
45
48
  }
46
49
 
47
50
  class ApiClient extends TypedEventEmitter<ApiClientEvents> {
48
51
  readonly appId: string;
49
52
  readonly logger: Logger;
50
53
  private _rest: RestClient;
51
- private _socket: WebSocketClient;
54
+ private _socket: IWebSocketClient;
52
55
  private _auth: AuthManager;
53
56
  private _reconnectAttempts = WS_RECONNECT_ATTEMPTS;
54
57
  private _disableSocket: boolean = false;
@@ -60,7 +63,8 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
60
63
  networkType,
61
64
  authType,
62
65
  logger,
63
- disableSocket = false
66
+ disableSocket = false,
67
+ multiInstance = false
64
68
  }: ApiClientOptions) {
65
69
  super();
66
70
  this.appId = appId;
@@ -68,7 +72,13 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
68
72
  this._auth =
69
73
  authType === 'token' ? new TokenAuthManager(baseUrl, logger) : new CookieAuthManager(logger);
70
74
  this._rest = new RestClient(baseUrl, this._auth, logger);
71
- this._socket = new WebSocketClient(socketUrl, this._auth, appId, networkType, logger);
75
+ const supportMultiInstance = !isNodejs && this._auth instanceof CookieAuthManager;
76
+ if (supportMultiInstance && multiInstance) {
77
+ // Use coordinated WebSocket client to share single connection between tabs
78
+ this._socket = new BrowserWebSocketClient(socketUrl, this._auth, appId, networkType, logger);
79
+ } else {
80
+ this._socket = new WebSocketClient(socketUrl, this._auth, appId, networkType, logger);
81
+ }
72
82
  this._disableSocket = disableSocket;
73
83
  this._auth.on('updated', this.handleAuthUpdated.bind(this));
74
84
  this._socket.on('connected', this.handleSocketConnect.bind(this));
@@ -83,7 +93,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
83
93
  return this._auth;
84
94
  }
85
95
 
86
- get socket(): WebSocketClient {
96
+ get socket(): IWebSocketClient {
87
97
  return this._socket;
88
98
  }
89
99
 
@@ -101,14 +111,26 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
101
111
  }
102
112
 
103
113
  handleSocketDisconnect(data: ServerDisconnectData) {
114
+ // If user is not authenticated, we don't need to reconnect
115
+ if (!this.auth.isAuthenticated || data.code === 1000) {
116
+ this.emit('disconnected', data);
117
+ return;
118
+ }
104
119
  if (!data.code || isNotRecoverable(data.code)) {
120
+ // If this is browser, another tab is probably claiming the connection, so we don't need to reconnect
121
+ if (
122
+ this._socket instanceof BrowserWebSocketClient &&
123
+ data.code === ErrorCode.SWITCH_CONNECTION
124
+ ) {
125
+ this.logger.debug('Switching network connection, not reconnecting');
126
+ return;
127
+ }
105
128
  this.auth.clear();
106
129
  this.emit('disconnected', data);
107
130
  this.logger.error('Not recoverable socket error', data);
108
131
  return;
109
132
  }
110
133
  if (this._reconnectAttempts <= 0) {
111
- this.auth.clear();
112
134
  this.emit('disconnected', data);
113
135
  this._reconnectAttempts = WS_RECONNECT_ATTEMPTS;
114
136
  return;
@@ -122,7 +144,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
122
144
  if (this.socket.isConnected) {
123
145
  this.socket.disconnect();
124
146
  }
125
- } else if (!this._disableSocket) {
147
+ } else if (!this._disableSocket && !this.socket.isConnected) {
126
148
  this.socket.connect();
127
149
  }
128
150
  }
@@ -9,6 +9,7 @@ import Project from './Project';
9
9
  import { SupernetType } from '../ApiClient/WebSocketClient/types';
10
10
  import { getEnhacementStrength } from './utils';
11
11
  import { TokenType } from '../types/token';
12
+ import { has } from 'lodash';
12
13
 
13
14
  export const enhancementDefaults = {
14
15
  network: 'fast' as SupernetType,
@@ -19,7 +20,7 @@ export const enhancementDefaults = {
19
20
  startingImageStrength: 0.5,
20
21
  steps: 5,
21
22
  guidance: 1,
22
- numberOfImages: 1,
23
+ numberOfMedia: 1,
23
24
  numberOfPreviews: 0
24
25
  };
25
26
 
@@ -61,6 +62,17 @@ export interface JobData {
61
62
  positivePrompt?: string;
62
63
  negativePrompt?: string;
63
64
  jobIndex?: number;
65
+ /**
66
+ * Estimated time remaining in seconds (for long-running jobs like video generation).
67
+ * Updated by ComfyUI workers during inference.
68
+ * @deprecated Use `eta` instead.
69
+ */
70
+ etaSeconds?: number;
71
+ /**
72
+ * Estimate completion time of the job (for long-running jobs like video generation).
73
+ * Updated by ComfyUI workers during inference.
74
+ */
75
+ eta?: Date;
64
76
  }
65
77
 
66
78
  export interface JobEventMap extends EntityEvents {
@@ -178,10 +190,21 @@ class Job extends DataEntity<JobData, JobEventMap> {
178
190
  return this.data.error;
179
191
  }
180
192
 
181
- get hasResultImage() {
193
+ /**
194
+ * Whether this job has a result media file available for download.
195
+ * Returns true if completed and not NSFW filtered.
196
+ */
197
+ get hasResultMedia() {
182
198
  return this.status === 'completed' && !this.isNSFW;
183
199
  }
184
200
 
201
+ /**
202
+ * Whether this job produces video output (based on the model used)
203
+ */
204
+ get type(): 'image' | 'video' {
205
+ return this._api.isVideoModelId(this._project.params.modelId) ? 'video' : 'image';
206
+ }
207
+
185
208
  get enhancedImage() {
186
209
  if (!this._enhancementProject) {
187
210
  return null;
@@ -199,17 +222,27 @@ class Job extends DataEntity<JobData, JobEventMap> {
199
222
 
200
223
  /**
201
224
  * Get the result URL of the job. This method will make a request to the API to get signed URL.
202
- * IMPORTANT: URL expires after 30 minutes, so make sure to download the image as soon as possible.
225
+ * IMPORTANT: URL expires after 30 minutes, so make sure to download the result as soon as possible.
226
+ * For video jobs, this returns a video URL. For image jobs, this returns an image URL.
203
227
  */
204
228
  async getResultUrl(): Promise<string> {
205
229
  if (this.data.status !== 'completed') {
206
230
  throw new Error('Job is not completed yet');
207
231
  }
208
- const url = await this._api.downloadUrl({
209
- jobId: this.projectId,
210
- imageId: this.id,
211
- type: 'complete'
212
- });
232
+ let url: string;
233
+ if (this.type === 'video') {
234
+ url = await this._api.mediaDownloadUrl({
235
+ jobId: this.projectId,
236
+ id: this.id,
237
+ type: 'complete'
238
+ });
239
+ } else {
240
+ url = await this._api.downloadUrl({
241
+ jobId: this.projectId,
242
+ imageId: this.id,
243
+ type: 'complete'
244
+ });
245
+ }
213
246
  this._update({ resultUrl: url });
214
247
  return url;
215
248
  }
@@ -230,6 +263,26 @@ class Job extends DataEntity<JobData, JobEventMap> {
230
263
  return this.data.workerName;
231
264
  }
232
265
 
266
+ /**
267
+ * Estimated time remaining in seconds for long-running jobs (e.g., video generation).
268
+ * Only available for ComfyUI-based workers during inference.
269
+ * Returns undefined if no ETA has been received.
270
+ * @deprecated Use `timeLeft` instead.
271
+ */
272
+ get etaSeconds() {
273
+ return this.data.etaSeconds;
274
+ }
275
+
276
+ /**
277
+ * Estimate completion time of the job.
278
+ * Only available for ComfyUI-based workers during inference.
279
+ * Is useful when data is persisted
280
+ * Returns undefined if no ETA has been received.
281
+ */
282
+ get eta() {
283
+ return this.data.eta;
284
+ }
285
+
233
286
  /**
234
287
  * Syncs the job data with the data received from the REST API.
235
288
  * @internal
@@ -247,11 +300,19 @@ class Job extends DataEntity<JobData, JobEventMap> {
247
300
  }
248
301
  if (!this.data.resultUrl && delta.status === 'completed' && !data.triggeredNSFWFilter) {
249
302
  try {
250
- delta.resultUrl = await this._api.downloadUrl({
251
- jobId: this.projectId,
252
- imageId: this.id,
253
- type: 'complete'
254
- });
303
+ if (this.type === 'video') {
304
+ delta.resultUrl = await this._api.mediaDownloadUrl({
305
+ jobId: this.projectId,
306
+ id: this.id,
307
+ type: 'complete'
308
+ });
309
+ } else {
310
+ delta.resultUrl = await this._api.downloadUrl({
311
+ jobId: this.projectId,
312
+ imageId: this.id,
313
+ type: 'complete'
314
+ });
315
+ }
255
316
  } catch (error) {
256
317
  this._logger.error(error);
257
318
  }
@@ -259,6 +320,21 @@ class Job extends DataEntity<JobData, JobEventMap> {
259
320
  this._update(delta);
260
321
  }
261
322
 
323
+ /**
324
+ * Updates the job data with the provided delta.
325
+ * @internal
326
+ * @param delta
327
+ */
328
+ _update(delta: Partial<JobData>) {
329
+ if (has(delta, 'eta')) {
330
+ // Keeping etaSeconds for backwards compatibility
331
+ if (delta.eta) {
332
+ delta.etaSeconds = Math.round((delta.eta.getTime() - Date.now()) / 1000);
333
+ }
334
+ }
335
+ super._update(delta);
336
+ }
337
+
262
338
  private handleUpdated(keys: string[]) {
263
339
  if (keys.includes('step') || keys.includes('stepCount')) {
264
340
  this.emit('progress', this.progress);
@@ -276,8 +352,8 @@ class Job extends DataEntity<JobData, JobEventMap> {
276
352
  }
277
353
 
278
354
  async getResultData() {
279
- if (!this.hasResultImage) {
280
- throw new Error('No result image available');
355
+ if (!this.hasResultMedia) {
356
+ throw new Error('No result media available');
281
357
  }
282
358
  const url = await this.getResultUrl();
283
359
  const response = await fetch(url);
@@ -297,6 +373,10 @@ class Job extends DataEntity<JobData, JobEventMap> {
297
373
  strength: EnhancementStrength,
298
374
  overrides: { positivePrompt?: string; stylePrompt?: string; tokenType?: TokenType } = {}
299
375
  ) {
376
+ const parentProjectParams = this._project.params;
377
+ if (parentProjectParams.type !== 'image') {
378
+ throw new Error('Enhancement is only available for images');
379
+ }
300
380
  if (this.status !== 'completed') {
301
381
  throw new Error('Job is not completed yet');
302
382
  }
@@ -309,6 +389,7 @@ class Job extends DataEntity<JobData, JobEventMap> {
309
389
  }
310
390
  const imageData = await this.getResultData();
311
391
  const project = await this._api.create({
392
+ type: 'image',
312
393
  ...enhancementDefaults,
313
394
  positivePrompt: overrides.positivePrompt || this._project.params.positivePrompt,
314
395
  stylePrompt: overrides.stylePrompt || this._project.params.stylePrompt,
@@ -316,7 +397,7 @@ class Job extends DataEntity<JobData, JobEventMap> {
316
397
  seed: this.seed || this._project.params.seed,
317
398
  startingImage: imageData,
318
399
  startingImageStrength: 1 - getEnhacementStrength(strength),
319
- sizePreset: this._project.params.sizePreset
400
+ sizePreset: parentProjectParams.sizePreset
320
401
  });
321
402
  this._enhancementProject = project;
322
403
  this._enhancementProject.on('updated', this.handleEnhancementUpdate);