@sogni-ai/sogni-client 4.0.0-alpha.3 → 4.0.0-alpha.30
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/CHANGELOG.md +213 -0
- package/README.md +279 -28
- package/dist/Account/index.d.ts +18 -16
- package/dist/Account/index.js +31 -20
- package/dist/Account/index.js.map +1 -1
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.d.ts +66 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js +332 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.d.ts +28 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js +203 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/events.d.ts +11 -0
- package/dist/ApiClient/WebSocketClient/index.d.ts +2 -2
- package/dist/ApiClient/WebSocketClient/index.js +13 -3
- package/dist/ApiClient/WebSocketClient/index.js.map +1 -1
- package/dist/ApiClient/WebSocketClient/types.d.ts +13 -0
- package/dist/ApiClient/index.d.ts +4 -4
- package/dist/ApiClient/index.js +23 -4
- package/dist/ApiClient/index.js.map +1 -1
- package/dist/Projects/Job.d.ts +24 -4
- package/dist/Projects/Job.js +58 -16
- package/dist/Projects/Job.js.map +1 -1
- package/dist/Projects/Project.d.ts +8 -0
- package/dist/Projects/Project.js +27 -6
- package/dist/Projects/Project.js.map +1 -1
- package/dist/Projects/createJobRequestMessage.js +109 -15
- package/dist/Projects/createJobRequestMessage.js.map +1 -1
- package/dist/Projects/index.d.ts +110 -11
- package/dist/Projects/index.js +412 -42
- package/dist/Projects/index.js.map +1 -1
- package/dist/Projects/types/EstimationResponse.d.ts +2 -0
- package/dist/Projects/types/SamplerParams.d.ts +13 -0
- package/dist/Projects/types/SamplerParams.js +26 -0
- package/dist/Projects/types/SamplerParams.js.map +1 -0
- package/dist/Projects/types/SchedulerParams.d.ts +14 -0
- package/dist/Projects/types/SchedulerParams.js +24 -0
- package/dist/Projects/types/SchedulerParams.js.map +1 -0
- package/dist/Projects/types/events.d.ts +5 -1
- package/dist/Projects/types/index.d.ts +150 -39
- package/dist/Projects/types/index.js +13 -0
- package/dist/Projects/types/index.js.map +1 -1
- package/dist/Projects/utils.d.ts +19 -1
- package/dist/Projects/utils.js +68 -0
- package/dist/Projects/utils.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/AuthManager/TokenAuthManager.js +0 -2
- package/dist/lib/AuthManager/TokenAuthManager.js.map +1 -1
- package/dist/lib/DataEntity.js +4 -2
- package/dist/lib/DataEntity.js.map +1 -1
- package/dist/lib/validation.d.ts +7 -0
- package/dist/lib/validation.js +36 -0
- package/dist/lib/validation.js.map +1 -1
- package/package.json +4 -4
- package/src/Account/index.ts +30 -19
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.ts +426 -0
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +237 -0
- package/src/ApiClient/WebSocketClient/events.ts +13 -0
- package/src/ApiClient/WebSocketClient/index.ts +15 -5
- package/src/ApiClient/WebSocketClient/types.ts +16 -0
- package/src/ApiClient/index.ts +30 -8
- package/src/Projects/Job.ts +64 -16
- package/src/Projects/Project.ts +29 -9
- package/src/Projects/createJobRequestMessage.ts +155 -36
- package/src/Projects/index.ts +437 -46
- package/src/Projects/types/EstimationResponse.ts +2 -0
- package/src/Projects/types/SamplerParams.ts +24 -0
- package/src/Projects/types/SchedulerParams.ts +22 -0
- package/src/Projects/types/events.ts +6 -0
- package/src/Projects/types/index.ts +181 -47
- package/src/Projects/utils.ts +66 -1
- package/src/index.ts +38 -11
- package/src/lib/AuthManager/TokenAuthManager.ts +0 -2
- package/src/lib/DataEntity.ts +4 -2
- package/src/lib/validation.ts +41 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import getUUID from '../../../lib/getUUID';
|
|
2
|
+
import { Logger } from '../../../lib/DefaultLogger';
|
|
3
|
+
|
|
4
|
+
const PRIMARY_HEARTBEAT_INTERVAL = 2000;
|
|
5
|
+
const PRIMARY_TIMEOUT = 4000;
|
|
6
|
+
const ACK_TIMEOUT = 5000;
|
|
7
|
+
|
|
8
|
+
enum MessageType {
|
|
9
|
+
ELECTION = 'election',
|
|
10
|
+
ELECTION_RESPONSE = 'election_response',
|
|
11
|
+
PRIMARY_ANNOUNCE = 'primary_announce',
|
|
12
|
+
PRIMARY_HEARTBEAT = 'primary_heartbeat',
|
|
13
|
+
BROADCAST = 'broadcast',
|
|
14
|
+
REQUEST = 'request',
|
|
15
|
+
REQUEST_ACK = 'request_ack'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Election {
|
|
19
|
+
type: MessageType.ELECTION;
|
|
20
|
+
payload: { priority: number };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ElectionResponse {
|
|
24
|
+
type: MessageType.ELECTION_RESPONSE;
|
|
25
|
+
payload: { id: string; priority: number };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PrimaryAnnounce {
|
|
29
|
+
type: MessageType.PRIMARY_ANNOUNCE;
|
|
30
|
+
payload: { id: string; priority: number };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PrimaryHeartbeat {
|
|
34
|
+
type: MessageType.PRIMARY_HEARTBEAT;
|
|
35
|
+
payload: { id: string; priority: number };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface BroadcastMessage {
|
|
39
|
+
type: MessageType.BROADCAST;
|
|
40
|
+
payload: any;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface RequestMessage {
|
|
44
|
+
type: MessageType.REQUEST;
|
|
45
|
+
payload: any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface RequestAckMessage {
|
|
49
|
+
type: MessageType.REQUEST_ACK;
|
|
50
|
+
payload: { id: string; error?: any };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Message =
|
|
54
|
+
| Election
|
|
55
|
+
| ElectionResponse
|
|
56
|
+
| PrimaryAnnounce
|
|
57
|
+
| PrimaryHeartbeat
|
|
58
|
+
| BroadcastMessage
|
|
59
|
+
| RequestMessage
|
|
60
|
+
| RequestAckMessage;
|
|
61
|
+
|
|
62
|
+
interface Envelope {
|
|
63
|
+
id: string;
|
|
64
|
+
senderId: string;
|
|
65
|
+
recipientId?: string;
|
|
66
|
+
timestamp: number;
|
|
67
|
+
message: Message;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const CHANNEL_NAME = 'sogni-websocket-channel';
|
|
71
|
+
|
|
72
|
+
let isActiveTab = false;
|
|
73
|
+
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
|
|
74
|
+
isActiveTab = window.document.visibilityState === 'visible';
|
|
75
|
+
window.addEventListener(
|
|
76
|
+
'visibilitychange',
|
|
77
|
+
() => (isActiveTab = window.document.visibilityState === 'visible')
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface Callbacks<M, N> {
|
|
82
|
+
onRoleChange: (isPrimary: boolean) => void;
|
|
83
|
+
onMessage: (message: M) => Promise<void>;
|
|
84
|
+
onNotification: (notification: N) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Options<M, N> {
|
|
88
|
+
callbacks: Callbacks<M, N>;
|
|
89
|
+
logger: Logger;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A class responsible for coordinating communication across browser tabs or windows via BroadcastChannel API.
|
|
94
|
+
* It handles the role election of the primary (leader) tab, message broadcasting, heartbeats for primary tab monitoring,
|
|
95
|
+
* and message acknowledgment for reliable communication.
|
|
96
|
+
*
|
|
97
|
+
* The `ChannelCoordinator` ensures that one tab per session is assigned as the primary, while allowing others to act as secondaries,
|
|
98
|
+
* utilizing various message types for communication and coordination.
|
|
99
|
+
*
|
|
100
|
+
* Message is sent by secondary tabs to the primary tab via the `sendMessage` method. By sending message
|
|
101
|
+
* tab can tell primary to connect, disconnect, send message to socket etc.
|
|
102
|
+
*
|
|
103
|
+
* Notification is type of message broadcasted by primary to secondary tabs. This is mostly used to
|
|
104
|
+
* broadcast socket events from primary to secondary tabs.
|
|
105
|
+
*
|
|
106
|
+
* @template M - The type of messages being broadcast to other tabs/windows.
|
|
107
|
+
* @template N - The type of notifications being handled within the coordinator.
|
|
108
|
+
*/
|
|
109
|
+
class ChannelCoordinator<M, N> {
|
|
110
|
+
private readonly id: string = getUUID();
|
|
111
|
+
private priority = Date.now();
|
|
112
|
+
private readonly channel = new BroadcastChannel(CHANNEL_NAME);
|
|
113
|
+
|
|
114
|
+
private _isPrimary = false;
|
|
115
|
+
private callbacks: Callbacks<M, N>;
|
|
116
|
+
private logger: Logger;
|
|
117
|
+
|
|
118
|
+
private ackCallbacks: Record<string, (error?: any) => void> = {};
|
|
119
|
+
|
|
120
|
+
// User to handle election of primary tab
|
|
121
|
+
private electionInProgress: boolean = false;
|
|
122
|
+
private electionResponses: Map<string, number> = new Map();
|
|
123
|
+
|
|
124
|
+
// Heartbeat to detect primary tab death
|
|
125
|
+
private lastPrimaryHeartbeat: number = Date.now();
|
|
126
|
+
private heartbeatTimer: NodeJS.Timeout | null = null;
|
|
127
|
+
private primaryCheckTimer: NodeJS.Timeout | null = null;
|
|
128
|
+
private readyCallback: () => void | null = () => {};
|
|
129
|
+
private readonly readyPromise: Promise<void>;
|
|
130
|
+
|
|
131
|
+
constructor({ callbacks, logger }: Options<M, N>) {
|
|
132
|
+
this.readyPromise = new Promise((resolve) => {
|
|
133
|
+
let called = false;
|
|
134
|
+
this.readyCallback = () => {
|
|
135
|
+
if (!called) {
|
|
136
|
+
called = true;
|
|
137
|
+
resolve();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
this.callbacks = callbacks;
|
|
142
|
+
this.logger = logger;
|
|
143
|
+
this.channel.addEventListener('message', (event) => this.handleMessage(event.data));
|
|
144
|
+
this.startElections();
|
|
145
|
+
this.startPrimaryMonitor();
|
|
146
|
+
// Listen for tab closing to gracefully release primary role
|
|
147
|
+
if (typeof window !== 'undefined') {
|
|
148
|
+
window.addEventListener('beforeunload', () => {
|
|
149
|
+
this.priority = 0;
|
|
150
|
+
this.startElections();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get isPrimary() {
|
|
156
|
+
return this._isPrimary;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private get currentPriority() {
|
|
160
|
+
return isActiveTab ? this.priority * 10 : this.priority;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
isReady() {
|
|
164
|
+
return this.readyPromise;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private startElections() {
|
|
168
|
+
this.logger.debug(
|
|
169
|
+
`Start primary elections, my priority is ${this.currentPriority}, tab visibility is ${isActiveTab}`
|
|
170
|
+
);
|
|
171
|
+
this.electionInProgress = true;
|
|
172
|
+
this.electionResponses.clear();
|
|
173
|
+
this.electionResponses.set(this.id, this.currentPriority);
|
|
174
|
+
this.broadcast({
|
|
175
|
+
type: MessageType.ELECTION,
|
|
176
|
+
payload: { priority: this.currentPriority }
|
|
177
|
+
});
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
this.finishElections();
|
|
180
|
+
}, 500);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private finishElections() {
|
|
184
|
+
if (!this.electionInProgress) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Find highest priority
|
|
188
|
+
let highestPriority = -Infinity;
|
|
189
|
+
let winnerId: string | null = null;
|
|
190
|
+
|
|
191
|
+
for (const [id, priority] of this.electionResponses) {
|
|
192
|
+
if (priority > highestPriority || (priority === highestPriority && id > (winnerId || ''))) {
|
|
193
|
+
highestPriority = priority;
|
|
194
|
+
winnerId = id;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (winnerId === this.id) {
|
|
199
|
+
this.becomePrimary();
|
|
200
|
+
this.logger.debug(`Won elections! ${winnerId}`);
|
|
201
|
+
} else {
|
|
202
|
+
this.logger.debug(`Lost elections! Winner is ${winnerId}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.electionInProgress = false;
|
|
206
|
+
this.electionResponses.clear();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private becomePrimary() {
|
|
210
|
+
this._isPrimary = true;
|
|
211
|
+
this.lastPrimaryHeartbeat = Date.now();
|
|
212
|
+
this.broadcast({
|
|
213
|
+
type: MessageType.PRIMARY_ANNOUNCE,
|
|
214
|
+
payload: { id: this.id, priority: this.currentPriority }
|
|
215
|
+
});
|
|
216
|
+
this.startHeartbeat();
|
|
217
|
+
this.callbacks.onRoleChange(true);
|
|
218
|
+
this.readyCallback();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private startHeartbeat(): void {
|
|
222
|
+
if (this.heartbeatTimer) {
|
|
223
|
+
clearInterval(this.heartbeatTimer);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.heartbeatTimer = setInterval(() => {
|
|
227
|
+
if (this._isPrimary) {
|
|
228
|
+
this.broadcast({
|
|
229
|
+
type: MessageType.PRIMARY_HEARTBEAT,
|
|
230
|
+
payload: { id: this.id, priority: this.currentPriority }
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
this.stopHeartbeat();
|
|
234
|
+
}
|
|
235
|
+
}, PRIMARY_HEARTBEAT_INTERVAL);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private stopHeartbeat(): void {
|
|
239
|
+
if (this.heartbeatTimer) {
|
|
240
|
+
clearInterval(this.heartbeatTimer);
|
|
241
|
+
this.heartbeatTimer = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private startPrimaryMonitor(): void {
|
|
246
|
+
if (this.primaryCheckTimer) {
|
|
247
|
+
clearInterval(this.primaryCheckTimer);
|
|
248
|
+
}
|
|
249
|
+
this.primaryCheckTimer = setInterval(() => {
|
|
250
|
+
if (!this._isPrimary) {
|
|
251
|
+
const timeSinceLastHeartbeat = Date.now() - this.lastPrimaryHeartbeat;
|
|
252
|
+
if (timeSinceLastHeartbeat > PRIMARY_TIMEOUT) {
|
|
253
|
+
this.startElections();
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
this.lastPrimaryHeartbeat = Date.now();
|
|
257
|
+
}
|
|
258
|
+
}, PRIMARY_HEARTBEAT_INTERVAL);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private handleMessage(envelope: Envelope) {
|
|
262
|
+
const { senderId, recipientId, message } = envelope;
|
|
263
|
+
const isForOtherClient = recipientId && recipientId !== this.id;
|
|
264
|
+
if (senderId === this.id || isForOtherClient) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
switch (message.type) {
|
|
268
|
+
case MessageType.ELECTION:
|
|
269
|
+
return this.handleElection(message);
|
|
270
|
+
case MessageType.ELECTION_RESPONSE:
|
|
271
|
+
return this.handleElectionResponse(message);
|
|
272
|
+
case MessageType.PRIMARY_ANNOUNCE:
|
|
273
|
+
return this.handlePrimaryAnnounce(message);
|
|
274
|
+
case MessageType.PRIMARY_HEARTBEAT:
|
|
275
|
+
return this.handlePrimaryHeartbeat();
|
|
276
|
+
case MessageType.BROADCAST:
|
|
277
|
+
return this.handleBroadcast(message);
|
|
278
|
+
case MessageType.REQUEST:
|
|
279
|
+
return this.handleRequest(message, envelope);
|
|
280
|
+
case MessageType.REQUEST_ACK:
|
|
281
|
+
return this.handleRequestAck(message);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private handleElection(message: Election) {
|
|
286
|
+
this.broadcast({
|
|
287
|
+
type: MessageType.ELECTION_RESPONSE,
|
|
288
|
+
payload: { id: this.id, priority: this.currentPriority }
|
|
289
|
+
});
|
|
290
|
+
if (this.currentPriority > message.payload.priority && !this.electionInProgress) {
|
|
291
|
+
this.startElections();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private handleElectionResponse(message: ElectionResponse) {
|
|
296
|
+
if (this.electionInProgress) {
|
|
297
|
+
this.electionResponses.set(message.payload.id, message.payload.priority);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private handlePrimaryAnnounce(message: PrimaryAnnounce) {
|
|
302
|
+
this.logger.debug(`Primary announced: ${message.payload.id}`);
|
|
303
|
+
const wasPrimary = this._isPrimary;
|
|
304
|
+
this._isPrimary = false;
|
|
305
|
+
this.lastPrimaryHeartbeat = Date.now();
|
|
306
|
+
this.electionInProgress = false;
|
|
307
|
+
this.electionResponses.clear();
|
|
308
|
+
|
|
309
|
+
if (this.heartbeatTimer) {
|
|
310
|
+
clearInterval(this.heartbeatTimer);
|
|
311
|
+
this.heartbeatTimer = null;
|
|
312
|
+
}
|
|
313
|
+
if (wasPrimary) {
|
|
314
|
+
this.callbacks.onRoleChange(false);
|
|
315
|
+
}
|
|
316
|
+
this.readyCallback();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private handlePrimaryHeartbeat() {
|
|
320
|
+
this.lastPrimaryHeartbeat = Date.now();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private handleBroadcast(message: BroadcastMessage) {
|
|
324
|
+
this.logger.debug(`Received broadcast from primary`, message.payload);
|
|
325
|
+
this.callbacks.onNotification(message.payload);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private handleRequest(message: RequestMessage, envelope: Envelope) {
|
|
329
|
+
if (!this.isPrimary) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.logger.debug(`Received request from secondary`, message.payload);
|
|
333
|
+
this.callbacks
|
|
334
|
+
.onMessage(message.payload)
|
|
335
|
+
.then(() => {
|
|
336
|
+
this.send(
|
|
337
|
+
{
|
|
338
|
+
type: MessageType.REQUEST_ACK,
|
|
339
|
+
payload: { id: envelope.id }
|
|
340
|
+
},
|
|
341
|
+
envelope.senderId
|
|
342
|
+
);
|
|
343
|
+
})
|
|
344
|
+
.catch((error) => {
|
|
345
|
+
this.send(
|
|
346
|
+
{
|
|
347
|
+
type: MessageType.REQUEST_ACK,
|
|
348
|
+
payload: { id: envelope.id, error }
|
|
349
|
+
},
|
|
350
|
+
envelope.senderId
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private handleRequestAck(message: RequestAckMessage) {
|
|
356
|
+
const ackCallback = this.ackCallbacks[message.payload.id];
|
|
357
|
+
if (ackCallback) {
|
|
358
|
+
ackCallback(message.payload.error);
|
|
359
|
+
delete this.ackCallbacks[message.payload.id];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private async send(
|
|
364
|
+
message: RequestMessage | RequestAckMessage,
|
|
365
|
+
recipientId?: string
|
|
366
|
+
): Promise<void> {
|
|
367
|
+
const envelope: Envelope = {
|
|
368
|
+
id: getUUID(),
|
|
369
|
+
senderId: this.id,
|
|
370
|
+
recipientId,
|
|
371
|
+
timestamp: Date.now(),
|
|
372
|
+
message: message
|
|
373
|
+
};
|
|
374
|
+
if (message.type !== MessageType.REQUEST) {
|
|
375
|
+
this.channel.postMessage(envelope);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
return new Promise<void>((resolve, reject) => {
|
|
379
|
+
const ackTimeout = setTimeout(() => {
|
|
380
|
+
if (this.ackCallbacks[envelope.id]) {
|
|
381
|
+
this.ackCallbacks[envelope.id](new Error('Message delivery timeout'));
|
|
382
|
+
delete this.ackCallbacks[envelope.id];
|
|
383
|
+
}
|
|
384
|
+
}, ACK_TIMEOUT);
|
|
385
|
+
this.ackCallbacks[envelope.id] = (error?: any) => {
|
|
386
|
+
clearTimeout(ackTimeout);
|
|
387
|
+
delete this.ackCallbacks[envelope.id];
|
|
388
|
+
if (error) {
|
|
389
|
+
reject(error);
|
|
390
|
+
} else {
|
|
391
|
+
resolve();
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
this.channel.postMessage(envelope);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private broadcast(message: Message) {
|
|
399
|
+
const envelope: Envelope = {
|
|
400
|
+
id: getUUID(),
|
|
401
|
+
senderId: this.id,
|
|
402
|
+
timestamp: Date.now(),
|
|
403
|
+
message: message
|
|
404
|
+
};
|
|
405
|
+
this.channel.postMessage(envelope);
|
|
406
|
+
return envelope.id;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
public async sendMessage(message: M): Promise<any> {
|
|
410
|
+
this.logger.debug(`Sending message to primary`, message);
|
|
411
|
+
return this.send({
|
|
412
|
+
type: MessageType.REQUEST,
|
|
413
|
+
payload: message
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
public notify(message: N) {
|
|
418
|
+
this.logger.debug(`Sending notification to secondary tabs`, message);
|
|
419
|
+
this.broadcast({
|
|
420
|
+
type: MessageType.BROADCAST,
|
|
421
|
+
payload: message
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export default ChannelCoordinator;
|
|
@@ -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;
|
|
@@ -121,6 +127,11 @@ export type SocketEventMap = {
|
|
|
121
127
|
* @event WebSocketClient#jobProgress - Job progress update
|
|
122
128
|
*/
|
|
123
129
|
jobProgress: JobProgressData;
|
|
130
|
+
/**
|
|
131
|
+
* @event WebSocketClient#jobETA - Job ETA update (sent every second during inference by ComfyUI workers)
|
|
132
|
+
* Note: Only available for ComfyUI-based workers during video generation
|
|
133
|
+
*/
|
|
134
|
+
jobETA: JobETAData;
|
|
124
135
|
/**
|
|
125
136
|
* @event WebSocketClient#jobResult - Job result received
|
|
126
137
|
*/
|
|
@@ -148,3 +159,5 @@ export type SocketEventMap = {
|
|
|
148
159
|
|
|
149
160
|
artistCancelConfirmation: ArtistCancelConfirmation;
|
|
150
161
|
};
|
|
162
|
+
|
|
163
|
+
export type SocketEventName = keyof SocketEventMap;
|