@webex/plugin-meetings 3.8.1-web-workers-keepalive.1 → 3.9.0-multipleLLM.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.
Files changed (121) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +26 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/locus-info/index.js +77 -95
  8. package/dist/locus-info/index.js.map +1 -1
  9. package/dist/locus-info/parser.js +4 -1
  10. package/dist/locus-info/parser.js.map +1 -1
  11. package/dist/media/properties.js +53 -5
  12. package/dist/media/properties.js.map +1 -1
  13. package/dist/meeting/brbState.js +14 -12
  14. package/dist/meeting/brbState.js.map +1 -1
  15. package/dist/meeting/in-meeting-actions.js +8 -0
  16. package/dist/meeting/in-meeting-actions.js.map +1 -1
  17. package/dist/meeting/index.js +443 -225
  18. package/dist/meeting/index.js.map +1 -1
  19. package/dist/meeting/muteState.js +2 -5
  20. package/dist/meeting/muteState.js.map +1 -1
  21. package/dist/meeting/request.js +44 -0
  22. package/dist/meeting/request.js.map +1 -1
  23. package/dist/meeting/request.type.js.map +1 -1
  24. package/dist/meeting/type.js +7 -0
  25. package/dist/meeting/type.js.map +1 -0
  26. package/dist/meeting/util.js +98 -13
  27. package/dist/meeting/util.js.map +1 -1
  28. package/dist/meeting-info/meeting-info-v2.js +29 -21
  29. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  30. package/dist/meetings/index.js +18 -10
  31. package/dist/meetings/index.js.map +1 -1
  32. package/dist/member/index.js.map +1 -1
  33. package/dist/member/types.js.map +1 -1
  34. package/dist/members/collection.js +13 -0
  35. package/dist/members/collection.js.map +1 -1
  36. package/dist/members/index.js +53 -29
  37. package/dist/members/index.js.map +1 -1
  38. package/dist/members/request.js +3 -3
  39. package/dist/members/request.js.map +1 -1
  40. package/dist/members/util.js +25 -8
  41. package/dist/members/util.js.map +1 -1
  42. package/dist/metrics/constants.js +2 -1
  43. package/dist/metrics/constants.js.map +1 -1
  44. package/dist/multistream/mediaRequestManager.js +1 -1
  45. package/dist/multistream/mediaRequestManager.js.map +1 -1
  46. package/dist/multistream/remoteMedia.js +34 -5
  47. package/dist/multistream/remoteMedia.js.map +1 -1
  48. package/dist/multistream/remoteMediaGroup.js +42 -2
  49. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  50. package/dist/multistream/sendSlotManager.js +32 -2
  51. package/dist/multistream/sendSlotManager.js.map +1 -1
  52. package/dist/reachability/index.js +3 -3
  53. package/dist/reachability/index.js.map +1 -1
  54. package/dist/types/constants.d.ts +24 -0
  55. package/dist/types/locus-info/index.d.ts +54 -10
  56. package/dist/types/media/properties.d.ts +21 -0
  57. package/dist/types/meeting/brbState.d.ts +0 -1
  58. package/dist/types/meeting/in-meeting-actions.d.ts +8 -0
  59. package/dist/types/meeting/index.d.ts +51 -20
  60. package/dist/types/meeting/request.d.ts +18 -1
  61. package/dist/types/meeting/request.type.d.ts +74 -0
  62. package/dist/types/meeting/type.d.ts +9 -0
  63. package/dist/types/meeting/util.d.ts +13 -3
  64. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  65. package/dist/types/meetings/index.d.ts +3 -1
  66. package/dist/types/member/types.d.ts +1 -0
  67. package/dist/types/members/collection.d.ts +6 -0
  68. package/dist/types/members/index.d.ts +22 -9
  69. package/dist/types/members/request.d.ts +1 -1
  70. package/dist/types/members/util.d.ts +13 -6
  71. package/dist/types/metrics/constants.d.ts +1 -0
  72. package/dist/types/multistream/remoteMedia.d.ts +20 -1
  73. package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
  74. package/dist/types/multistream/sendSlotManager.d.ts +16 -0
  75. package/dist/webinar/index.js +1 -1
  76. package/package.json +23 -24
  77. package/src/constants.ts +25 -2
  78. package/src/locus-info/index.ts +133 -96
  79. package/src/locus-info/parser.ts +5 -1
  80. package/src/media/properties.ts +43 -0
  81. package/src/meeting/brbState.ts +9 -7
  82. package/src/meeting/in-meeting-actions.ts +17 -0
  83. package/src/meeting/index.ts +273 -42
  84. package/src/meeting/muteState.ts +2 -6
  85. package/src/meeting/request.ts +39 -0
  86. package/src/meeting/request.type.ts +64 -0
  87. package/src/meeting/type.ts +9 -0
  88. package/src/meeting/util.ts +114 -22
  89. package/src/meeting-info/meeting-info-v2.ts +24 -5
  90. package/src/meetings/index.ts +12 -5
  91. package/src/member/index.ts +1 -0
  92. package/src/member/types.ts +1 -0
  93. package/src/members/collection.ts +11 -0
  94. package/src/members/index.ts +51 -15
  95. package/src/members/request.ts +2 -2
  96. package/src/members/util.ts +34 -6
  97. package/src/metrics/constants.ts +1 -0
  98. package/src/multistream/mediaRequestManager.ts +7 -7
  99. package/src/multistream/remoteMedia.ts +34 -4
  100. package/src/multistream/remoteMediaGroup.ts +37 -2
  101. package/src/multistream/sendSlotManager.ts +34 -2
  102. package/src/reachability/index.ts +3 -3
  103. package/test/unit/spec/locus-info/index.js +229 -98
  104. package/test/unit/spec/locus-info/parser.js +3 -2
  105. package/test/unit/spec/media/properties.ts +137 -0
  106. package/test/unit/spec/meeting/brbState.ts +9 -9
  107. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -0
  108. package/test/unit/spec/meeting/index.js +1022 -93
  109. package/test/unit/spec/meeting/muteState.js +32 -6
  110. package/test/unit/spec/meeting/request.js +92 -0
  111. package/test/unit/spec/meeting/utils.js +167 -17
  112. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  113. package/test/unit/spec/meetings/index.js +12 -1
  114. package/test/unit/spec/members/collection.js +120 -0
  115. package/test/unit/spec/members/index.js +140 -12
  116. package/test/unit/spec/members/request.js +57 -2
  117. package/test/unit/spec/members/utils.js +139 -17
  118. package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
  119. package/test/unit/spec/multistream/remoteMedia.ts +66 -2
  120. package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
  121. package/test/unit/spec/reachability/index.ts +158 -1
