@webex/plugin-meetings 3.3.1-next.2 → 3.3.1-next.20

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.
Files changed (51) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +7 -2
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/interpretation/index.js +1 -1
  5. package/dist/interpretation/siLanguage.js +1 -1
  6. package/dist/media/MediaConnectionAwaiter.js +50 -13
  7. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  8. package/dist/mediaQualityMetrics/config.js +16 -6
  9. package/dist/mediaQualityMetrics/config.js.map +1 -1
  10. package/dist/meeting/connectionStateHandler.js +67 -0
  11. package/dist/meeting/connectionStateHandler.js.map +1 -0
  12. package/dist/meeting/index.js +98 -46
  13. package/dist/meeting/index.js.map +1 -1
  14. package/dist/metrics/constants.js +2 -1
  15. package/dist/metrics/constants.js.map +1 -1
  16. package/dist/reachability/clusterReachability.js +108 -53
  17. package/dist/reachability/clusterReachability.js.map +1 -1
  18. package/dist/reachability/index.js +415 -56
  19. package/dist/reachability/index.js.map +1 -1
  20. package/dist/statsAnalyzer/index.js +81 -27
  21. package/dist/statsAnalyzer/index.js.map +1 -1
  22. package/dist/statsAnalyzer/mqaUtil.js +36 -10
  23. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  24. package/dist/types/media/MediaConnectionAwaiter.d.ts +18 -4
  25. package/dist/types/mediaQualityMetrics/config.d.ts +11 -0
  26. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  27. package/dist/types/meeting/index.d.ts +2 -0
  28. package/dist/types/metrics/constants.d.ts +1 -0
  29. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  30. package/dist/types/reachability/index.d.ts +93 -2
  31. package/dist/types/statsAnalyzer/index.d.ts +15 -6
  32. package/dist/types/statsAnalyzer/mqaUtil.d.ts +17 -4
  33. package/dist/webinar/index.js +1 -1
  34. package/package.json +23 -22
  35. package/src/breakouts/index.ts +7 -1
  36. package/src/media/MediaConnectionAwaiter.ts +66 -11
  37. package/src/mediaQualityMetrics/config.ts +14 -3
  38. package/src/meeting/connectionStateHandler.ts +65 -0
  39. package/src/meeting/index.ts +72 -14
  40. package/src/metrics/constants.ts +1 -0
  41. package/src/reachability/clusterReachability.ts +86 -25
  42. package/src/reachability/index.ts +313 -27
  43. package/src/statsAnalyzer/index.ts +85 -24
  44. package/src/statsAnalyzer/mqaUtil.ts +55 -7
  45. package/test/unit/spec/breakouts/index.ts +51 -32
  46. package/test/unit/spec/media/MediaConnectionAwaiter.ts +90 -32
  47. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  48. package/test/unit/spec/meeting/index.js +158 -36
  49. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  50. package/test/unit/spec/reachability/index.ts +1120 -84
  51. package/test/unit/spec/stats-analyzer/index.js +647 -319
@@ -2216,7 +2216,7 @@ describe('plugin-meetings', () => {
2216
2216
  turnDiscoverySkippedReason: undefined,
2217
2217
  });
2218
2218
  meeting.meetingState = 'ACTIVE';
2219
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
2219
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2220
2220
 
2221
2221
  const forceRtcMetricsSend = sinon.stub().resolves();
2222
2222
  const closeMediaConnectionStub = sinon.stub();
@@ -2240,12 +2240,12 @@ describe('plugin-meetings', () => {
2240
2240
  assert.calledTwice(generateClientErrorCodeForIceFailureStub);
2241
2241
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2242
2242
  signalingState: 'unknown',
2243
- iceConnectionState: 'unknown',
2243
+ iceConnected: false,
2244
2244
  turnServerUsed: false,
2245
2245
  });
2246
2246
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2247
2247
  signalingState: 'unknown',
2248
- iceConnectionState: 'unknown',
2248
+ iceConnected: false,
2249
2249
  turnServerUsed: true,
2250
2250
  });
2251
2251
 
@@ -2440,7 +2440,7 @@ describe('plugin-meetings', () => {
2440
2440
  assert.calledOnce(generateClientErrorCodeForIceFailureStub);
2441
2441
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2442
2442
  signalingState: 'unknown',
2443
- iceConnectionState: 'unknown',
2443
+ iceConnected: undefined,
2444
2444
  turnServerUsed: false,
2445
2445
  });
