@stream-io/video-client 1.11.12 → 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.
@@ -23,7 +23,7 @@ export declare class StableWSConnection {
23
23
  private connectionOpenSafe?;
24
24
  consecutiveFailures: number;
25
25
  pingInterval: number;
26
- healthCheckTimeoutRef?: NodeJS.Timeout;
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;
@@ -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,12 @@
1
+ export type TimerWorkerRequest = {
2
+ type: 'setInterval' | 'setTimeout';
3
+ id: number;
4
+ timeout: number;
5
+ } | {
6
+ type: 'clearInterval' | 'clearTimeout';
7
+ id: number;
8
+ };
9
+ export type TimerWorkerEvent = {
10
+ type: 'tick';
11
+ id: number;
12
+ };
@@ -0,0 +1,3 @@
1
+ export declare const timerWorker: {
2
+ src: string;
3
+ };
@@ -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.12",
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",
@@ -35,6 +35,7 @@ import {
35
35
  makeSafePromise,
36
36
  SafePromise,
37
37
  } from './helpers/promise';
38
+ import { getTimers } from './timers';
38
39
 
39
40
  export type StreamSfuClientConstructor = {
40
41
  /**
@@ -111,7 +112,7 @@ export class StreamSfuClient {
111
112
  isLeaving = false;
112
113
 
113
114
  private readonly rpc: SignalServerClient;
114
- private keepAliveInterval?: NodeJS.Timeout;
115
+ private keepAliveInterval?: number;
115
116
  private connectionCheckTimeout?: NodeJS.Timeout;
116
117
  private migrateAwayTimeout?: NodeJS.Timeout;
117
118
  private pingIntervalInMs = 10 * 1000;
@@ -262,7 +263,7 @@ export class StreamSfuClient {
262
263
 
263
264
  private handleWebSocketClose = () => {
264
265
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
265
- clearInterval(this.keepAliveInterval);
266
+ getTimers().clearInterval(this.keepAliveInterval);
266
267
  clearTimeout(this.connectionCheckTimeout);
267
268
  this.onSignalClose?.();
268
269
  };
@@ -488,8 +489,9 @@ export class StreamSfuClient {
488
489
  };
489
490
 
490
491
  private keepAlive = () => {
491
- clearInterval(this.keepAliveInterval);
492
- this.keepAliveInterval = setInterval(() => {
492
+ const timers = getTimers();
493
+ timers.clearInterval(this.keepAliveInterval);
494
+ this.keepAliveInterval = timers.setInterval(() => {
493
495
  this.ping().catch((e) => {
494
496
  this.logger('error', 'Error sending healthCheckRequest to SFU', e);
495
497
  });
@@ -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?: NodeJS.Timeout;
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
- clearInterval(this.healthCheckTimeoutRef);
228
- clearInterval(this.connectionCheckTimeoutRef);
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
- clearTimeout(this.healthCheckTimeoutRef);
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>;
@@ -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
+ };
@@ -0,0 +1,26 @@
1
+ export const timerWorker = {
2
+ src: `var timerIdMapping = new Map();
3
+ self.addEventListener('message', function (event) {
4
+ var request = event.data;
5
+ switch (request.type) {
6
+ case 'setTimeout':
7
+ case 'setInterval':
8
+ timerIdMapping.set(request.id, (request.type === 'setTimeout' ? setTimeout : setInterval)(function () {
9
+ tick(request.id);
10
+ if (request.type === 'setTimeout') {
11
+ timerIdMapping.delete(request.id);
12
+ }
13
+ }, request.timeout));
14
+ break;
15
+ case 'clearTimeout':
16
+ case 'clearInterval':
17
+ (request.type === 'clearTimeout' ? clearTimeout : clearInterval)(timerIdMapping.get(request.id));
18
+ timerIdMapping.delete(request.id);
19
+ break;
20
+ }
21
+ });
22
+ function tick(id) {
23
+ var message = { type: 'tick', id: id };
24
+ self.postMessage(message);
25
+ }`,
26
+ };
@@ -0,0 +1,40 @@
1
+ /* eslint-disable */
2
+
3
+ import type { TimerWorkerEvent, TimerWorkerRequest } from './types';
4
+
5
+ const timerIdMapping = new Map<number, NodeJS.Timeout>();
6
+
7
+ self.addEventListener('message', (event: MessageEvent) => {
8
+ const request = event.data as TimerWorkerRequest;
9
+
10
+ switch (request.type) {
11
+ case 'setTimeout':
12
+ case 'setInterval':
13
+ timerIdMapping.set(
14
+ request.id,
15
+ (request.type === 'setTimeout' ? setTimeout : setInterval)(() => {
16
+ tick(request.id);
17
+
18
+ if (request.type === 'setTimeout') {
19
+ timerIdMapping.delete(request.id);
20
+ }
21
+ }, request.timeout),
22
+ );
23
+ break;
24
+
25
+ case 'clearTimeout':
26
+ case 'clearInterval':
27
+ (request.type === 'clearTimeout' ? clearTimeout : clearInterval)(
28
+ timerIdMapping.get(request.id),
29
+ );
30
+ timerIdMapping.delete(request.id);
31
+ break;
32
+ }
33
+ });
34
+
35
+ function tick(id: number) {
36
+ const message: TimerWorkerEvent = { type: 'tick', id };
37
+ self.postMessage(message);
38
+ }
39
+
40
+ /* eslint-enable */