@livedigital/client 2.4.2 → 2.5.0-join-retries.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 CHANGED
@@ -30,7 +30,8 @@ There are three log severities:
30
30
  All the logs generated by LiveDigital client have a namespace starting with “LiveDigital” plus colon, followed by the log severity in upper case plus colon (just if “warn” or “error”), and followed by the internal component name (if any) and the log message.
31
31
 
32
32
  ### Enable Logging
33
- By default, logging is turned off. In order to enable it, the debug key in the browser's localStorage must be set.
33
+ By default, logging is turned off. In order to enable it, the debug key in the browser's
34
+ localStorage must be set.
34
35
  >Check the [debug](https://www.npmjs.com/package/debug) module documentation for further information regarding how to filter specific log messages based on namespace matching rules.
35
36
 
36
37
  ### Example
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@livedigital/client",
3
3
  "author": "vlprojects",
4
4
  "license": "MIT",
5
- "version": "2.4.2",
5
+ "version": "2.5.0-join-retries.1",
6
6
  "private": false,
7
7
  "bugs": {
8
8
  "url": "https://github.com/vlprojects/livedigital-sdk/issues"
@@ -26,6 +26,7 @@ import { GetNodeRequest } from '../types/network';
26
26
  import VideoTrack from './media/tracks/VideoTrack';
27
27
  import AudioTrack from './media/tracks/AudioTrack';
28
28
  import PeerTrack from './media/tracks/PeerTrack';
29
+ import { retryAsync } from '../helpers/retry';
29
30
 
30
31
  type EngineParams = {
31
32
  clientEventEmitter: EnhancedEventEmitter,
@@ -118,7 +119,7 @@ class Engine {
118
119
  this.network.socket.observer.on('state', ({ state }: { state: SocketIOEvents }) => {
119
120
  this.clientEventEmitter.emit(state);
120
121
 
121
- if (state === SocketIOEvents.Reconnected) {
122
+ if (state === SocketIOEvents.Reconnecting) {
122
123
  this.network.socket.disconnect();
123
124
  this.clientEventEmitter.emit(CLIENT_EVENTS.channelRejoinRequired);
124
125
  }
@@ -145,12 +146,7 @@ class Engine {
145
146
  try {
146
147
  this.logger.debug('join()', { params });
147
148
  this.isRoomJoining = true;
148
- const { webSocketUrl } = await this.getAvailableNode({
149
- channelId: params.channelId,
150
- role: params.role,
151
- });
152
- this.network.socket.connect(webSocketUrl);
153
- await this.waitForSocketConnection();
149
+ await this.connectToSocketServerWithRetry(params);
154
150
  await this.performJoin(params);
155
151
  } catch (error) {
156
152
  this.logger.error('join()', { error });
@@ -424,6 +420,37 @@ class Engine {
424
420
  return this.app;
425
421
  }
426
422
 
423
+ private async connectToSocketServerWithRetry(params: { channelId: string, role: Role }): Promise<void> {
424
+ const connectToSocketServerAction = async () => this.connectToSocketServer(params);
425
+ return retryAsync(connectToSocketServerAction, {
426
+ maxRetries: 15,
427
+ minBackoffDelayMs: 150,
428
+ maxBackoffDelayMs: 5_000,
429
+ actionName: 'connectToSocketServer',
430
+ });
431
+ }
432
+
433
+ private async connectToSocketServer(params: { channelId: string, role: Role }): Promise<void> {
434
+ const { webSocketUrl } = await this.getAvailableNode({
435
+ channelId: params.channelId,
436
+ role: params.role,
437
+ });
438
+ this.network.socket.connect(webSocketUrl);
439
+
440
+ try {
441
+ await this.waitForSocketConnection();
442
+ } catch (error: unknown) {
443
+ this.logger.error('Failed to connect to socket server', {
444
+ error,
445
+ role: params.role,
446
+ channelId: params.channelId,
447
+ });
448
+
449
+ this.network.socket.disconnect();
450
+ throw error;
451
+ }
452
+ }
453
+
427
454
  private async getAvailableNode(params: GetNodeRequest): Promise<{ webSocketUrl: string }> {
428
455
  try {
429
456
  const response = await this.network.loadBalancerClient.getNode(params);
@@ -438,14 +465,17 @@ class Engine {
438
465
  return new Promise((resolve, reject) => {
439
466
  const onSocketStateChange = async (data: { state: SocketIOEvents, error?: string }) => {
440
467
  const { error, state } = data;
441
- if (error) {
442
- this.network.socket.observer.removeListener('state', onSocketStateChange);
443
- reject(error);
468
+ const stopListening = () => this.network.socket.observer.removeListener('state', onSocketStateChange);
469
+ const isStateNotExpected = [SocketIOEvents.Disconnected, SocketIOEvents.Reconnecting].includes(state);
470
+
471
+ if (error || (this.isRoomJoining && isStateNotExpected)) {
472
+ stopListening();
473
+ reject(error || 'Not expected socket state for new connection');
444
474
  return;
445
475
  }
446
476
 
447
- if (this.isRoomJoining && state === SocketIOEvents.Connected) {
448
- this.network.socket.observer.removeListener('state', onSocketStateChange);
477
+ if (this.isRoomJoining && [SocketIOEvents.Connected, SocketIOEvents.Reconnected].includes(state)) {
478
+ stopListening();
449
479
  resolve();
450
480
  }
451
481
  };
@@ -456,7 +486,7 @@ class Engine {
456
486
 
457
487
  private async performJoin(params: JoinChannelParams): Promise<void> {
458
488
  try {
459
- await this.network.socket.request(CHANNEL_EVENTS.channelJoin, params);
489
+ await this.sendJoinChannelRequestWithRetry(params);
460
490
  await this.initialize();
461
491
  this.channelEventsHandler.subscribeToEvents();
462
492
  this.mediaSoupEventsHandler.subscribeToEvents();
@@ -470,6 +500,19 @@ class Engine {
470
500
  }
471
501
  }
472
502
 
503
+ private async sendJoinChannelRequestWithRetry(params: JoinChannelParams): Promise<SocketResponse> {
504
+ const joinChannelAction = async () => this.sendJoinChannelRequest(params);
505
+ return retryAsync(joinChannelAction, {
506
+ maxRetries: 3,
507
+ minBackoffDelayMs: 300,
508
+ actionName: 'sendJoinChannelRequest',
509
+ });
510
+ }
511
+
512
+ private async sendJoinChannelRequest(params: JoinChannelParams): Promise<SocketResponse> {
513
+ return this.network.socket.request(CHANNEL_EVENTS.channelJoin, params);
514
+ }
515
+
473
516
  get cahPublish(): boolean {
474
517
  return this.peers.find((item) => item.isMe)?.role === 'host';
475
518
  }
@@ -45,7 +45,7 @@ class SocketIO {
45
45
  : SocketIOEvents.Reconnected;
46
46
 
47
47
  this.isConnected = true;
48
- this.reconnectingAttempt = 0;
48
+ this.setReconnectingAttempt(0);
49
49
 
50
50
  this.logger.debug('connection.on(`connect`)');
51
51
  this.observer.safeEmit('state', { state });
@@ -59,14 +59,14 @@ class SocketIO {
59
59
 
60
60
  connection.on('disconnect', ((reason: string) => {
61
61
  this.isConnected = false;
62
- this.reconnectingAttempt = 0;
62
+ this.setReconnectingAttempt(0);
63
63
  this.disconnectReason = reason;
64
- this.logger.error('connection.on(`disconnect`)', reason);
64
+ this.logger.warn('connection.on(`disconnect`)', reason);
65
65
  this.observer.safeEmit('state', { state: SocketIOEvents.Disconnected });
66
66
  }));
67
67
 
68
68
  connection.io.on('reconnect_attempt', (attempt: number) => {
69
- this.reconnectingAttempt = attempt;
69
+ this.setReconnectingAttempt(attempt);
70
70
  this.logger.warn('connection.on(`reconnect_attempt`)', attempt);
71
71
  this.observer.safeEmit('state', { state: SocketIOEvents.Reconnecting });
72
72
  });
@@ -82,6 +82,8 @@ class SocketIO {
82
82
  this.connection.offAny();
83
83
  this.connection.volatile.offAny();
84
84
  this.connection.disconnect();
85
+ // we need to reset this counter in order not to get Reconnected event on retry
86
+ this.setReconnectingAttempt(0);
85
87
  }
86
88
  }
87
89
 
@@ -103,6 +105,10 @@ class SocketIO {
103
105
  });
104
106
  });
105
107
  }
108
+
109
+ private setReconnectingAttempt(attemptNo: number): void {
110
+ this.reconnectingAttempt = attemptNo;
111
+ }
106
112
  }
107
113
 
108
114
  export default SocketIO;
@@ -0,0 +1,13 @@
1
+ class DefaultTimeProvider {
2
+ // eslint-disable-next-line class-methods-use-this
3
+ async sleepMs(ms: number): Promise<void> {
4
+ return new Promise((resolve) => setTimeout(resolve, ms));
5
+ }
6
+
7
+ // eslint-disable-next-line class-methods-use-this
8
+ now(): Date {
9
+ return new Date(Date.now());
10
+ }
11
+ }
12
+
13
+ export default DefaultTimeProvider;
@@ -0,0 +1,50 @@
1
+ import DefaultTimeProvider from './datetime';
2
+ import Logger from '../engine/Logger';
3
+
4
+ export type RetryOpts = {
5
+ maxRetries?: number;
6
+ minBackoffDelayMs?: number;
7
+ maxBackoffDelayMs?: number;
8
+ actionName?: string; // human-readable action name for debug purposes
9
+ };
10
+
11
+ const logger = new Logger('Retry');
12
+ const timeProvider = new DefaultTimeProvider();
13
+
14
+ const retryAsync = async <ReturnType>(
15
+ action: () => Promise<ReturnType>,
16
+ opts: RetryOpts = {},
17
+ ): Promise<ReturnType> => {
18
+ const maxRetries = opts.maxRetries || 2;
19
+ const minBackoffDelayMs = opts.minBackoffDelayMs || 50;
20
+ let currentAttempt = 0;
21
+ let lastError;
22
+
23
+ while (currentAttempt < maxRetries) {
24
+ currentAttempt += 1;
25
+
26
+ try {
27
+ // eslint-disable-next-line no-await-in-loop
28
+ return await action();
29
+ } catch (err: unknown) {
30
+ lastError = err;
31
+ logger.warn('Action retry failed', currentAttempt, opts.actionName, err);
32
+
33
+ if (currentAttempt < maxRetries) {
34
+ const delay = 2 ** (currentAttempt - 1) * minBackoffDelayMs;
35
+ // eslint-disable-next-line no-await-in-loop
36
+ await timeProvider.sleepMs(Math.min(delay, opts.maxBackoffDelayMs ?? delay));
37
+ }
38
+ }
39
+ }
40
+
41
+ throw lastError;
42
+ };
43
+
44
+ const retry = async <ReturnType>(
45
+ action: () => ReturnType,
46
+ opts: RetryOpts = {},
47
+ ): Promise<ReturnType> => retryAsync(async () => action(), opts);
48
+
49
+ export { retryAsync };
50
+ export default retry;