stream-chat 4.4.3 → 4.5.0-beta.0
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/README.md +4 -13
- package/dist/browser.es.js +1571 -1071
- package/dist/browser.es.js.map +1 -1
- package/dist/browser.full-bundle.min.js +1 -1
- package/dist/browser.full-bundle.min.js.map +1 -1
- package/dist/browser.js +1571 -1071
- package/dist/browser.js.map +1 -1
- package/dist/index.es.js +1571 -1071
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1571 -1071
- package/dist/index.js.map +1 -1
- package/dist/types/base64.d.ts.map +1 -1
- package/dist/types/channel.d.ts +19 -15
- package/dist/types/channel.d.ts.map +1 -1
- package/dist/types/channel_state.d.ts +2 -2
- package/dist/types/channel_state.d.ts.map +1 -1
- package/dist/types/client.d.ts +18 -39
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/client_state.d.ts +2 -2
- package/dist/types/client_state.d.ts.map +1 -1
- package/dist/types/connection.d.ts +14 -49
- package/dist/types/connection.d.ts.map +1 -1
- package/dist/types/connection_fallback.d.ts +42 -0
- package/dist/types/connection_fallback.d.ts.map +1 -0
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/insights.d.ts +6 -6
- package/dist/types/insights.d.ts.map +1 -1
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/types/signing.d.ts +3 -3
- package/dist/types/signing.d.ts.map +1 -1
- package/dist/types/token_manager.d.ts +2 -2
- package/dist/types/token_manager.d.ts.map +1 -1
- package/dist/types/types.d.ts +94 -80
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +12 -2
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/base64.ts +1 -4
- package/src/channel.ts +133 -461
- package/src/channel_state.ts +31 -158
- package/src/client.ts +277 -674
- package/src/client_state.ts +2 -2
- package/src/connection.ts +143 -394
- package/src/connection_fallback.ts +205 -0
- package/src/errors.ts +58 -0
- package/src/insights.ts +15 -23
- package/src/permissions.ts +3 -24
- package/src/signing.ts +6 -17
- package/src/token_manager.ts +6 -18
- package/src/types.ts +268 -504
- package/src/utils.ts +39 -19
- package/CHANGELOG.md +0 -844
package/src/connection.ts
CHANGED
|
@@ -1,55 +1,23 @@
|
|
|
1
1
|
import WebSocket from 'isomorphic-ws';
|
|
2
|
-
import { chatCodes, convertErrorToJson, sleep, retryInterval, randomId } from './utils';
|
|
3
|
-
import { TokenManager } from './token_manager';
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Logger,
|
|
17
|
-
UnknownType,
|
|
18
|
-
UserResponse,
|
|
19
|
-
} from './types';
|
|
3
|
+
chatCodes,
|
|
4
|
+
convertErrorToJson,
|
|
5
|
+
sleep,
|
|
6
|
+
retryInterval,
|
|
7
|
+
randomId,
|
|
8
|
+
removeConnectionEventListeners,
|
|
9
|
+
addConnectionEventListeners,
|
|
10
|
+
} from './utils';
|
|
11
|
+
import { buildWsFatalInsight, buildWsSuccessAfterFailureInsight, postInsights } from './insights';
|
|
12
|
+
import { ConnectAPIResponse, ConnectionOpen, LiteralStringForUnion, UR, LogLevel } from './types';
|
|
13
|
+
import { StreamChat } from './client';
|
|
20
14
|
|
|
21
15
|
// Type guards to check WebSocket error type
|
|
22
|
-
const isCloseEvent = (
|
|
23
|
-
res
|
|
24
|
-
): res is WebSocket.CloseEvent => (res as WebSocket.CloseEvent).code !== undefined;
|
|
25
|
-
|
|
26
|
-
const isErrorEvent = (
|
|
27
|
-
res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
|
|
28
|
-
): res is WebSocket.ErrorEvent => (res as WebSocket.ErrorEvent).error !== undefined;
|
|
16
|
+
const isCloseEvent = (res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent): res is WebSocket.CloseEvent =>
|
|
17
|
+
(res as WebSocket.CloseEvent).code !== undefined;
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
CommandType extends string = LiteralStringForUnion,
|
|
33
|
-
UserType extends UnknownType = UnknownType
|
|
34
|
-
> = {
|
|
35
|
-
apiKey: string;
|
|
36
|
-
authType: 'anonymous' | 'jwt';
|
|
37
|
-
clientID: string;
|
|
38
|
-
eventCallback: (event: ConnectionChangeEvent) => void;
|
|
39
|
-
insightMetrics: InsightMetrics;
|
|
40
|
-
logger: Logger | (() => void);
|
|
41
|
-
messageCallback: (messageEvent: WebSocket.MessageEvent) => void;
|
|
42
|
-
recoverCallback: (
|
|
43
|
-
open?: ConnectionOpen<ChannelType, CommandType, UserType>,
|
|
44
|
-
) => Promise<void>;
|
|
45
|
-
tokenManager: TokenManager<UserType>;
|
|
46
|
-
user: UserResponse<UserType>;
|
|
47
|
-
userAgent: string;
|
|
48
|
-
userID: string;
|
|
49
|
-
wsBaseURL: string;
|
|
50
|
-
device?: BaseDeviceFields;
|
|
51
|
-
enableInsights?: boolean;
|
|
52
|
-
};
|
|
19
|
+
const isErrorEvent = (res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent): res is WebSocket.ErrorEvent =>
|
|
20
|
+
(res as WebSocket.ErrorEvent).error !== undefined;
|
|
53
21
|
|
|
54
22
|
/**
|
|
55
23
|
* StableWSConnection - A WS connection that reconnects upon failure.
|
|
@@ -69,90 +37,59 @@ type Constructor<
|
|
|
69
37
|
* - if the servers fails to publish a message to the client, the WS connection is destroyed
|
|
70
38
|
*/
|
|
71
39
|
export class StableWSConnection<
|
|
72
|
-
|
|
40
|
+
// CAUTION: generics are out of usual order here
|
|
41
|
+
ChannelType extends UR = UR,
|
|
73
42
|
CommandType extends string = LiteralStringForUnion,
|
|
74
|
-
UserType extends
|
|
43
|
+
UserType extends UR = UR,
|
|
44
|
+
AttachmentType extends UR = UR,
|
|
45
|
+
EventType extends UR = UR,
|
|
46
|
+
MessageType extends UR = UR,
|
|
47
|
+
ReactionType extends UR = UR
|
|
75
48
|
> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
logger: Constructor<ChannelType, CommandType, UserType>['logger'];
|
|
81
|
-
messageCallback: Constructor<ChannelType, CommandType, UserType>['messageCallback'];
|
|
82
|
-
recoverCallback: Constructor<ChannelType, CommandType, UserType>['recoverCallback'];
|
|
83
|
-
tokenManager: Constructor<ChannelType, CommandType, UserType>['tokenManager'];
|
|
84
|
-
user: Constructor<ChannelType, CommandType, UserType>['user'];
|
|
85
|
-
userAgent: Constructor<ChannelType, CommandType, UserType>['userAgent'];
|
|
86
|
-
userID: Constructor<ChannelType, CommandType, UserType>['userID'];
|
|
87
|
-
wsBaseURL: Constructor<ChannelType, CommandType, UserType>['wsBaseURL'];
|
|
88
|
-
device: Constructor<ChannelType, CommandType, UserType>['device'];
|
|
49
|
+
// global from constructor
|
|
50
|
+
client: StreamChat<AttachmentType, ChannelType, CommandType, EventType, MessageType, ReactionType, UserType>;
|
|
51
|
+
|
|
52
|
+
// local vars
|
|
89
53
|
connectionID?: string;
|
|
90
54
|
connectionOpen?: ConnectAPIResponse<ChannelType, CommandType, UserType>;
|
|
91
55
|
consecutiveFailures: number;
|
|
92
56
|
pingInterval: number;
|
|
93
57
|
healthCheckTimeoutRef?: NodeJS.Timeout;
|
|
94
58
|
isConnecting: boolean;
|
|
59
|
+
isDisconnected: boolean;
|
|
95
60
|
isHealthy: boolean;
|
|
96
61
|
isResolved?: boolean;
|
|
97
62
|
lastEvent: Date | null;
|
|
98
63
|
connectionCheckTimeout: number;
|
|
99
64
|
connectionCheckTimeoutRef?: NodeJS.Timeout;
|
|
100
65
|
rejectPromise?: (
|
|
101
|
-
reason?: Error & {
|
|
102
|
-
code?: string | number;
|
|
103
|
-
isWSFailure?: boolean;
|
|
104
|
-
StatusCode?: string | number;
|
|
105
|
-
},
|
|
66
|
+
reason?: Error & { code?: string | number; isWSFailure?: boolean; StatusCode?: string | number },
|
|
106
67
|
) => void;
|
|
107
68
|
requestID: string | undefined;
|
|
108
|
-
|
|
109
|
-
resolvePromise?: (value: WebSocket.MessageEvent) => void;
|
|
69
|
+
resolvePromise?: (value: ConnectionOpen<ChannelType, CommandType, UserType>) => void;
|
|
110
70
|
totalFailures: number;
|
|
111
71
|
ws?: WebSocket;
|
|
112
72
|
wsID: number;
|
|
113
|
-
|
|
114
|
-
insightMetrics: InsightMetrics;
|
|
73
|
+
|
|
115
74
|
constructor({
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
recoverCallback,
|
|
123
|
-
tokenManager,
|
|
124
|
-
user,
|
|
125
|
-
userAgent,
|
|
126
|
-
userID,
|
|
127
|
-
wsBaseURL,
|
|
128
|
-
device,
|
|
129
|
-
enableInsights,
|
|
130
|
-
insightMetrics,
|
|
131
|
-
}: Constructor<ChannelType, CommandType, UserType>) {
|
|
132
|
-
this.wsBaseURL = wsBaseURL;
|
|
133
|
-
this.clientID = clientID;
|
|
134
|
-
this.userID = userID;
|
|
135
|
-
this.user = user;
|
|
136
|
-
this.authType = authType;
|
|
137
|
-
this.userAgent = userAgent;
|
|
138
|
-
this.apiKey = apiKey;
|
|
139
|
-
this.tokenManager = tokenManager;
|
|
140
|
-
this.device = device;
|
|
75
|
+
client,
|
|
76
|
+
}: {
|
|
77
|
+
client: StreamChat<AttachmentType, ChannelType, CommandType, EventType, MessageType, ReactionType, UserType>;
|
|
78
|
+
}) {
|
|
79
|
+
/** StreamChat client */
|
|
80
|
+
this.client = client;
|
|
141
81
|
/** consecutive failures influence the duration of the timeout */
|
|
142
82
|
this.consecutiveFailures = 0;
|
|
143
83
|
/** keep track of the total number of failures */
|
|
144
84
|
this.totalFailures = 0;
|
|
145
85
|
/** We only make 1 attempt to reconnect at the same time.. */
|
|
146
86
|
this.isConnecting = false;
|
|
87
|
+
/** To avoid reconnect if client is disconnected */
|
|
88
|
+
this.isDisconnected = false;
|
|
147
89
|
/** Boolean that indicates if the connection promise is resolved */
|
|
148
90
|
this.isResolved = false;
|
|
149
91
|
/** Boolean that indicates if we have a working connection to the server */
|
|
150
92
|
this.isHealthy = false;
|
|
151
|
-
/** Callback when the connection fails and recovers */
|
|
152
|
-
this.recoverCallback = recoverCallback;
|
|
153
|
-
this.messageCallback = messageCallback;
|
|
154
|
-
this.eventCallback = eventCallback;
|
|
155
|
-
this.logger = logger;
|
|
156
93
|
/** Incremented when a new WS connection is made */
|
|
157
94
|
this.wsID = 1;
|
|
158
95
|
/** Store the last event time for health checks */
|
|
@@ -160,46 +97,37 @@ export class StableWSConnection<
|
|
|
160
97
|
/** Send a health check message every 25 seconds */
|
|
161
98
|
this.pingInterval = 25 * 1000;
|
|
162
99
|
this.connectionCheckTimeout = this.pingInterval + 10 * 1000;
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
|
|
100
|
+
|
|
101
|
+
addConnectionEventListeners(this.onlineStatusChanged);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_log(msg: string, extra: UR = {}, level: LogLevel = 'info') {
|
|
105
|
+
this.client.logger(level, 'connection:' + msg, { tags: ['connection'], ...extra });
|
|
166
106
|
}
|
|
167
107
|
|
|
168
108
|
/**
|
|
169
109
|
* connect - Connect to the WS URL
|
|
170
|
-
*
|
|
110
|
+
* the default 15s timeout allows between 2~3 tries
|
|
171
111
|
* @return {ConnectAPIResponse<ChannelType, CommandType, UserType>} Promise that completes once the first health check message is received
|
|
172
112
|
*/
|
|
173
|
-
async connect() {
|
|
113
|
+
async connect(timeout = 15000) {
|
|
174
114
|
if (this.isConnecting) {
|
|
175
|
-
throw Error(
|
|
176
|
-
`You've called connect twice, can only attempt 1 connection at the time`,
|
|
177
|
-
);
|
|
115
|
+
throw Error(`You've called connect twice, can only attempt 1 connection at the time`);
|
|
178
116
|
}
|
|
179
117
|
|
|
118
|
+
this.isDisconnected = false;
|
|
119
|
+
|
|
180
120
|
try {
|
|
181
121
|
const healthCheck = await this._connect();
|
|
182
122
|
this.consecutiveFailures = 0;
|
|
183
123
|
|
|
184
|
-
this.
|
|
185
|
-
'info',
|
|
186
|
-
`connection:connect() - Established ws connection with healthcheck: ${healthCheck}`,
|
|
187
|
-
{
|
|
188
|
-
tags: ['connection'],
|
|
189
|
-
},
|
|
190
|
-
);
|
|
124
|
+
this._log(`connect() - Established ws connection with healthcheck: ${healthCheck}`);
|
|
191
125
|
} catch (error) {
|
|
192
126
|
this.isHealthy = false;
|
|
193
127
|
this.consecutiveFailures += 1;
|
|
194
128
|
|
|
195
|
-
if (error.code === chatCodes.TOKEN_EXPIRED && !this.tokenManager.isStatic()) {
|
|
196
|
-
this.
|
|
197
|
-
'info',
|
|
198
|
-
'connection:connect() - WS failure due to expired token, so going to try to reload token and reconnect',
|
|
199
|
-
{
|
|
200
|
-
tags: ['connection'],
|
|
201
|
-
},
|
|
202
|
-
);
|
|
129
|
+
if (error.code === chatCodes.TOKEN_EXPIRED && !this.client.tokenManager.isStatic()) {
|
|
130
|
+
this._log('connect() - WS failure due to expired token, so going to try to reload token and reconnect');
|
|
203
131
|
this._reconnect({ refreshToken: true });
|
|
204
132
|
} else if (!error.isWSFailure) {
|
|
205
133
|
// API rejected the connection and we should not retry
|
|
@@ -214,7 +142,7 @@ export class StableWSConnection<
|
|
|
214
142
|
}
|
|
215
143
|
}
|
|
216
144
|
|
|
217
|
-
return await this._waitForHealthy();
|
|
145
|
+
return await this._waitForHealthy(timeout);
|
|
218
146
|
}
|
|
219
147
|
|
|
220
148
|
/**
|
|
@@ -246,6 +174,7 @@ export class StableWSConnection<
|
|
|
246
174
|
})(),
|
|
247
175
|
(async () => {
|
|
248
176
|
await sleep(timeout);
|
|
177
|
+
this.isConnecting = false;
|
|
249
178
|
throw new Error(
|
|
250
179
|
JSON.stringify({
|
|
251
180
|
code: '',
|
|
@@ -260,21 +189,16 @@ export class StableWSConnection<
|
|
|
260
189
|
|
|
261
190
|
/**
|
|
262
191
|
* Builds and returns the url for websocket.
|
|
263
|
-
* @
|
|
192
|
+
* @private
|
|
264
193
|
* @returns url string
|
|
265
194
|
*/
|
|
266
|
-
_buildUrl = (
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
client_request_id: reqID,
|
|
274
|
-
};
|
|
275
|
-
const qs = encodeURIComponent(JSON.stringify(params));
|
|
276
|
-
const token = this.tokenManager.getToken();
|
|
277
|
-
return `${this.wsBaseURL}/connect?json=${qs}&api_key=${this.apiKey}&authorization=${token}&stream-auth-type=${this.authType}&X-Stream-Client=${this.userAgent}`;
|
|
195
|
+
_buildUrl = () => {
|
|
196
|
+
const qs = encodeURIComponent(this.client._buildWSPayload(this.requestID));
|
|
197
|
+
const token = this.client.tokenManager.getToken();
|
|
198
|
+
|
|
199
|
+
return `${this.client.wsBaseURL}/connect?json=${qs}&api_key=${
|
|
200
|
+
this.client.key
|
|
201
|
+
}&authorization=${token}&stream-auth-type=${this.client.getAuthType()}&X-Stream-Client=${this.client.getUserAgent()}`;
|
|
278
202
|
};
|
|
279
203
|
|
|
280
204
|
/**
|
|
@@ -282,15 +206,11 @@ export class StableWSConnection<
|
|
|
282
206
|
*
|
|
283
207
|
*/
|
|
284
208
|
disconnect(timeout?: number) {
|
|
285
|
-
this.
|
|
286
|
-
'info',
|
|
287
|
-
`connection:disconnect() - Closing the websocket connection for wsID ${this.wsID}`,
|
|
288
|
-
{
|
|
289
|
-
tags: ['connection'],
|
|
290
|
-
},
|
|
291
|
-
);
|
|
209
|
+
this._log(`disconnect() - Closing the websocket connection for wsID ${this.wsID}`);
|
|
292
210
|
|
|
293
211
|
this.wsID += 1;
|
|
212
|
+
this.isConnecting = false;
|
|
213
|
+
this.isDisconnected = true;
|
|
294
214
|
|
|
295
215
|
// start by removing all the listeners
|
|
296
216
|
if (this.healthCheckTimeoutRef) {
|
|
@@ -300,7 +220,7 @@ export class StableWSConnection<
|
|
|
300
220
|
clearInterval(this.connectionCheckTimeoutRef);
|
|
301
221
|
}
|
|
302
222
|
|
|
303
|
-
this.
|
|
223
|
+
removeConnectionEventListeners(this.onlineStatusChanged);
|
|
304
224
|
|
|
305
225
|
this.isHealthy = false;
|
|
306
226
|
|
|
@@ -317,16 +237,7 @@ export class StableWSConnection<
|
|
|
317
237
|
if (ws && ws.close && ws.readyState === ws.OPEN) {
|
|
318
238
|
isClosedPromise = new Promise((resolve) => {
|
|
319
239
|
const onclose = (event: WebSocket.CloseEvent) => {
|
|
320
|
-
this.
|
|
321
|
-
'info',
|
|
322
|
-
`connection:disconnect() - resolving isClosedPromise ${
|
|
323
|
-
event ? 'with' : 'without'
|
|
324
|
-
} close frame`,
|
|
325
|
-
{
|
|
326
|
-
tags: ['connection'],
|
|
327
|
-
event,
|
|
328
|
-
},
|
|
329
|
-
);
|
|
240
|
+
this._log(`disconnect() - resolving isClosedPromise ${event ? 'with' : 'without'} close frame`, { event });
|
|
330
241
|
resolve();
|
|
331
242
|
};
|
|
332
243
|
|
|
@@ -336,26 +247,11 @@ export class StableWSConnection<
|
|
|
336
247
|
setTimeout(onclose, timeout != null ? timeout : 1000);
|
|
337
248
|
});
|
|
338
249
|
|
|
339
|
-
this.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
{
|
|
343
|
-
tags: ['connection'],
|
|
344
|
-
},
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
ws.close(
|
|
348
|
-
chatCodes.WS_CLOSED_SUCCESS,
|
|
349
|
-
'Manually closed connection by calling client.disconnect()',
|
|
350
|
-
);
|
|
250
|
+
this._log(`disconnect() - Manually closed connection by calling client.disconnect()`);
|
|
251
|
+
|
|
252
|
+
ws.close(chatCodes.WS_CLOSED_SUCCESS, 'Manually closed connection by calling client.disconnect()');
|
|
351
253
|
} else {
|
|
352
|
-
this.
|
|
353
|
-
'info',
|
|
354
|
-
`connection:disconnect() - ws connection doesn't exist or it is already closed.`,
|
|
355
|
-
{
|
|
356
|
-
tags: ['connection'],
|
|
357
|
-
},
|
|
358
|
-
);
|
|
254
|
+
this._log(`disconnect() - ws connection doesn't exist or it is already closed.`);
|
|
359
255
|
isClosedPromise = Promise.resolve();
|
|
360
256
|
}
|
|
361
257
|
|
|
@@ -370,14 +266,14 @@ export class StableWSConnection<
|
|
|
370
266
|
* @return {ConnectAPIResponse<ChannelType, CommandType, UserType>} Promise that completes once the first health check message is received
|
|
371
267
|
*/
|
|
372
268
|
async _connect() {
|
|
373
|
-
if (this.isConnecting) return; // simply ignore _connect if it's currently trying to connect
|
|
269
|
+
if (this.isConnecting || this.isDisconnected) return; // simply ignore _connect if it's currently trying to connect
|
|
374
270
|
this.isConnecting = true;
|
|
375
271
|
this.requestID = randomId();
|
|
376
|
-
this.insightMetrics.connectionStartTimestamp = new Date().getTime();
|
|
272
|
+
this.client.insightMetrics.connectionStartTimestamp = new Date().getTime();
|
|
377
273
|
try {
|
|
378
|
-
await this.tokenManager.tokenReady();
|
|
274
|
+
await this.client.tokenManager.tokenReady();
|
|
379
275
|
this._setupConnectionPromise();
|
|
380
|
-
const wsURL = this._buildUrl(
|
|
276
|
+
const wsURL = this._buildUrl();
|
|
381
277
|
this.ws = new WebSocket(wsURL);
|
|
382
278
|
this.ws.onopen = this.onopen.bind(this, this.wsID);
|
|
383
279
|
this.ws.onclose = this.onclose.bind(this, this.wsID);
|
|
@@ -388,26 +284,23 @@ export class StableWSConnection<
|
|
|
388
284
|
|
|
389
285
|
if (response) {
|
|
390
286
|
this.connectionID = response.connection_id;
|
|
391
|
-
if (this.insightMetrics.wsConsecutiveFailures > 0 && this.enableInsights) {
|
|
287
|
+
if (this.client.insightMetrics.wsConsecutiveFailures > 0 && this.client.options.enableInsights) {
|
|
392
288
|
postInsights(
|
|
393
289
|
'ws_success_after_failure',
|
|
394
290
|
buildWsSuccessAfterFailureInsight((this as unknown) as StableWSConnection),
|
|
395
291
|
);
|
|
396
|
-
this.insightMetrics.wsConsecutiveFailures = 0;
|
|
292
|
+
this.client.insightMetrics.wsConsecutiveFailures = 0;
|
|
397
293
|
}
|
|
398
294
|
return response;
|
|
399
295
|
}
|
|
400
296
|
} catch (err) {
|
|
401
297
|
this.isConnecting = false;
|
|
402
298
|
|
|
403
|
-
if (this.enableInsights) {
|
|
404
|
-
this.insightMetrics.wsConsecutiveFailures++;
|
|
405
|
-
this.insightMetrics.wsTotalFailures++;
|
|
299
|
+
if (this.client.options.enableInsights) {
|
|
300
|
+
this.client.insightMetrics.wsConsecutiveFailures++;
|
|
301
|
+
this.client.insightMetrics.wsTotalFailures++;
|
|
406
302
|
|
|
407
|
-
const insights = buildWsFatalInsight(
|
|
408
|
-
(this as unknown) as StableWSConnection,
|
|
409
|
-
convertErrorToJson(err as Error),
|
|
410
|
-
);
|
|
303
|
+
const insights = buildWsFatalInsight((this as unknown) as StableWSConnection, convertErrorToJson(err as Error));
|
|
411
304
|
postInsights?.('ws_fatal', insights);
|
|
412
305
|
}
|
|
413
306
|
throw err;
|
|
@@ -422,21 +315,12 @@ export class StableWSConnection<
|
|
|
422
315
|
* - `interval` {int} number of ms that function should wait before reconnecting
|
|
423
316
|
* - `refreshToken` {boolean} reload/refresh user token be refreshed before attempting reconnection.
|
|
424
317
|
*/
|
|
425
|
-
async _reconnect(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
this.logger('info', 'connection:_reconnect() - Initiating the reconnect', {
|
|
429
|
-
tags: ['connection'],
|
|
430
|
-
});
|
|
318
|
+
async _reconnect(options: { interval?: number; refreshToken?: boolean } = {}): Promise<void> {
|
|
319
|
+
this._log('_reconnect() - Initiating the reconnect');
|
|
320
|
+
|
|
431
321
|
// only allow 1 connection at the time
|
|
432
322
|
if (this.isConnecting || this.isHealthy) {
|
|
433
|
-
this.
|
|
434
|
-
'info',
|
|
435
|
-
'connection:_reconnect() - Abort (1) since already connecting or healthy',
|
|
436
|
-
{
|
|
437
|
-
tags: ['connection'],
|
|
438
|
-
},
|
|
439
|
-
);
|
|
323
|
+
this._log('_reconnect() - Abort (1) since already connecting or healthy');
|
|
440
324
|
return;
|
|
441
325
|
}
|
|
442
326
|
|
|
@@ -452,70 +336,48 @@ export class StableWSConnection<
|
|
|
452
336
|
// Check once again if by some other call to _reconnect is active or connection is
|
|
453
337
|
// already restored, then no need to proceed.
|
|
454
338
|
if (this.isConnecting || this.isHealthy) {
|
|
455
|
-
this.
|
|
456
|
-
'info',
|
|
457
|
-
'connection:_reconnect() - Abort (2) since already connecting or healthy',
|
|
458
|
-
{
|
|
459
|
-
tags: ['connection'],
|
|
460
|
-
},
|
|
461
|
-
);
|
|
339
|
+
this._log('_reconnect() - Abort (2) since already connecting or healthy');
|
|
462
340
|
return;
|
|
463
341
|
}
|
|
464
342
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
343
|
+
if (this.isDisconnected) {
|
|
344
|
+
this._log('_reconnect() - Abort (3) since disconnect() is called');
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this._log('_reconnect() - Destroying current WS connection');
|
|
469
349
|
|
|
350
|
+
// cleanup the old connection
|
|
470
351
|
this._destroyCurrentWSConnection();
|
|
471
352
|
|
|
472
353
|
if (options.refreshToken) {
|
|
473
|
-
await this.tokenManager.loadToken();
|
|
354
|
+
await this.client.tokenManager.loadToken();
|
|
474
355
|
}
|
|
475
356
|
|
|
476
357
|
try {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
await this.recoverCallback(open);
|
|
483
|
-
this.logger('info', 'connection:_reconnect() - Finished recoverCallBack', {
|
|
484
|
-
tags: ['connection'],
|
|
485
|
-
});
|
|
486
|
-
}
|
|
358
|
+
await this._connect();
|
|
359
|
+
this._log('_reconnect() - Waiting for recoverCallBack');
|
|
360
|
+
await this.client.recoverState();
|
|
361
|
+
this._log('_reconnect() - Finished recoverCallBack');
|
|
362
|
+
|
|
487
363
|
this.consecutiveFailures = 0;
|
|
488
364
|
} catch (error) {
|
|
489
365
|
this.isHealthy = false;
|
|
490
366
|
this.consecutiveFailures += 1;
|
|
491
|
-
if (error.code === chatCodes.TOKEN_EXPIRED && !this.tokenManager.isStatic()) {
|
|
492
|
-
this.
|
|
493
|
-
'info',
|
|
494
|
-
'connection:_reconnect() - WS failure due to expired token, so going to try to reload token and reconnect',
|
|
495
|
-
{
|
|
496
|
-
tags: ['connection'],
|
|
497
|
-
},
|
|
498
|
-
);
|
|
367
|
+
if (error.code === chatCodes.TOKEN_EXPIRED && !this.client.tokenManager.isStatic()) {
|
|
368
|
+
this._log('_reconnect() - WS failure due to expired token, so going to try to reload token and reconnect');
|
|
499
369
|
|
|
500
370
|
return this._reconnect({ refreshToken: true });
|
|
501
371
|
}
|
|
502
372
|
|
|
503
373
|
// reconnect on WS failures, don't reconnect if there is a code bug
|
|
504
374
|
if (error.isWSFailure) {
|
|
505
|
-
this.
|
|
506
|
-
'info',
|
|
507
|
-
'connection:_reconnect() - WS failure, so going to try to reconnect',
|
|
508
|
-
{
|
|
509
|
-
tags: ['connection'],
|
|
510
|
-
},
|
|
511
|
-
);
|
|
375
|
+
this._log('_reconnect() - WS failure, so going to try to reconnect');
|
|
512
376
|
|
|
513
377
|
this._reconnect();
|
|
514
378
|
}
|
|
515
379
|
}
|
|
516
|
-
this.
|
|
517
|
-
tags: ['connection'],
|
|
518
|
-
});
|
|
380
|
+
this._log('_reconnect() - == END ==');
|
|
519
381
|
}
|
|
520
382
|
|
|
521
383
|
/**
|
|
@@ -527,26 +389,14 @@ export class StableWSConnection<
|
|
|
527
389
|
onlineStatusChanged = (event: Event) => {
|
|
528
390
|
if (event.type === 'offline') {
|
|
529
391
|
// mark the connection as down
|
|
530
|
-
this.
|
|
531
|
-
'info',
|
|
532
|
-
'connection:onlineStatusChanged() - Status changing to offline',
|
|
533
|
-
{
|
|
534
|
-
tags: ['connection'],
|
|
535
|
-
},
|
|
536
|
-
);
|
|
392
|
+
this._log('onlineStatusChanged() - Status changing to offline');
|
|
537
393
|
this._setHealth(false);
|
|
538
394
|
} else if (event.type === 'online') {
|
|
539
395
|
// retry right now...
|
|
540
396
|
// We check this.isHealthy, not sure if it's always
|
|
541
397
|
// smart to create a new WS connection if the old one is still up and running.
|
|
542
398
|
// it's possible we didn't miss any messages, so this process is just expensive and not needed.
|
|
543
|
-
this.
|
|
544
|
-
'info',
|
|
545
|
-
`connection:onlineStatusChanged() - Status changing to online. isHealthy: ${this.isHealthy}`,
|
|
546
|
-
{
|
|
547
|
-
tags: ['connection'],
|
|
548
|
-
},
|
|
549
|
-
);
|
|
399
|
+
this._log(`onlineStatusChanged() - Status changing to online. isHealthy: ${this.isHealthy}`);
|
|
550
400
|
if (!this.isHealthy) {
|
|
551
401
|
this._reconnect({ interval: 10 });
|
|
552
402
|
}
|
|
@@ -556,10 +406,7 @@ export class StableWSConnection<
|
|
|
556
406
|
onopen = (wsID: number) => {
|
|
557
407
|
if (this.wsID !== wsID) return;
|
|
558
408
|
|
|
559
|
-
this.
|
|
560
|
-
tags: ['connection'],
|
|
561
|
-
wsID,
|
|
562
|
-
});
|
|
409
|
+
this._log('onopen() - onopen callback', { wsID });
|
|
563
410
|
};
|
|
564
411
|
|
|
565
412
|
onmessage = (wsID: number, event: WebSocket.MessageEvent) => {
|
|
@@ -572,46 +419,36 @@ export class StableWSConnection<
|
|
|
572
419
|
// after that a ws.onclose..
|
|
573
420
|
if (!this.isResolved && data) {
|
|
574
421
|
this.isResolved = true;
|
|
575
|
-
if (data.error
|
|
422
|
+
if (data.error) {
|
|
576
423
|
this.rejectPromise?.(this._errorFromWSEvent(data, false));
|
|
577
424
|
return;
|
|
578
|
-
} else {
|
|
579
|
-
this.resolvePromise?.(event);
|
|
580
|
-
this._setHealth(true);
|
|
581
425
|
}
|
|
426
|
+
|
|
427
|
+
this.resolvePromise?.(data);
|
|
428
|
+
this._setHealth(true);
|
|
582
429
|
}
|
|
583
430
|
|
|
584
431
|
// trigger the event..
|
|
585
432
|
this.lastEvent = new Date();
|
|
586
|
-
this.
|
|
587
|
-
tags: ['connection'],
|
|
588
|
-
event,
|
|
589
|
-
wsID,
|
|
590
|
-
});
|
|
433
|
+
this._log('onmessage() - onmessage callback', { event, wsID });
|
|
591
434
|
|
|
592
435
|
if (data && data.type === 'health.check') {
|
|
593
436
|
this.scheduleNextPing();
|
|
594
437
|
}
|
|
595
438
|
|
|
596
|
-
this.
|
|
439
|
+
this.client.handleEvent(event);
|
|
597
440
|
this.scheduleConnectionCheck();
|
|
598
441
|
};
|
|
599
442
|
|
|
600
443
|
onclose = (wsID: number, event: WebSocket.CloseEvent) => {
|
|
601
444
|
if (this.wsID !== wsID) return;
|
|
602
445
|
|
|
603
|
-
this.
|
|
604
|
-
tags: ['connection'],
|
|
605
|
-
event,
|
|
606
|
-
wsID,
|
|
607
|
-
});
|
|
446
|
+
this._log('onclose() - onclose callback - ' + event.code, { event, wsID });
|
|
608
447
|
|
|
609
448
|
if (event.code === chatCodes.WS_CLOSED_SUCCESS) {
|
|
610
449
|
// this is a permanent error raised by stream..
|
|
611
450
|
// usually caused by invalid auth details
|
|
612
|
-
const error = new Error(
|
|
613
|
-
`WS connection reject with error ${event.reason}`,
|
|
614
|
-
) as Error & WebSocket.CloseEvent;
|
|
451
|
+
const error = new Error(`WS connection reject with error ${event.reason}`) as Error & WebSocket.CloseEvent;
|
|
615
452
|
|
|
616
453
|
error.reason = event.reason;
|
|
617
454
|
error.code = event.code;
|
|
@@ -619,14 +456,7 @@ export class StableWSConnection<
|
|
|
619
456
|
error.target = event.target;
|
|
620
457
|
|
|
621
458
|
this.rejectPromise?.(error);
|
|
622
|
-
this.
|
|
623
|
-
'info',
|
|
624
|
-
`connection:onclose() - WS connection reject with error ${event.reason}`,
|
|
625
|
-
{
|
|
626
|
-
tags: ['connection'],
|
|
627
|
-
event,
|
|
628
|
-
},
|
|
629
|
-
);
|
|
459
|
+
this._log(`onclose() - WS connection reject with error ${event.reason}`, { event });
|
|
630
460
|
} else {
|
|
631
461
|
this.consecutiveFailures += 1;
|
|
632
462
|
this.totalFailures += 1;
|
|
@@ -635,14 +465,7 @@ export class StableWSConnection<
|
|
|
635
465
|
|
|
636
466
|
this.rejectPromise?.(this._errorFromWSEvent(event));
|
|
637
467
|
|
|
638
|
-
this.
|
|
639
|
-
'info',
|
|
640
|
-
`connection:onclose() - WS connection closed. Calling reconnect ...`,
|
|
641
|
-
{
|
|
642
|
-
tags: ['connection'],
|
|
643
|
-
event,
|
|
644
|
-
},
|
|
645
|
-
);
|
|
468
|
+
this._log(`onclose() - WS connection closed. Calling reconnect ...`, { event });
|
|
646
469
|
|
|
647
470
|
// reconnect if its an abnormal failure
|
|
648
471
|
this._reconnect();
|
|
@@ -658,10 +481,7 @@ export class StableWSConnection<
|
|
|
658
481
|
this.isConnecting = false;
|
|
659
482
|
|
|
660
483
|
this.rejectPromise?.(this._errorFromWSEvent(event));
|
|
661
|
-
this.
|
|
662
|
-
tags: ['connection'],
|
|
663
|
-
event,
|
|
664
|
-
});
|
|
484
|
+
this._log(`onerror() - WS connection resulted into error`, { event });
|
|
665
485
|
|
|
666
486
|
this._reconnect();
|
|
667
487
|
};
|
|
@@ -674,37 +494,29 @@ export class StableWSConnection<
|
|
|
674
494
|
*
|
|
675
495
|
*/
|
|
676
496
|
_setHealth = (healthy: boolean) => {
|
|
677
|
-
if (healthy
|
|
678
|
-
// yes we are online:
|
|
679
|
-
this.isHealthy = true;
|
|
680
|
-
this.eventCallback({
|
|
681
|
-
type: 'connection.changed',
|
|
682
|
-
online: true,
|
|
683
|
-
});
|
|
684
|
-
}
|
|
497
|
+
if (healthy === this.isHealthy) return;
|
|
685
498
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
type: 'connection.changed',
|
|
693
|
-
online: false,
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
}, 5000);
|
|
499
|
+
this.isHealthy = healthy;
|
|
500
|
+
|
|
501
|
+
if (this.isHealthy) {
|
|
502
|
+
//@ts-expect-error
|
|
503
|
+
this.client.dispatchEvent({ type: 'connection.changed', online: this.isHealthy });
|
|
504
|
+
return;
|
|
697
505
|
}
|
|
506
|
+
|
|
507
|
+
// we're offline, wait few seconds and fire and event if still offline
|
|
508
|
+
setTimeout(() => {
|
|
509
|
+
if (this.isHealthy) return;
|
|
510
|
+
//@ts-expect-error
|
|
511
|
+
this.client.dispatchEvent({ type: 'connection.changed', online: this.isHealthy });
|
|
512
|
+
}, 5000);
|
|
698
513
|
};
|
|
699
514
|
|
|
700
515
|
/**
|
|
701
516
|
* _errorFromWSEvent - Creates an error object for the WS event
|
|
702
517
|
*
|
|
703
518
|
*/
|
|
704
|
-
_errorFromWSEvent = (
|
|
705
|
-
event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
|
|
706
|
-
isWSFailure = true,
|
|
707
|
-
) => {
|
|
519
|
+
_errorFromWSEvent = (event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, isWSFailure = true) => {
|
|
708
520
|
let code;
|
|
709
521
|
let statusCode;
|
|
710
522
|
let message;
|
|
@@ -721,14 +533,9 @@ export class StableWSConnection<
|
|
|
721
533
|
}
|
|
722
534
|
|
|
723
535
|
// Keeping this `warn` level log, to avoid cluttering of error logs from ws failures.
|
|
724
|
-
this.
|
|
725
|
-
tags: ['connection'],
|
|
726
|
-
event,
|
|
727
|
-
});
|
|
536
|
+
this._log(`_errorFromWSEvent() - WS failed with code ${code}`, { event }, 'warn');
|
|
728
537
|
|
|
729
|
-
const error = new Error(
|
|
730
|
-
`WS failed with code ${code} and reason - ${message}`,
|
|
731
|
-
) as Error & {
|
|
538
|
+
const error = new Error(`WS failed with code ${code} and reason - ${message}`) as Error & {
|
|
732
539
|
code?: string | number;
|
|
733
540
|
isWSFailure?: boolean;
|
|
734
541
|
StatusCode?: string | number;
|
|
@@ -743,27 +550,6 @@ export class StableWSConnection<
|
|
|
743
550
|
return error;
|
|
744
551
|
};
|
|
745
552
|
|
|
746
|
-
/**
|
|
747
|
-
* _listenForConnectionChanges - Adds an event listener for the browser going online or offline
|
|
748
|
-
*/
|
|
749
|
-
_listenForConnectionChanges = () => {
|
|
750
|
-
// (typeof window !== 'undefined') check is for environments where window is not defined, such as nextjs environment,
|
|
751
|
-
// and thus (window === undefined) will result in ReferenceError.
|
|
752
|
-
if (typeof window !== 'undefined' && window?.addEventListener) {
|
|
753
|
-
window.addEventListener('offline', this.onlineStatusChanged);
|
|
754
|
-
window.addEventListener('online', this.onlineStatusChanged);
|
|
755
|
-
}
|
|
756
|
-
};
|
|
757
|
-
|
|
758
|
-
_removeConnectionListeners = () => {
|
|
759
|
-
// (typeof window !== 'undefined') check is for environments where window is not defined, such as nextjs environment,
|
|
760
|
-
// and thus (window === undefined) will result in ReferenceError.
|
|
761
|
-
if (typeof window !== 'undefined' && window?.removeEventListener) {
|
|
762
|
-
window.removeEventListener('offline', this.onlineStatusChanged);
|
|
763
|
-
window.removeEventListener('online', this.onlineStatusChanged);
|
|
764
|
-
}
|
|
765
|
-
};
|
|
766
|
-
|
|
767
553
|
/**
|
|
768
554
|
* _destroyCurrentWSConnection - Removes the current WS connection
|
|
769
555
|
*
|
|
@@ -774,13 +560,8 @@ export class StableWSConnection<
|
|
|
774
560
|
this.wsID += 1;
|
|
775
561
|
|
|
776
562
|
try {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (this.ws && this.ws.close) {
|
|
782
|
-
this.ws.close();
|
|
783
|
-
}
|
|
563
|
+
this?.ws?.removeAllListeners();
|
|
564
|
+
this?.ws?.close();
|
|
784
565
|
} catch (e) {
|
|
785
566
|
// we don't care
|
|
786
567
|
}
|
|
@@ -790,34 +571,12 @@ export class StableWSConnection<
|
|
|
790
571
|
* _setupPromise - sets up the this.connectOpen promise
|
|
791
572
|
*/
|
|
792
573
|
_setupConnectionPromise = () => {
|
|
793
|
-
const that = this;
|
|
794
574
|
this.isResolved = false;
|
|
795
575
|
/** a promise that is resolved once ws.open is called */
|
|
796
|
-
this.connectionOpen = new Promise<
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
})
|
|
800
|
-
(e) => {
|
|
801
|
-
if (e.data && typeof e.data === 'string') {
|
|
802
|
-
const data = JSON.parse(e.data) as ConnectionOpen<
|
|
803
|
-
ChannelType,
|
|
804
|
-
CommandType,
|
|
805
|
-
UserType
|
|
806
|
-
> & {
|
|
807
|
-
error?: unknown;
|
|
808
|
-
};
|
|
809
|
-
if (data && data.error != null) {
|
|
810
|
-
throw new Error(JSON.stringify(data.error));
|
|
811
|
-
}
|
|
812
|
-
return data;
|
|
813
|
-
} else {
|
|
814
|
-
return undefined;
|
|
815
|
-
}
|
|
816
|
-
},
|
|
817
|
-
(error) => {
|
|
818
|
-
throw error;
|
|
819
|
-
},
|
|
820
|
-
);
|
|
576
|
+
this.connectionOpen = new Promise<ConnectionOpen<ChannelType, CommandType, UserType>>((resolve, reject) => {
|
|
577
|
+
this.resolvePromise = resolve;
|
|
578
|
+
this.rejectPromise = reject;
|
|
579
|
+
});
|
|
821
580
|
};
|
|
822
581
|
|
|
823
582
|
/**
|
|
@@ -831,12 +590,7 @@ export class StableWSConnection<
|
|
|
831
590
|
// 30 seconds is the recommended interval (messenger uses this)
|
|
832
591
|
this.healthCheckTimeoutRef = setTimeout(() => {
|
|
833
592
|
// send the healthcheck.., server replies with a health check event
|
|
834
|
-
const data = [
|
|
835
|
-
{
|
|
836
|
-
type: 'health.check',
|
|
837
|
-
client_id: this.clientID,
|
|
838
|
-
},
|
|
839
|
-
];
|
|
593
|
+
const data = [{ type: 'health.check', client_id: this.client.clientID }];
|
|
840
594
|
// try to send on the connection
|
|
841
595
|
try {
|
|
842
596
|
this.ws?.send(JSON.stringify(data));
|
|
@@ -858,13 +612,8 @@ export class StableWSConnection<
|
|
|
858
612
|
|
|
859
613
|
this.connectionCheckTimeoutRef = setTimeout(() => {
|
|
860
614
|
const now = new Date();
|
|
861
|
-
if (
|
|
862
|
-
this.
|
|
863
|
-
now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout
|
|
864
|
-
) {
|
|
865
|
-
this.logger('info', 'connection:scheduleConnectionCheck - going to reconnect', {
|
|
866
|
-
tags: ['connection'],
|
|
867
|
-
});
|
|
615
|
+
if (this.lastEvent && now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout) {
|
|
616
|
+
this._log('scheduleConnectionCheck - going to reconnect');
|
|
868
617
|
this._setHealth(false);
|
|
869
618
|
this._reconnect();
|
|
870
619
|
}
|