2446
2446
 
@@ -2715,7 +2715,7 @@ describe('plugin-meetings', () => {
2715
2715
  turnDiscoverySkippedReason: undefined,
2716
2716
  });
2717
2717
  meeting.meetingState = 'ACTIVE';
2718
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
2718
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2719
2719
 
2720
2720
  const forceRtcMetricsSend = sinon.stub().resolves();
2721
2721
  const closeMediaConnectionStub = sinon.stub();
@@ -2762,6 +2762,66 @@ describe('plugin-meetings', () => {
2762
2762
  assert.isOk(errorThrown);
2763
2763
  });
2764
2764
 
2765
+ it('should send ICE_CANDIDATE_ERROR metric if media connection fails and ice candidate errors have been gathered', async () => {
2766
+ let errorThrown = undefined;
2767
+
2768
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
2769
+ turnServerInfo: undefined,
2770
+ turnDiscoverySkippedReason: undefined,
2771
+ });
2772
+ meeting.meetingState = 'ACTIVE';
2773
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2774
+
2775
+ const forceRtcMetricsSend = sinon.stub().resolves();
2776
+ const closeMediaConnectionStub = sinon.stub();
2777
+ Media.createMediaConnection = sinon.stub().returns({
2778
+ close: closeMediaConnectionStub,
2779
+ forceRtcMetricsSend,
2780
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2781
+ initiateOffer: sinon.stub().resolves({}),
2782
+ on: sinon.stub(),
2783
+ });
2784
+
2785
+ meeting.iceCandidateErrors.set('701_error', 2);
2786
+ meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
2787
+
2788
+ await meeting
2789
+ .addMedia({
2790
+ mediaSettings: {},
2791
+ })
2792
+ .catch((err) => {
2793
+ errorThrown = err;
2794
+ assert.instanceOf(err, AddMediaFailed);
2795
+ });
2796
+
2797
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
2798
+ assert.calledOnceWithExactly(
2799
+ Metrics.sendBehavioralMetric,
2800
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
2801
+ {
2802
+ correlation_id: meeting.correlationId,
2803
+ locus_id: meeting.locusUrl.split('/').pop(),
2804
+ reason: errorThrown.message,
2805
+ stack: errorThrown.stack,
2806
+ code: errorThrown.code,
2807
+ turnDiscoverySkippedReason: undefined,
2808
+ turnServerUsed: true,
2809
+ retriedWithTurnServer: false,
2810
+ isMultistream: false,
2811
+ isJoinWithMediaRetry: false,
2812
+ signalingState: 'unknown',
2813
+ connectionState: 'unknown',
2814
+ iceConnectionState: 'unknown',
2815
+ selectedCandidatePairChanges: 2,
2816
+ numTransports: 1,
2817
+ '701_error': 2,
2818
+ '701_turn_host_lookup_received_error': 1
2819
+ }
2820
+ );
2821
+
2822
+ assert.isOk(errorThrown);
2823
+ });
2824
+
2765
2825
  describe('handles StatsAnalyzer events', () => {
2766
2826
  let prevConfigValue;
2767
2827
  let statsAnalyzerStub;
@@ -3058,7 +3118,7 @@ describe('plugin-meetings', () => {
3058
3118
 
3059
3119
  meeting.meetingState = 'ACTIVE';
3060
3120
  meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
3061
- new Error('fake error')
3121
+ {iceConnected: false}
3062
3122
  );
3063
3123
 
3064
3124
  let errorThrown = false;
@@ -3073,7 +3133,7 @@ describe('plugin-meetings', () => {
3073
3133
 
3074
3134
  assert.calledOnceWithExactly(generateClientErrorCodeForIceFailureStub, {
3075
3135
  signalingState: 'unknown',
3076
- iceConnectionState: 'unknown',
3136
+ iceConnected: false,
3077
3137
  turnServerUsed: true,
3078
3138
  });
3079
3139
 
@@ -6275,7 +6335,7 @@ describe('plugin-meetings', () => {
6275
6335
  },
6276
6336
  'SELF_OBSERVING'
6277
6337
  );
6278
-
6338
+
6279
6339
 
6280
6340
  // Verify that the event handler behaves as expected
6281
6341
  expect(meeting.statsAnalyzer.stopAnalyzer.calledOnce).to.be.true;
@@ -7079,6 +7139,10 @@ describe('plugin-meetings', () => {
7079
7139
  id: 'stream',
7080
7140
  getTracks: () => [{id: 'track', addEventListener: sinon.stub()}],
7081
7141
  };
7142
+ const simulateConnectionStateChange = (newState) => {
7143
+ meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon.stub().returns(newState);
7144
+ eventListeners[Event.PEER_CONNECTION_STATE_CHANGED]();
7145
+ }
7082
7146
 
7083
7147
  beforeEach(() => {
7084
7148
  eventListeners = {};
@@ -7088,6 +7152,7 @@ describe('plugin-meetings', () => {
7088
7152
  on: sinon.stub().callsFake((event, listener) => {
7089
7153
  eventListeners[event] = listener;
7090
7154
  }),
7155
+ getConnectionState: sinon.stub().returns(ConnectionState.New),
7091
7156
  };
7092
7157
  MediaUtil.createMediaStream.returns(fakeStream);
7093
7158
  });
@@ -7099,7 +7164,9 @@ describe('plugin-meetings', () => {
7099
7164
  assert.isFunction(eventListeners[Event.ROAP_FAILURE]);
7100
7165
  assert.isFunction(eventListeners[Event.ROAP_MESSAGE_TO_SEND]);
7101
7166
  assert.isFunction(eventListeners[Event.REMOTE_TRACK_ADDED]);
7102
- assert.isFunction(eventListeners[Event.CONNECTION_STATE_CHANGED]);
7167
+ assert.isFunction(eventListeners[Event.PEER_CONNECTION_STATE_CHANGED]);
7168
+ assert.isFunction(eventListeners[Event.ICE_CONNECTION_STATE_CHANGED]);
7169
+ assert.isFunction(eventListeners[Event.ICE_CANDIDATE_ERROR]);
7103
7170
  });
7104
7171
 
7105
7172
  it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
@@ -7135,13 +7202,45 @@ describe('plugin-meetings', () => {
7135
7202
  });
7136
7203
  });
7137
7204
 
7205
+ describe('should react on a ICE_CANDIDATE_ERROR event', () => {
7206
+ beforeEach(() => {
7207
+ meeting.setupMediaConnectionListeners();
7208
+
7209
+ });
7210
+
7211
+ it('should not collect skipped ice candidates error', () => {
7212
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 600, errorText: 'Address not associated with the desired network interface.' }});
7213
+
7214
+ assert.equal(meeting.iceCandidateErrors.size, 0);
7215
+ });
7216
+
7217
+ it('should collect valid ice candidates error', () => {
7218
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }});
7219
+
7220
+ assert.equal(meeting.iceCandidateErrors.size, 1);
7221
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7222
+ });
7223
+
7224
+ it('should increment counter if same valid ice candidates error collected', () => {
7225
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }});
7226
+
7227
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }});
7228
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }});
7229
+
7230
+ assert.equal(meeting.iceCandidateErrors.size, 2);
7231
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7232
+ assert.equal(meeting.iceCandidateErrors.get('701_'), 1);
7233
+ assert.equal(meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'), true);
7234
+ assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2);
7235
+ });
7236
+ });
7237
+
7138
7238
  describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
