@stream-io/video-client 0.0.1-alpha.7
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/LICENSE +219 -0
- package/README.md +14 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +14663 -0
- package/dist/index.js.map +1 -0
- package/dist/src/Batcher.d.ts +12 -0
- package/dist/src/CallDropScheduler.d.ts +44 -0
- package/dist/src/StreamSfuClient.d.ts +25 -0
- package/dist/src/StreamVideoClient.d.ts +145 -0
- package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
- package/dist/src/config/defaultConfigs.d.ts +2 -0
- package/dist/src/config/types.d.ts +29 -0
- package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
- package/dist/src/coordinator/connection/base64.d.ts +2 -0
- package/dist/src/coordinator/connection/client.d.ts +174 -0
- package/dist/src/coordinator/connection/connection.d.ts +139 -0
- package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
- package/dist/src/coordinator/connection/errors.d.ts +16 -0
- package/dist/src/coordinator/connection/events.d.ts +7 -0
- package/dist/src/coordinator/connection/insights.d.ts +58 -0
- package/dist/src/coordinator/connection/signing.d.ts +30 -0
- package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
- package/dist/src/coordinator/connection/types.d.ts +96 -0
- package/dist/src/coordinator/connection/utils.d.ts +25 -0
- package/dist/src/devices.d.ts +79 -0
- package/dist/src/events/call.d.ts +26 -0
- package/dist/src/events/internal.d.ts +8 -0
- package/dist/src/events/participant.d.ts +21 -0
- package/dist/src/events/speaker.d.ts +10 -0
- package/dist/src/gen/coordinator/index.d.ts +1664 -0
- package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
- package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
- package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
- package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
- package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
- package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
- package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
- package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
- package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
- package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
- package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
- package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
- package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
- package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
- package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
- package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
- package/dist/src/helpers/browsers.d.ts +8 -0
- package/dist/src/helpers/sound-detector.d.ts +34 -0
- package/dist/src/rpc/createClient.d.ts +10 -0
- package/dist/src/rpc/index.d.ts +2 -0
- package/dist/src/rpc/latency.d.ts +9 -0
- package/dist/src/rtc/Call.d.ts +180 -0
- package/dist/src/rtc/CallMetadata.d.ts +9 -0
- package/dist/src/rtc/Dispatcher.d.ts +9 -0
- package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
- package/dist/src/rtc/callEventHandlers.d.ts +5 -0
- package/dist/src/rtc/codecs.d.ts +2 -0
- package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -0
- package/dist/src/rtc/publisher.d.ts +53 -0
- package/dist/src/rtc/signal.d.ts +5 -0
- package/dist/src/rtc/subscriber.d.ts +7 -0
- package/dist/src/rtc/types.d.ts +84 -0
- package/dist/src/rtc/videoLayers.d.ts +17 -0
- package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
- package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
- package/dist/src/stats/types.d.ts +42 -0
- package/dist/src/store/index.d.ts +2 -0
- package/dist/src/store/rxUtils.d.ts +18 -0
- package/dist/src/store/stateStore.d.ts +182 -0
- package/generate-openapi.sh +32 -0
- package/index.ts +30 -0
- package/openapitools.json +7 -0
- package/package.json +54 -0
- package/rollup.config.mjs +48 -0
- package/src/Batcher.ts +43 -0
- package/src/CallDropScheduler.ts +192 -0
- package/src/StreamSfuClient.ts +185 -0
- package/src/StreamVideoClient.ts +487 -0
- package/src/__tests__/StreamVideoClient.test.ts +83 -0
- package/src/config/defaultConfigs.ts +15 -0
- package/src/config/types.ts +30 -0
- package/src/coordinator/StreamCoordinatorClient.ts +111 -0
- package/src/coordinator/connection/base64.ts +80 -0
- package/src/coordinator/connection/client.ts +815 -0
- package/src/coordinator/connection/connection.ts +750 -0
- package/src/coordinator/connection/connection_fallback.ts +239 -0
- package/src/coordinator/connection/errors.ts +70 -0
- package/src/coordinator/connection/events.ts +10 -0
- package/src/coordinator/connection/insights.ts +88 -0
- package/src/coordinator/connection/signing.ts +104 -0
- package/src/coordinator/connection/token_manager.ts +160 -0
- package/src/coordinator/connection/types.ts +120 -0
- package/src/coordinator/connection/utils.ts +148 -0
- package/src/devices.ts +266 -0
- package/src/events/call.ts +166 -0
- package/src/events/internal.ts +47 -0
- package/src/events/participant.ts +97 -0
- package/src/events/speaker.ts +62 -0
- package/src/gen/coordinator/index.ts +1653 -0
- package/src/gen/google/protobuf/descriptor.ts +3466 -0
- package/src/gen/google/protobuf/duration.ts +232 -0
- package/src/gen/google/protobuf/struct.ts +481 -0
- package/src/gen/google/protobuf/timestamp.ts +291 -0
- package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
- package/src/gen/video/coordinator/call_v1/call.ts +651 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
- package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
- package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
- package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
- package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
- package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
- package/src/gen/video/coordinator/member_v1/member.ts +138 -0
- package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
- package/src/gen/video/coordinator/push_v1/push.ts +651 -0
- package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
- package/src/gen/video/coordinator/user_v1/user.ts +277 -0
- package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
- package/src/gen/video/sfu/event/events.ts +1962 -0
- package/src/gen/video/sfu/models/models.ts +1062 -0
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
- package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
- package/src/helpers/browsers.ts +13 -0
- package/src/helpers/sound-detector.ts +85 -0
- package/src/rpc/createClient.ts +50 -0
- package/src/rpc/index.ts +2 -0
- package/src/rpc/latency.ts +43 -0
- package/src/rtc/Call.ts +585 -0
- package/src/rtc/CallMetadata.ts +24 -0
- package/src/rtc/Dispatcher.ts +46 -0
- package/src/rtc/IceTrickleBuffer.ts +21 -0
- package/src/rtc/callEventHandlers.ts +37 -0
- package/src/rtc/codecs.ts +61 -0
- package/src/rtc/helpers/iceCandidate.ts +16 -0
- package/src/rtc/helpers/tracks.ts +18 -0
- package/src/rtc/publisher.ts +305 -0
- package/src/rtc/signal.ts +34 -0
- package/src/rtc/subscriber.ts +85 -0
- package/src/rtc/types.ts +105 -0
- package/src/rtc/videoLayers.ts +103 -0
- package/src/stats/coordinator-stats-reporter.ts +167 -0
- package/src/stats/state-store-stats-reporter.ts +364 -0
- package/src/stats/types.ts +46 -0
- package/src/store/index.ts +2 -0
- package/src/store/rxUtils.ts +42 -0
- package/src/store/stateStore.ts +341 -0
- package/tsconfig.json +25 -0
- package/typedoc.json +11 -0
- package/vite.config.ts +11 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import WebSocket from 'isomorphic-ws';
|
|
2
|
+
import { StreamClient } from './client';
|
|
3
|
+
import {
|
|
4
|
+
buildWsFatalInsight,
|
|
5
|
+
buildWsSuccessAfterFailureInsight,
|
|
6
|
+
postInsights,
|
|
7
|
+
} from './insights';
|
|
8
|
+
import {
|
|
9
|
+
addConnectionEventListeners,
|
|
10
|
+
chatCodes,
|
|
11
|
+
convertErrorToJson,
|
|
12
|
+
randomId,
|
|
13
|
+
removeConnectionEventListeners,
|
|
14
|
+
retryInterval,
|
|
15
|
+
sleep,
|
|
16
|
+
} from './utils';
|
|
17
|
+
import type { ConnectAPIResponse, ConnectionOpen, LogLevel, UR } from './types';
|
|
18
|
+
import type { VideoWSAuthMessageRequest } from '../../gen/coordinator';
|
|
19
|
+
|
|
20
|
+
// Type guards to check WebSocket error type
|
|
21
|
+
const isCloseEvent = (
|
|
22
|
+
res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
|
|
23
|
+
): res is WebSocket.CloseEvent =>
|
|
24
|
+
(res as WebSocket.CloseEvent).code !== undefined;
|
|
25
|
+
|
|
26
|
+
const isErrorEvent = (
|
|
27
|
+
res: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
|
|
28
|
+
): res is WebSocket.ErrorEvent =>
|
|
29
|
+
(res as WebSocket.ErrorEvent).error !== undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* StableWSConnection - A WS connection that reconnects upon failure.
|
|
33
|
+
* - the browser will sometimes report that you're online or offline
|
|
34
|
+
* - the WS connection can break and fail (there is a 30s health check)
|
|
35
|
+
* - sometimes your WS connection will seem to work while the user is in fact offline
|
|
36
|
+
* - to speed up online/offline detection you can use the window.addEventListener('offline');
|
|
37
|
+
*
|
|
38
|
+
* There are 4 ways in which a connection can become unhealthy:
|
|
39
|
+
* - websocket.onerror is called
|
|
40
|
+
* - websocket.onclose is called
|
|
41
|
+
* - the health check fails and no event is received for ~40 seconds
|
|
42
|
+
* - the browser indicates the connection is now offline
|
|
43
|
+
*
|
|
44
|
+
* There are 2 assumptions we make about the server:
|
|
45
|
+
* - state can be recovered by querying the channel again
|
|
46
|
+
* - if the servers fails to publish a message to the client, the WS connection is destroyed
|
|
47
|
+
*/
|
|
48
|
+
export class StableWSConnection {
|
|
49
|
+
// local vars
|
|
50
|
+
connectionID?: string;
|
|
51
|
+
connectionOpen?: ConnectAPIResponse;
|
|
52
|
+
consecutiveFailures: number;
|
|
53
|
+
pingInterval: number;
|
|
54
|
+
healthCheckTimeoutRef?: NodeJS.Timeout;
|
|
55
|
+
isConnecting: boolean;
|
|
56
|
+
isDisconnected: boolean;
|
|
57
|
+
isHealthy: boolean;
|
|
58
|
+
isResolved?: boolean;
|
|
59
|
+
lastEvent: Date | null;
|
|
60
|
+
connectionCheckTimeout: number;
|
|
61
|
+
connectionCheckTimeoutRef?: NodeJS.Timeout;
|
|
62
|
+
rejectPromise?: (
|
|
63
|
+
reason?: Error & {
|
|
64
|
+
code?: string | number;
|
|
65
|
+
isWSFailure?: boolean;
|
|
66
|
+
StatusCode?: string | number;
|
|
67
|
+
},
|
|
68
|
+
) => void;
|
|
69
|
+
requestID: string | undefined;
|
|
70
|
+
resolvePromise?: (value: ConnectionOpen) => void;
|
|
71
|
+
totalFailures: number;
|
|
72
|
+
ws?: WebSocket;
|
|
73
|
+
wsID: number;
|
|
74
|
+
|
|
75
|
+
token = '';
|
|
76
|
+
|
|
77
|
+
client: StreamClient;
|
|
78
|
+
|
|
79
|
+
constructor(client: StreamClient) {
|
|
80
|
+
this.client = client;
|
|
81
|
+
/** consecutive failures influence the duration of the timeout */
|
|
82
|
+
this.consecutiveFailures = 0;
|
|
83
|
+
/** keep track of the total number of failures */
|
|
84
|
+
this.totalFailures = 0;
|
|
85
|
+
/** We only make 1 attempt to reconnect at the same time.. */
|
|
86
|
+
this.isConnecting = false;
|
|
87
|
+
/** To avoid reconnect if client is disconnected */
|
|
88
|
+
this.isDisconnected = false;
|
|
89
|
+
/** Boolean that indicates if the connection promise is resolved */
|
|
90
|
+
this.isResolved = false;
|
|
91
|
+
/** Boolean that indicates if we have a working connection to the server */
|
|
92
|
+
this.isHealthy = false;
|
|
93
|
+
/** Incremented when a new WS connection is made */
|
|
94
|
+
this.wsID = 1;
|
|
95
|
+
/** Store the last event time for health checks */
|
|
96
|
+
this.lastEvent = null;
|
|
97
|
+
/** Send a health check message every 25 seconds */
|
|
98
|
+
this.pingInterval = 25 * 1000;
|
|
99
|
+
this.connectionCheckTimeout = this.pingInterval + 10 * 1000;
|
|
100
|
+
|
|
101
|
+
addConnectionEventListeners(this.onlineStatusChanged);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_log(msg: string, extra: UR = {}, level: LogLevel = 'info') {
|
|
105
|
+
console.log(msg, extra);
|
|
106
|
+
this.client.logger(level, 'connection:' + msg, {
|
|
107
|
+
tags: ['connection'],
|
|
108
|
+
...extra,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setClient = (client: StreamClient) => {
|
|
113
|
+
this.client = client;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* connect - Connect to the WS URL
|
|
118
|
+
* the default 15s timeout allows between 2~3 tries
|
|
119
|
+
* @return {ConnectAPIResponse<ChannelType, CommandType, UserType>} Promise that completes once the first health check message is received
|
|
120
|
+
*/
|
|
121
|
+
async connect(timeout = 15000) {
|
|
122
|
+
if (this.isConnecting) {
|
|
123
|
+
throw Error(
|
|
124
|
+
`You've called connect twice, can only attempt 1 connection at the time`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.isDisconnected = false;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const healthCheck = await this._connect();
|
|
132
|
+
this.consecutiveFailures = 0;
|
|
133
|
+
|
|
134
|
+
this._log(
|
|
135
|
+
`connect() - Established ws connection with healthcheck: ${healthCheck}`,
|
|
136
|
+
);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
this.isHealthy = false;
|
|
139
|
+
this.consecutiveFailures += 1;
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
// @ts-ignore
|
|
143
|
+
error.code === chatCodes.TOKEN_EXPIRED &&
|
|
144
|
+
!this.client.tokenManager.isStatic()
|
|
145
|
+
) {
|
|
146
|
+
this._log(
|
|
147
|
+
'connect() - WS failure due to expired token, so going to try to reload token and reconnect',
|
|
148
|
+
);
|
|
149
|
+
this._reconnect({ refreshToken: true });
|
|
150
|
+
} else {
|
|
151
|
+
// @ts-ignore
|
|
152
|
+
if (!error.isWSFailure) {
|
|
153
|
+
// API rejected the connection and we should not retry
|
|
154
|
+
throw new Error(
|
|
155
|
+
JSON.stringify({
|
|
156
|
+
// @ts-ignore
|
|
157
|
+
code: error.code,
|
|
158
|
+
// @ts-ignore
|
|
159
|
+
StatusCode: error.StatusCode,
|
|
160
|
+
// @ts-ignore
|
|
161
|
+
message: error.message,
|
|
162
|
+
// @ts-ignore
|
|
163
|
+
isWSFailure: error.isWSFailure,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return await this._waitForHealthy(timeout);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* _waitForHealthy polls the promise connection to see if its resolved until it times out
|
|
175
|
+
* the default 15s timeout allows between 2~3 tries
|
|
176
|
+
* @param timeout duration(ms)
|
|
177
|
+
*/
|
|
178
|
+
async _waitForHealthy(timeout = 15000) {
|
|
179
|
+
return Promise.race([
|
|
180
|
+
(async () => {
|
|
181
|
+
const interval = 50; // ms
|
|
182
|
+
for (let i = 0; i <= timeout; i += interval) {
|
|
183
|
+
try {
|
|
184
|
+
return await this.connectionOpen;
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
if (i === timeout) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
JSON.stringify({
|
|
189
|
+
code: error.code,
|
|
190
|
+
StatusCode: error.StatusCode,
|
|
191
|
+
message: error.message,
|
|
192
|
+
isWSFailure: error.isWSFailure,
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
await sleep(interval);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
})(),
|
|
200
|
+
(async () => {
|
|
201
|
+
await sleep(timeout);
|
|
202
|
+
this.isConnecting = false;
|
|
203
|
+
throw new Error(
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
code: '',
|
|
206
|
+
StatusCode: '',
|
|
207
|
+
message: 'initial WS connection could not be established',
|
|
208
|
+
isWSFailure: true,
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
})(),
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Builds and returns the url for websocket.
|
|
217
|
+
* @private
|
|
218
|
+
* @returns url string
|
|
219
|
+
*/
|
|
220
|
+
_buildUrl = () => {
|
|
221
|
+
const params = new URLSearchParams();
|
|
222
|
+
// const qs = encodeURIComponent(this.client._buildWSPayload(this.requestID));
|
|
223
|
+
// params.set('json', qs);
|
|
224
|
+
params.set('api_key', this.client.key);
|
|
225
|
+
params.set('stream-auth-type', this.client.getAuthType());
|
|
226
|
+
params.set('X-Stream-Client', this.client.getUserAgent());
|
|
227
|
+
// params.append('authorization', this.client._getToken()!);
|
|
228
|
+
|
|
229
|
+
return `${this.client.wsBaseURL}/connect?${params.toString()}`;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* disconnect - Disconnect the connection and doesn't recover...
|
|
234
|
+
*
|
|
235
|
+
*/
|
|
236
|
+
disconnect(timeout?: number) {
|
|
237
|
+
this._log(
|
|
238
|
+
`disconnect() - Closing the websocket connection for wsID ${this.wsID}`,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
this.wsID += 1;
|
|
242
|
+
this.isConnecting = false;
|
|
243
|
+
this.isDisconnected = true;
|
|
244
|
+
|
|
245
|
+
// start by removing all the listeners
|
|
246
|
+
if (this.healthCheckTimeoutRef) {
|
|
247
|
+
clearInterval(this.healthCheckTimeoutRef);
|
|
248
|
+
}
|
|
249
|
+
if (this.connectionCheckTimeoutRef) {
|
|
250
|
+
clearInterval(this.connectionCheckTimeoutRef);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
removeConnectionEventListeners(this.onlineStatusChanged);
|
|
254
|
+
|
|
255
|
+
this.isHealthy = false;
|
|
256
|
+
|
|
257
|
+
// remove ws handlers...
|
|
258
|
+
if (this.ws && this.ws.removeAllListeners) {
|
|
259
|
+
this.ws.removeAllListeners();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let isClosedPromise: Promise<void>;
|
|
263
|
+
// and finally close...
|
|
264
|
+
// Assigning to local here because we will remove it from this before the
|
|
265
|
+
// promise resolves.
|
|
266
|
+
const { ws } = this;
|
|
267
|
+
if (ws && ws.close && ws.readyState === ws.OPEN) {
|
|
268
|
+
isClosedPromise = new Promise((resolve) => {
|
|
269
|
+
const onclose = (event: WebSocket.CloseEvent) => {
|
|
270
|
+
this._log(
|
|
271
|
+
`disconnect() - resolving isClosedPromise ${
|
|
272
|
+
event ? 'with' : 'without'
|
|
273
|
+
} close frame`,
|
|
274
|
+
{ event },
|
|
275
|
+
);
|
|
276
|
+
resolve();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
ws.onclose = onclose;
|
|
280
|
+
// In case we don't receive close frame websocket server in time,
|
|
281
|
+
// lets not wait for more than 1 seconds.
|
|
282
|
+
setTimeout(onclose, timeout != null ? timeout : 1000);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
this._log(
|
|
286
|
+
`disconnect() - Manually closed connection by calling client.disconnect()`,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
ws.close(
|
|
290
|
+
chatCodes.WS_CLOSED_SUCCESS,
|
|
291
|
+
'Manually closed connection by calling client.disconnect()',
|
|
292
|
+
);
|
|
293
|
+
} else {
|
|
294
|
+
this._log(
|
|
295
|
+
`disconnect() - ws connection doesn't exist or it is already closed.`,
|
|
296
|
+
);
|
|
297
|
+
isClosedPromise = Promise.resolve();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
delete this.ws;
|
|
301
|
+
|
|
302
|
+
return isClosedPromise;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* _connect - Connect to the WS endpoint
|
|
307
|
+
*
|
|
308
|
+
* @return {ConnectAPIResponse<ChannelType, CommandType, UserType>} Promise that completes once the first health check message is received
|
|
309
|
+
*/
|
|
310
|
+
async _connect() {
|
|
311
|
+
if (
|
|
312
|
+
this.isConnecting ||
|
|
313
|
+
(this.isDisconnected && this.client.options.enableWSFallback)
|
|
314
|
+
)
|
|
315
|
+
return; // simply ignore _connect if it's currently trying to connect
|
|
316
|
+
this.isConnecting = true;
|
|
317
|
+
this.requestID = randomId();
|
|
318
|
+
this.client.insightMetrics.connectionStartTimestamp = new Date().getTime();
|
|
319
|
+
let isTokenReady = false;
|
|
320
|
+
try {
|
|
321
|
+
this._log(`_connect() - waiting for token`);
|
|
322
|
+
await this.client.tokenManager.tokenReady();
|
|
323
|
+
isTokenReady = true;
|
|
324
|
+
} catch (e) {
|
|
325
|
+
// token provider has failed before, so try again
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
if (!isTokenReady) {
|
|
330
|
+
this._log(
|
|
331
|
+
`_connect() - tokenProvider failed before, so going to retry`,
|
|
332
|
+
);
|
|
333
|
+
await this.client.tokenManager.loadToken();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this._setupConnectionPromise();
|
|
337
|
+
const wsURL = this._buildUrl();
|
|
338
|
+
this._log(`_connect() - Connecting to ${wsURL}`, {
|
|
339
|
+
wsURL,
|
|
340
|
+
requestID: this.requestID,
|
|
341
|
+
});
|
|
342
|
+
this.ws = new WebSocket(wsURL);
|
|
343
|
+
this.ws.onopen = this.onopen.bind(this, this.wsID);
|
|
344
|
+
this.ws.onclose = this.onclose.bind(this, this.wsID);
|
|
345
|
+
this.ws.onerror = this.onerror.bind(this, this.wsID);
|
|
346
|
+
this.ws.onmessage = this.onmessage.bind(this, this.wsID);
|
|
347
|
+
const response = await this.connectionOpen;
|
|
348
|
+
this.isConnecting = false;
|
|
349
|
+
|
|
350
|
+
if (response) {
|
|
351
|
+
this.connectionID = response.connection_id;
|
|
352
|
+
if (
|
|
353
|
+
this.client.insightMetrics.wsConsecutiveFailures > 0 &&
|
|
354
|
+
this.client.options.enableInsights
|
|
355
|
+
) {
|
|
356
|
+
postInsights(
|
|
357
|
+
'ws_success_after_failure',
|
|
358
|
+
buildWsSuccessAfterFailureInsight(
|
|
359
|
+
this as unknown as StableWSConnection,
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
this.client.insightMetrics.wsConsecutiveFailures = 0;
|
|
363
|
+
}
|
|
364
|
+
return response;
|
|
365
|
+
}
|
|
366
|
+
} catch (err) {
|
|
367
|
+
this.isConnecting = false;
|
|
368
|
+
// @ts-ignore
|
|
369
|
+
this._log(`_connect() - Error - `, err);
|
|
370
|
+
if (this.client.options.enableInsights) {
|
|
371
|
+
this.client.insightMetrics.wsConsecutiveFailures++;
|
|
372
|
+
this.client.insightMetrics.wsTotalFailures++;
|
|
373
|
+
|
|
374
|
+
const insights = buildWsFatalInsight(
|
|
375
|
+
this as unknown as StableWSConnection,
|
|
376
|
+
convertErrorToJson(err as Error),
|
|
377
|
+
);
|
|
378
|
+
postInsights?.('ws_fatal', insights);
|
|
379
|
+
}
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* _reconnect - Retry the connection to WS endpoint
|
|
386
|
+
*
|
|
387
|
+
* @param {{ interval?: number; refreshToken?: boolean }} options Following options are available
|
|
388
|
+
*
|
|
389
|
+
* - `interval` {int} number of ms that function should wait before reconnecting
|
|
390
|
+
* - `refreshToken` {boolean} reload/refresh user token be refreshed before attempting reconnection.
|
|
391
|
+
*/
|
|
392
|
+
async _reconnect(
|
|
393
|
+
options: { interval?: number; refreshToken?: boolean } = {},
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
this._log('_reconnect() - Initiating the reconnect');
|
|
396
|
+
|
|
397
|
+
// only allow 1 connection at the time
|
|
398
|
+
if (this.isConnecting || this.isHealthy) {
|
|
399
|
+
this._log('_reconnect() - Abort (1) since already connecting or healthy');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// reconnect in case of on error or on close
|
|
404
|
+
// also reconnect if the health check cycle fails
|
|
405
|
+
let interval = options.interval;
|
|
406
|
+
if (!interval) {
|
|
407
|
+
interval = retryInterval(this.consecutiveFailures);
|
|
408
|
+
}
|
|
409
|
+
// reconnect, or try again after a little while...
|
|
410
|
+
await sleep(interval);
|
|
411
|
+
|
|
412
|
+
// Check once again if by some other call to _reconnect is active or connection is
|
|
413
|
+
// already restored, then no need to proceed.
|
|
414
|
+
if (this.isConnecting || this.isHealthy) {
|
|
415
|
+
this._log('_reconnect() - Abort (2) since already connecting or healthy');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (this.isDisconnected /* && this.client.options.enableWSFallback */) {
|
|
420
|
+
this._log('_reconnect() - Abort (3) since disconnect() is called');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
this._log('_reconnect() - Destroying current WS connection');
|
|
425
|
+
|
|
426
|
+
// cleanup the old connection
|
|
427
|
+
this._destroyCurrentWSConnection();
|
|
428
|
+
|
|
429
|
+
if (options.refreshToken) {
|
|
430
|
+
await this.client.tokenManager.loadToken();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
await this._connect();
|
|
435
|
+
this._log('_reconnect() - Waiting for recoverCallBack');
|
|
436
|
+
// await this.client.recoverState();
|
|
437
|
+
this._log('_reconnect() - Finished recoverCallBack');
|
|
438
|
+
|
|
439
|
+
this.consecutiveFailures = 0;
|
|
440
|
+
} catch (error: any) {
|
|
441
|
+
this.isHealthy = false;
|
|
442
|
+
this.consecutiveFailures += 1;
|
|
443
|
+
if (
|
|
444
|
+
error.code === chatCodes.TOKEN_EXPIRED &&
|
|
445
|
+
!this.client.tokenManager.isStatic()
|
|
446
|
+
) {
|
|
447
|
+
this._log(
|
|
448
|
+
'_reconnect() - WS failure due to expired token, so going to try to reload token and reconnect',
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
return this._reconnect({ refreshToken: true });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// reconnect on WS failures, don't reconnect if there is a code bug
|
|
455
|
+
if (error.isWSFailure) {
|
|
456
|
+
this._log('_reconnect() - WS failure, so going to try to reconnect');
|
|
457
|
+
|
|
458
|
+
this._reconnect();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
this._log('_reconnect() - == END ==');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* onlineStatusChanged - this function is called when the browser connects or disconnects from the internet.
|
|
466
|
+
*
|
|
467
|
+
* @param {Event} event Event with type online or offline
|
|
468
|
+
*
|
|
469
|
+
*/
|
|
470
|
+
onlineStatusChanged = (event: Event) => {
|
|
471
|
+
if (event.type === 'offline') {
|
|
472
|
+
// mark the connection as down
|
|
473
|
+
this._log('onlineStatusChanged() - Status changing to offline');
|
|
474
|
+
this._setHealth(false);
|
|
475
|
+
} else if (event.type === 'online') {
|
|
476
|
+
// retry right now...
|
|
477
|
+
// We check this.isHealthy, not sure if it's always
|
|
478
|
+
// smart to create a new WS connection if the old one is still up and running.
|
|
479
|
+
// it's possible we didn't miss any messages, so this process is just expensive and not needed.
|
|
480
|
+
this._log(
|
|
481
|
+
`onlineStatusChanged() - Status changing to online. isHealthy: ${this.isHealthy}`,
|
|
482
|
+
);
|
|
483
|
+
if (!this.isHealthy) {
|
|
484
|
+
this._reconnect({ interval: 10 });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
onopen = (wsID: number) => {
|
|
490
|
+
if (this.wsID !== wsID) return;
|
|
491
|
+
|
|
492
|
+
const user = this.client.user;
|
|
493
|
+
if (!user) {
|
|
494
|
+
console.error(`User not set, can't connect to WS`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const token = this.client._getToken();
|
|
499
|
+
if (!token) {
|
|
500
|
+
console.error(`Token not set, can't connect authenticate`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const authMessage: VideoWSAuthMessageRequest = {
|
|
505
|
+
token,
|
|
506
|
+
user_details: {
|
|
507
|
+
id: user.id,
|
|
508
|
+
role: user.role,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
this.ws?.send(JSON.stringify(authMessage));
|
|
513
|
+
this._log('onopen() - onopen callback', { wsID });
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
onmessage = (wsID: number, event: WebSocket.MessageEvent) => {
|
|
517
|
+
if (this.wsID !== wsID) return;
|
|
518
|
+
|
|
519
|
+
this._log('onmessage() - onmessage callback', { event, wsID });
|
|
520
|
+
const data = typeof event.data === 'string' ? JSON.parse(event.data) : null;
|
|
521
|
+
|
|
522
|
+
console.log('Received data', data);
|
|
523
|
+
|
|
524
|
+
// we wait till the first message before we consider the connection open..
|
|
525
|
+
// the reason for this is that auth errors and similar errors trigger a ws.onopen and immediately
|
|
526
|
+
// after that a ws.onclose..
|
|
527
|
+
if (!this.isResolved && data) {
|
|
528
|
+
this.isResolved = true;
|
|
529
|
+
if (data.error) {
|
|
530
|
+
this.rejectPromise?.(this._errorFromWSEvent(data, false));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
this.resolvePromise?.(data);
|
|
535
|
+
this._setHealth(true);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// trigger the event..
|
|
539
|
+
this.lastEvent = new Date();
|
|
540
|
+
|
|
541
|
+
if (data && data.type === 'health.check') {
|
|
542
|
+
this.scheduleNextPing();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
this.client.handleEvent(event);
|
|
546
|
+
this.scheduleConnectionCheck();
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
onclose = (wsID: number, event: WebSocket.CloseEvent) => {
|
|
550
|
+
if (this.wsID !== wsID) return;
|
|
551
|
+
|
|
552
|
+
this._log('onclose() - onclose callback - ' + event.code, { event, wsID });
|
|
553
|
+
|
|
554
|
+
if (event.code === chatCodes.WS_CLOSED_SUCCESS) {
|
|
555
|
+
// this is a permanent error raised by stream..
|
|
556
|
+
// usually caused by invalid auth details
|
|
557
|
+
const error = new Error(
|
|
558
|
+
`WS connection reject with error ${event.reason}`,
|
|
559
|
+
) as Error & WebSocket.CloseEvent;
|
|
560
|
+
|
|
561
|
+
error.reason = event.reason;
|
|
562
|
+
error.code = event.code;
|
|
563
|
+
error.wasClean = event.wasClean;
|
|
564
|
+
error.target = event.target;
|
|
565
|
+
|
|
566
|
+
this.rejectPromise?.(error);
|
|
567
|
+
this._log(`onclose() - WS connection reject with error ${event.reason}`, {
|
|
568
|
+
event,
|
|
569
|
+
});
|
|
570
|
+
} else {
|
|
571
|
+
this.consecutiveFailures += 1;
|
|
572
|
+
this.totalFailures += 1;
|
|
573
|
+
this._setHealth(false);
|
|
574
|
+
this.isConnecting = false;
|
|
575
|
+
|
|
576
|
+
this.rejectPromise?.(this._errorFromWSEvent(event));
|
|
577
|
+
|
|
578
|
+
this._log(`onclose() - WS connection closed. Calling reconnect ...`, {
|
|
579
|
+
event,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// reconnect if its an abnormal failure
|
|
583
|
+
this._reconnect();
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
onerror = (wsID: number, event: WebSocket.ErrorEvent) => {
|
|
588
|
+
if (this.wsID !== wsID) return;
|
|
589
|
+
|
|
590
|
+
this.consecutiveFailures += 1;
|
|
591
|
+
this.totalFailures += 1;
|
|
592
|
+
this._setHealth(false);
|
|
593
|
+
this.isConnecting = false;
|
|
594
|
+
|
|
595
|
+
this.rejectPromise?.(this._errorFromWSEvent(event));
|
|
596
|
+
this._log(`onerror() - WS connection resulted into error`, { event });
|
|
597
|
+
|
|
598
|
+
this._reconnect();
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* _setHealth - Sets the connection to healthy or unhealthy.
|
|
603
|
+
* Broadcasts an event in case the connection status changed.
|
|
604
|
+
*
|
|
605
|
+
* @param {boolean} healthy boolean indicating if the connection is healthy or not
|
|
606
|
+
*
|
|
607
|
+
*/
|
|
608
|
+
_setHealth = (healthy: boolean) => {
|
|
609
|
+
if (healthy === this.isHealthy) return;
|
|
610
|
+
|
|
611
|
+
this.isHealthy = healthy;
|
|
612
|
+
|
|
613
|
+
if (this.isHealthy) {
|
|
614
|
+
this.client.dispatchEvent({
|
|
615
|
+
type: 'connection.changed',
|
|
616
|
+
online: this.isHealthy,
|
|
617
|
+
});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// we're offline, wait few seconds and fire and event if still offline
|
|
622
|
+
setTimeout(() => {
|
|
623
|
+
if (this.isHealthy) return;
|
|
624
|
+
this.client.dispatchEvent({
|
|
625
|
+
type: 'connection.changed',
|
|
626
|
+
online: this.isHealthy,
|
|
627
|
+
});
|
|
628
|
+
}, 5000);
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* _errorFromWSEvent - Creates an error object for the WS event
|
|
633
|
+
*
|
|
634
|
+
*/
|
|
635
|
+
_errorFromWSEvent = (
|
|
636
|
+
event: WebSocket.CloseEvent | WebSocket.Data | WebSocket.ErrorEvent,
|
|
637
|
+
isWSFailure = true,
|
|
638
|
+
) => {
|
|
639
|
+
let code;
|
|
640
|
+
let statusCode;
|
|
641
|
+
let message;
|
|
642
|
+
if (isCloseEvent(event)) {
|
|
643
|
+
code = event.code;
|
|
644
|
+
statusCode = 'unknown';
|
|
645
|
+
message = event.reason;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (isErrorEvent(event)) {
|
|
649
|
+
code = event.error.code;
|
|
650
|
+
statusCode = event.error.StatusCode;
|
|
651
|
+
message = event.error.message;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Keeping this `warn` level log, to avoid cluttering of error logs from ws failures.
|
|
655
|
+
this._log(
|
|
656
|
+
`_errorFromWSEvent() - WS failed with code ${code}`,
|
|
657
|
+
{ event },
|
|
658
|
+
'warn',
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
const error = new Error(
|
|
662
|
+
`WS failed with code ${code} and reason - ${message}`,
|
|
663
|
+
) as Error & {
|
|
664
|
+
code?: string | number;
|
|
665
|
+
isWSFailure?: boolean;
|
|
666
|
+
StatusCode?: string | number;
|
|
667
|
+
};
|
|
668
|
+
error.code = code;
|
|
669
|
+
/**
|
|
670
|
+
* StatusCode does not exist on any event types but has been left
|
|
671
|
+
* as is to preserve JS functionality during the TS implementation
|
|
672
|
+
*/
|
|
673
|
+
error.StatusCode = statusCode;
|
|
674
|
+
error.isWSFailure = isWSFailure;
|
|
675
|
+
return error;
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* _destroyCurrentWSConnection - Removes the current WS connection
|
|
680
|
+
*
|
|
681
|
+
*/
|
|
682
|
+
_destroyCurrentWSConnection() {
|
|
683
|
+
// increment the ID, meaning we will ignore all messages from the old
|
|
684
|
+
// ws connection from now on.
|
|
685
|
+
this.wsID += 1;
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
this?.ws?.removeAllListeners();
|
|
689
|
+
this?.ws?.close();
|
|
690
|
+
} catch (e) {
|
|
691
|
+
// we don't care
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* _setupPromise - sets up the this.connectOpen promise
|
|
697
|
+
*/
|
|
698
|
+
_setupConnectionPromise = () => {
|
|
699
|
+
this.isResolved = false;
|
|
700
|
+
/** a promise that is resolved once ws.open is called */
|
|
701
|
+
this.connectionOpen = new Promise<ConnectionOpen>((resolve, reject) => {
|
|
702
|
+
this.resolvePromise = resolve;
|
|
703
|
+
this.rejectPromise = reject;
|
|
704
|
+
});
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Schedules a next health check ping for websocket.
|
|
709
|
+
*/
|
|
710
|
+
scheduleNextPing = () => {
|
|
711
|
+
if (this.healthCheckTimeoutRef) {
|
|
712
|
+
clearTimeout(this.healthCheckTimeoutRef);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// 30 seconds is the recommended interval (messenger uses this)
|
|
716
|
+
this.healthCheckTimeoutRef = setTimeout(() => {
|
|
717
|
+
// send the healthcheck..., server replies with a health check event
|
|
718
|
+
const data = [{ type: 'health.check', client_id: this.client.clientID }];
|
|
719
|
+
// try to send on the connection
|
|
720
|
+
try {
|
|
721
|
+
this.ws?.send(JSON.stringify(data));
|
|
722
|
+
} catch (e) {
|
|
723
|
+
// error will already be detected elsewhere
|
|
724
|
+
}
|
|
725
|
+
}, this.pingInterval);
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* scheduleConnectionCheck - schedules a check for time difference between last received event and now.
|
|
730
|
+
* If the difference is more than 35 seconds, it means our health check logic has failed and websocket needs
|
|
731
|
+
* to be reconnected.
|
|
732
|
+
*/
|
|
733
|
+
scheduleConnectionCheck = () => {
|
|
734
|
+
if (this.connectionCheckTimeoutRef) {
|
|
735
|
+
clearTimeout(this.connectionCheckTimeoutRef);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
this.connectionCheckTimeoutRef = setTimeout(() => {
|
|
739
|
+
const now = new Date();
|
|
740
|
+
if (
|
|
741
|
+
this.lastEvent &&
|
|
742
|
+
now.getTime() - this.lastEvent.getTime() > this.connectionCheckTimeout
|
|
743
|
+
) {
|
|
744
|
+
this._log('scheduleConnectionCheck - going to reconnect');
|
|
745
|
+
this._setHealth(false);
|
|
746
|
+
this._reconnect();
|
|
747
|
+
}
|
|
748
|
+
}, this.connectionCheckTimeout);
|
|
749
|
+
};
|
|
750
|
+
}
|