@sogni-ai/sogni-client 4.0.0-alpha.11 → 4.0.0-alpha.13
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 +14 -0
- package/dist/Account/index.d.ts +1 -0
- package/dist/Account/index.js +11 -2
- package/dist/Account/index.js.map +1 -1
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/BrowserWebSocketClient.d.ts +34 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/BrowserWebSocketClient.js +195 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/BrowserWebSocketClient.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/WSCoordinator.d.ts +101 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/WSCoordinator.js +359 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/WSCoordinator.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/types.d.ts +101 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/types.js +3 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/types.js.map +1 -0
- package/dist/ApiClient/WebSocketClient/events.d.ts +1 -0
- package/dist/ApiClient/WebSocketClient/index.d.ts +2 -2
- package/dist/ApiClient/WebSocketClient/types.d.ts +13 -0
- package/dist/ApiClient/index.d.ts +2 -3
- package/dist/ApiClient/index.js +20 -2
- package/dist/ApiClient/index.js.map +1 -1
- package/dist/Projects/Job.d.ts +1 -1
- package/dist/Projects/index.d.ts +3 -9
- package/dist/Projects/index.js +3 -1
- package/dist/Projects/index.js.map +1 -1
- package/dist/Projects/types/EstimationResponse.d.ts +2 -0
- package/dist/Projects/types/index.d.ts +13 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/DataEntity.js +4 -2
- package/dist/lib/DataEntity.js.map +1 -1
- package/package.json +1 -1
- package/src/Account/index.ts +11 -2
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/BrowserWebSocketClient.ts +206 -0
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/WSCoordinator.ts +425 -0
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/types.ts +107 -0
- package/src/ApiClient/WebSocketClient/events.ts +2 -0
- package/src/ApiClient/WebSocketClient/index.ts +2 -2
- package/src/ApiClient/WebSocketClient/types.ts +16 -0
- package/src/ApiClient/index.ts +25 -6
- package/src/Projects/index.ts +5 -2
- package/src/Projects/types/EstimationResponse.ts +2 -0
- package/src/Projects/types/index.ts +14 -0
- package/src/index.ts +7 -3
- package/src/lib/DataEntity.ts +4 -2
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { Logger } from '../../../lib/DefaultLogger';
|
|
2
|
+
import getUUID from '../../../lib/getUUID';
|
|
3
|
+
import { AuthenticatedData, SocketEventMap, SocketEventName } from '../events';
|
|
4
|
+
import { MessageType, SocketMessageMap } from '../messages';
|
|
5
|
+
import { Balances } from '../../../Account/types';
|
|
6
|
+
import {
|
|
7
|
+
ChannelMessage,
|
|
8
|
+
Heartbeat,
|
|
9
|
+
MessageEnvelope,
|
|
10
|
+
SocketEventReceived,
|
|
11
|
+
SendSocketMessage
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
interface StartingEvents {
|
|
15
|
+
authenticated?: AuthenticatedData;
|
|
16
|
+
balanceUpdate?: Balances;
|
|
17
|
+
swarmModels?: Record<string, number>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface WSCoordinatorCallbacks {
|
|
21
|
+
/**
|
|
22
|
+
* Invoked when authentication state changes (authenticated/unauthenticated).
|
|
23
|
+
* @param isAuthenticated - true if client is authenticated, false if not.
|
|
24
|
+
*/
|
|
25
|
+
onAuthChanged: (isAuthenticated: boolean) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Invoked when role changes (primary/secondary).
|
|
28
|
+
* @param isPrimary - true if client is primary, false if secondary.
|
|
29
|
+
*/
|
|
30
|
+
onRoleChange: (isPrimary: boolean) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Invoked when connection state must change (connected/disconnected).
|
|
33
|
+
* @param isConnected
|
|
34
|
+
*/
|
|
35
|
+
onConnectionToggle: (isConnected: boolean) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Invoked when secondary client receives a socket event from primary.
|
|
38
|
+
* @param message
|
|
39
|
+
*/
|
|
40
|
+
onMessageFromPrimary: (message: SocketEventReceived | Heartbeat) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Invoked when primary client receives a socket send request from secondary.
|
|
43
|
+
* @param message
|
|
44
|
+
*/
|
|
45
|
+
onSendRequest: (message: SendSocketMessage) => Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class WSCoordinator {
|
|
49
|
+
private static readonly HEARTBEAT_INTERVAL = 2000;
|
|
50
|
+
private static readonly PRIMARY_TIMEOUT = 3000;
|
|
51
|
+
private static readonly CHANNEL_NAME = 'sogni-websocket-clients';
|
|
52
|
+
private static readonly ACK_TIMEOUT = 5000;
|
|
53
|
+
|
|
54
|
+
private id: string;
|
|
55
|
+
private callbacks: WSCoordinatorCallbacks;
|
|
56
|
+
private channel: BroadcastChannel;
|
|
57
|
+
private _isPrimary: boolean;
|
|
58
|
+
private _isConnected = false;
|
|
59
|
+
private lastPrimaryHeartbeat: number = 0;
|
|
60
|
+
private logger: Logger;
|
|
61
|
+
private heartbeatInterval: NodeJS.Timeout | null = null;
|
|
62
|
+
private primaryCheckInterval: NodeJS.Timeout | null = null;
|
|
63
|
+
private startingEvents: StartingEvents = {};
|
|
64
|
+
private ackCallbacks: Record<string, (error?: any) => void> = {};
|
|
65
|
+
|
|
66
|
+
constructor(callbacks: WSCoordinatorCallbacks, logger: Logger) {
|
|
67
|
+
this.id = getUUID();
|
|
68
|
+
this.logger = logger;
|
|
69
|
+
this.callbacks = callbacks;
|
|
70
|
+
this.channel = new BroadcastChannel(WSCoordinator.CHANNEL_NAME);
|
|
71
|
+
this.channel.onmessage = this.handleMessage.bind(this);
|
|
72
|
+
this._isPrimary = false;
|
|
73
|
+
|
|
74
|
+
// Listen for tab closing to gracefully release primary role
|
|
75
|
+
if (typeof window !== 'undefined') {
|
|
76
|
+
window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Initialize tab coordination and determine role
|
|
82
|
+
*/
|
|
83
|
+
async initialize(): Promise<boolean> {
|
|
84
|
+
this.logger.info(`WSCoordinator ${this.id} initializing...`);
|
|
85
|
+
|
|
86
|
+
// Announce our presence
|
|
87
|
+
this.broadcast({
|
|
88
|
+
type: 'announce'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Wait to see if there's an existing primary
|
|
92
|
+
await this.waitForPrimaryResponse();
|
|
93
|
+
if (!this._isPrimary) {
|
|
94
|
+
this.logger.info(`Client ${this.id} is secondary, primary exists`);
|
|
95
|
+
this.startPrimaryCheck();
|
|
96
|
+
} else {
|
|
97
|
+
this.logger.info(`Client ${this.id} becoming primary`);
|
|
98
|
+
this.becomePrimary();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this._isPrimary;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
connect() {
|
|
105
|
+
if (this._isPrimary) {
|
|
106
|
+
throw new Error('Primary should connect the socket directly.');
|
|
107
|
+
}
|
|
108
|
+
this.broadcast({
|
|
109
|
+
type: 'connection-toggle',
|
|
110
|
+
payload: { connected: true }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
disconnect() {
|
|
115
|
+
if (this._isPrimary) {
|
|
116
|
+
throw new Error('Primary should disconnect socket directly.');
|
|
117
|
+
}
|
|
118
|
+
this.broadcast({
|
|
119
|
+
type: 'connection-toggle',
|
|
120
|
+
payload: { connected: false }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async changeAuthState(isAuthenticated: boolean) {
|
|
125
|
+
this.broadcast({
|
|
126
|
+
type: 'authentication',
|
|
127
|
+
payload: { authenticated: isAuthenticated }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Wait briefly to see if a primary tab responds
|
|
133
|
+
*/
|
|
134
|
+
private waitForPrimaryResponse(): Promise<void> {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const timeout = setTimeout(() => {
|
|
137
|
+
// No primary responded, we become primary
|
|
138
|
+
this._isPrimary = true;
|
|
139
|
+
resolve();
|
|
140
|
+
}, 500);
|
|
141
|
+
|
|
142
|
+
const messageHandler = (e: MessageEvent<MessageEnvelope>) => {
|
|
143
|
+
const envelope = e.data;
|
|
144
|
+
const message = envelope.payload;
|
|
145
|
+
if (message.type === 'primary-present' || message.type === 'primary-claim') {
|
|
146
|
+
// Primary exists
|
|
147
|
+
this._isPrimary = false;
|
|
148
|
+
this.lastPrimaryHeartbeat = envelope.timestamp;
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
this.channel.removeEventListener('message', messageHandler);
|
|
151
|
+
resolve();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
this.channel.addEventListener('message', messageHandler);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Become the primary tab
|
|
161
|
+
*/
|
|
162
|
+
private becomePrimary() {
|
|
163
|
+
this._isPrimary = true;
|
|
164
|
+
this.callbacks.onRoleChange(true);
|
|
165
|
+
|
|
166
|
+
// Broadcast that we're claiming primary role
|
|
167
|
+
this.broadcast({
|
|
168
|
+
type: 'primary-claim'
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Start sending heartbeats
|
|
172
|
+
this.startHeartbeat();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Release primary role (when closing)
|
|
177
|
+
*/
|
|
178
|
+
private releasePrimary() {
|
|
179
|
+
if (this._isPrimary) {
|
|
180
|
+
this.broadcast({
|
|
181
|
+
type: 'primary-release'
|
|
182
|
+
});
|
|
183
|
+
this.stopHeartbeat();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Start sending heartbeat messages as primary
|
|
189
|
+
*/
|
|
190
|
+
private startHeartbeat() {
|
|
191
|
+
if (this.heartbeatInterval)
|
|
192
|
+
throw new Error('Heartbeat interval already started. This should never happen.');
|
|
193
|
+
this.heartbeatInterval = setInterval(() => {
|
|
194
|
+
this.broadcast({
|
|
195
|
+
type: 'primary-present',
|
|
196
|
+
payload: { connected: this._isConnected }
|
|
197
|
+
});
|
|
198
|
+
}, WSCoordinator.HEARTBEAT_INTERVAL);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Stop sending heartbeat messages
|
|
203
|
+
*/
|
|
204
|
+
private stopHeartbeat() {
|
|
205
|
+
if (this.heartbeatInterval) {
|
|
206
|
+
clearInterval(this.heartbeatInterval);
|
|
207
|
+
this.heartbeatInterval = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Start checking for primary heartbeat (as secondary)
|
|
213
|
+
*/
|
|
214
|
+
private startPrimaryCheck() {
|
|
215
|
+
this.primaryCheckInterval = setInterval(() => {
|
|
216
|
+
const timeSinceLastHeartbeat = Date.now() - this.lastPrimaryHeartbeat;
|
|
217
|
+
if (timeSinceLastHeartbeat > WSCoordinator.PRIMARY_TIMEOUT) {
|
|
218
|
+
this.logger.warn(`Primary tab timeout, becoming primary`);
|
|
219
|
+
this.stopPrimaryCheck();
|
|
220
|
+
this.becomePrimary();
|
|
221
|
+
}
|
|
222
|
+
}, 1000);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Stop checking for primary heartbeat
|
|
227
|
+
*/
|
|
228
|
+
private stopPrimaryCheck() {
|
|
229
|
+
if (this.primaryCheckInterval) {
|
|
230
|
+
clearInterval(this.primaryCheckInterval);
|
|
231
|
+
this.primaryCheckInterval = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private handleMessage(e: MessageEvent<MessageEnvelope>) {
|
|
236
|
+
const envelope = e.data;
|
|
237
|
+
const message = envelope.payload;
|
|
238
|
+
// If a message sent to specific recipient and not to us, ignore it
|
|
239
|
+
if (!!envelope.recipientId && envelope.recipientId !== this.id) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
switch (message.type) {
|
|
243
|
+
case 'announce':
|
|
244
|
+
if (this._isPrimary) {
|
|
245
|
+
this.broadcast({
|
|
246
|
+
type: 'primary-present',
|
|
247
|
+
payload: { connected: this._isConnected }
|
|
248
|
+
});
|
|
249
|
+
// Re-broadcast starting events for the new tab
|
|
250
|
+
Object.entries(this.startingEvents).forEach(([eventType, payload]) => {
|
|
251
|
+
this.broadcast(
|
|
252
|
+
{
|
|
253
|
+
type: 'socket-event',
|
|
254
|
+
payload: { eventType: eventType as SocketEventName, payload }
|
|
255
|
+
},
|
|
256
|
+
envelope.senderId
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
case 'primary-claim':
|
|
262
|
+
case 'primary-present':
|
|
263
|
+
this.lastPrimaryHeartbeat = envelope.timestamp;
|
|
264
|
+
if (this._isPrimary) {
|
|
265
|
+
this.logger.info(`Stepping down from primary, ${envelope.senderId} claimed it`);
|
|
266
|
+
this.stopHeartbeat();
|
|
267
|
+
this._isPrimary = false;
|
|
268
|
+
this.callbacks.onRoleChange(false);
|
|
269
|
+
this.startPrimaryCheck();
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
case 'primary-release':
|
|
273
|
+
if (!this._isPrimary) {
|
|
274
|
+
// Wait random time (0 - 300ms) before claiming "primary" role, so not all tabs try to claim at the same time
|
|
275
|
+
setTimeout(
|
|
276
|
+
() => {
|
|
277
|
+
const timeSinceRelease = Date.now() - envelope.timestamp;
|
|
278
|
+
const timeSinceLastHeartbeat = Date.now() - this.lastPrimaryHeartbeat;
|
|
279
|
+
if (timeSinceLastHeartbeat > timeSinceRelease) {
|
|
280
|
+
this.logger.info(`Primary released, becoming primary`);
|
|
281
|
+
this.stopPrimaryCheck();
|
|
282
|
+
this.becomePrimary();
|
|
283
|
+
} else {
|
|
284
|
+
this.logger.info(`Another primary exists, do nothing`);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
Math.round(Math.random() * 300)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
case 'authentication':
|
|
292
|
+
this.callbacks.onAuthChanged(message.payload.authenticated);
|
|
293
|
+
return;
|
|
294
|
+
case 'connection-toggle':
|
|
295
|
+
if (this._isPrimary) {
|
|
296
|
+
this.logger.info(
|
|
297
|
+
`Should ${message.payload.connected ? 'connect' : 'disconnect'} socket.`
|
|
298
|
+
);
|
|
299
|
+
this.callbacks.onConnectionToggle(message.payload.connected);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
case 'socket-event': {
|
|
303
|
+
if (!this._isPrimary) {
|
|
304
|
+
this.callbacks.onMessageFromPrimary(message);
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
case 'socket-send': {
|
|
309
|
+
if (this._isPrimary) {
|
|
310
|
+
this.callbacks.onSendRequest(message).then(() => {
|
|
311
|
+
//Acknowledge the request
|
|
312
|
+
this.broadcast({
|
|
313
|
+
type: 'socket-ack',
|
|
314
|
+
payload: {
|
|
315
|
+
envelopeId: envelope.id
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
case 'socket-ack': {
|
|
323
|
+
if (!this._isPrimary) {
|
|
324
|
+
if (this.ackCallbacks[message.payload.envelopeId]) {
|
|
325
|
+
this.ackCallbacks[message.payload.envelopeId]();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private broadcast(message: ChannelMessage, recipientId?: string): string {
|
|
333
|
+
const envelope: MessageEnvelope = {
|
|
334
|
+
id: getUUID(),
|
|
335
|
+
senderId: this.id,
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
payload: message
|
|
338
|
+
};
|
|
339
|
+
if (recipientId) {
|
|
340
|
+
envelope.recipientId = recipientId;
|
|
341
|
+
}
|
|
342
|
+
this.channel.postMessage(envelope);
|
|
343
|
+
return envelope.id;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Send a message to be transmitted over the socket (from secondary to primary)
|
|
348
|
+
*/
|
|
349
|
+
sendToSocket<T extends MessageType = MessageType>(messageType: T, data: SocketMessageMap[T]) {
|
|
350
|
+
if (this._isPrimary) {
|
|
351
|
+
throw new Error('Primary tab should send directly');
|
|
352
|
+
}
|
|
353
|
+
this.logger.debug(`Sending socket message ${messageType}`, data);
|
|
354
|
+
const messageId = this.broadcast({
|
|
355
|
+
type: 'socket-send',
|
|
356
|
+
payload: { messageType, data }
|
|
357
|
+
});
|
|
358
|
+
return new Promise<void>((resolve, reject) => {
|
|
359
|
+
const ackTimeout = setTimeout(() => {
|
|
360
|
+
//If callback is not called within 5 seconds, call it with an error
|
|
361
|
+
if (this.ackCallbacks[messageId]) {
|
|
362
|
+
this.ackCallbacks[messageId](new Error('Message delivery timeout'));
|
|
363
|
+
}
|
|
364
|
+
}, WSCoordinator.ACK_TIMEOUT);
|
|
365
|
+
this.ackCallbacks[messageId] = (error?: any) => {
|
|
366
|
+
delete this.ackCallbacks[messageId];
|
|
367
|
+
clearTimeout(ackTimeout);
|
|
368
|
+
if (error) {
|
|
369
|
+
reject(error);
|
|
370
|
+
} else {
|
|
371
|
+
resolve();
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Broadcast a socket event from primary to all secondaries
|
|
379
|
+
*/
|
|
380
|
+
broadcastSocketEvent<E extends SocketEventName = SocketEventName>(
|
|
381
|
+
eventType: E,
|
|
382
|
+
payload: SocketEventMap[E]
|
|
383
|
+
) {
|
|
384
|
+
if (!this._isPrimary) {
|
|
385
|
+
throw new Error('Only primary tab can broadcast socket events');
|
|
386
|
+
}
|
|
387
|
+
if (eventType === 'connected') {
|
|
388
|
+
this._isConnected = true;
|
|
389
|
+
} else if (eventType === 'disconnected') {
|
|
390
|
+
this._isConnected = false;
|
|
391
|
+
}
|
|
392
|
+
this.updateStartingState(eventType, payload);
|
|
393
|
+
this.logger.debug(`Broadcasting socket event ${eventType}`, payload);
|
|
394
|
+
this.broadcast({
|
|
395
|
+
type: 'socket-event',
|
|
396
|
+
payload: { eventType, payload }
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private updateStartingState<E extends SocketEventName>(eventType: E, payload: SocketEventMap[E]) {
|
|
401
|
+
if (eventType === 'authenticated') {
|
|
402
|
+
this.startingEvents.authenticated = payload as AuthenticatedData;
|
|
403
|
+
} else if (eventType === 'balanceUpdate') {
|
|
404
|
+
this.startingEvents.balanceUpdate = payload as Balances;
|
|
405
|
+
} else if (eventType === 'swarmModels') {
|
|
406
|
+
this.startingEvents.swarmModels = payload as Record<string, number>;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
isPrimary() {
|
|
411
|
+
return this._isPrimary;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Handle tab closing event
|
|
416
|
+
*/
|
|
417
|
+
private handleBeforeUnload = () => {
|
|
418
|
+
if (this._isPrimary) {
|
|
419
|
+
this.logger.info(`Client ${this.id} closing, releasing primary role`);
|
|
420
|
+
this.releasePrimary();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export default WSCoordinator;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SocketEventMap, SocketEventName } from '../events';
|
|
2
|
+
import { MessageType, SocketMessageMap } from '../messages';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Primary tab to broadcast socket events to secondary tabs.
|
|
6
|
+
* @param eventType - The event type.
|
|
7
|
+
* @param payload - The event payload. See {@link SocketEventMap} for the list of available events.
|
|
8
|
+
*/
|
|
9
|
+
export interface SocketEventReceived<T extends SocketEventName = SocketEventName> {
|
|
10
|
+
type: 'socket-event';
|
|
11
|
+
payload: { eventType: T; payload: SocketEventMap[T] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sent by secondary tabs to the primary tab to send a message to WebSocket.
|
|
16
|
+
* @param messageType - The message type.
|
|
17
|
+
* @param data - The message payload. See {@link SocketMessageMap} for the list of available messages.
|
|
18
|
+
*/
|
|
19
|
+
export interface SendSocketMessage<T extends MessageType = MessageType> {
|
|
20
|
+
type: 'socket-send';
|
|
21
|
+
payload: { messageType: T; data: SocketMessageMap[T] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sent by the primary tab to acknowledge that a message was sent to WebSocket.
|
|
26
|
+
*/
|
|
27
|
+
export interface SocketMessageAck {
|
|
28
|
+
type: 'socket-ack';
|
|
29
|
+
payload: { envelopeId: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sent by the primary tab to notify the secondary tabs that the primary tab is still alive.
|
|
34
|
+
* @param connected - true if the primary tab is connected to the server, false otherwise.
|
|
35
|
+
*/
|
|
36
|
+
export interface Heartbeat {
|
|
37
|
+
type: 'primary-present';
|
|
38
|
+
payload: { connected: boolean };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Used to tell the primary tab to connect/disconnect socket.
|
|
43
|
+
* @param connected - true to connect, false to disconnect.
|
|
44
|
+
*/
|
|
45
|
+
export interface ConnectionToggle {
|
|
46
|
+
type: 'connection-toggle';
|
|
47
|
+
payload: { connected: boolean };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sent by tab when it is opened to notify other tabs. If another tab is present,
|
|
52
|
+
* it will respond with {@link Heartbeat}.
|
|
53
|
+
*/
|
|
54
|
+
export interface ClientAnnounce {
|
|
55
|
+
type: 'announce';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sent by the client to let other know that it is claiming the primary role.
|
|
60
|
+
*/
|
|
61
|
+
export interface PrimaryClaim {
|
|
62
|
+
type: 'primary-claim';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Sent by the client to let other know that it is releasing the primary role.
|
|
67
|
+
* This usually happens when the tab is closed.
|
|
68
|
+
*/
|
|
69
|
+
export interface PrimaryRelease {
|
|
70
|
+
type: 'primary-release';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sent by the tab where user has auth state changed.
|
|
75
|
+
* @param authenticated - true if the user is authenticated, false otherwise.
|
|
76
|
+
*/
|
|
77
|
+
export interface AuthenticationChange {
|
|
78
|
+
type: 'authentication';
|
|
79
|
+
payload: { authenticated: boolean };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type ChannelMessage =
|
|
83
|
+
| AuthenticationChange
|
|
84
|
+
| SocketEventReceived
|
|
85
|
+
| SendSocketMessage
|
|
86
|
+
| SocketMessageAck
|
|
87
|
+
| ConnectionToggle
|
|
88
|
+
| Heartbeat
|
|
89
|
+
| ClientAnnounce
|
|
90
|
+
| PrimaryClaim
|
|
91
|
+
| PrimaryRelease;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Envelope for messages sent between tabs.
|
|
95
|
+
* @param id - Unique message ID.
|
|
96
|
+
* @param senderId - ID of the tab that sent the message.
|
|
97
|
+
* @param recipientId - ID of the tab that should receive the message. If not specified, the message will be broadcasted to all tabs.
|
|
98
|
+
* @param timestamp - Timestamp of the message.
|
|
99
|
+
* @param payload - Message payload.
|
|
100
|
+
*/
|
|
101
|
+
export interface MessageEnvelope {
|
|
102
|
+
id: string;
|
|
103
|
+
senderId: string;
|
|
104
|
+
recipientId?: string;
|
|
105
|
+
timestamp: number;
|
|
106
|
+
payload: ChannelMessage;
|
|
107
|
+
}
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
package/src/ApiClient/index.ts
CHANGED
|
@@ -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/BrowserWebSocketClient';
|
|
12
14
|
|
|
13
15
|
const WS_RECONNECT_ATTEMPTS = 5;
|
|
14
16
|
|
|
@@ -48,7 +50,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
48
50
|
readonly appId: string;
|
|
49
51
|
readonly logger: Logger;
|
|
50
52
|
private _rest: RestClient;
|
|
51
|
-
private _socket:
|
|
53
|
+
private _socket: IWebSocketClient;
|
|
52
54
|
private _auth: AuthManager;
|
|
53
55
|
private _reconnectAttempts = WS_RECONNECT_ATTEMPTS;
|
|
54
56
|
private _disableSocket: boolean = false;
|
|
@@ -68,7 +70,12 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
68
70
|
this._auth =
|
|
69
71
|
authType === 'token' ? new TokenAuthManager(baseUrl, logger) : new CookieAuthManager(logger);
|
|
70
72
|
this._rest = new RestClient(baseUrl, this._auth, logger);
|
|
71
|
-
|
|
73
|
+
// Use coordinated WebSocket client in browser, regular in Node.js
|
|
74
|
+
if (this._auth instanceof TokenAuthManager || isNodejs) {
|
|
75
|
+
this._socket = new WebSocketClient(socketUrl, this._auth, appId, networkType, logger);
|
|
76
|
+
} else {
|
|
77
|
+
this._socket = new BrowserWebSocketClient(socketUrl, this._auth, appId, networkType, logger);
|
|
78
|
+
}
|
|
72
79
|
this._disableSocket = disableSocket;
|
|
73
80
|
this._auth.on('updated', this.handleAuthUpdated.bind(this));
|
|
74
81
|
this._socket.on('connected', this.handleSocketConnect.bind(this));
|
|
@@ -83,7 +90,7 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
83
90
|
return this._auth;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
get socket():
|
|
93
|
+
get socket(): IWebSocketClient {
|
|
87
94
|
return this._socket;
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -101,14 +108,26 @@ class ApiClient extends TypedEventEmitter<ApiClientEvents> {
|
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
handleSocketDisconnect(data: ServerDisconnectData) {
|
|
111
|
+
// If user is not authenticated, we don't need to reconnect
|
|
112
|
+
if (!this.auth.isAuthenticated) {
|
|
113
|
+
this.emit('disconnected', data);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
104
116
|
if (!data.code || isNotRecoverable(data.code)) {
|
|
117
|
+
// If this is browser, another tab is probably claiming the connection, so we don't need to reconnect
|
|
118
|
+
if (
|
|
119
|
+
this._socket instanceof BrowserWebSocketClient &&
|
|
120
|
+
data.code === ErrorCode.SWITCH_CONNECTION
|
|
121
|
+
) {
|
|
122
|
+
this.logger.debug('Switching network connection, not reconnecting');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
105
125
|
this.auth.clear();
|
|
106
126
|
this.emit('disconnected', data);
|
|
107
127
|
this.logger.error('Not recoverable socket error', data);
|
|
108
128
|
return;
|
|
109
129
|
}
|
|
110
130
|
if (this._reconnectAttempts <= 0) {
|
|
111
|
-
this.auth.clear();
|
|
112
131
|
this.emit('disconnected', data);
|
|
113
132
|
this._reconnectAttempts = WS_RECONNECT_ATTEMPTS;
|
|
114
133
|
return;
|
package/src/Projects/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
EnhancementStrength,
|
|
5
5
|
EstimateRequest,
|
|
6
6
|
ImageUrlParams,
|
|
7
|
+
CostEstimation,
|
|
7
8
|
ProjectParams,
|
|
8
9
|
SizePreset,
|
|
9
10
|
SupportedModel
|
|
@@ -518,7 +519,7 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
518
519
|
guidance,
|
|
519
520
|
sampler,
|
|
520
521
|
contextImages
|
|
521
|
-
}: EstimateRequest) {
|
|
522
|
+
}: EstimateRequest): Promise<CostEstimation> {
|
|
522
523
|
let apiVersion = 2;
|
|
523
524
|
const pathParams = [
|
|
524
525
|
tokenType || 'sogni',
|
|
@@ -553,7 +554,9 @@ class ProjectsApi extends ApiGroup<ProjectApiEvents> {
|
|
|
553
554
|
);
|
|
554
555
|
return {
|
|
555
556
|
token: r.quote.project.costInToken,
|
|
556
|
-
usd: r.quote.project.costInUSD
|
|
557
|
+
usd: r.quote.project.costInUSD,
|
|
558
|
+
spark: r.quote.project.costInSpark,
|
|
559
|
+
sogni: r.quote.project.costInSogni
|
|
557
560
|
};
|
|
558
561
|
}
|
|
559
562
|
|