7139
7239
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
7140
7240
  meeting.hasMediaConnectionConnectedAtLeastOnce = true;
7141
7241
  meeting.setupMediaConnectionListeners();
7142
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7143
- state: 'Connecting',
7144
- });
7242
+
7243
+ simulateConnectionStateChange(ConnectionState.Connecting);
7145
7244
 
7146
7245
  assert.notCalled(webex.internal.newMetrics.submitClientEvent);
7147
7246
  });
@@ -7149,9 +7248,8 @@ describe('plugin-meetings', () => {
7149
7248
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
7150
7249
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
7151
7250
  meeting.setupMediaConnectionListeners();
7152
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7153
- state: 'Connecting',
7154
- });
7251
+
7252
+ simulateConnectionStateChange(ConnectionState.Connecting);
7155
7253
 
7156
7254
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7157
7255
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7177,6 +7275,7 @@ describe('plugin-meetings', () => {
7177
7275
  on: sinon.stub().callsFake((event, listener) => {
7178
7276
  eventListeners[event] = listener;
7179
7277
  }),
7278
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
7180
7279
  };
7181
7280
  };
7182
7281
 
@@ -7230,9 +7329,7 @@ describe('plugin-meetings', () => {
7230
7329
  assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
7231
7330
 
7232
7331
  // simulate first connection success
7233
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7234
- state: 'Connected',
7235
- });
7332
+ simulateConnectionStateChange(ConnectionState.Connected);
7236
7333
  checkExpectedSpies({
7237
7334
  icePhase: 'JOIN_MEETING_FINAL',
7238
7335
  setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
@@ -7242,12 +7339,9 @@ describe('plugin-meetings', () => {
7242
7339
  // now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
7243
7340
  resetSpies();
7244
7341
 
7245
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7246
- state: 'Disconnected',
7247
- });
7248
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7249
- state: 'Connected',
7250
- });
7342
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7343
+
7344
+ simulateConnectionStateChange(ConnectionState.Connected);
7251
7345
 
