@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.
- package/CHANGELOG.md +345 -0
- package/README.md +295 -58
- package/dist/Account/index.d.ts +18 -16
- package/dist/Account/index.js +42 -21
- 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 +12 -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 +44 -4
- package/dist/Projects/Job.js +83 -16
- package/dist/Projects/Job.js.map +1 -1
- package/dist/Projects/Project.d.ts +18 -0
- package/dist/Projects/Project.js +38 -10
- package/dist/Projects/Project.js.map +1 -1
- package/dist/Projects/createJobRequestMessage.d.ts +2 -1
- package/dist/Projects/createJobRequestMessage.js +173 -14
- package/dist/Projects/createJobRequestMessage.js.map +1 -1
- package/dist/Projects/index.d.ts +114 -11
- package/dist/Projects/index.js +504 -47
- package/dist/Projects/index.js.map +1 -1
- package/dist/Projects/types/ComfySamplerParams.d.ts +0 -0
- package/dist/Projects/types/ComfySamplerParams.js +2 -0
- package/dist/Projects/types/ComfySamplerParams.js.map +1 -0
- package/dist/Projects/types/EstimationResponse.d.ts +2 -0
- package/dist/Projects/types/ModelOptions.d.ts +31 -0
- package/dist/Projects/types/ModelOptions.js +56 -0
- package/dist/Projects/types/ModelOptions.js.map +1 -0
- package/dist/Projects/types/ModelTiersRaw.d.ts +67 -0
- package/dist/Projects/types/ModelTiersRaw.js +15 -0
- package/dist/Projects/types/ModelTiersRaw.js.map +1 -0
- package/dist/Projects/types/events.d.ts +5 -1
- package/dist/Projects/types/index.d.ts +201 -42
- package/dist/Projects/types/index.js +8 -0
- package/dist/Projects/types/index.js.map +1 -1
- package/dist/Projects/utils/index.d.ts +20 -0
- package/dist/Projects/utils/index.js +82 -0
- package/dist/Projects/utils/index.js.map +1 -0
- package/dist/Projects/utils/samplers.d.ts +6 -0
- package/dist/Projects/utils/samplers.js +39 -0
- package/dist/Projects/utils/samplers.js.map +1 -0
- package/dist/Projects/utils/scheduler.d.ts +6 -0
- package/dist/Projects/utils/scheduler.js +30 -0
- package/dist/Projects/utils/scheduler.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +8 -3
- 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/RestClient.js +15 -2
- package/dist/lib/RestClient.js.map +1 -1
- package/dist/lib/{utils.js → utils/index.js} +1 -1
- package/dist/lib/utils/index.js.map +1 -0
- package/dist/lib/validation.d.ts +31 -2
- package/dist/lib/validation.js +80 -13
- package/dist/lib/validation.js.map +1 -1
- package/package.json +4 -4
- package/src/Account/index.ts +39 -20
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.ts +426 -0
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +237 -0
- package/src/ApiClient/WebSocketClient/events.ts +14 -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 +97 -16
- package/src/Projects/Project.ts +46 -13
- package/src/Projects/createJobRequestMessage.ts +234 -34
- package/src/Projects/index.ts +533 -51
- package/src/Projects/types/ComfySamplerParams.ts +0 -0
- package/src/Projects/types/EstimationResponse.ts +2 -0
- package/src/Projects/types/ModelOptions.ts +92 -0
- package/src/Projects/types/ModelTiersRaw.ts +86 -0
- package/src/Projects/types/events.ts +6 -0
- package/src/Projects/types/index.ts +235 -45
- package/src/Projects/utils/index.ts +77 -0
- package/src/Projects/utils/samplers.ts +36 -0
- package/src/Projects/utils/scheduler.ts +27 -0
- package/src/index.ts +36 -9
- package/src/lib/AuthManager/TokenAuthManager.ts +0 -2
- package/src/lib/DataEntity.ts +4 -2
- package/src/lib/RestClient.ts +16 -2
- package/src/lib/validation.ts +90 -17
- package/dist/Projects/types/SamplerParams.d.ts +0 -15
- package/dist/Projects/types/SamplerParams.js +0 -21
- package/dist/Projects/types/SamplerParams.js.map +0 -1
- package/dist/Projects/types/SchedulerParams.d.ts +0 -13
- package/dist/Projects/types/SchedulerParams.js +0 -19
- package/dist/Projects/types/SchedulerParams.js.map +0 -1
- package/dist/Projects/utils.d.ts +0 -2
- package/dist/Projects/utils.js +0 -14
- package/dist/Projects/utils.js.map +0 -1
- package/dist/lib/utils.js.map +0 -1
- package/src/Projects/types/SamplerParams.ts +0 -19
- package/src/Projects/types/SchedulerParams.ts +0 -17
- package/src/Projects/utils.ts +0 -12
- /package/dist/lib/{utils.d.ts → utils/index.d.ts} +0 -0
- /package/src/lib/{utils.ts → utils/index.ts} +0 -0
package/src/Account/index.ts
CHANGED
|
@@ -29,11 +29,11 @@ enum ErrorCode {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Account API methods that let you interact with the user's account.
|
|
32
|
-
* Can be accessed via `
|
|
32
|
+
* Can be accessed via `sogni.account`. Look for more samples below.
|
|
33
33
|
*
|
|
34
34
|
* @example Retrieve the current account balance
|
|
35
35
|
* ```typescript
|
|
36
|
-
* const balance = await
|
|
36
|
+
* const balance = await sogni.account.refreshBalance();
|
|
37
37
|
* console.log(balance);
|
|
38
38
|
* ```
|
|
39
39
|
*
|
|
@@ -43,7 +43,12 @@ class AccountApi extends ApiGroup {
|
|
|
43
43
|
|
|
44
44
|
constructor(config: ApiConfig) {
|
|
45
45
|
super(config);
|
|
46
|
+
this.currentAccount._update({
|
|
47
|
+
networkStatus: this.client.socket.isConnected ? 'connected' : 'disconnected',
|
|
48
|
+
network: this.client.socket.supernetType
|
|
49
|
+
});
|
|
46
50
|
this.client.socket.on('balanceUpdate', this.handleBalanceUpdate.bind(this));
|
|
51
|
+
this.client.socket.on('changeNetwork', this.handleChangeNetwork.bind(this));
|
|
47
52
|
this.client.on('connected', this.handleServerConnected.bind(this));
|
|
48
53
|
this.client.on('disconnected', this.handleServerDisconnected.bind(this));
|
|
49
54
|
this.client.auth.on('updated', this.handleAuthUpdated.bind(this));
|
|
@@ -53,6 +58,10 @@ class AccountApi extends ApiGroup {
|
|
|
53
58
|
this.currentAccount._update({ balance: data });
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
private handleChangeNetwork({ network }: { network: SupernetType }) {
|
|
62
|
+
this.currentAccount._update({ network, networkStatus: 'connected' });
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
private handleServerConnected({ network }: { network: SupernetType }) {
|
|
57
66
|
this.currentAccount._update({
|
|
58
67
|
networkStatus: 'connected',
|
|
@@ -70,6 +79,8 @@ class AccountApi extends ApiGroup {
|
|
|
70
79
|
private handleAuthUpdated(isAuthenticated: boolean) {
|
|
71
80
|
if (!isAuthenticated) {
|
|
72
81
|
this.currentAccount._clear();
|
|
82
|
+
} else {
|
|
83
|
+
this.me();
|
|
73
84
|
}
|
|
74
85
|
}
|
|
75
86
|
|
|
@@ -92,7 +103,7 @@ class AccountApi extends ApiGroup {
|
|
|
92
103
|
*
|
|
93
104
|
* @example Create a wallet from username and password
|
|
94
105
|
* ```typescript
|
|
95
|
-
* const wallet =
|
|
106
|
+
* const wallet = sogni.account.getWallet('username', 'password');
|
|
96
107
|
* console.log(wallet.address);
|
|
97
108
|
* ```
|
|
98
109
|
*
|
|
@@ -137,7 +148,6 @@ class AccountApi extends ApiGroup {
|
|
|
137
148
|
} else {
|
|
138
149
|
await auth.authenticate();
|
|
139
150
|
}
|
|
140
|
-
await this.me();
|
|
141
151
|
return res.data;
|
|
142
152
|
}
|
|
143
153
|
|
|
@@ -146,7 +156,7 @@ class AccountApi extends ApiGroup {
|
|
|
146
156
|
*
|
|
147
157
|
* @example Login with username and password
|
|
148
158
|
* ```typescript
|
|
149
|
-
* await
|
|
159
|
+
* await sogni.account.login('username', 'password');
|
|
150
160
|
* console.log('Logged in');
|
|
151
161
|
* ```
|
|
152
162
|
*
|
|
@@ -173,7 +183,6 @@ class AccountApi extends ApiGroup {
|
|
|
173
183
|
} else {
|
|
174
184
|
await auth.authenticate();
|
|
175
185
|
}
|
|
176
|
-
await this.me();
|
|
177
186
|
return res.data;
|
|
178
187
|
}
|
|
179
188
|
|
|
@@ -182,12 +191,20 @@ class AccountApi extends ApiGroup {
|
|
|
182
191
|
*
|
|
183
192
|
* @example Logout the user
|
|
184
193
|
* ```typescript
|
|
185
|
-
* await
|
|
194
|
+
* await sogni.account.logout();
|
|
186
195
|
* console.log('Logged out');
|
|
187
196
|
* ```
|
|
188
197
|
*/
|
|
189
198
|
async logout(): Promise<void> {
|
|
190
|
-
|
|
199
|
+
try {
|
|
200
|
+
await this.client.rest.post('/v1/account/logout');
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (e instanceof ApiError && e.status === 401) {
|
|
203
|
+
this.client.logger.warn('Failed to logout, probably already logged out');
|
|
204
|
+
} else {
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
191
208
|
this.client.auth.clear();
|
|
192
209
|
}
|
|
193
210
|
|
|
@@ -200,7 +217,7 @@ class AccountApi extends ApiGroup {
|
|
|
200
217
|
*
|
|
201
218
|
* @example Refresh user account balance
|
|
202
219
|
* ```typescript
|
|
203
|
-
* const balance = await
|
|
220
|
+
* const balance = await sogni.account.refreshBalance();
|
|
204
221
|
* console.log(balance);
|
|
205
222
|
* ```
|
|
206
223
|
*/
|
|
@@ -216,7 +233,7 @@ class AccountApi extends ApiGroup {
|
|
|
216
233
|
*
|
|
217
234
|
* @example Get the account balance of the current user
|
|
218
235
|
* ```typescript
|
|
219
|
-
* const balance = await
|
|
236
|
+
* const balance = await sogni.account.accountBalance();
|
|
220
237
|
* console.log(balance);
|
|
221
238
|
* ```
|
|
222
239
|
*/
|
|
@@ -232,19 +249,21 @@ class AccountApi extends ApiGroup {
|
|
|
232
249
|
*
|
|
233
250
|
* @example Get the balance of the wallet address
|
|
234
251
|
* ```typescript
|
|
235
|
-
* const address =
|
|
236
|
-
* const balance = await
|
|
252
|
+
* const address = sogni.account.currentAccount.walletAddress;
|
|
253
|
+
* const balance = await sogni.account.walletBalance(address);
|
|
237
254
|
* console.log(balance);
|
|
238
255
|
* // { token: '100.000000', ether: '0.000000' }
|
|
239
256
|
* ```
|
|
240
257
|
*
|
|
241
258
|
* @param walletAddress
|
|
259
|
+
* @param provider - blockchain provider, 'base' or 'etherlink' defaults to 'base'
|
|
242
260
|
*/
|
|
243
|
-
async walletBalance(walletAddress: string) {
|
|
261
|
+
async walletBalance(walletAddress: string, provider: 'base' | 'etherlink' = 'base') {
|
|
244
262
|
const res = await this.client.rest.get<
|
|
245
263
|
ApiResponse<{ sogni: string; spark: string; ether: string }>
|
|
246
264
|
>('/v2/wallet/balance', {
|
|
247
|
-
walletAddress
|
|
265
|
+
walletAddress,
|
|
266
|
+
provider
|
|
248
267
|
});
|
|
249
268
|
return res.data;
|
|
250
269
|
}
|
|
@@ -287,9 +306,9 @@ class AccountApi extends ApiGroup {
|
|
|
287
306
|
*
|
|
288
307
|
* @example Switch to the fast network
|
|
289
308
|
* ```typescript
|
|
290
|
-
* await
|
|
309
|
+
* await sogni.account.switchNetwork('fast');
|
|
291
310
|
* console.log('Switched to the fast network, now lets wait until we get list of models');
|
|
292
|
-
* await
|
|
311
|
+
* await sogni.projects.waitForModels();
|
|
293
312
|
* ```
|
|
294
313
|
* @param network - Network type to switch to
|
|
295
314
|
*/
|
|
@@ -311,10 +330,10 @@ class AccountApi extends ApiGroup {
|
|
|
311
330
|
*
|
|
312
331
|
* @example Get the transaction history
|
|
313
332
|
* ```typescript
|
|
314
|
-
* const { entries, next } = await
|
|
333
|
+
* const { entries, next } = await sogni.account.transactionHistory({
|
|
315
334
|
* status: 'completed',
|
|
316
335
|
* limit: 10,
|
|
317
|
-
* address:
|
|
336
|
+
* address: sogni.account.currentAccount.walletAddress
|
|
318
337
|
* });
|
|
319
338
|
* ```
|
|
320
339
|
*
|
|
@@ -420,7 +439,7 @@ class AccountApi extends ApiGroup {
|
|
|
420
439
|
* Withdraw funds from the current account to wallet.
|
|
421
440
|
* @example withdraw to current wallet address
|
|
422
441
|
* ```typescript
|
|
423
|
-
* await
|
|
442
|
+
* await sogni.account.withdraw('your-account-password', 100);
|
|
424
443
|
* ```
|
|
425
444
|
*
|
|
426
445
|
* @param password - account password
|
|
@@ -452,7 +471,7 @@ class AccountApi extends ApiGroup {
|
|
|
452
471
|
* Deposit tokens from wallet to account
|
|
453
472
|
* @example withdraw to current wallet address
|
|
454
473
|
* ```typescript
|
|
455
|
-
* await
|
|
474
|
+
* await sogni.account.deposit('your-account-password', 100);
|
|
456
475
|
* ```
|
|
457
476
|
*
|
|
458
477
|
* @param password - account password
|
|
@@ -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;
|