@stream-io/video-client 1.11.11 → 1.11.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/index.browser.es.js +232 -73
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +232 -73
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +232 -73
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +7 -0
- package/dist/src/StreamSfuClient.d.ts +1 -3
- package/dist/src/coordinator/connection/connection.d.ts +1 -1
- package/dist/src/coordinator/connection/types.d.ts +5 -0
- package/dist/src/helpers/promise.d.ts +14 -0
- package/dist/src/timers/index.d.ts +22 -0
- package/dist/src/timers/types.d.ts +12 -0
- package/dist/src/timers/worker.build.d.ts +3 -0
- package/dist/src/timers/worker.d.ts +1 -0
- package/package.json +4 -3
- package/src/Call.ts +38 -3
- package/src/StreamSfuClient.ts +33 -32
- package/src/StreamVideoClient.ts +4 -0
- package/src/coordinator/connection/connection.ts +14 -5
- package/src/coordinator/connection/types.ts +6 -0
- package/src/helpers/promise.ts +44 -0
- package/src/timers/index.ts +137 -0
- package/src/timers/types.ts +15 -0
- package/src/timers/worker.build.ts +26 -0
- package/src/timers/worker.ts +40 -0
- package/dist/src/helpers/withResolvers.d.ts +0 -14
- package/src/helpers/withResolvers.ts +0 -43
package/dist/src/Call.d.ts
CHANGED
|
@@ -81,6 +81,7 @@ export declare class Call {
|
|
|
81
81
|
private reconnectAttempts;
|
|
82
82
|
private reconnectStrategy;
|
|
83
83
|
private fastReconnectDeadlineSeconds;
|
|
84
|
+
private disconnectionTimeoutSeconds;
|
|
84
85
|
private lastOfflineTimestamp;
|
|
85
86
|
private networkAvailableTask;
|
|
86
87
|
private trackPublishOrder;
|
|
@@ -652,4 +653,10 @@ export declare class Call {
|
|
|
652
653
|
* and removes any preference for preferred resolution.
|
|
653
654
|
*/
|
|
654
655
|
setIncomingVideoEnabled: (enabled: boolean) => void;
|
|
656
|
+
/**
|
|
657
|
+
* Sets the maximum amount of time a user can remain waiting for a reconnect
|
|
658
|
+
* after a network disruption
|
|
659
|
+
* @param timeoutSeconds Timeout in seconds, or 0 to keep reconnecting indefinetely
|
|
660
|
+
*/
|
|
661
|
+
setDisconnectionTimeout: (timeoutSeconds: number) => void;
|
|
655
662
|
}
|
|
@@ -72,7 +72,6 @@ export declare class StreamSfuClient {
|
|
|
72
72
|
private pingIntervalInMs;
|
|
73
73
|
private unhealthyTimeoutInMs;
|
|
74
74
|
private lastMessageTimestamp?;
|
|
75
|
-
private readonly restoreWebSocketConcurrencyTag;
|
|
76
75
|
private readonly unsubscribeIceTrickle;
|
|
77
76
|
private readonly unsubscribeNetworkChanged;
|
|
78
77
|
private readonly onSignalClose;
|
|
@@ -80,7 +79,7 @@ export declare class StreamSfuClient {
|
|
|
80
79
|
private readonly logTag;
|
|
81
80
|
private readonly credentials;
|
|
82
81
|
private readonly dispatcher;
|
|
83
|
-
private readonly joinResponseTimeout
|
|
82
|
+
private readonly joinResponseTimeout;
|
|
84
83
|
private networkAvailableTask;
|
|
85
84
|
/**
|
|
86
85
|
* Promise that resolves when the JoinResponse is received.
|
|
@@ -119,7 +118,6 @@ export declare class StreamSfuClient {
|
|
|
119
118
|
constructor({ dispatcher, credentials, sessionId, logTag, joinResponseTimeout, onSignalClose, streamClient, }: StreamSfuClientConstructor);
|
|
120
119
|
private createWebSocket;
|
|
121
120
|
private cleanUpWebSocket;
|
|
122
|
-
private restoreWebSocket;
|
|
123
121
|
get isHealthy(): boolean;
|
|
124
122
|
get joinTask(): Promise<JoinResponse>;
|
|
125
123
|
private handleWebSocketClose;
|
|
@@ -23,7 +23,7 @@ export declare class StableWSConnection {
|
|
|
23
23
|
private connectionOpenSafe?;
|
|
24
24
|
consecutiveFailures: number;
|
|
25
25
|
pingInterval: number;
|
|
26
|
-
healthCheckTimeoutRef?:
|
|
26
|
+
healthCheckTimeoutRef?: number;
|
|
27
27
|
isConnecting: boolean;
|
|
28
28
|
isDisconnected: boolean;
|
|
29
29
|
isHealthy: boolean;
|
|
@@ -119,6 +119,11 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
119
119
|
* In Node.js environment, you can use the `ws` package.
|
|
120
120
|
*/
|
|
121
121
|
WebSocketImpl?: typeof WebSocket;
|
|
122
|
+
/**
|
|
123
|
+
* Create Web Worker to initiate timer events like health checks. Can possibly prevent
|
|
124
|
+
* timer throttling issues in inactive browser tabs.
|
|
125
|
+
*/
|
|
126
|
+
expertimental_enableTimerWorker?: boolean;
|
|
122
127
|
};
|
|
123
128
|
export type TokenProvider = () => Promise<string>;
|
|
124
129
|
export type TokenOrProvider = null | string | TokenProvider | undefined;
|
|
@@ -16,3 +16,17 @@ export interface SafePromise<T> {
|
|
|
16
16
|
* with the original promise
|
|
17
17
|
*/
|
|
18
18
|
export declare function makeSafePromise<T>(promise: Promise<T>): SafePromise<T>;
|
|
19
|
+
export type PromiseWithResolvers<T> = {
|
|
20
|
+
promise: Promise<T>;
|
|
21
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
22
|
+
reject: (reason: any) => void;
|
|
23
|
+
isResolved: boolean;
|
|
24
|
+
isRejected: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new promise with resolvers.
|
|
28
|
+
*
|
|
29
|
+
* Based on:
|
|
30
|
+
* - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
|
|
31
|
+
*/
|
|
32
|
+
export declare const promiseWithResolvers: <T = void>() => PromiseWithResolvers<T>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
declare class TimerWorker {
|
|
2
|
+
private currentTimerId;
|
|
3
|
+
private callbacks;
|
|
4
|
+
private worker;
|
|
5
|
+
private fallback;
|
|
6
|
+
setup({ useTimerWorker }?: {
|
|
7
|
+
useTimerWorker?: boolean;
|
|
8
|
+
}): void;
|
|
9
|
+
destroy(): void;
|
|
10
|
+
get ready(): boolean;
|
|
11
|
+
setInterval(callback: () => void, timeout: number): number;
|
|
12
|
+
clearInterval(id?: number): void;
|
|
13
|
+
setTimeout(callback: () => void, timeout: number): number;
|
|
14
|
+
clearTimeout(id?: number): void;
|
|
15
|
+
private setTimer;
|
|
16
|
+
private clearTimer;
|
|
17
|
+
private getTimerId;
|
|
18
|
+
private sendMessage;
|
|
19
|
+
}
|
|
20
|
+
export declare const enableTimerWorker: () => void;
|
|
21
|
+
export declare const getTimers: () => TimerWorker;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/video-client",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.13",
|
|
4
4
|
"packageManager": "yarn@3.2.4",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"module": "dist/index.es.js",
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"clean": "rimraf dist",
|
|
13
13
|
"start": "rollup -w -c",
|
|
14
|
-
"build": "yarn clean && rollup -c",
|
|
14
|
+
"build": "yarn clean && ./generate-timer-worker.sh && rollup -c",
|
|
15
15
|
"test": "vitest",
|
|
16
16
|
"clean:docs": "rimraf generated-docs",
|
|
17
17
|
"test-ci": "vitest --coverage",
|
|
18
18
|
"generate:open-api": "./generate-openapi.sh protocol",
|
|
19
|
-
"generate:open-api:dev": "./generate-openapi.sh chat"
|
|
19
|
+
"generate:open-api:dev": "./generate-openapi.sh chat",
|
|
20
|
+
"generate:timer-worker": "./generate-timer-worker.sh"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"dist",
|
package/src/Call.ts
CHANGED
|
@@ -120,9 +120,10 @@ import { getSdkSignature } from './stats/utils';
|
|
|
120
120
|
import { withoutConcurrency } from './helpers/concurrency';
|
|
121
121
|
import { ensureExhausted } from './helpers/ensureExhausted';
|
|
122
122
|
import {
|
|
123
|
+
makeSafePromise,
|
|
123
124
|
PromiseWithResolvers,
|
|
124
125
|
promiseWithResolvers,
|
|
125
|
-
} from './helpers/
|
|
126
|
+
} from './helpers/promise';
|
|
126
127
|
|
|
127
128
|
/**
|
|
128
129
|
* An object representation of a `Call`.
|
|
@@ -213,6 +214,7 @@ export class Call {
|
|
|
213
214
|
private reconnectAttempts = 0;
|
|
214
215
|
private reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
|
|
215
216
|
private fastReconnectDeadlineSeconds: number = 0;
|
|
217
|
+
private disconnectionTimeoutSeconds: number = 0;
|
|
216
218
|
private lastOfflineTimestamp: number = 0;
|
|
217
219
|
private networkAvailableTask: PromiseWithResolvers<void> | undefined;
|
|
218
220
|
// maintain the order of publishing tracks to restore them after a reconnection
|
|
@@ -1051,6 +1053,9 @@ export class Call {
|
|
|
1051
1053
|
*/
|
|
1052
1054
|
private handleSfuSignalClose = (sfuClient: StreamSfuClient) => {
|
|
1053
1055
|
this.logger('debug', '[Reconnect] SFU signal connection closed');
|
|
1056
|
+
// SFU WS closed before we finished current join, no need to schedule reconnect
|
|
1057
|
+
// because join operation will fail
|
|
1058
|
+
if (this.state.callingState === CallingState.JOINING) return;
|
|
1054
1059
|
// normal close, no need to reconnect
|
|
1055
1060
|
if (sfuClient.isLeaving) return;
|
|
1056
1061
|
this.reconnect(WebsocketReconnectStrategy.REJOIN).catch((err) => {
|
|
@@ -1068,14 +1073,35 @@ export class Call {
|
|
|
1068
1073
|
private reconnect = async (
|
|
1069
1074
|
strategy: WebsocketReconnectStrategy,
|
|
1070
1075
|
): Promise<void> => {
|
|
1076
|
+
if (
|
|
1077
|
+
this.state.callingState === CallingState.RECONNECTING ||
|
|
1078
|
+
this.state.callingState === CallingState.RECONNECTING_FAILED
|
|
1079
|
+
)
|
|
1080
|
+
return;
|
|
1081
|
+
|
|
1071
1082
|
return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
|
|
1072
1083
|
this.logger(
|
|
1073
1084
|
'info',
|
|
1074
1085
|
`[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`,
|
|
1075
1086
|
);
|
|
1076
1087
|
|
|
1088
|
+
let reconnectStartTime = Date.now();
|
|
1077
1089
|
this.reconnectStrategy = strategy;
|
|
1090
|
+
|
|
1078
1091
|
do {
|
|
1092
|
+
if (
|
|
1093
|
+
this.disconnectionTimeoutSeconds > 0 &&
|
|
1094
|
+
(Date.now() - reconnectStartTime) / 1000 >
|
|
1095
|
+
this.disconnectionTimeoutSeconds
|
|
1096
|
+
) {
|
|
1097
|
+
this.logger(
|
|
1098
|
+
'warn',
|
|
1099
|
+
'[Reconnect] Stopping reconnection attempts after reaching disconnection timeout',
|
|
1100
|
+
);
|
|
1101
|
+
this.state.setCallingState(CallingState.RECONNECTING_FAILED);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1079
1105
|
// we don't increment reconnect attempts for the FAST strategy.
|
|
1080
1106
|
if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
|
|
1081
1107
|
this.reconnectAttempts++;
|
|
@@ -1193,7 +1219,7 @@ export class Call {
|
|
|
1193
1219
|
currentSubscriber?.detachEventHandlers();
|
|
1194
1220
|
currentPublisher?.detachEventHandlers();
|
|
1195
1221
|
|
|
1196
|
-
const migrationTask = currentSfuClient.enterMigration();
|
|
1222
|
+
const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
|
|
1197
1223
|
|
|
1198
1224
|
try {
|
|
1199
1225
|
const currentSfu = currentSfuClient.edgeName;
|
|
@@ -1211,7 +1237,7 @@ export class Call {
|
|
|
1211
1237
|
// Wait for the migration to complete, then close the previous SFU client
|
|
1212
1238
|
// and the peer connection instances. In case of failure, the migration
|
|
1213
1239
|
// task would throw an error and REJOIN would be attempted.
|
|
1214
|
-
await migrationTask;
|
|
1240
|
+
await migrationTask();
|
|
1215
1241
|
|
|
1216
1242
|
// in MIGRATE, we can consider the call as joined only after
|
|
1217
1243
|
// `participantMigrationComplete` event is received, signaled by
|
|
@@ -2337,4 +2363,13 @@ export class Call {
|
|
|
2337
2363
|
);
|
|
2338
2364
|
this.dynascaleManager.applyTrackSubscriptions();
|
|
2339
2365
|
};
|
|
2366
|
+
|
|
2367
|
+
/**
|
|
2368
|
+
* Sets the maximum amount of time a user can remain waiting for a reconnect
|
|
2369
|
+
* after a network disruption
|
|
2370
|
+
* @param timeoutSeconds Timeout in seconds, or 0 to keep reconnecting indefinetely
|
|
2371
|
+
*/
|
|
2372
|
+
setDisconnectionTimeout = (timeoutSeconds: number) => {
|
|
2373
|
+
this.disconnectionTimeoutSeconds = timeoutSeconds;
|
|
2374
|
+
};
|
|
2340
2375
|
}
|
package/src/StreamSfuClient.ts
CHANGED
|
@@ -25,15 +25,17 @@ import {
|
|
|
25
25
|
} from './gen/video/sfu/signal_rpc/signal';
|
|
26
26
|
import { ICETrickle, TrackType } from './gen/video/sfu/models/models';
|
|
27
27
|
import { StreamClient } from './coordinator/connection/client';
|
|
28
|
-
import { generateUUIDv4
|
|
28
|
+
import { generateUUIDv4 } from './coordinator/connection/utils';
|
|
29
29
|
import { Credentials } from './gen/coordinator';
|
|
30
30
|
import { Logger } from './coordinator/connection/types';
|
|
31
31
|
import { getLogger, getLogLevel } from './logger';
|
|
32
|
-
import { withoutConcurrency } from './helpers/concurrency';
|
|
33
32
|
import {
|
|
34
33
|
promiseWithResolvers,
|
|
35
34
|
PromiseWithResolvers,
|
|
36
|
-
|
|
35
|
+
makeSafePromise,
|
|
36
|
+
SafePromise,
|
|
37
|
+
} from './helpers/promise';
|
|
38
|
+
import { getTimers } from './timers';
|
|
37
39
|
|
|
38
40
|
export type StreamSfuClientConstructor = {
|
|
39
41
|
/**
|
|
@@ -101,7 +103,7 @@ export class StreamSfuClient {
|
|
|
101
103
|
/**
|
|
102
104
|
* Promise that resolves when the WebSocket connection is ready (open).
|
|
103
105
|
*/
|
|
104
|
-
private signalReady!:
|
|
106
|
+
private signalReady!: SafePromise<WebSocket>;
|
|
105
107
|
|
|
106
108
|
/**
|
|
107
109
|
* Flag to indicate if the client is in the process of leaving the call.
|
|
@@ -110,13 +112,12 @@ export class StreamSfuClient {
|
|
|
110
112
|
isLeaving = false;
|
|
111
113
|
|
|
112
114
|
private readonly rpc: SignalServerClient;
|
|
113
|
-
private keepAliveInterval?:
|
|
115
|
+
private keepAliveInterval?: number;
|
|
114
116
|
private connectionCheckTimeout?: NodeJS.Timeout;
|
|
115
117
|
private migrateAwayTimeout?: NodeJS.Timeout;
|
|
116
118
|
private pingIntervalInMs = 10 * 1000;
|
|
117
119
|
private unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
|
|
118
120
|
private lastMessageTimestamp?: Date;
|
|
119
|
-
private readonly restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
|
|
120
121
|
private readonly unsubscribeIceTrickle: () => void;
|
|
121
122
|
private readonly unsubscribeNetworkChanged: () => void;
|
|
122
123
|
private readonly onSignalClose: (() => void) | undefined;
|
|
@@ -124,7 +125,7 @@ export class StreamSfuClient {
|
|
|
124
125
|
private readonly logTag: string;
|
|
125
126
|
private readonly credentials: Credentials;
|
|
126
127
|
private readonly dispatcher: Dispatcher;
|
|
127
|
-
private readonly joinResponseTimeout
|
|
128
|
+
private readonly joinResponseTimeout: number;
|
|
128
129
|
private networkAvailableTask: PromiseWithResolvers<void> | undefined;
|
|
129
130
|
/**
|
|
130
131
|
* Promise that resolves when the JoinResponse is received.
|
|
@@ -227,32 +228,31 @@ export class StreamSfuClient {
|
|
|
227
228
|
});
|
|
228
229
|
|
|
229
230
|
this.signalWs.addEventListener('close', this.handleWebSocketClose);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
|
|
232
|
+
this.signalReady = makeSafePromise(
|
|
233
|
+
Promise.race<WebSocket>([
|
|
234
|
+
new Promise((resolve) => {
|
|
235
|
+
const onOpen = () => {
|
|
236
|
+
this.signalWs.removeEventListener('open', onOpen);
|
|
237
|
+
resolve(this.signalWs);
|
|
238
|
+
};
|
|
239
|
+
this.signalWs.addEventListener('open', onOpen);
|
|
240
|
+
}),
|
|
241
|
+
|
|
242
|
+
new Promise((resolve, reject) => {
|
|
243
|
+
setTimeout(
|
|
244
|
+
() => reject(new Error('SFU WS connection timed out')),
|
|
245
|
+
this.joinResponseTimeout,
|
|
246
|
+
);
|
|
247
|
+
}),
|
|
248
|
+
]),
|
|
249
|
+
);
|
|
239
250
|
};
|
|
240
251
|
|
|
241
252
|
private cleanUpWebSocket = () => {
|
|
242
|
-
this.signalWs.removeEventListener('error', this.restoreWebSocket);
|
|
243
253
|
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
|
|
244
254
|
};
|
|
245
255
|
|
|
246
|
-
private restoreWebSocket = () => {
|
|
247
|
-
withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
|
|
248
|
-
await this.networkAvailableTask?.promise;
|
|
249
|
-
this.logger('debug', 'Restoring SFU WS connection');
|
|
250
|
-
this.cleanUpWebSocket();
|
|
251
|
-
await sleep(500);
|
|
252
|
-
this.createWebSocket();
|
|
253
|
-
}).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
|
|
254
|
-
};
|
|
255
|
-
|
|
256
256
|
get isHealthy() {
|
|
257
257
|
return this.signalWs.readyState === WebSocket.OPEN;
|
|
258
258
|
}
|
|
@@ -263,7 +263,7 @@ export class StreamSfuClient {
|
|
|
263
263
|
|
|
264
264
|
private handleWebSocketClose = () => {
|
|
265
265
|
this.signalWs.removeEventListener('close', this.handleWebSocketClose);
|
|
266
|
-
clearInterval(this.keepAliveInterval);
|
|
266
|
+
getTimers().clearInterval(this.keepAliveInterval);
|
|
267
267
|
clearTimeout(this.connectionCheckTimeout);
|
|
268
268
|
this.onSignalClose?.();
|
|
269
269
|
};
|
|
@@ -409,7 +409,7 @@ export class StreamSfuClient {
|
|
|
409
409
|
data: Omit<JoinRequest, 'sessionId' | 'token'>,
|
|
410
410
|
): Promise<JoinResponse> => {
|
|
411
411
|
// wait for the signal web socket to be ready before sending "joinRequest"
|
|
412
|
-
await this.signalReady;
|
|
412
|
+
await this.signalReady();
|
|
413
413
|
if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
|
|
414
414
|
// we need to lock the RPC requests until we receive a JoinResponse.
|
|
415
415
|
// that's why we have this primitive lock mechanism.
|
|
@@ -478,7 +478,7 @@ export class StreamSfuClient {
|
|
|
478
478
|
};
|
|
479
479
|
|
|
480
480
|
private send = async (message: SfuRequest) => {
|
|
481
|
-
await this.signalReady; // wait for the signal ws to be open
|
|
481
|
+
await this.signalReady(); // wait for the signal ws to be open
|
|
482
482
|
const msgJson = SfuRequest.toJson(message);
|
|
483
483
|
if (this.signalWs.readyState !== WebSocket.OPEN) {
|
|
484
484
|
this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);
|
|
@@ -489,8 +489,9 @@ export class StreamSfuClient {
|
|
|
489
489
|
};
|
|
490
490
|
|
|
491
491
|
private keepAlive = () => {
|
|
492
|
-
|
|
493
|
-
this.keepAliveInterval
|
|
492
|
+
const timers = getTimers();
|
|
493
|
+
timers.clearInterval(this.keepAliveInterval);
|
|
494
|
+
this.keepAliveInterval = timers.setInterval(() => {
|
|
494
495
|
this.ping().catch((e) => {
|
|
495
496
|
this.logger('error', 'Error sending healthCheckRequest to SFU', e);
|
|
496
497
|
});
|
package/src/StreamVideoClient.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { getLogger, logToConsole, setLogger } from './logger';
|
|
|
31
31
|
import { getSdkInfo } from './client-details';
|
|
32
32
|
import { SdkType } from './gen/video/sfu/models/models';
|
|
33
33
|
import { withoutConcurrency } from './helpers/concurrency';
|
|
34
|
+
import { enableTimerWorker } from './timers';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
@@ -86,9 +87,12 @@ export class StreamVideoClient {
|
|
|
86
87
|
if (typeof apiKeyOrArgs === 'string') {
|
|
87
88
|
logLevel = opts?.logLevel || logLevel;
|
|
88
89
|
logger = opts?.logger || logger;
|
|
90
|
+
if (opts?.expertimental_enableTimerWorker) enableTimerWorker();
|
|
89
91
|
} else {
|
|
90
92
|
logLevel = apiKeyOrArgs.options?.logLevel || logLevel;
|
|
91
93
|
logger = apiKeyOrArgs.options?.logger || logger;
|
|
94
|
+
if (apiKeyOrArgs.options?.expertimental_enableTimerWorker)
|
|
95
|
+
enableTimerWorker();
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
setLogger(logger, logLevel);
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
WSAuthMessage,
|
|
16
16
|
} from '../../gen/coordinator';
|
|
17
17
|
import { makeSafePromise, type SafePromise } from '../../helpers/promise';
|
|
18
|
+
import { getTimers } from '../../timers';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* StableWSConnection - A WS connection that reconnects upon failure.
|
|
@@ -39,7 +40,7 @@ export class StableWSConnection {
|
|
|
39
40
|
private connectionOpenSafe?: SafePromise<ConnectedEvent>;
|
|
40
41
|
consecutiveFailures: number;
|
|
41
42
|
pingInterval: number;
|
|
42
|
-
healthCheckTimeoutRef?:
|
|
43
|
+
healthCheckTimeoutRef?: number;
|
|
43
44
|
isConnecting: boolean;
|
|
44
45
|
isDisconnected: boolean;
|
|
45
46
|
isHealthy: boolean;
|
|
@@ -224,8 +225,12 @@ export class StableWSConnection {
|
|
|
224
225
|
this.isDisconnected = true;
|
|
225
226
|
|
|
226
227
|
// start by removing all the listeners
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
if (this.healthCheckTimeoutRef) {
|
|
229
|
+
getTimers().clearInterval(this.healthCheckTimeoutRef);
|
|
230
|
+
}
|
|
231
|
+
if (this.connectionCheckTimeoutRef) {
|
|
232
|
+
clearInterval(this.connectionCheckTimeoutRef);
|
|
233
|
+
}
|
|
229
234
|
|
|
230
235
|
removeConnectionEventListeners(this.onlineStatusChanged);
|
|
231
236
|
|
|
@@ -689,9 +694,13 @@ export class StableWSConnection {
|
|
|
689
694
|
* Schedules a next health check ping for websocket.
|
|
690
695
|
*/
|
|
691
696
|
scheduleNextPing = () => {
|
|
697
|
+
const timers = getTimers();
|
|
698
|
+
if (this.healthCheckTimeoutRef) {
|
|
699
|
+
timers.clearTimeout(this.healthCheckTimeoutRef);
|
|
700
|
+
}
|
|
701
|
+
|
|
692
702
|
// 30 seconds is the recommended interval (messenger uses this)
|
|
693
|
-
|
|
694
|
-
this.healthCheckTimeoutRef = setTimeout(() => {
|
|
703
|
+
this.healthCheckTimeoutRef = timers.setTimeout(() => {
|
|
695
704
|
// send the healthcheck..., server replies with a health check event
|
|
696
705
|
const data = [{ type: 'health.check', client_id: this.client.clientID }];
|
|
697
706
|
// try to send on the connection
|
|
@@ -146,6 +146,12 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
146
146
|
* In Node.js environment, you can use the `ws` package.
|
|
147
147
|
*/
|
|
148
148
|
WebSocketImpl?: typeof WebSocket;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create Web Worker to initiate timer events like health checks. Can possibly prevent
|
|
152
|
+
* timer throttling issues in inactive browser tabs.
|
|
153
|
+
*/
|
|
154
|
+
expertimental_enableTimerWorker?: boolean;
|
|
149
155
|
};
|
|
150
156
|
|
|
151
157
|
export type TokenProvider = () => Promise<string>;
|
package/src/helpers/promise.ts
CHANGED
|
@@ -45,3 +45,47 @@ export function makeSafePromise<T>(promise: Promise<T>): SafePromise<T> {
|
|
|
45
45
|
unwrapPromise.checkPending = () => isPending;
|
|
46
46
|
return unwrapPromise;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
export type PromiseWithResolvers<T> = {
|
|
50
|
+
promise: Promise<T>;
|
|
51
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
52
|
+
reject: (reason: any) => void;
|
|
53
|
+
isResolved: boolean;
|
|
54
|
+
isRejected: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new promise with resolvers.
|
|
59
|
+
*
|
|
60
|
+
* Based on:
|
|
61
|
+
* - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
|
|
62
|
+
*/
|
|
63
|
+
export const promiseWithResolvers = <T = void>(): PromiseWithResolvers<T> => {
|
|
64
|
+
let resolve: (value: T | PromiseLike<T>) => void;
|
|
65
|
+
let reject: (reason: any) => void;
|
|
66
|
+
const promise = new Promise<T>((_resolve, _reject) => {
|
|
67
|
+
resolve = _resolve;
|
|
68
|
+
reject = _reject;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let isResolved = false;
|
|
72
|
+
let isRejected = false;
|
|
73
|
+
|
|
74
|
+
const resolver = (value: T | PromiseLike<T>) => {
|
|
75
|
+
isResolved = true;
|
|
76
|
+
resolve(value);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const rejecter = (reason: any) => {
|
|
80
|
+
isRejected = true;
|
|
81
|
+
reject(reason);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
promise,
|
|
86
|
+
resolve: resolver,
|
|
87
|
+
reject: rejecter,
|
|
88
|
+
isResolved,
|
|
89
|
+
isRejected,
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { lazy } from '../helpers/lazy';
|
|
2
|
+
import { getLogger } from '../logger';
|
|
3
|
+
import { TimerWorkerEvent, TimerWorkerRequest } from './types';
|
|
4
|
+
import { timerWorker } from './worker.build';
|
|
5
|
+
|
|
6
|
+
class TimerWorker {
|
|
7
|
+
private currentTimerId = 1;
|
|
8
|
+
private callbacks = new Map<number, () => void>();
|
|
9
|
+
private worker: Worker | undefined;
|
|
10
|
+
private fallback = false;
|
|
11
|
+
|
|
12
|
+
setup({ useTimerWorker = true }: { useTimerWorker?: boolean } = {}): void {
|
|
13
|
+
if (!useTimerWorker) {
|
|
14
|
+
this.fallback = true;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const source = timerWorker.src;
|
|
20
|
+
const blob = new Blob([source], {
|
|
21
|
+
type: 'application/javascript; charset=utf-8',
|
|
22
|
+
});
|
|
23
|
+
const script = URL.createObjectURL(blob);
|
|
24
|
+
this.worker = new Worker(script, { name: 'str-timer-worker' });
|
|
25
|
+
this.worker.addEventListener('message', (event) => {
|
|
26
|
+
const { type, id } = event.data as TimerWorkerEvent;
|
|
27
|
+
if (type === 'tick') {
|
|
28
|
+
this.callbacks.get(id)?.();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
} catch (err: any) {
|
|
32
|
+
getLogger(['timer-worker'])('error', err);
|
|
33
|
+
this.fallback = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
destroy(): void {
|
|
38
|
+
this.callbacks.clear();
|
|
39
|
+
this.worker?.terminate();
|
|
40
|
+
this.worker = undefined;
|
|
41
|
+
this.fallback = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get ready() {
|
|
45
|
+
return this.fallback || Boolean(this.worker);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setInterval(callback: () => void, timeout: number): number {
|
|
49
|
+
return this.setTimer('setInterval', callback, timeout);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
clearInterval(id?: number): void {
|
|
53
|
+
this.clearTimer('clearInterval', id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setTimeout(callback: () => void, timeout: number): number {
|
|
57
|
+
return this.setTimer('setTimeout', callback, timeout);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clearTimeout(id?: number): void {
|
|
61
|
+
this.clearTimer('clearTimeout', id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private setTimer(
|
|
65
|
+
type: 'setTimeout' | 'setInterval',
|
|
66
|
+
callback: () => void,
|
|
67
|
+
timeout: number,
|
|
68
|
+
) {
|
|
69
|
+
if (!this.ready) {
|
|
70
|
+
this.setup();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.fallback) {
|
|
74
|
+
return (type === 'setTimeout' ? setTimeout : setInterval)(
|
|
75
|
+
callback,
|
|
76
|
+
timeout,
|
|
77
|
+
) as unknown as number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const id = this.getTimerId();
|
|
81
|
+
|
|
82
|
+
this.callbacks.set(id, () => {
|
|
83
|
+
callback();
|
|
84
|
+
|
|
85
|
+
// Timeouts are one-off operations, so no need to keep callback reference
|
|
86
|
+
// after timer has fired
|
|
87
|
+
if (type === 'setTimeout') {
|
|
88
|
+
this.callbacks.delete(id);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.sendMessage({ type, id, timeout });
|
|
93
|
+
return id;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private clearTimer(type: 'clearTimeout' | 'clearInterval', id?: number) {
|
|
97
|
+
if (!id) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!this.ready) {
|
|
102
|
+
this.setup();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.fallback) {
|
|
106
|
+
(type === 'clearTimeout' ? clearTimeout : clearInterval)(id);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.callbacks.delete(id);
|
|
111
|
+
this.sendMessage({ type, id });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private getTimerId() {
|
|
115
|
+
return this.currentTimerId++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private sendMessage(message: TimerWorkerRequest) {
|
|
119
|
+
if (!this.worker) {
|
|
120
|
+
throw new Error("Cannot use timer worker before it's set up");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.worker.postMessage(message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let timerWorkerEnabled = false;
|
|
128
|
+
|
|
129
|
+
export const enableTimerWorker = () => {
|
|
130
|
+
timerWorkerEnabled = true;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const getTimers = lazy(() => {
|
|
134
|
+
const instance = new TimerWorker();
|
|
135
|
+
instance.setup({ useTimerWorker: timerWorkerEnabled });
|
|
136
|
+
return instance;
|
|
137
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type TimerWorkerRequest =
|
|
2
|
+
| {
|
|
3
|
+
type: 'setInterval' | 'setTimeout';
|
|
4
|
+
id: number;
|
|
5
|
+
timeout: number;
|
|
6
|
+
}
|
|
7
|
+
| {
|
|
8
|
+
type: 'clearInterval' | 'clearTimeout';
|
|
9
|
+
id: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type TimerWorkerEvent = {
|
|
13
|
+
type: 'tick';
|
|
14
|
+
id: number;
|
|
15
|
+
};
|