7252
7346
  checkExpectedSpies({
7253
7347
  setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
@@ -7255,12 +7349,9 @@ describe('plugin-meetings', () => {
7255
7349
 
7256
7350
  resetSpies();
7257
7351
 
7258
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7259
- state: 'Disconnected',
7260
- });
7261
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7262
- state: 'Connected',
7263
- });
7352
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7353
+
7354
+ simulateConnectionStateChange(ConnectionState.Connected);
7264
7355
  });
7265
7356
  });
7266
7357
 
@@ -7282,9 +7373,8 @@ describe('plugin-meetings', () => {
7282
7373
 
7283
7374
  const mockDisconnectedEvent = () => {
7284
7375
  meeting.setupMediaConnectionListeners();
7285
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7286
- state: 'Disconnected',
7287
- });
7376
+
7377
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7288
7378
  };
7289
7379
 
7290
7380
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -7348,9 +7438,8 @@ describe('plugin-meetings', () => {
7348
7438
  describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
7349
7439
  const mockFailedEvent = () => {
7350
7440
  meeting.setupMediaConnectionListeners();
7351
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7352
- state: 'Failed',
7353
- });
7441
+
7442
+ simulateConnectionStateChange(ConnectionState.Failed);
7354
7443
  };
7355
7444
 