@@ -305,7 +305,7 @@ describe('plugin-meetings', () => {
305
305
  {state: newControls.rdcControl}
306
306
  );
307
307
  });
308
-
308
+
309
309
  it('should trigger the CONTROLS_POLLING_QA_CHANGED event when necessary', () => {
310
310
  locusInfo.controls = {};
311
311
  locusInfo.emitScoped = sinon.stub();
@@ -772,7 +772,7 @@ describe('plugin-meetings', () => {
772
772
  },
773
773
  };
774
774
  locusInfo.emitScoped = sinon.stub();
775
- locusInfo.updateParticipants({});
775
+ locusInfo.updateParticipants({}, []);
776
776
 
777
777
  // if this assertion fails, double-check the attributes used in
778
778
  // the updateParticipants function in locus-info/index.js
@@ -790,6 +790,7 @@ describe('plugin-meetings', () => {
790
790
  selfId: '2',
791
791
  hostId: '3',
792
792
  isReplace: undefined,
793
+ removedParticipantIds: [],
793
794
  }
794
795
  );
795
796
  // note: in a real use case, recordingId, selfId, and hostId would all be the same
@@ -814,7 +815,7 @@ describe('plugin-meetings', () => {
814
815
  };
815
816
 
816
817
  locusInfo.emitScoped = sinon.stub();
817
- locusInfo.updateParticipants({}, true);
818
+ locusInfo.updateParticipants({}, [], true);
818
819
 
819
820
  assert.calledWith(
820
821
  locusInfo.emitScoped,
@@ -830,43 +831,11 @@ describe('plugin-meetings', () => {
830
831
  selfId: '2',
831
832
  hostId: '3',
832
833
  isReplace: true,
834
+ removedParticipantIds: [],
833
835
  }
834
836
  );
835
837
  });
836
838
 
837
- it('should update the deltaParticipants object', () => {
838
- const prev = locusInfo.deltaParticipants;
839
-
840
- locusInfo.updateParticipantDeltas(newParticipants);
841
-
842
- assert.notEqual(locusInfo.deltaParticipants, prev);
843
- });
844
-
845
- it('should update the delta property on all changed states', () => {
846
- locusInfo.updateParticipantDeltas(newParticipants);
847
-
848
- const [exampleParticipant] = locusInfo.deltaParticipants;
849
-
850
- assert.isTrue(exampleParticipant.delta.audioStatus);
851
- assert.isTrue(exampleParticipant.delta.videoSlidesStatus);
852
- assert.isTrue(exampleParticipant.delta.videoStatus);
853
- });
854
-
855
- it('should include the person details of the changed participant', () => {
856
- locusInfo.updateParticipantDeltas(newParticipants);
857
-
858
- const [exampleParticipant] = locusInfo.deltaParticipants;
859
-
860
- assert.equal(exampleParticipant.person, newParticipants[0].person);
861
- });
862
-
863
- it('should clear deltaParticipants when no changes occured', () => {
864
- locusInfo.participants = [...newParticipants];
865
-
866
- locusInfo.updateParticipantDeltas(locusInfo.participants);
867
-
868
- assert.isTrue(locusInfo.deltaParticipants.length === 0);
869
- });
870
839
 
871
840
  it('should call with participant display name', () => {
872
841
  const failureParticipant = [
@@ -880,7 +849,7 @@ describe('plugin-meetings', () => {
880
849
  ];
881
850
 
882
851
  locusInfo.emitScoped = sinon.stub();
883
- locusInfo.updateParticipants(failureParticipant);
852
+ locusInfo.updateParticipants(failureParticipant, []);
884
853
  assert.calledWith(
885
854
  locusInfo.emitScoped,
886
855
  {
@@ -1674,6 +1643,28 @@ describe('plugin-meetings', () => {
1674
1643
  );
1675
1644
  });
1676
1645
 
1646
+ it('should trigger MEETING_INFO_UPDATED even if the roles array is empty', () => {
1647
+ const initialInfo = cloneDeep(meetingInfo);
1648
+
1649
+ const updateSelf = cloneDeep(self);
1650
+ updateSelf.controls.role.roles = [];
1651
+
1652
+ locusInfo.emitScoped = sinon.stub();
1653
+ locusInfo.updateMeetingInfo(initialInfo, updateSelf);
1654
+
1655
+ assert.calledWith(
1656
+ locusInfo.emitScoped,
1657
+ {
1658
+ file: 'locus-info',
1659
+ function: 'updateMeetingInfo',
1660
+ },
1661
+ LOCUSINFO.EVENTS.MEETING_INFO_UPDATED,
1662
+ {
1663
+ isInitializing: !self,
1664
+ }
1665
+ );
1666
+ });
1667
+
1677
1668
  const checkMeetingInfoUpdatedCalled = (expected, payload) => {
1678
1669
  const expectedArgs = [
1679
1670
  locusInfo.emitScoped,
@@ -2049,6 +2040,18 @@ describe('plugin-meetings', () => {
2049
2040
  });
2050
2041
  });
2051
2042
 
2043
+ describe('#handleLocusAPIResponse', () => {
2044
+ it('calls handleLocusDelta', () => {
2045
+ const fakeLocus = {eventType: LOCUSEVENT.DIFFERENCE};
2046
+
2047
+ sinon.stub(locusInfo, 'handleLocusDelta');
2048
+
2049
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus: fakeLocus});
2050
+
2051
+ assert.calledWith(locusInfo.handleLocusDelta, fakeLocus, mockMeeting);
2052
+ });
2053
+ });
2054
+
2052
2055
  describe('#LocusDeltaEvents', () => {
2053
2056
  const fakeMeeting = 'fakeMeeting';
2054
2057
  let sandbox = null;
@@ -2061,7 +2064,7 @@ describe('plugin-meetings', () => {
2061
2064
 
2062
2065
  fakeLocus = {
2063
2066
  meeting: true,
2064
- participants: true,
2067
+ participants: [],
2065
2068
  url: 'newLocusUrl',
2066
2069
  syncUrl: 'newSyncUrl',
2067
2070
  };
@@ -2108,6 +2111,38 @@ describe('plugin-meetings', () => {
2108
2111
  assert.isFunction(locusParser.onDeltaAction);
2109
2112
  });
2110
2113
 
2114
+ it("#updateLocusInfo invokes updateLocusUrl before updateMeetingInfo", () => {
2115
+ const callOrder = [];
2116
+ sinon.stub(locusInfo, "updateControls");
2117
+ sinon.stub(locusInfo, "updateConversationUrl");
2118
+ sinon.stub(locusInfo, "updateCreated");
2119
+ sinon.stub(locusInfo, "updateFullState");
2120
+ sinon.stub(locusInfo, "updateHostInfo");
2121
+ sinon.stub(locusInfo, "updateMeetingInfo").callsFake(() => {
2122
+ callOrder.push("updateMeetingInfo");
2123
+ });
2124
+ sinon.stub(locusInfo, "updateMediaShares");
2125
+ sinon.stub(locusInfo, "updateParticipantsUrl");
2126
+ sinon.stub(locusInfo, "updateReplace");
2127
+ sinon.stub(locusInfo, "updateSelf");
2128
+ sinon.stub(locusInfo, "updateLocusUrl").callsFake(() => {
2129
+ callOrder.push("updateLocusUrl");
2130
+ });
2131
+ sinon.stub(locusInfo, "updateAclUrl");
2132
+ sinon.stub(locusInfo, "updateBasequence");
2133
+ sinon.stub(locusInfo, "updateSequence");
2134
+ sinon.stub(locusInfo, "updateMemberShip");
2135
+ sinon.stub(locusInfo, "updateIdentifiers");
2136
+ sinon.stub(locusInfo, "updateEmbeddedApps");
2137
+ sinon.stub(locusInfo, "updateResources");
2138
+ sinon.stub(locusInfo, "compareAndUpdate");
2139
+
2140
+ locusInfo.updateLocusInfo(locus);
2141
+
2142
+ // Ensure updateLocusUrl is called before updateMeetingInfo if both are called
2143
+ assert.deepEqual(callOrder, ['updateLocusUrl', 'updateMeetingInfo']);
2144
+ });
2145
+
2111
2146
  it('#updateLocusInfo ignores breakout LEFT message', () => {
2112
2147
  const newLocus = {
2113
2148
  self: {
@@ -2159,10 +2194,11 @@ describe('plugin-meetings', () => {
2159
2194
  assert.notCalled(locusInfo.compareAndUpdate);
2160
2195
  });
2161
2196
 
2197
+
2198
+
2162
2199
  it('onFullLocus() updates the working-copy of locus parser', () => {
2163
2200
  const eventType = 'fakeEvent';
2164
2201
 
2165
- sandbox.stub(locusInfo, 'updateParticipantDeltas');
2166
2202
  sandbox.stub(locusInfo, 'updateLocusInfo');
2167
2203
  sandbox.stub(locusInfo, 'updateParticipants');
2168
2204
  sandbox.stub(locusInfo, 'isMeetingActive');
@@ -2182,7 +2218,6 @@ describe('plugin-meetings', () => {
2182
2218
  const oldWorkingCopy = locusParser.workingCopy;
2183
2219
 
2184
2220
  const spies = [
2185
- sandbox.stub(locusInfo, 'updateParticipantDeltas'),
2186
2221
  sandbox.stub(locusInfo, 'updateLocusInfo'),
2187
2222
  sandbox.stub(locusInfo, 'updateParticipants'),
2188
2223
  sandbox.stub(locusInfo, 'isMeetingActive'),
@@ -2257,7 +2292,7 @@ describe('plugin-meetings', () => {
2257
2292
 
2258
2293
  it('applyLocusDeltaData gets delta locus on DESYNC action if we have a syncUrl', () => {
2259
2294
  const {DESYNC} = LocusDeltaParser.loci;
2260
- const fakeDeltaLocus = {id: 'fake delta locus'};
2295
+ const fakeDeltaLocus = {baseSequence: {}, id: 'fake delta locus'};
2261
2296
  const meeting = {
2262
2297
  meetingRequest: {
2263
2298
  getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
@@ -2348,23 +2383,23 @@ describe('plugin-meetings', () => {
2348
2383
 
2349
2384
  it('applyLocusDeltaData handles LOCUS_URL_CHANGED action correctly', () => {
2350
2385
  const {LOCUS_URL_CHANGED} = LocusDeltaParser.loci;
2351
- const fakeDeltaLocus = {id: 'fake delta locus'};
2386
+ const fakeFullLocus = {
2387
+ url: 'new full loci url',
2388
+ };
2352
2389
  const meeting = {
2353
2390
  meetingRequest: {
2354
- getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
2391
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocus}),
2355
2392
  },
2356
2393
  locusInfo: {
2357
2394
  handleLocusDelta: sandbox.stub(),
2358
2395
  },
2359
- locusUrl: 'current locus url',
2396
+ locusUrl: 'current BO session locus url',
2360
2397
  };
2361
2398
 
2362
- locusInfo.locusParser.workingCopy = {
2363
- syncUrl: 'current sync url',
2364
- };
2399
+ locusInfo.locusParser.workingCopy = null;
2365
2400
 
2366
2401
  locusInfo.applyLocusDeltaData(LOCUS_URL_CHANGED, fakeLocus, meeting);
2367
- assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'current sync url'});
2402
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: fakeLocus.url});
2368
2403
  });
2369
2404
 
2370
2405
  describe('edge cases for sync failing', () => {
@@ -2392,25 +2427,22 @@ describe('plugin-meetings', () => {
2392
2427
  };
2393
2428
  });
2394
2429
 
2395
- it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', () => {
2430
+ it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', async () => {
2396
2431
  meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
2397
2432
 
2398
2433
  locusInfo.locusParser.workingCopy = {}; // no syncUrl
2399
2434
 
2400
- // Since we have a promise inside a function we want to test that's not returned,
2401
- // we will wait and stub it's last function to resolve this waiting promise.
2402
- return new Promise((resolve) => {
2403
- webex.meetings.destroy.callsFake(() => resolve());
2404
- locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2405
- }).then(() => {
2406
- assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
2435
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2407
2436
 
2408
- assert.notCalled(meeting.locusInfo.handleLocusDelta);
2409
- assert.notCalled(meeting.locusInfo.onFullLocus);
2410
- assert.notCalled(locusInfo.locusParser.resume);
2437
+ await testUtils.flushPromises();
2411
2438
 
2412
- assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2413
- });
2439
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
2440
+
2441
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2442
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2443
+ assert.notCalled(locusInfo.locusParser.resume);
2444
+
2445
+ assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2414
2446
  });
2415
2447
 
2416
2448
  it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails, does a full locus sync', () => {
@@ -2447,44 +2479,67 @@ describe('plugin-meetings', () => {
2447
2479
  });
2448
2480
  });
2449
2481
 
2450
- it('applyLocusDeltaData destroys the meeting if both delta sync and full sync fail', () => {
2482
+ it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails with 403, it does not do a full locus sync', async () => {
2483
+ const fake403Error = new Error('fake error');
2484
+ fake403Error.statusCode = 403;
2485
+
2486
+ meeting.meetingRequest.getLocusDTO.onCall(0).rejects(fake403Error);
2487
+
2488
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2489
+
2490
+ await testUtils.flushPromises();
2491
+
2492
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'deltaSyncUrl'});
2493
+
2494
+ assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2495
+ correlationId: meeting.correlationId,
2496
+ url: 'deltaSyncUrl',
2497
+ reason: 'fake error',
2498
+ errorName: 'Error',
2499
+ stack: sinon.match.any,
2500
+ code: sinon.match.any,
2501
+ });
2502
+
2503
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2504
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2505
+ assert.notCalled(locusInfo.locusParser.resume);
2506
+ });
2507
+
2508
+ it('applyLocusDeltaData destroys the meeting if both delta sync and full sync fail', async () => {
2451
2509
  meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
2452
2510
 
2453
- // Since we have a promise inside a function we want to test that's not returned,
2454
- // we will wait and stub it's last function to resolve this waiting promise.
2455
- return new Promise((resolve) => {
2456
- webex.meetings.destroy.callsFake(() => resolve());
2457
- locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2458
- }).then(() => {
2459
- assert.calledTwice(meeting.meetingRequest.getLocusDTO);
2511
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2460
2512
 
2461
- assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [
2462
- {url: 'deltaSyncUrl'},
2463
- ]);
2464
- assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [
2465
- {url: 'fullSyncUrl'},
2466
- ]);
2513
+ await testUtils.flushPromises();
2467
2514
 
2468
- assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2469
- correlationId: meeting.correlationId,
2470
- url: 'deltaSyncUrl',
2471
- reason: 'fake error',
2472
- errorName: 'Error',
2473
- stack: sinon.match.any,
2474
- code: sinon.match.any,
2475
- });
2515
+ assert.calledTwice(meeting.meetingRequest.getLocusDTO);
2476
2516
 
2477
- assert.notCalled(meeting.locusInfo.handleLocusDelta);
2478
- assert.notCalled(meeting.locusInfo.onFullLocus);
2479
- assert.notCalled(locusInfo.locusParser.resume);
2517
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [
2518
+ {url: 'deltaSyncUrl'},
2519
+ ]);
2520
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [
2521
+ {url: 'fullSyncUrl'},
2522
+ ]);
2480
2523
 
2481
- assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2524
+ assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2525
+ correlationId: meeting.correlationId,
2526
+ url: 'deltaSyncUrl',
2527
+ reason: 'fake error',
2528
+ errorName: 'Error',
2529
+ stack: sinon.match.any,
2530
+ code: sinon.match.any,
2482
2531
  });
2532
+
2533
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2534
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2535
+ assert.notCalled(locusInfo.locusParser.resume);
2536
+
2537
+ assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2483
2538
  });
2484
2539
  });
2485
2540
 
2486
2541
  it('onDeltaLocus handle delta data', () => {
2487
- fakeLocus.participants = {};
2542
+ fakeLocus.participants = [];
2488
2543
  const fakeBreakout = {
2489
2544
  sessionId: 'sessionId',
2490
2545
  groupId: 'groupId',
@@ -2501,17 +2556,15 @@ describe('plugin-meetings', () => {
2501
2556
  };
2502
2557
  locusInfo.updateParticipants = sinon.stub();
2503
2558
  locusInfo.onDeltaLocus(fakeLocus);
2504
- assert.calledWith(locusInfo.updateParticipants, {}, false);
2559
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, false);
2505
2560
 
2506
2561
  fakeLocus.controls.breakout.sessionId = 'sessionId2';
2507
2562
  locusInfo.onDeltaLocus(fakeLocus);
2508
- assert.calledWith(locusInfo.updateParticipants, {}, true);
2563
+ assert.calledWith(locusInfo.updateParticipants, [], undefined, true);
2509
2564
  });
2510
2565
 
2511
2566
  it('onDeltaLocus merges delta participants with existing participants', () => {
2512
- const FAKE_DELTA_PARTICIPANTS = [
2513
- {id: '1111'}, {id: '2222'}
2514
- ]
2567
+ const FAKE_DELTA_PARTICIPANTS = [{id: '1111'}, {id: '2222'}];
2515
2568
  fakeLocus.participants = FAKE_DELTA_PARTICIPANTS;
2516
2569
 
2517
2570
  sinon.spy(locusInfo, 'mergeParticipants');
@@ -2519,8 +2572,86 @@ describe('plugin-meetings', () => {
2519
2572
  const existingParticipants = locusInfo.participants;
2520
2573
 
2521
2574
  locusInfo.onDeltaLocus(fakeLocus);
2522
- assert.calledOnceWithExactly(locusInfo.mergeParticipants, existingParticipants, FAKE_DELTA_PARTICIPANTS);
2523
- assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, false);
2575
+ assert.calledOnceWithExactly(
2576
+ locusInfo.mergeParticipants,
2577
+ existingParticipants,
2578
+ FAKE_DELTA_PARTICIPANTS
2579
+ );
2580
+ assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, undefined, false);
2581
+ });
2582
+
2583
+ [true, false].forEach((isDelta) =>
2584
+ it(`applyLocusDeltaData - handles empty ${
2585
+ isDelta ? 'delta' : 'full'
2586
+ } DTO in response`, async () => {
2587
+ const {DESYNC} = LocusDeltaParser.loci;
2588
+ const fakeFullLocusDto = {};
2589
+ const meeting = {
2590
+ meetingRequest: {
2591
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
2592
+ },
2593
+ locusInfo: {
2594
+ onFullLocus: sandbox.stub(),
2595
+ handleLocusDelta: sandbox.stub(),
2596
+ },
2597
+ locusUrl: 'fake locus FULL url',
2598
+ };
2599
+
2600
+ sinon.stub(locusInfo.locusParser, 'resume').resolves();
2601
+
2602
+ if (isDelta) {
2603
+ locusInfo.locusParser.workingCopy = {syncUrl: 'fake locus DELTA url'};
2604
+ } else {
2605
+ locusInfo.locusParser.workingCopy = {}; // no syncUrl (to trigger FULL DTO request)
2606
+ }
2607
+
2608
+ await locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2609
+
2610
+ await testUtils.flushPromises();
2611
+
2612
+ if (isDelta) {
2613
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2614
+ url: 'fake locus DELTA url',
2615
+ });
2616
+ } else {
2617
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2618
+ url: 'fake locus FULL url',
2619
+ });
2620
+ }
2621
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2622
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2623
+ assert.calledOnce(locusInfo.locusParser.resume);
2624
+ })
2625
+ );
2626
+
2627
+ it(`applyLocusDeltaData - handles the case when we get FULL DTO when we asked for DELTA DTO`, async () => {
2628
+ const {DESYNC} = LocusDeltaParser.loci;
2629
+ const fakeFullLocusDto = {someStuff: 'data'}; // non-empty DTO, without baseSequence
2630
+ const meeting = {
2631
+ meetingRequest: {
2632
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
2633
+ },
2634
+ locusInfo: {
2635
+ onFullLocus: sandbox.stub(),
2636
+ handleLocusDelta: sandbox.stub(),
2637
+ },
2638
+ locusUrl: 'fake locus FULL url',
2639
+ };
2640
+
2641
+ sinon.stub(locusInfo.locusParser, 'resume').resolves();
2642
+
2643
+ locusInfo.locusParser.workingCopy = {syncUrl: 'fake locus DELTA url'};
2644
+
2645
+ await locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2646
+
2647
+ await testUtils.flushPromises();
2648
+
2649
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2650
+ url: 'fake locus DELTA url',
2651
+ });
2652
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2653
+ assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
2654
+ assert.calledOnce(locusInfo.locusParser.resume);
2524
2655
  });
