@sogni-ai/sogni-client 4.0.0-alpha.2 → 4.0.0-alpha.21
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 +142 -0
- package/README.md +38 -2
- package/dist/Account/index.d.ts +5 -2
- package/dist/Account/index.js +20 -7
- 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 +207 -0
- package/dist/ApiClient/WebSocketClient/BrowserWebSocketClient/index.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/index.js +2 -2
- 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 +22 -3
- package/dist/ApiClient/index.js.map +1 -1
- package/dist/Projects/Job.d.ts +1 -1
- package/dist/Projects/createJobRequestMessage.js +3 -3
- package/dist/Projects/createJobRequestMessage.js.map +1 -1
- package/dist/Projects/index.d.ts +3 -9
- package/dist/Projects/index.js +7 -4
- 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/index.d.ts +21 -6
- package/dist/Projects/types/index.js +5 -0
- package/dist/Projects/types/index.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/DataEntity.js +4 -2
- package/dist/lib/DataEntity.js.map +1 -1
- package/dist/lib/validation.d.ts +2 -0
- package/dist/lib/validation.js +28 -0
- package/dist/lib/validation.js.map +1 -1
- package/package.json +4 -4
- package/src/Account/index.ts +22 -13
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/ChannelCoordinator.ts +426 -0
- package/src/ApiClient/WebSocketClient/BrowserWebSocketClient/index.ts +241 -0
- package/src/ApiClient/WebSocketClient/events.ts +2 -0
- package/src/ApiClient/WebSocketClient/index.ts +4 -4
- package/src/ApiClient/WebSocketClient/types.ts +16 -0
- package/src/ApiClient/index.ts +29 -7
- package/src/Projects/createJobRequestMessage.ts +11 -5
- package/src/Projects/index.ts +9 -5
- 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/index.ts +22 -12
- package/src/index.ts +23 -8
- package/src/lib/DataEntity.ts +4 -2
- package/src/lib/validation.ts +33 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataEntity.js","sourceRoot":"","sources":["../../src/lib/DataEntity.ts"],"names":[],"mappings":";;;;;AAAA,mCAAmC;AACnC,4EAAoD;AASpD,MAAe,UAAqD,SAAQ,2BAAoB;IAK9F,YAAY,IAAO;QACjB,KAAK,EAAE,CAAC;QAHA,gBAAW,GAAS,IAAI,IAAI,EAAE,CAAC;QAIvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,KAAiB;QACvB,YAAY;QACZ,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QACtF,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"DataEntity.js","sourceRoot":"","sources":["../../src/lib/DataEntity.ts"],"names":[],"mappings":";;;;;AAAA,mCAAmC;AACnC,4EAAoD;AASpD,MAAe,UAAqD,SAAQ,2BAAoB;IAK9F,YAAY,IAAO;QACjB,KAAK,EAAE,CAAC;QAHA,gBAAW,GAAS,IAAI,IAAI,EAAE,CAAC;QAIvC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,KAAiB;QACvB,YAAY;QACZ,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QACtF,IAAI,CAAC,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC;QAC9B,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,mCAAQ,IAAI,CAAC,IAAI,GAAK,KAAK,CAAE,CAAC;YACvC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED;;OAEG;IACH,MAAM;QACJ,OAAO,IAAA,kBAAS,EAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;CACF;AAED,kBAAe,UAAU,CAAC"}
|
package/dist/lib/validation.d.ts
CHANGED
|
@@ -6,4 +6,6 @@ interface NumberValidationOptions {
|
|
|
6
6
|
defaultValue?: number;
|
|
7
7
|
}
|
|
8
8
|
export declare function validateNumber(value: any, { min, max, propertyName, defaultValue }?: NumberValidationOptions): number;
|
|
9
|
+
export declare function validateSampler(value?: string): string | null;
|
|
10
|
+
export declare function validateScheduler(value?: string): string | null;
|
|
9
11
|
export {};
|
package/dist/lib/validation.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateCustomImageSize = validateCustomImageSize;
|
|
4
4
|
exports.validateNumber = validateNumber;
|
|
5
|
+
exports.validateSampler = validateSampler;
|
|
6
|
+
exports.validateScheduler = validateScheduler;
|
|
7
|
+
const SamplerParams_1 = require("../Projects/types/SamplerParams");
|
|
8
|
+
const SchedulerParams_1 = require("../Projects/types/SchedulerParams");
|
|
5
9
|
function validateCustomImageSize(value) {
|
|
6
10
|
return validateNumber(value, { min: 256, max: 2048, propertyName: 'Width and height' });
|
|
7
11
|
}
|
|
@@ -31,4 +35,28 @@ function validateNumber(value, { min, max, propertyName, defaultValue } = {}) {
|
|
|
31
35
|
}
|
|
32
36
|
return number;
|
|
33
37
|
}
|
|
38
|
+
function validateSampler(value) {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if ((0, SamplerParams_1.isRawSampler)(value)) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if ((0, SamplerParams_1.isSampler)(value)) {
|
|
46
|
+
return SamplerParams_1.SupportedSamplers[value];
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Invalid sampler: ${value}. Supported options: ${Object.keys(SamplerParams_1.SupportedSamplers).join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
function validateScheduler(value) {
|
|
51
|
+
if (!value) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if ((0, SamplerParams_1.isRawSampler)(value)) {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if ((0, SchedulerParams_1.isScheduler)(value)) {
|
|
58
|
+
return SchedulerParams_1.SupportedSchedulers[value];
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Invalid scheduler: ${value}. Supported options: ${Object.keys(SchedulerParams_1.SupportedSchedulers).join(', ')}`);
|
|
61
|
+
}
|
|
34
62
|
//# sourceMappingURL=validation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/lib/validation.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/lib/validation.ts"],"names":[],"mappings":";;AAGA,0DAEC;AASD,wCA4BC;AAED,0CAaC;AAED,8CAaC;AAxED,mEAA6F;AAC7F,uEAAqF;AAErF,SAAgB,uBAAuB,CAAC,KAAU;IAChD,OAAO,cAAc,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,CAAC,CAAC;AAC1F,CAAC;AASD,SAAgB,cAAc,CAC5B,KAAU,EACV,EAAE,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,YAAY,KAA8B,EAAE;IAEtE,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,MAAM,eAAe,GAAG,YAAY,KAAK,SAAS,CAAC;IACnD,IAAI,YAAY,EAAE,CAAC;QACjB,YAAY,GAAG,OAAO,CAAC;IACzB,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QAClB,IAAI,eAAe,EAAE,CAAC;YACpB,OAAO,YAAY,CAAC;QACtB,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,YAAY,0BAA0B,KAAK,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,GAAG,KAAK,SAAS,IAAI,MAAM,GAAG,GAAG,EAAE,CAAC;QACtC,IAAI,eAAe,EAAE,CAAC;YACpB,OAAO,YAAY,CAAC;QACtB,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,YAAY,0BAA0B,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,GAAG,KAAK,SAAS,IAAI,MAAM,GAAG,GAAG,EAAE,CAAC;QACtC,IAAI,eAAe,EAAE,CAAC;YACpB,OAAO,YAAY,CAAC;QACtB,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,GAAG,YAAY,0BAA0B,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAgB,eAAe,CAAC,KAAc;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,IAAA,4BAAY,EAAC,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,IAAA,yBAAS,EAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,iCAAiB,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IACD,MAAM,IAAI,KAAK,CACb,oBAAoB,KAAK,wBAAwB,MAAM,CAAC,IAAI,CAAC,iCAAiB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC7F,CAAC;AACJ,CAAC;AAED,SAAgB,iBAAiB,CAAC,KAAc;IAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,IAAA,4BAAY,EAAC,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,IAAA,6BAAW,EAAC,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,qCAAmB,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IACD,MAAM,IAAI,KAAK,CACb,sBAAsB,KAAK,wBAAwB,MAAM,CAAC,IAAI,CAAC,qCAAmB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACjG,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "4.0.0-alpha.
|
|
6
|
+
"version": "4.0.0-alpha.21",
|
|
7
7
|
"description": "Sogni Supernet Client",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"homepage": "https://github.com/Sogni-AI/sogni-client#readme",
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@lukeed/uuid": "^2.0.1",
|
|
40
|
+
"@types/js-cookie": "^3.0.6",
|
|
41
|
+
"@types/lodash": "^4.17.13",
|
|
42
|
+
"@types/ws": "^8.5.13",
|
|
40
43
|
"ethers": "^6.13.4",
|
|
41
44
|
"isomorphic-ws": "^5.0.0",
|
|
42
45
|
"js-cookie": "^3.0.5",
|
|
@@ -47,9 +50,6 @@
|
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@semantic-release/changelog": "^6.0.3",
|
|
49
52
|
"@semantic-release/git": "^10.0.1",
|
|
50
|
-
"@types/js-cookie": "^3.0.6",
|
|
51
|
-
"@types/lodash": "^4.17.13",
|
|
52
|
-
"@types/ws": "^8.5.13",
|
|
53
53
|
"prettier": "^3.3.3",
|
|
54
54
|
"rimraf": "^6.0.1",
|
|
55
55
|
"semantic-release": "^24.2.1",
|
package/src/Account/index.ts
CHANGED
|
@@ -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
|
|
|
@@ -110,14 +121,10 @@ class AccountApi extends ApiGroup {
|
|
|
110
121
|
* Create a new account with the given username, email, and password.
|
|
111
122
|
* @internal
|
|
112
123
|
*/
|
|
113
|
-
async create(
|
|
114
|
-
username,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
subscribe,
|
|
118
|
-
turnstileToken,
|
|
119
|
-
referralCode
|
|
120
|
-
}: AccountCreateParams): Promise<AccountCreateData> {
|
|
124
|
+
async create(
|
|
125
|
+
{ username, email, password, subscribe, turnstileToken, referralCode }: AccountCreateParams,
|
|
126
|
+
rememberMe = false
|
|
127
|
+
): Promise<AccountCreateData> {
|
|
121
128
|
const wallet = this.getWallet(username, password);
|
|
122
129
|
const nonce = await this.getNonce(wallet.address);
|
|
123
130
|
const payload = {
|
|
@@ -132,7 +139,8 @@ class AccountApi extends ApiGroup {
|
|
|
132
139
|
const res = await this.client.rest.post<ApiResponse<AccountCreateData>>('/v1/account/create', {
|
|
133
140
|
...payload,
|
|
134
141
|
referralCode,
|
|
135
|
-
signature
|
|
142
|
+
signature,
|
|
143
|
+
rememberMe
|
|
136
144
|
});
|
|
137
145
|
const auth = this.client.auth;
|
|
138
146
|
if (auth instanceof TokenAuthManager) {
|
|
@@ -140,7 +148,6 @@ class AccountApi extends ApiGroup {
|
|
|
140
148
|
} else {
|
|
141
149
|
await auth.authenticate();
|
|
142
150
|
}
|
|
143
|
-
await this.me();
|
|
144
151
|
return res.data;
|
|
145
152
|
}
|
|
146
153
|
|
|
@@ -155,8 +162,10 @@ class AccountApi extends ApiGroup {
|
|
|
155
162
|
*
|
|
156
163
|
* @param username
|
|
157
164
|
* @param password
|
|
165
|
+
* @param rememberMe - Whether to establish a long-lived session. Default is false. Only
|
|
166
|
+
* applicable for cookie-based authentication.
|
|
158
167
|
*/
|
|
159
|
-
async login(username: string, password: string): Promise<LoginData> {
|
|
168
|
+
async login(username: string, password: string, rememberMe = false): Promise<LoginData> {
|
|
160
169
|
const wallet = this.getWallet(username, password);
|
|
161
170
|
const nonce = await this.getNonce(wallet.address);
|
|
162
171
|
const signature = await this.eip712.signTypedData(wallet, 'authentication', {
|
|
@@ -165,7 +174,8 @@ class AccountApi extends ApiGroup {
|
|
|
165
174
|
});
|
|
166
175
|
const res = await this.client.rest.post<ApiResponse<LoginData>>('/v1/account/login', {
|
|
167
176
|
walletAddress: wallet.address,
|
|
168
|
-
signature
|
|
177
|
+
signature,
|
|
178
|
+
rememberMe
|
|
169
179
|
});
|
|
170
180
|
const auth = this.client.auth;
|
|
171
181
|
if (auth instanceof TokenAuthManager) {
|
|
@@ -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
|
|
|
@@ -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;
|