7356
7445
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -9781,6 +9870,7 @@ describe('plugin-meetings', () => {
9781
9870
  beforeEach(() => {
9782
9871
  webex.internal.llm.isConnected = sinon.stub().returns(false);
9783
9872
  webex.internal.llm.getLocusUrl = sinon.stub();
9873
+ webex.internal.llm.getDatachannelUrl = sinon.stub();
9784
9874
  webex.internal.llm.registerAndConnect = sinon
9785
9875
  .stub()
9786
9876
  .returns(Promise.resolve('something'));
@@ -9808,6 +9898,7 @@ describe('plugin-meetings', () => {
9808
9898
  meeting.joinedWith = {state: 'JOINED'};
9809
9899
  webex.internal.llm.isConnected.returns(true);
9810
9900
  webex.internal.llm.getLocusUrl.returns('a url');
9901
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9811
9902
 
9812
9903
  meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
9813
9904
 
@@ -9844,6 +9935,7 @@ describe('plugin-meetings', () => {
9844
9935
  meeting.joinedWith = {state: 'JOINED'};
9845
9936
  webex.internal.llm.isConnected.returns(true);
9846
9937
  webex.internal.llm.getLocusUrl.returns('a url');
9938
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9847
9939
 
9848
9940
  meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
9849
9941
 
@@ -9869,6 +9961,36 @@ describe('plugin-meetings', () => {
9869
9961
  );
9870
9962
  });
9871
9963
 
9964
+ it('disconnects if first if the data channel url has changed', async () => {
9965
+ meeting.joinedWith = {state: 'JOINED'};
9966
+ webex.internal.llm.isConnected.returns(true);
9967
+ webex.internal.llm.getLocusUrl.returns('a url');
9968
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9969
+
9970
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
9971
+
9972
+ const result = await meeting.updateLLMConnection();
9973
+
9974
+ assert.calledWith(webex.internal.llm.disconnectLLM);
9975
+ assert.calledWith(
9976
+ webex.internal.llm.registerAndConnect,
9977
+ 'a url',
9978
+ 'a different datachannel url'
9979
+ );
9980
+ assert.equal(result, 'something');
9981
+ assert.calledWithExactly(
9982
+ meeting.webex.internal.llm.off,
9983
+ 'event:relay.event',
9984
+ meeting.processRelayEvent
9985
+ );
9986
+ assert.calledTwice(meeting.webex.internal.llm.off);
9987
+ assert.calledOnceWithExactly(
9988
+ meeting.webex.internal.llm.on,
9989
+ 'event:relay.event',
9990
+ meeting.processRelayEvent
9991
+ );
9992
+ });
9993
+
9872
9994
  it('disconnects when the state is not JOINED', async () => {
9873
9995
  meeting.joinedWith = {state: 'any other state'};
9874
9996
  webex.internal.llm.isConnected.returns(true);
@@ -4,15 +4,28 @@ import sinon from 'sinon';
4
4
  import testUtils from '../../../utils/testUtils';
5
5
 
6
6
  // packages/@webex/plugin-meetings/test/unit/spec/reachability/clusterReachability.ts
7
- import {ClusterReachability} from '@webex/plugin-meetings/src/reachability/clusterReachability'; // replace with actual path
7
+ import {
8
+ ClusterReachability,
9
+ ResultEventData,
10
+ Events,
11
+ ClientMediaIpsUpdatedEventData,
12
+ } from '@webex/plugin-meetings/src/reachability/clusterReachability'; // replace with actual path
8
13
 
9
14
  describe('ClusterReachability', () => {
10
15
  let previousRTCPeerConnection;
11
16
  let clusterReachability;
12
17
  let fakePeerConnection;
13
18
 
19
+ const emittedEvents: Record<Events, (ResultEventData | ClientMediaIpsUpdatedEventData)[]> = {
20
+ [Events.resultReady]: [],
21
+ [Events.clientMediaIpsUpdated]: [],
22
+ };
14
23
  const FAKE_OFFER = {type: 'offer', sdp: 'fake sdp'};
15
24
 
25
+ const resetEmittedEvents = () => {
26
+ emittedEvents[Events.resultReady].length = 0;
27
+ emittedEvents[Events.clientMediaIpsUpdated].length = 0;
28
+ };
16
29
  beforeEach(() => {
17
30
  fakePeerConnection = {
18
31
  createOffer: sinon.stub().resolves(FAKE_OFFER),
@@ -30,6 +43,16 @@ describe('ClusterReachability', () => {
30
43
  tcp: ['stun:tcp1.webex.com', 'stun:tcp2.webex.com:5004'],
31
44
  xtls: ['stun:xtls1.webex.com', 'stun:xtls2.webex.com:443'],
32
45
  });
46
+
47
+ resetEmittedEvents();
48
+
49
+ clusterReachability.on(Events.resultReady, (data: ResultEventData) => {
50
+ emittedEvents[Events.resultReady].push(data);
51
+ });
52
+
53
+ clusterReachability.on(Events.clientMediaIpsUpdated, (data: ClientMediaIpsUpdatedEventData) => {
54
+ emittedEvents[Events.clientMediaIpsUpdated].push(data);
55
+ });
33
56
  });
34
57
 
35
58
  afterEach(() => {
@@ -98,6 +121,10 @@ describe('ClusterReachability', () => {
98
121
  tcp: {result: 'untested'},
99
122
  xtls: {result: 'untested'},
100
123
  });
124
+
125
+ // verify that no events were emitted
126
+ assert.deepEqual(emittedEvents[Events.resultReady], []);
127
+ assert.deepEqual(emittedEvents[Events.clientMediaIpsUpdated], []);
101
128
  });
102
129
 
103
130
  describe('#start', () => {
@@ -124,8 +151,12 @@ describe('ClusterReachability', () => {
124
151
  assert.calledOnceWithExactly(fakePeerConnection.createOffer, {offerToReceiveAudio: true});
125
152
  assert.calledOnce(fakePeerConnection.setLocalDescription);
126
153
 
127
- await clock.tickAsync(3000); // move the clock so that reachability times out
154
+ clusterReachability.abort();
128
155
  await promise;
156
+
157
+ // verify that no events were emitted
158
+ assert.deepEqual(emittedEvents[Events.resultReady], []);
159
+ assert.deepEqual(emittedEvents[Events.clientMediaIpsUpdated], []);
129
160
  });
130
161
 
131
162
  it('resolves and has correct result as soon as it finds that all udp, tcp and tls are reachable', async () => {
@@ -134,14 +165,44 @@ describe('ClusterReachability', () => {
134
165
  await clock.tickAsync(100);
135
166
  fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp'}});
136
167
 
168
+ // check the right events were emitted
169
+ assert.equal(emittedEvents[Events.resultReady].length, 1);
170
+ assert.deepEqual(emittedEvents[Events.resultReady][0], {
171
+ protocol: 'udp',
172
+ result: 'reachable',
173
+ latencyInMilliseconds: 100,
174
+ clientMediaIPs: ['somePublicIp'],
175
+ });
176
+
177
+ // clientMediaIpsUpdated shouldn't be emitted, because the IP is already passed in the resultReady event
178
+ assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
179
+
137
180
  await clock.tickAsync(100);
138
181
  fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
139
182
 
183
+ // check the right event was emitted
184
+ assert.equal(emittedEvents[Events.resultReady].length, 2);
185
+ assert.deepEqual(emittedEvents[Events.resultReady][1], {
186
+ protocol: 'tcp',
187
+ result: 'reachable',
188
+ latencyInMilliseconds: 200,
189
+ });
190
+ assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
191
+
140
192
  await clock.tickAsync(100);
141
193
  fakePeerConnection.onicecandidate({
142
194
  candidate: {type: 'relay', address: 'someTurnRelayIp', port: 443},
143
195
  });
144
196
 
197
+ // check the right event was emitted
198
+ assert.equal(emittedEvents[Events.resultReady].length, 3);
199
+ assert.deepEqual(emittedEvents[Events.resultReady][2], {
200
+ protocol: 'xtls',
201
+ result: 'reachable',
202
+ latencyInMilliseconds: 300,
203
+ });
204
+ assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
205
+
145
206
  await promise;
146
207
 
147
208
  assert.deepEqual(clusterReachability.getResult(), {
@@ -151,13 +212,17 @@ describe('ClusterReachability', () => {
151
212
  });
152
213
  });
153
214
 
154
- it('times out correctly', async () => {
215
+ it('resolves and returns correct results when aborted before it gets any candidates', async () => {
155
216
  const promise = clusterReachability.start();
156
217
 
157
218
  // progress time without any candidates
158
- await clock.tickAsync(3000);
219
+ clusterReachability.abort();
159
220
  await promise;
160
221
 
222
+ // verify that no events were emitted
223
+ assert.deepEqual(emittedEvents[Events.resultReady], []);
224
+ assert.deepEqual(emittedEvents[Events.clientMediaIpsUpdated], []);
225
+
161
226
  assert.deepEqual(clusterReachability.getResult(), {
162
227
  udp: {result: 'unreachable'},
163
228
  tcp: {result: 'unreachable'},
@@ -165,22 +230,26 @@ describe('ClusterReachability', () => {
165
230
  });
166
231
  });
167
232
 
168
- it('times out correctly for video mesh nodes', async () => {
169
- clusterReachability = new ClusterReachability('testName', {
170
- isVideoMesh: true,
171
- udp: ['stun:udp1', 'stun:udp2'],
172
- tcp: ['stun:tcp1.webex.com', 'stun:tcp2.webex.com:5004'],
173
- xtls: ['stun:xtls1.webex.com', 'stun:xtls1.webex.com:443'],
174
- });
175
-
233
+ it('resolves and returns correct results when aborted after getting some candidates', async () => {
176
234
  const promise = clusterReachability.start();
177
235
 
178
- // video mesh nodes have shorter timeout of just 1s
179
- await clock.tickAsync(1000);
236
+ await clock.tickAsync(100);
237
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp'}});
238
+
239
+ // check the right event was emitted
240
+ assert.equal(emittedEvents[Events.resultReady].length, 1);
241
+ assert.deepEqual(emittedEvents[Events.resultReady][0], {
242
+ protocol: 'udp',
243
+ result: 'reachable',
244
+ latencyInMilliseconds: 100,
245
+ clientMediaIPs: ['somePublicIp'],
246
+ });
247
+
248
+ clusterReachability.abort();
180
249
  await promise;
181
250
 
182
251
  assert.deepEqual(clusterReachability.getResult(), {
183
- udp: {result: 'unreachable'},
252
+ udp: {result: 'reachable', latencyInMilliseconds: 100, clientMediaIPs: ['somePublicIp']},
184
253
  tcp: {result: 'unreachable'},
185
254
  xtls: {result: 'unreachable'},
186
255
  });
@@ -233,8 +302,7 @@ describe('ClusterReachability', () => {
233
302
  await clock.tickAsync(10);
234
303
  fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp3'}});
235
304
 
236
- await clock.tickAsync(3000); // move the clock so that reachability times out
237
-
305
+ clusterReachability.abort();
238
306
  await promise;
239
307
 
240
308
  // latency should be from only the first candidates, but the clientMediaIps should be from all UDP candidates (not TCP)
@@ -262,8 +330,7 @@ describe('ClusterReachability', () => {
262
330
  await clock.tickAsync(10);
263
331
  fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp3'}});
264
332
 
265
- await clock.tickAsync(3000); // move the clock so that reachability times out
266
-
333
+ clusterReachability.abort();
267
334
  await promise;
268
335
 
269
336
  // latency should be from only the first candidates, but the clientMediaIps should be from only from UDP candidates
@@ -293,8 +360,7 @@ describe('ClusterReachability', () => {
293
360
  candidate: {type: 'relay', address: 'someTurnRelayIp3', port: 443},
294
361
  });
295
362
 
296
- await clock.tickAsync(3000); // move the clock so that reachability times out
297
-
363
+ clusterReachability.abort();
298
364
  await promise;
299
365
 
300
366
  // latency should be from only the first candidates, but the clientMediaIps should be from only from UDP candidates
@@ -305,22 +371,50 @@ describe('ClusterReachability', () => {
305
371
  });
306
372
  });
307
373
 
308
- it('ignores duplicate clientMediaIps', async () => {
374
+ it('handles new found public IPs and ignores duplicate IPs', async () => {
309
375
  const promise = clusterReachability.start();
310
376
 
311
377
  // generate candidates with duplicate addresses
312
378
  await clock.tickAsync(10);
313
379
  fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
314
380
 
381
+ // check events emitted: there should be a resultReady and no clientMediaIpsUpdated
382
+ assert.equal(emittedEvents[Events.resultReady].length, 1);
383
+ assert.deepEqual(emittedEvents[Events.resultReady][0], {
384
+ protocol: 'udp',
385
+ result: 'reachable',
386
+ latencyInMilliseconds: 10,
387
+ clientMediaIPs: ['somePublicIp1'],
388
+ });
389
+ assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
390
+ resetEmittedEvents();
391
+
315
392
  await clock.tickAsync(10);
316
393
  fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1'}});
317
394
 
395
+ // no new event was emitted
396
+ assert.equal(emittedEvents[Events.resultReady].length, 0);
397
+ assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
398
+
318
399
  await clock.tickAsync(10);
319
400
  fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp2'}});
320
401
 
402
+ // check new events: now only clientMediaIpsUpdated event and no resultReady events
403
+ assert.equal(emittedEvents[Events.resultReady].length, 0);
404
+ assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 1);
405
+ assert.deepEqual(emittedEvents[Events.clientMediaIpsUpdated][0], {
406
+ protocol: 'udp',
407
+ clientMediaIPs: ['somePublicIp1', 'somePublicIp2'],
408
+ });
409
+ resetEmittedEvents();
410
+
321
411
  await clock.tickAsync(10);
322
412
  fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp2'}});
323
413
 
414
+ // no new event was emitted
415
+ assert.equal(emittedEvents[Events.resultReady].length, 0);
416
+ assert.equal(emittedEvents[Events.clientMediaIpsUpdated].length, 0);
417
+
324
418
  // send also a relay candidate so that the reachability check finishes
325
419
  fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
326
420
  fakePeerConnection.onicecandidate({