@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 +2 -1
- package/package.json +1 -1
- package/src/engine/index.ts +56 -13
- package/src/engine/network/Socket.ts +10 -4
- package/src/helpers/datetime.ts +13 -0
- package/src/helpers/retry.ts +50 -0
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
|
|
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
package/src/engine/index.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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 &&
|
|
448
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
62
|
+
this.setReconnectingAttempt(0);
|
|
63
63
|
this.disconnectReason = reason;
|
|
64
|
-
this.logger.
|
|
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.
|
|
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;
|