stream-chat 4.4.3-dev.3 → 5.0.1
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 +1258 -722
- 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 +1258 -721
- package/dist/browser.js.map +1 -1
- package/dist/index.es.js +1258 -722
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +1258 -721
- 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 +25 -42
- 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 +41 -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 +16 -9
- 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 +95 -89
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +13 -3
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/base64.ts +1 -4
- package/src/channel.ts +133 -461
- package/src/channel_state.ts +31 -158
- package/src/client.ts +298 -712
- package/src/client_state.ts +2 -2
- package/src/connection.ts +146 -395
- package/src/connection_fallback.ts +209 -0
- package/src/errors.ts +58 -0
- package/src/insights.ts +37 -31
- package/src/permissions.ts +3 -24
- package/src/signing.ts +6 -17
- package/src/token_manager.ts +6 -18
- package/src/types.ts +269 -512
- package/src/utils.ts +58 -24
- 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
|
-
postInsights?: (eventType: InsightTypes, event: Record<string, unknown>) => void;
|
|
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
|
-
postInsights,
|
|
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,25 +284,24 @@ export class StableWSConnection<
|
|
|
388
284
|
|
|
389
285
|
if (response) {
|
|
390
286
|
this.connectionID = response.connection_id;
|
|
391
|
-
if (this.insightMetrics.wsConsecutiveFailures > 0 && this.
|
|
392
|
-
|
|
287
|
+
if (this.client.insightMetrics.wsConsecutiveFailures > 0 && this.client.options.enableInsights) {
|
|
288
|
+
postInsights(
|
|
393
289
|
'ws_success_after_failure',
|
|
394
|
-
buildWsSuccessAfterFailureInsight(this),
|
|
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.
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
this.postInsights?.('ws_fatal', insights);
|
|
303
|
+
const insights = buildWsFatalInsight((this as unknown) as StableWSConnection, convertErrorToJson(err as Error));
|
|
304
|
+
postInsights?.('ws_fatal', insights);
|
|
410
305
|
}
|
|
411
306
|
throw err;
|
|
412
307
|
}
|
|
@@ -420,21 +315,12 @@ export class StableWSConnection<
|
|
|
420
315
|
* - `interval` {int} number of ms that function should wait before reconnecting
|
|
421
316
|
* - `refreshToken` {boolean} reload/refresh user token be refreshed before attempting reconnection.
|
|
422
317
|
*/
|
|
423
|
-
async _reconnect(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
this.logger('info', 'connection:_reconnect() - Initiating the reconnect', {
|
|
427
|
-
tags: ['connection'],
|
|
428
|
-
});
|
|
318
|
+
async _reconnect(options: { interval?: number; refreshToken?: boolean } = {}): Promise<void> {
|
|
319
|
+
this._log('_reconnect() - Initiating the reconnect');
|
|
320
|
+
|
|
429
321
|
// only allow 1 connection at the time
|
|
430
322
|
if (this.isConnecting || this.isHealthy) {
|
|
431
|
-
this.
|
|
432
|
-
'info',
|
|
433
|
-
'connection:_reconnect() - Abort (1) since already connecting or healthy',
|
|
434
|
-
{
|
|
435
|
-
tags: ['connection'],
|
|
436
|
-
},
|
|
437
|
-
);
|
|
323
|
+
this._log('_reconnect() - Abort (1) since already connecting or healthy');
|
|
438
324
|
return;
|
|
439
325
|
}
|
|
440
326
|
|
|
@@ -450,70 +336,48 @@ export class StableWSConnection<
|
|
|
450
336
|
// Check once again if by some other call to _reconnect is active or connection is
|
|
451
337
|
// already restored, then no need to proceed.
|
|
452
338
|
if (this.isConnecting || this.isHealthy) {
|
|
453
|
-
this.
|
|
454
|
-
'info',
|
|
455
|
-
'connection:_reconnect() - Abort (2) since already connecting or healthy',
|
|
456
|
-
{
|
|
457
|
-
tags: ['connection'],
|
|
458
|
-
},
|
|
459
|
-
);
|
|
339
|
+
this._log('_reconnect() - Abort (2) since already connecting or healthy');
|
|
460
340
|
return;
|
|
461
341
|
}
|
|
462
342
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
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');
|
|
467
349
|
|
|
350
|
+
// cleanup the old connection
|
|
468
351
|
this._destroyCurrentWSConnection();
|
|
469
352
|
|
|
470
353
|
if (options.refreshToken) {
|
|
471
|
-
await this.tokenManager.loadToken();
|
|
354
|
+
await this.client.tokenManager.loadToken();
|
|
472
355
|
}
|
|
473
356
|
|
|
474
357
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
await this.recoverCallback(open);
|
|
481
|
-
this.logger('info', 'connection:_reconnect() - Finished recoverCallBack', {
|
|
482
|
-
tags: ['connection'],
|
|
483
|
-
});
|
|
484
|
-
}
|
|
358
|
+
await this._connect();
|
|
359
|
+
this._log('_reconnect() - Waiting for recoverCallBack');
|
|
360
|
+
await this.client.recoverState();
|
|
361
|
+
this._log('_reconnect() - Finished recoverCallBack');
|
|
362
|
+
|
|
485
363
|
this.consecutiveFailures = 0;
|
|
486
364
|
} catch (error) {
|
|
487
365
|
this.isHealthy = false;
|
|
488
366
|
this.consecutiveFailures += 1;
|
|
489
|
-
if (error.code === chatCodes.TOKEN_EXPIRED && !this.tokenManager.isStatic()) {
|
|
490
|
-
this.
|
|
491
|
-
'info',
|
|
492
|
-
'connection:_reconnect() - WS failure due to expired token, so going to try to reload token and reconnect',
|
|
493
|
-
{
|
|
494
|
-
tags: ['connection'],
|
|
495
|
-
},
|
|
496
|
-
);
|
|
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');
|
|
497
369
|
|
|
498
370
|
return this._reconnect({ refreshToken: true });
|
|
499
371
|
}
|
|
500
372
|
|
|
501
373
|
// reconnect on WS failures, don't reconnect if there is a code bug
|
|
502
374
|
if (error.isWSFailure) {
|
|
503
|
-
this.
|
|
504
|
-
'info',
|
|
505
|
-
'connection:_reconnect() - WS failure, so going to try to reconnect',
|
|
506
|
-
{
|
|
507
|
-
tags: ['connection'],
|
|
508
|
-
},
|
|
509
|
-
);
|
|
375
|
+
this._log('_reconnect() - WS failure, so going to try to reconnect');
|
|
510
376
|
|
|
511
377
|
this._reconnect();
|
|
512
378
|
}
|
|
513
379
|
}
|
|
514
|
-
this.
|
|
515
|
-
tags: ['connection'],
|
|
516
|
-
});
|
|
380
|
+
this._log('_reconnect() - == END ==');
|
|
517
381
|
}
|
|
518
382
|
|
|
519
383
|
/**
|
|
@@ -525,26 +389,14 @@ export class StableWSConnection<
|
|
|
525
389
|
onlineStatusChanged = (event: Event) => {
|
|
526
390
|
if (event.type === 'offline') {
|
|
527
391
|
// mark the connection as down
|
|
528
|
-
this.
|
|
529
|
-
'info',
|
|
530
|
-
'connection:onlineStatusChanged() - Status changing to offline',
|
|
531
|
-
{
|
|
532
|
-
tags: ['connection'],
|
|
533
|
-
},
|
|
534
|
-
);
|
|
392
|
+
this._log('onlineStatusChanged() - Status changing to offline');
|
|
535
393
|
this._setHealth(false);
|
|
536
394
|
} else if (event.type === 'online') {
|
|
537
395
|
// retry right now...
|
|
538
396
|
// We check this.isHealthy, not sure if it's always
|
|
539
397
|
// smart to create a new WS connection if the old one is still up and running.
|
|
540
398
|
// it's possible we didn't miss any messages, so this process is just expensive and not needed.
|
|
541
|
-
this.
|
|
542
|
-
'info',
|
|
543
|
-
`connection:onlineStatusChanged() - Status changing to online. isHealthy: ${this.isHealthy}`,
|
|
544
|
-
{
|
|
545
|
-
tags: ['connection'],
|
|
546
|
-
},
|
|
547
|
-
);
|
|
399
|
+
this._log(`onlineStatusChanged() - Status changing to online. isHealthy: ${this.isHealthy}`);
|
|
548
400
|
if (!this.isHealthy) {
|
|
549
401
|
this._reconnect({ interval: 10 });
|
|
550
402
|
}
|
|
@@ -554,10 +406,7 @@ export class StableWSConnection<
|
|
|
554
406
|
onopen = (wsID: number) => {
|
|
555
407
|
if (this.wsID !== wsID) return;
|
|
556
408
|
|
|
557
|
-
this.
|
|
558
|
-
tags: ['connection'],
|
|
559
|
-
wsID,
|
|
560
|
-
});
|
|
409
|
+
this._log('onopen() - onopen callback', { wsID });
|
|
561
410
|
};
|
|
562
411
|
|
|
563
412
|
onmessage = (wsID: number, event: WebSocket.MessageEvent) => {
|
|
@@ -570,46 +419,36 @@ export class StableWSConnection<
|
|
|
570
419
|
// after that a ws.onclose..
|
|
571
420
|
if (!this.isResolved && data) {
|
|
572
421
|
this.isResolved = true;
|
|
573
|
-
if (data.error
|
|
422
|
+
if (data.error) {
|
|
574
423
|
this.rejectPromise?.(this._errorFromWSEvent(data, false));
|
|
575
424
|
return;
|
|
576
|
-
} else {
|
|
577
|
-
this.resolvePromise?.(event);
|
|
578
|
-
this._setHealth(true);
|
|
579
425
|
}
|
|
426
|
+
|
|
427
|
+
this.resolvePromise?.(data);
|
|
428
|
+
this._setHealth(true);
|
|
580
429
|
}
|
|
581
430
|
|
|
582
431
|
// trigger the event..
|
|
583
432
|
this.lastEvent = new Date();
|
|
584
|
-
this.
|
|
585
|
-
tags: ['connection'],
|
|
586
|
-
event,
|
|
587
|
-
wsID,
|
|
588
|
-
});
|
|
433
|
+
this._log('onmessage() - onmessage callback', { event, wsID });
|
|
589
434
|
|
|
590
435
|
if (data && data.type === 'health.check') {
|
|
591
436
|
this.scheduleNextPing();
|
|
592
437
|
}
|
|
593
438
|
|
|
594
|
-
this.
|
|
439
|
+
this.client.handleEvent(event);
|
|
595
440
|
this.scheduleConnectionCheck();
|
|
596
441
|
};
|
|
597
442
|
|
|
598
443
|
onclose = (wsID: number, event: WebSocket.CloseEvent) => {
|
|
599
444
|
if (this.wsID !== wsID) return;
|
|
600
445
|
|
|
601
|
-
this.
|
|
602
|
-
tags: ['connection'],
|
|
603
|
-
event,
|
|
604
|
-
wsID,
|
|
605
|
-
});
|
|
446
|
+
this._log('onclose() - onclose callback - ' + event.code, { event, wsID });
|
|
606
447
|
|
|
607
448
|
if (event.code === chatCodes.WS_CLOSED_SUCCESS) {
|
|
608
449
|
// this is a permanent error raised by stream..
|
|
609
450
|
// usually caused by invalid auth details
|
|
610
|
-
const error = new Error(
|
|
611
|
-
`WS connection reject with error ${event.reason}`,
|
|
612
|
-
) as Error & WebSocket.CloseEvent;
|
|
451
|
+
const error = new Error(`WS connection reject with error ${event.reason}`) as Error & WebSocket.CloseEvent;
|
|
613
452
|
|
|
614
453
|
error.reason = event.reason;
|
|
615
454
|
error.code = event.code;
|
|
@@ -617,14 +456,7 @@ export class StableWSConnection<
|
|
|
617
456
|
error.target = event.target;
|
|
618
457
|
|
|
619
458
|
this.rejectPromise?.(error);
|
|
620
|
-
this.
|
|
621
|
-
'info',
|
|
622
|
-
`connection:onclose() - WS connection reject with error ${event.reason}`,
|
|
623
|
-
{
|
|
624
|
-
tags: ['connection'],
|
|
625
|
-
event,
|
|
626
|
-
},
|
|
627
|
-
);
|
|
459
|
+
this._log(`onclose() - WS connection reject with error ${event.reason}`, { event });
|
|
628
460
|
} else {
|
|
629
461
|
this.consecutiveFailures += 1;
|
|
630
462
|
this.totalFailures += 1;
|
|
@@ -633,14 +465,7 @@ export class StableWSConnection<
|
|
|
633
465
|
|
|
634
466
|
this.rejectPromise?.(this._errorFromWSEvent(event));
|
|
635
467
|
|
|
636
|
-
this.
|
|
637
|
-
'info',
|
|
638
|
-
`connection:onclose() - WS connection closed. Calling reconnect ...`,
|
|
639
|
-
{
|
|
640
|
-
tags: ['connection'],
|
|
641
|
-
event,
|
|
642
|
-
},
|
|
643
|
-
);
|
|
468
|
+
this._log(`onclose() - WS connection closed. Calling reconnect ...`, { event });
|
|
644
469
|
|
|
645
470
|
// reconnect if its an abnormal failure
|
|
646
471
|
this._reconnect();
|
|
@@ -656,10 +481,7 @@ export class StableWSConnection<
|
|
|
656
481
|
this.isConnecting = false;
|
|
657
482
|
|
|
658
483
|
this.rejectPromise?.(this._errorFromWSEvent(event));
|
|
659
|
-
this.
|
|
660
|
-
tags: ['connection'],
|
|
661
|
-
event,
|
|
662
|
-
});
|
|
484
|
+
this._log(`onerror() - WS connection resulted into error`, { event });
|
|
663
485
|
|
|
664
486
|
this._reconnect();
|
|
665
487
|
};
|
|
@@ -672,37 +494,29 @@ export class StableWSConnection<
|
|
|
672
494
|
*
|
|
673
495
|
*/
|
|
674
496
|
_setHealth = (healthy: boolean) => {
|
|
675
|
-
if (healthy
|
|
676
|
-
// yes we are online:
|
|
677
|
-
this.isHealthy = true;
|
|
678
|
-
this.eventCallback({
|
|
679
|
-
type: 'connection.changed',
|
|
680
|
-
online: true,
|
|
681
|
-
});
|
|
682
|
-
}
|
|
497
|
+
if (healthy === this.isHealthy) return;
|
|
683
498
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
type: 'connection.changed',
|
|
691
|
-
online: false,
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
}, 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;
|
|
695
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);
|
|
696
513
|
};
|
|
697
514
|
|
|
698
515
|
/**
|
|
699
516
|
* _errorFromWSEvent - Creates an error object for the WS event
|
|
700
517
|
*
|
|
701
518
|
*/
|
|
702
|
-
_errorFromWSEvent = (
|
|
703
|
-
event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
|
|
704
|
-
isWSFailure = true,
|
|
705
|
-
) => {
|
|
519
|
+
_errorFromWSEvent = (event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent, isWSFailure = true) => {
|
|
706
520
|
let code;
|
|
707
521
|
let statusCode;
|
|
708
522
|
let message;
|
|
@@ -719,14 +533,9 @@ export class StableWSConnection<
|
|
|
719
533
|
}
|
|
720
534
|
|
|
721
535
|
// Keeping this `warn` level log, to avoid cluttering of error logs from ws failures.
|
|
722
|
-
this.
|
|
723
|
-
tags: ['connection'],
|
|
724
|
-
event,
|
|
725
|
-
});
|
|
536
|
+
this._log(`_errorFromWSEvent() - WS failed with code ${code}`, { event }, 'warn');
|
|
726
537
|
|
|
727
|
-
const error = new Error(
|
|
728
|
-
`WS failed with code ${code} and reason - ${message}`,
|
|
729
|
-
) as Error & {
|
|
538
|
+
const error = new Error(`WS failed with code ${code} and reason - ${message}`) as Error & {
|
|
730
539
|
code?: string | number;
|
|
731
540
|
isWSFailure?: boolean;
|
|
732
541
|
StatusCode?: string | number;
|
|
@@ -741,27 +550,6 @@ export class StableWSConnection<
|
|
|
741
550
|
return error;
|
|
742
551
|
};
|
|
743
552
|
|
|
744
|
-
/**
|
|
745
|
-
* _listenForConnectionChanges - Adds an event listener for the browser going online or offline
|
|
746
|
-
*/
|
|
747
|
-
_listenForConnectionChanges = () => {
|
|
748
|
-
// (typeof window !== 'undefined') check is for environments where window is not defined, such as nextjs environment,
|
|
749
|
-
// and thus (window === undefined) will result in ReferenceError.
|
|
750
|
-
if (typeof window !== 'undefined' && window?.addEventListener) {
|
|
751
|
-
window.addEventListener('offline', this.onlineStatusChanged);
|
|
752
|
-
window.addEventListener('online', this.onlineStatusChanged);
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
_removeConnectionListeners = () => {
|
|
757
|
-
// (typeof window !== 'undefined') check is for environments where window is not defined, such as nextjs environment,
|
|
758
|
-
// and thus (window === undefined) will result in ReferenceError.
|
|
759
|
-
if (typeof window !== 'undefined' && window?.removeEventListener) {
|
|
760
|
-
window.removeEventListener('offline', this.onlineStatusChanged);
|
|
761
|
-
window.removeEventListener('online', this.onlineStatusChanged);
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
|
|
765
553
|
/**
|
|
766
554
|
* _destroyCurrentWSConnection - Removes the current WS connection
|
|
767
555
|
*
|
|
@@ -772,13 +560,8 @@ export class StableWSConnection<
|
|
|
772
560
|
this.wsID += 1;
|
|
773
561
|
|
|
774
562
|
try {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (this.ws && this.ws.close) {
|
|
780
|
-
this.ws.close();
|
|
781
|
-
}
|
|
563
|
+
this?.ws?.removeAllListeners();
|
|
564
|
+
this?.ws?.close();
|
|
782
565
|
} catch (e) {
|
|
783
566
|
// we don't care
|
|
784
567
|
}
|
|
@@ -788,34 +571,12 @@ export class StableWSConnection<
|
|
|
788
571
|
* _setupPromise - sets up the this.connectOpen promise
|
|
789
572
|
*/
|
|
790
573
|
_setupConnectionPromise = () => {
|
|
791
|
-
const that = this;
|
|
792
574
|
this.isResolved = false;
|
|
793
575
|
/** a promise that is resolved once ws.open is called */
|
|
794
|
-
this.connectionOpen = new Promise<
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
})
|
|
798
|
-
(e) => {
|
|
799
|
-
if (e.data && typeof e.data === 'string') {
|
|
800
|
-
const data = JSON.parse(e.data) as ConnectionOpen<
|
|
801
|
-
ChannelType,
|
|
802
|
-
CommandType,
|
|
803
|
-
UserType
|
|
804
|
-
> & {
|
|
805
|
-
error?: unknown;
|
|
806
|
-
};
|
|
807
|
-
if (data && data.error != null) {
|
|
808
|
-
throw new Error(JSON.stringify(data.error));
|
|
809
|
-
}
|
|
810
|
-
return data;
|
|
811
|
-
} else {
|
|
812
|
-
return undefined;
|
|
813
|
-
}
|
|
814
|
-
},
|
|
815
|
-
(error) => {
|
|
816
|
-
throw error;
|
|
817
|
-
},
|
|
818
|
-
);
|
|
576
|
+
this.connectionOpen = new Promise<ConnectionOpen<ChannelType, CommandType, UserType>>((resolve, reject) => {
|
|
577
|
+
this.resolvePromise = resolve;
|
|
578
|
+
this.rejectPromise = reject;
|
|
579
|
+
});
|
|
819
580
|
};
|
|
820
581
|
|
|
821
582
|
/**
|
|
@@ -829,12 +590,7 @@ export class StableWSConnection<
|
|
|
829
590
|
// 30 seconds is the recommended interval (messenger uses this)
|
|
830
591
|
this.healthCheckTimeoutRef = setTimeout(() => {
|
|
831
592
|
// send the healthcheck.., server replies with a health check event
|
|
832
|
-
const data = [
|
|
833
|
-
{
|
|
834
|
-
type: 'health.check',
|
|
835
|
-
client_id: this.clientID,
|
|
836
|
-
},
|
|
837
|
-
];
|
|
593
|
+
const data = [{ type: 'health.check', client_id: this.client.clientID }];
|
|
838
594
|
// try to send on the connection
|
|
839
595
|
try {
|
|
840
596
|
this.ws?.send(JSON.stringify(data));
|
|
@@ -856,13 +612,8 @@ export class StableWSConnection<
|
|
|
856
612
|
|
|
857
613
|
this.connectionCheckTimeoutRef = setTimeout(() => {
|
|
858
614
|
const now = new Date();
|
|
859
|
-
if (
|
|
860
|
-
this.
|
|
861
|
-
now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout
|
|
862
|
-
) {
|
|
863
|
-
this.logger('info', 'connection:scheduleConnectionCheck - going to reconnect', {
|
|
864
|
-
tags: ['connection'],
|
|
865
|
-
});
|
|
615
|
+
if (this.lastEvent && now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout) {
|
|
616
|
+
this._log('scheduleConnectionCheck - going to reconnect');
|
|
866
617
|
this._setHealth(false);
|
|
867
618
|
this._reconnect();
|
|
868
619
|
}
|