2525
2656
  });
2526
2657
 
@@ -2934,10 +3065,9 @@ describe('plugin-meetings', () => {
2934
3065
  beforeEach(() => {
2935
3066
  clock = sinon.useFakeTimers();
2936
3067
 
2937
- sinon.stub(locusInfo, 'updateParticipantDeltas');
2938
3068
  sinon.stub(locusInfo, 'updateParticipants');
2939
- sinon.stub(locusInfo, 'isMeetingActive'),
2940
- sinon.stub(locusInfo, 'handleOneOnOneEvent'),
3069
+ sinon.stub(locusInfo, 'isMeetingActive');
3070
+ sinon.stub(locusInfo, 'handleOneOnOneEvent');
2941
3071
  (updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo'));
2942
3072
  syncRequestStub = sinon.stub().resolves({body: {}});
2943
3073
 
@@ -2958,6 +3088,7 @@ describe('plugin-meetings', () => {
2958
3088
  id: 'test person id',
2959
3089
  },
2960
3090
  },
3091
+ participants: [],
2961
3092
  });
2962
3093
 
2963
3094
  updateLocusInfoStub.resetHistory();
@@ -253,7 +253,8 @@ describe('locus-info/parser', () => {
253
253
  });
254
254
 
255
255
  it('replaces current loci when the locus URL changes and incoming sequence is later, even when baseSequence doesn\'t match', () => {
256
- const {USE_INCOMING} = LocusDeltaParser.loci;
256
+ const {LOCUS_URL_CHANGED} = LocusDeltaParser.loci;
257
+ sandbox.stub(LocusDeltaParser, 'compare').returns(LOCUS_URL_CHANGED);
257
258
 
258
259
  parser.queue.dequeue = sandbox.stub().returns(NEW_LOCI);
259
260
  parser.onDeltaAction = sandbox.stub();
@@ -262,7 +263,7 @@ describe('locus-info/parser', () => {
262
263
 
263
264
  parser.processDeltaEvent();
264
265
 
265
- assert.equal(parser.workingCopy, NEW_LOCI);
266
+ assert.equal(parser.workingCopy, null);
266
267
  });
267
268
 
268
269
  it('does not replace current loci when the locus URL changes but incoming sequence is not later', () => {
@@ -6,6 +6,8 @@ import * as tsSdpModule from '@webex/ts-sdp';
6
6
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
7
7
  import {Defer} from '@webex/common';
8
8
  import MediaConnectionAwaiter from '../../../../src/media/MediaConnectionAwaiter';
9
+ import Metrics from '../../../../src/metrics';
10
+ import BEHAVIORAL_METRICS from '../../../../src/metrics/constants';
9
11
 
10
12
  describe('MediaProperties', () => {
11
13
  let mediaProperties;
@@ -389,4 +391,139 @@ describe('MediaProperties', () => {
389
391
  });
390
392
  });
391
393
  });
394
+
395
+ // issue types and subtypes used in these tests are just examples
396
+ // they don't reflect real issue types/subtypes used in production
397
+ describe('sendMediaIssueMetric', () => {
398
+ let sendBehavioralMetricStub;
399
+ let clock;
400
+
401
+ beforeEach(() => {
402
+ clock = sinon.useFakeTimers();
403
+ sendBehavioralMetricStub = sinon.stub(Metrics, 'sendBehavioralMetric');
404
+ });
405
+
406
+ afterEach(() => {
407
+ clock.restore();
408
+ });
409
+
410
+ it('should send a behavioral metric with correct parameters', () => {
411
+ const issueType = 'audio';
412
+ const issueSubType = 'packet-loss';
413
+ const correlationId = 'test-correlation-id-123';
414
+
415
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
416
+
417
+ assert.calledOnce(sendBehavioralMetricStub);
418
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED, {
419
+ correlationId,
420
+ 'audio_packet-loss': 1,
421
+ });
422
+ });
423
+
424
+ it('should increment count while being throttled and reset it once metric goes out', () => {
425
+ const issueType = 'video';
426
+ const issueSubType = 'freeze';
427
+ const correlationId = 'test-correlation-id';
428
+
429
+ // Call multiple times with same issue type/subtype
430
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
431
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
432
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
433
+
434
+ // First call should go through immediately, subsequent calls are throttled
435
+ assert.calledOnce(sendBehavioralMetricStub);
436
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED, {
437
+ correlationId,
438
+ video_freeze: 1, // Only the first call goes through due to throttling
439
+ });
440
+ sendBehavioralMetricStub.resetHistory();
441
+
442
+ assert.equal(mediaProperties.mediaIssueCounters['video_freeze'], 2); // counter should be reset after the first metric goes out, hence only 2 not 3 here
443
+
444
+ clock.tick(5 * 60 * 1000); // Advance time by 5 minutes to expire throttle
445
+
446
+ assert.calledOnceWithExactly(
447
+ sendBehavioralMetricStub,
448
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
449
+ {
450
+ correlationId,
451
+ video_freeze: 2,
452
+ }
453
+ );
454
+ });
455
+
456
+ it('should track different issue types separately in counters', () => {
457
+ const correlationId = 'test-correlation-id';
458
+
459
+ // Send different issue types
460
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
461
+ mediaProperties.sendMediaIssueMetric('video', 'freeze', correlationId);
462
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
463
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
464
+ mediaProperties.sendMediaIssueMetric('audio', 'packet-loss', correlationId);
465
+ mediaProperties.sendMediaIssueMetric('video', 'freeze', correlationId);
466
+
467
+ // First call should go through immediately, subsequent calls are throttled
468
+ assert.calledOnceWithExactly(
469
+ sendBehavioralMetricStub,
470
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
471
+ {
472
+ correlationId,
473
+ 'audio_packet-loss': 1,
474
+ }
475
+ );
476
+
477
+ // But the counters should be tracked separately
478
+ assert.equal(mediaProperties.mediaIssueCounters['audio_packet-loss'], 3);
479
+ assert.equal(mediaProperties.mediaIssueCounters['video_freeze'], 2);
480
+
481
+ sendBehavioralMetricStub.resetHistory();
482
+
483
+ clock.tick(5 * 60 * 1000); // Advance time by 5 minutes to expire throttle
484
+
485
+ assert.calledOnceWithExactly(
486
+ sendBehavioralMetricStub,
487
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
488
+ {
489
+ correlationId,
490
+ video_freeze: 2,
491
+ 'audio_packet-loss': 3,
492
+ }
493
+ );
494
+ });
495
+
496
+ it('should flush throttled metrics when unsetPeerConnection is called', () => {
497
+ const issueType = 'share';
498
+ const issueSubType = 'connection-lost';
499
+ const correlationId = 'test-correlation-id';
500
+
501
+ // Send metrics multiple times
502
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
503
+ mediaProperties.sendMediaIssueMetric(issueType, issueSubType, correlationId);
504
+
505
+ // First call should go through immediately
506
+ assert.calledOnceWithExactly(
507
+ sendBehavioralMetricStub,
508
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
509
+ {
510
+ correlationId,
511
+ 'share_connection-lost': 1,
512
+ }
513
+ );
514
+ sendBehavioralMetricStub.resetHistory();
515
+
516
+ // Call unsetPeerConnection which should flush throttled metrics
517
+ mediaProperties.unsetPeerConnection();
518
+
519
+ assert.calledOnceWithExactly(
520
+ sendBehavioralMetricStub,
521
+ BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED,
522
+ {
523
+ correlationId,
524
+ 'share_connection-lost': 1,
525
+ }
526
+ );
527
+ });
528
+ });
392
529
  });