@webex/plugin-meetings 3.12.0-next.77 → 3.12.0-next.79
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/dist/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meeting/index.js +66 -49
- package/dist/meeting/index.js.map +1 -1
- package/dist/types/meeting/index.d.ts +1 -1
- package/dist/webinar/index.js +1 -1
- package/package.json +3 -3
- package/src/meeting/index.ts +45 -17
- package/test/unit/spec/meeting/index.js +312 -5
|
@@ -500,7 +500,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
500
500
|
private deferSDPAnswer?;
|
|
501
501
|
private sdpResponseTimer?;
|
|
502
502
|
private hasMediaConnectionConnectedAtLeastOnce;
|
|
503
|
-
private joinWithMediaRetryInfo
|
|
503
|
+
private joinWithMediaRetryInfo;
|
|
504
504
|
private connectionStateHandler?;
|
|
505
505
|
private iceCandidateErrors;
|
|
506
506
|
private iceCandidatesCount;
|
package/dist/webinar/index.js
CHANGED
package/package.json
CHANGED
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@webex/common": "3.12.0-next.4",
|
|
65
|
-
"@webex/internal-media-core": "2.
|
|
65
|
+
"@webex/internal-media-core": "2.26.1",
|
|
66
66
|
"@webex/internal-plugin-conversation": "3.12.0-next.24",
|
|
67
67
|
"@webex/internal-plugin-device": "3.12.0-next.22",
|
|
68
68
|
"@webex/internal-plugin-llm": "3.12.0-next.26",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@webex/internal-plugin-support": "3.12.0-next.24",
|
|
72
72
|
"@webex/internal-plugin-user": "3.12.0-next.23",
|
|
73
73
|
"@webex/internal-plugin-voicea": "3.12.0-next.26",
|
|
74
|
-
"@webex/media-helpers": "3.12.0-next.
|
|
74
|
+
"@webex/media-helpers": "3.12.0-next.8",
|
|
75
75
|
"@webex/plugin-people": "3.12.0-next.23",
|
|
76
76
|
"@webex/plugin-rooms": "3.12.0-next.24",
|
|
77
77
|
"@webex/ts-sdp": "^1.8.1",
|
|
@@ -94,5 +94,5 @@
|
|
|
94
94
|
"//": [
|
|
95
95
|
"TODO: upgrade jwt-decode when moving to node 18"
|
|
96
96
|
],
|
|
97
|
-
"version": "3.12.0-next.
|
|
97
|
+
"version": "3.12.0-next.79"
|
|
98
98
|
}
|
package/src/meeting/index.ts
CHANGED
|
@@ -191,6 +191,7 @@ import AIEnableRequest from '../aiEnableRequest';
|
|
|
191
191
|
const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
|
|
192
192
|
|
|
193
193
|
const LLM_HEALTHCHECK_TIMER_MS = 3 * 60 * 1000;
|
|
194
|
+
const JOIN_WITH_MEDIA_RETRY_MAX_COUNT = 2;
|
|
194
195
|
|
|
195
196
|
const logRequest = (request: any, {logText = ''}) => {
|
|
196
197
|
LoggerProxy.logger.info(`${logText} - sending request`);
|
|
@@ -794,7 +795,13 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
794
795
|
private deferSDPAnswer?: Defer; // used for waiting for a response
|
|
795
796
|
private sdpResponseTimer?: ReturnType<typeof setTimeout>;
|
|
796
797
|
private hasMediaConnectionConnectedAtLeastOnce: boolean;
|
|
797
|
-
private joinWithMediaRetryInfo
|
|
798
|
+
private joinWithMediaRetryInfo: {
|
|
799
|
+
retryCount: number;
|
|
800
|
+
prevJoinResponse?: any;
|
|
801
|
+
firstError?: Error;
|
|
802
|
+
prevError?: Error;
|
|
803
|
+
};
|
|
804
|
+
|
|
798
805
|
private connectionStateHandler?: ConnectionStateHandler;
|
|
799
806
|
private iceCandidateErrors: Map<string, number>;
|
|
800
807
|
private iceCandidatesCount: number;
|
|
@@ -1668,11 +1675,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1668
1675
|
/**
|
|
1669
1676
|
* Information needed for a retry of a call to joinWithMedia
|
|
1670
1677
|
* @instance
|
|
1671
|
-
* @type {{
|
|
1678
|
+
* @type {{retryCount: number; prevJoinResponse?: any}}
|
|
1672
1679
|
* @private
|
|
1673
1680
|
* @memberof Meeting
|
|
1674
1681
|
*/
|
|
1675
|
-
this.joinWithMediaRetryInfo = {
|
|
1682
|
+
this.joinWithMediaRetryInfo = {retryCount: 0, prevJoinResponse: undefined};
|
|
1676
1683
|
|
|
1677
1684
|
/**
|
|
1678
1685
|
* Connection state handler
|
|
@@ -5576,7 +5583,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5576
5583
|
} = {}
|
|
5577
5584
|
) {
|
|
5578
5585
|
const {mediaOptions, joinOptions = {}} = options;
|
|
5579
|
-
const {
|
|
5586
|
+
const {retryCount, prevJoinResponse, prevError} = this.joinWithMediaRetryInfo;
|
|
5580
5587
|
|
|
5581
5588
|
if (!mediaOptions?.allowMediaInLobby) {
|
|
5582
5589
|
return Promise.reject(
|
|
@@ -5602,12 +5609,17 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5602
5609
|
);
|
|
5603
5610
|
}
|
|
5604
5611
|
|
|
5612
|
+
const shouldJoin =
|
|
5613
|
+
!joinResponse || // first try, when the join response is empty
|
|
5614
|
+
(prevError && prevError instanceof UserNotJoinedError) || // last try failed with UserNotJoinedError
|
|
5615
|
+
MeetingUtil.isUserInLeftState(this.locusInfo); // locus dropped the connection before we can re-try addMedia
|
|
5616
|
+
|
|
5605
5617
|
try {
|
|
5606
5618
|
let turnServerInfo;
|
|
5607
5619
|
let turnDiscoverySkippedReason;
|
|
5608
5620
|
let forceTurnDiscovery = false;
|
|
5609
5621
|
|
|
5610
|
-
if (
|
|
5622
|
+
if (shouldJoin) {
|
|
5611
5623
|
// This is the 1st attempt or a retry after join request failed -> we need to do a join with TURN discovery
|
|
5612
5624
|
|
|
5613
5625
|
const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(
|
|
@@ -5648,14 +5660,17 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5648
5660
|
|
|
5649
5661
|
const mediaResponse = await this.addMediaInternal(
|
|
5650
5662
|
() => {
|
|
5651
|
-
|
|
5663
|
+
// callback is not called when UserNotJoinedError is thrown
|
|
5664
|
+
return this.joinWithMediaRetryInfo.retryCount >= 1
|
|
5665
|
+
? 'JOIN_MEETING_FINAL'
|
|
5666
|
+
: 'JOIN_MEETING_RETRY';
|
|
5652
5667
|
},
|
|
5653
5668
|
turnServerInfo,
|
|
5654
5669
|
forceTurnDiscovery,
|
|
5655
5670
|
mediaOptions
|
|
5656
5671
|
);
|
|
5657
5672
|
|
|
5658
|
-
this.joinWithMediaRetryInfo = {
|
|
5673
|
+
this.joinWithMediaRetryInfo = {retryCount: 0, prevJoinResponse: undefined};
|
|
5659
5674
|
|
|
5660
5675
|
return {
|
|
5661
5676
|
join: joinResponse,
|
|
@@ -5669,8 +5684,10 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5669
5684
|
|
|
5670
5685
|
this.roap.abortTurnDiscovery();
|
|
5671
5686
|
|
|
5672
|
-
//
|
|
5673
|
-
let shouldRetry =
|
|
5687
|
+
// let's do a retry
|
|
5688
|
+
let shouldRetry =
|
|
5689
|
+
retryCount < 1 ||
|
|
5690
|
+
(error instanceof UserNotJoinedError && retryCount < JOIN_WITH_MEDIA_RETRY_MAX_COUNT);
|
|
5674
5691
|
|
|
5675
5692
|
if (
|
|
5676
5693
|
CallDiagnosticUtils.isSdpOfferCreationError(error) ||
|
|
@@ -5695,8 +5712,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5695
5712
|
});
|
|
5696
5713
|
}
|
|
5697
5714
|
|
|
5698
|
-
// we only want to call leave if join was successful and
|
|
5699
|
-
if (joined &&
|
|
5715
|
+
// we only want to call leave if join was successful, and we won't be doing any more retries
|
|
5716
|
+
if (joined && !shouldRetry) {
|
|
5700
5717
|
try {
|
|
5701
5718
|
await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
|
|
5702
5719
|
} catch (e) {
|
|
@@ -5713,7 +5730,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5713
5730
|
reason: error.message,
|
|
5714
5731
|
stack: error.stack,
|
|
5715
5732
|
leaveErrorReason: leaveError?.message,
|
|
5716
|
-
isRetry,
|
|
5733
|
+
isRetry: retryCount > 0,
|
|
5717
5734
|
},
|
|
5718
5735
|
{
|
|
5719
5736
|
type: error.name,
|
|
@@ -5722,15 +5739,26 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5722
5739
|
|
|
5723
5740
|
if (shouldRetry) {
|
|
5724
5741
|
LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
|
|
5725
|
-
this.joinWithMediaRetryInfo.
|
|
5742
|
+
this.joinWithMediaRetryInfo.retryCount = retryCount + 1;
|
|
5726
5743
|
this.joinWithMediaRetryInfo.prevJoinResponse = joinResponse;
|
|
5744
|
+
this.joinWithMediaRetryInfo.prevError = error;
|
|
5745
|
+
if (!this.joinWithMediaRetryInfo.firstError) {
|
|
5746
|
+
this.joinWithMediaRetryInfo.firstError = error;
|
|
5747
|
+
}
|
|
5727
5748
|
|
|
5728
5749
|
return this.joinWithMedia(options);
|
|
5729
5750
|
}
|
|
5730
5751
|
|
|
5731
|
-
|
|
5752
|
+
const {firstError} = this.joinWithMediaRetryInfo;
|
|
5732
5753
|
|
|
5733
|
-
|
|
5754
|
+
this.joinWithMediaRetryInfo = {
|
|
5755
|
+
retryCount: 0,
|
|
5756
|
+
prevJoinResponse: undefined,
|
|
5757
|
+
firstError: undefined,
|
|
5758
|
+
prevError: undefined,
|
|
5759
|
+
};
|
|
5760
|
+
|
|
5761
|
+
throw firstError ?? error;
|
|
5734
5762
|
}
|
|
5735
5763
|
}
|
|
5736
5764
|
|
|
@@ -8731,7 +8759,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8731
8759
|
numTransports,
|
|
8732
8760
|
isMultistream: this.isMultistream,
|
|
8733
8761
|
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
|
|
8734
|
-
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.
|
|
8762
|
+
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.retryCount > 0,
|
|
8735
8763
|
...reachabilityMetrics,
|
|
8736
8764
|
...iceCandidateErrors,
|
|
8737
8765
|
iceCandidatesCount: this.iceCandidatesCount,
|
|
@@ -8776,7 +8804,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8776
8804
|
turnServerUsed: this.turnServerUsed,
|
|
8777
8805
|
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
|
|
8778
8806
|
isMultistream: this.isMultistream,
|
|
8779
|
-
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.
|
|
8807
|
+
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.retryCount > 0,
|
|
8780
8808
|
signalingState:
|
|
8781
8809
|
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
|
|
8782
8810
|
?.signalingState ||
|
|
@@ -963,7 +963,7 @@ describe('plugin-meetings', () => {
|
|
|
963
963
|
|
|
964
964
|
// resets joinWithMediaRetryInfo
|
|
965
965
|
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
966
|
-
|
|
966
|
+
retryCount: 0,
|
|
967
967
|
prevJoinResponse: undefined,
|
|
968
968
|
});
|
|
969
969
|
});
|
|
@@ -1035,7 +1035,12 @@ describe('plugin-meetings', () => {
|
|
|
1035
1035
|
meeting.join = sinon.stub().returns(Promise.reject(error));
|
|
1036
1036
|
meeting.locusUrl = null; // when join fails, we end up with null locusUrl
|
|
1037
1037
|
|
|
1038
|
-
await assert.isRejected(
|
|
1038
|
+
const thrownError = await assert.isRejected(
|
|
1039
|
+
meeting.joinWithMedia({mediaOptions: {allowMediaInLobby: true}})
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
// should throw the first attempt's error
|
|
1043
|
+
assert.equal(thrownError, error);
|
|
1039
1044
|
|
|
1040
1045
|
assert.calledTwice(abortTurnDiscoveryStub);
|
|
1041
1046
|
|
|
@@ -1073,11 +1078,79 @@ describe('plugin-meetings', () => {
|
|
|
1073
1078
|
|
|
1074
1079
|
// resets joinWithMediaRetryInfo
|
|
1075
1080
|
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
1076
|
-
|
|
1081
|
+
retryCount: 0,
|
|
1077
1082
|
prevJoinResponse: undefined,
|
|
1083
|
+
firstError: undefined,
|
|
1084
|
+
prevError: undefined,
|
|
1078
1085
|
});
|
|
1079
1086
|
});
|
|
1080
1087
|
|
|
1088
|
+
it('should re-join on retry when join() fails on first attempt, and throw the first error if join fails again', async () => {
|
|
1089
|
+
const firstJoinError = new Error('first join error');
|
|
1090
|
+
const secondJoinError = new Error('second join error');
|
|
1091
|
+
|
|
1092
|
+
meeting.join = sinon
|
|
1093
|
+
.stub()
|
|
1094
|
+
.onFirstCall()
|
|
1095
|
+
.rejects(firstJoinError)
|
|
1096
|
+
.onSecondCall()
|
|
1097
|
+
.rejects(secondJoinError);
|
|
1098
|
+
meeting.locusUrl = null; // join never succeeds
|
|
1099
|
+
|
|
1100
|
+
const thrownError = await assert.isRejected(
|
|
1101
|
+
meeting.joinWithMedia({joinOptions, mediaOptions})
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
// join() should be called twice — once for the first attempt, once for the re-join
|
|
1105
|
+
assert.calledTwice(meeting.join);
|
|
1106
|
+
// TURN discovery should be attempted twice (once per join)
|
|
1107
|
+
assert.calledTwice(generateTurnDiscoveryRequestMessageStub);
|
|
1108
|
+
|
|
1109
|
+
// should throw the first attempt's error, not the second
|
|
1110
|
+
assert.equal(thrownError, firstJoinError);
|
|
1111
|
+
|
|
1112
|
+
assert.calledTwice(Metrics.sendBehavioralMetric);
|
|
1113
|
+
assert.calledWith(
|
|
1114
|
+
Metrics.sendBehavioralMetric.firstCall,
|
|
1115
|
+
BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
|
|
1116
|
+
sinon.match({reason: firstJoinError.message, isRetry: false}),
|
|
1117
|
+
sinon.match.any
|
|
1118
|
+
);
|
|
1119
|
+
assert.calledWith(
|
|
1120
|
+
Metrics.sendBehavioralMetric.secondCall,
|
|
1121
|
+
BEHAVIORAL_METRICS.JOIN_WITH_MEDIA_FAILURE,
|
|
1122
|
+
sinon.match({reason: secondJoinError.message, isRetry: true}),
|
|
1123
|
+
sinon.match.any
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
1127
|
+
retryCount: 0,
|
|
1128
|
+
prevJoinResponse: undefined,
|
|
1129
|
+
firstError: undefined,
|
|
1130
|
+
prevError: undefined,
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
it('should NOT call join() again on retry when join() succeeded but addMediaInternal() failed', async () => {
|
|
1135
|
+
const addMediaError = new Error('addMedia error');
|
|
1136
|
+
|
|
1137
|
+
meeting.addMediaInternal = sinon
|
|
1138
|
+
.stub()
|
|
1139
|
+
.onFirstCall()
|
|
1140
|
+
.rejects(addMediaError)
|
|
1141
|
+
.onSecondCall()
|
|
1142
|
+
.resolves(test4);
|
|
1143
|
+
|
|
1144
|
+
const result = await meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
1145
|
+
|
|
1146
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
1147
|
+
|
|
1148
|
+
// join() should only be called once — the successful join result is reused on retry
|
|
1149
|
+
assert.calledOnce(meeting.join);
|
|
1150
|
+
// TURN discovery request is only generated once (on the first attempt alongside join)
|
|
1151
|
+
assert.calledOnce(generateTurnDiscoveryRequestMessageStub);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1081
1154
|
it('should resolve if join() fails the first time but succeeds the second time', async () => {
|
|
1082
1155
|
const error = new Error('fake');
|
|
1083
1156
|
meeting.join = sinon
|
|
@@ -1119,7 +1192,7 @@ describe('plugin-meetings', () => {
|
|
|
1119
1192
|
|
|
1120
1193
|
// resets joinWithMediaRetryInfo
|
|
1121
1194
|
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
1122
|
-
|
|
1195
|
+
retryCount: 0,
|
|
1123
1196
|
prevJoinResponse: undefined,
|
|
1124
1197
|
});
|
|
1125
1198
|
});
|
|
@@ -1188,6 +1261,127 @@ describe('plugin-meetings', () => {
|
|
|
1188
1261
|
);
|
|
1189
1262
|
});
|
|
1190
1263
|
|
|
1264
|
+
it('should throw the first attempt error when retry also fails with a different error', async () => {
|
|
1265
|
+
const firstError = new Error('first attempt error');
|
|
1266
|
+
const secondError = new Error('second attempt error');
|
|
1267
|
+
|
|
1268
|
+
const addMediaInternalResults = [];
|
|
1269
|
+
meeting.addMediaInternal = sinon.stub().callsFake(() => {
|
|
1270
|
+
const defer = new Defer();
|
|
1271
|
+
addMediaInternalResults.push(defer);
|
|
1272
|
+
return defer.promise;
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
const leaveStub = sinon.stub(meeting, 'leave').resolves();
|
|
1276
|
+
|
|
1277
|
+
const result = meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
1278
|
+
|
|
1279
|
+
await testUtils.flushPromises();
|
|
1280
|
+
|
|
1281
|
+
// 1st attempt fails
|
|
1282
|
+
addMediaInternalResults[0].reject(firstError);
|
|
1283
|
+
await testUtils.flushPromises();
|
|
1284
|
+
|
|
1285
|
+
// leave() should NOT be called after the 1st attempt (intermediate retry)
|
|
1286
|
+
assert.notCalled(leaveStub);
|
|
1287
|
+
|
|
1288
|
+
// 2nd (final) attempt fails
|
|
1289
|
+
addMediaInternalResults[1].reject(secondError);
|
|
1290
|
+
const thrownError = await assert.isRejected(result);
|
|
1291
|
+
|
|
1292
|
+
// should throw the first error, not the second
|
|
1293
|
+
assert.equal(thrownError, firstError);
|
|
1294
|
+
|
|
1295
|
+
// leave() should only be called after the last (2nd) attempt
|
|
1296
|
+
assert.calledOnce(leaveStub);
|
|
1297
|
+
|
|
1298
|
+
assert.calledTwice(Metrics.sendBehavioralMetric);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
it('should throw the UserNotJoinedError as firstError when it occurs on the 1st attempt and 2nd attempt fails differently', async () => {
|
|
1302
|
+
const userNotJoinedError = new UserNotJoinedError();
|
|
1303
|
+
const secondError = new Error('second attempt error');
|
|
1304
|
+
|
|
1305
|
+
const addMediaInternalResults = [];
|
|
1306
|
+
meeting.addMediaInternal = sinon.stub().callsFake(() => {
|
|
1307
|
+
const defer = new Defer();
|
|
1308
|
+
addMediaInternalResults.push(defer);
|
|
1309
|
+
return defer.promise;
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
const leaveStub = sinon.stub(meeting, 'leave').resolves();
|
|
1313
|
+
|
|
1314
|
+
const result = meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
1315
|
+
|
|
1316
|
+
await testUtils.flushPromises();
|
|
1317
|
+
|
|
1318
|
+
// 1st attempt fails with UserNotJoinedError — triggers a re-join
|
|
1319
|
+
addMediaInternalResults[0].reject(userNotJoinedError);
|
|
1320
|
+
await testUtils.flushPromises();
|
|
1321
|
+
|
|
1322
|
+
// leave() should NOT be called after the 1st attempt (intermediate retry)
|
|
1323
|
+
assert.notCalled(leaveStub);
|
|
1324
|
+
|
|
1325
|
+
// 2nd (final) attempt fails
|
|
1326
|
+
addMediaInternalResults[1].reject(secondError);
|
|
1327
|
+
const thrownError = await assert.isRejected(result);
|
|
1328
|
+
|
|
1329
|
+
// should throw the first (UserNotJoinedError), not the second
|
|
1330
|
+
assert.equal(thrownError, userNotJoinedError);
|
|
1331
|
+
|
|
1332
|
+
// join() called twice: original + re-join triggered by UserNotJoinedError in prevError
|
|
1333
|
+
assert.calledTwice(meeting.join);
|
|
1334
|
+
// leave() should only be called after the last (2nd) attempt
|
|
1335
|
+
assert.calledOnce(leaveStub);
|
|
1336
|
+
assert.calledTwice(Metrics.sendBehavioralMetric);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
it('should throw the first UserNotJoinedError when it occurs on both 1st and 2nd attempts and a 3rd attempt also fails', async () => {
|
|
1340
|
+
const firstUserNotJoinedError = new UserNotJoinedError('first');
|
|
1341
|
+
const secondUserNotJoinedError = new UserNotJoinedError('second');
|
|
1342
|
+
const thirdError = new Error('third attempt error');
|
|
1343
|
+
|
|
1344
|
+
const addMediaInternalResults = [];
|
|
1345
|
+
meeting.addMediaInternal = sinon.stub().callsFake(() => {
|
|
1346
|
+
const defer = new Defer();
|
|
1347
|
+
addMediaInternalResults.push(defer);
|
|
1348
|
+
return defer.promise;
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
const leaveStub = sinon.stub(meeting, 'leave').resolves();
|
|
1352
|
+
|
|
1353
|
+
const result = meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
1354
|
+
|
|
1355
|
+
await testUtils.flushPromises();
|
|
1356
|
+
|
|
1357
|
+
// 1st attempt fails with UserNotJoinedError — triggers a re-join
|
|
1358
|
+
addMediaInternalResults[0].reject(firstUserNotJoinedError);
|
|
1359
|
+
await testUtils.flushPromises();
|
|
1360
|
+
|
|
1361
|
+
// leave() should NOT be called after the 1st attempt (intermediate retry)
|
|
1362
|
+
assert.notCalled(leaveStub);
|
|
1363
|
+
|
|
1364
|
+
// 2nd attempt fails with UserNotJoinedError again — triggers another re-join
|
|
1365
|
+
addMediaInternalResults[1].reject(secondUserNotJoinedError);
|
|
1366
|
+
await testUtils.flushPromises();
|
|
1367
|
+
|
|
1368
|
+
// leave() should NOT be called after the 2nd attempt (intermediate retry)
|
|
1369
|
+
assert.notCalled(leaveStub);
|
|
1370
|
+
|
|
1371
|
+
// 3rd (final) attempt fails
|
|
1372
|
+
addMediaInternalResults[2].reject(thirdError);
|
|
1373
|
+
const thrownError = await assert.isRejected(result);
|
|
1374
|
+
|
|
1375
|
+
// should throw the very first error across all 3 attempts
|
|
1376
|
+
assert.equal(thrownError, firstUserNotJoinedError);
|
|
1377
|
+
|
|
1378
|
+
// join() called 3 times: original + 2 re-joins triggered by prevError being UserNotJoinedError
|
|
1379
|
+
assert.calledThrice(meeting.join);
|
|
1380
|
+
// leave() should only be called after the last (3rd) attempt
|
|
1381
|
+
assert.calledOnce(leaveStub);
|
|
1382
|
+
assert.calledThrice(Metrics.sendBehavioralMetric);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1191
1385
|
it('should call leave() if addMediaInternal() fails with a browser media error (TypeError)', async () => {
|
|
1192
1386
|
const addMediaError = new Error('fake addMedia error');
|
|
1193
1387
|
addMediaError.name = 'TypeError'; // This makes it a browser media error
|
|
@@ -1435,7 +1629,8 @@ describe('plugin-meetings', () => {
|
|
|
1435
1629
|
|
|
1436
1630
|
await testUtils.flushPromises();
|
|
1437
1631
|
|
|
1438
|
-
// check the callback works correctly on the 2nd attempt
|
|
1632
|
+
// check the callback works correctly on the 2nd attempt:
|
|
1633
|
+
// retryCount=1 with a non-UserNotJoinedError is terminal, so icePhase must be JOIN_MEETING_FINAL
|
|
1439
1634
|
assert.equal(icePhaseCallbacks.length, 2);
|
|
1440
1635
|
assert.equal(icePhaseCallbacks[1](), 'JOIN_MEETING_FINAL');
|
|
1441
1636
|
|
|
@@ -1445,6 +1640,118 @@ describe('plugin-meetings', () => {
|
|
|
1445
1640
|
await assert.isRejected(result);
|
|
1446
1641
|
});
|
|
1447
1642
|
|
|
1643
|
+
it('should allow an additional retry when UserNotJoinedError occurs and return JOIN_MEETING_FINAL on the 3rd attempt', async () => {
|
|
1644
|
+
const genericError = new Error('generic error');
|
|
1645
|
+
const userNotJoinedError = new UserNotJoinedError();
|
|
1646
|
+
const thirdError = new Error('third error');
|
|
1647
|
+
|
|
1648
|
+
const icePhaseCallbacks = [];
|
|
1649
|
+
const addMediaInternalResults = [];
|
|
1650
|
+
|
|
1651
|
+
meeting.addMediaInternal = sinon
|
|
1652
|
+
.stub()
|
|
1653
|
+
.callsFake((icePhaseCallback) => {
|
|
1654
|
+
const defer = new Defer();
|
|
1655
|
+
|
|
1656
|
+
icePhaseCallbacks.push(icePhaseCallback);
|
|
1657
|
+
addMediaInternalResults.push(defer);
|
|
1658
|
+
return defer.promise;
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
const leaveStub = sinon.stub(meeting, 'leave').resolves();
|
|
1662
|
+
|
|
1663
|
+
const result = meeting.joinWithMedia({
|
|
1664
|
+
joinOptions,
|
|
1665
|
+
mediaOptions,
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
await testUtils.flushPromises();
|
|
1669
|
+
|
|
1670
|
+
// 1st attempt: retryCount=0 → JOIN_MEETING_RETRY
|
|
1671
|
+
assert.equal(icePhaseCallbacks.length, 1);
|
|
1672
|
+
assert.equal(icePhaseCallbacks[0](), 'JOIN_MEETING_RETRY');
|
|
1673
|
+
addMediaInternalResults[0].reject(genericError);
|
|
1674
|
+
|
|
1675
|
+
await testUtils.flushPromises();
|
|
1676
|
+
|
|
1677
|
+
// leave() should NOT be called after the 1st attempt (intermediate retry)
|
|
1678
|
+
assert.notCalled(leaveStub);
|
|
1679
|
+
|
|
1680
|
+
// 2nd attempt: retryCount=1 → JOIN_MEETING_FINAL
|
|
1681
|
+
// (In real usage this callback is never invoked when UserNotJoinedError is thrown,
|
|
1682
|
+
// because UserNotJoinedError is thrown before waitForMediaConnectionConnected() is reached.)
|
|
1683
|
+
assert.equal(icePhaseCallbacks.length, 2);
|
|
1684
|
+
assert.equal(icePhaseCallbacks[1](), 'JOIN_MEETING_FINAL');
|
|
1685
|
+
addMediaInternalResults[1].reject(userNotJoinedError);
|
|
1686
|
+
|
|
1687
|
+
await testUtils.flushPromises();
|
|
1688
|
+
|
|
1689
|
+
// leave() should NOT be called after the 2nd attempt (intermediate retry)
|
|
1690
|
+
assert.notCalled(leaveStub);
|
|
1691
|
+
|
|
1692
|
+
// 3rd attempt: retryCount=2 → JOIN_MEETING_FINAL
|
|
1693
|
+
assert.equal(icePhaseCallbacks.length, 3);
|
|
1694
|
+
assert.equal(icePhaseCallbacks[2](), 'JOIN_MEETING_FINAL');
|
|
1695
|
+
addMediaInternalResults[2].reject(thirdError);
|
|
1696
|
+
|
|
1697
|
+
const thrownError = await assert.isRejected(result);
|
|
1698
|
+
|
|
1699
|
+
// should throw the first error
|
|
1700
|
+
assert.equal(thrownError, genericError);
|
|
1701
|
+
|
|
1702
|
+
// leave() should only be called once, after the last (3rd) attempt
|
|
1703
|
+
assert.calledOnce(leaveStub);
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
it('should re-join when retrying after a UserNotJoinedError', async () => {
|
|
1707
|
+
const userNotJoinedError = new UserNotJoinedError();
|
|
1708
|
+
|
|
1709
|
+
const leaveStub = sinon.stub(meeting, 'leave').resolves();
|
|
1710
|
+
|
|
1711
|
+
meeting.addMediaInternal = sinon
|
|
1712
|
+
.stub()
|
|
1713
|
+
.onFirstCall()
|
|
1714
|
+
.rejects(userNotJoinedError)
|
|
1715
|
+
.onSecondCall()
|
|
1716
|
+
.resolves(test4);
|
|
1717
|
+
|
|
1718
|
+
const result = await meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
1719
|
+
|
|
1720
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
1721
|
+
|
|
1722
|
+
// join() should be called twice — once for the first attempt, once for the re-join after UserNotJoinedError
|
|
1723
|
+
assert.calledTwice(meeting.join);
|
|
1724
|
+
// TURN discovery should be attempted twice (once per join)
|
|
1725
|
+
assert.calledTwice(generateTurnDiscoveryRequestMessageStub);
|
|
1726
|
+
// leave() should never be called when retrying after UserNotJoinedError
|
|
1727
|
+
assert.notCalled(leaveStub);
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
it('should re-join when isUserInLeftState returns true on retry', async () => {
|
|
1731
|
+
const addMediaError = new Error('addMedia error');
|
|
1732
|
+
|
|
1733
|
+
sinon.stub(MeetingUtil, 'isUserInLeftState').returns(true);
|
|
1734
|
+
|
|
1735
|
+
const leaveStub = sinon.stub(meeting, 'leave').resolves();
|
|
1736
|
+
|
|
1737
|
+
meeting.addMediaInternal = sinon
|
|
1738
|
+
.stub()
|
|
1739
|
+
.onFirstCall()
|
|
1740
|
+
.rejects(addMediaError)
|
|
1741
|
+
.onSecondCall()
|
|
1742
|
+
.resolves(test4);
|
|
1743
|
+
|
|
1744
|
+
const result = await meeting.joinWithMedia({joinOptions, mediaOptions});
|
|
1745
|
+
|
|
1746
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
1747
|
+
|
|
1748
|
+
// join() should be called twice — once for the first attempt, once because the user is in left state
|
|
1749
|
+
assert.calledTwice(meeting.join);
|
|
1750
|
+
assert.calledTwice(generateTurnDiscoveryRequestMessageStub);
|
|
1751
|
+
// leave() should never be called when retrying after isUserInLeftState returns true
|
|
1752
|
+
assert.notCalled(leaveStub);
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1448
1755
|
[
|
|
1449
1756
|
{
|
|
1450
1757
|
errorName: 'SdpOfferCreationError',
|