@webex/plugin-meetings 3.12.0-next.3 → 3.12.0-next.31

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 (90) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/constants.js +3 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/controls-options-manager/constants.js +11 -1
  8. package/dist/controls-options-manager/constants.js.map +1 -1
  9. package/dist/controls-options-manager/index.js +23 -21
  10. package/dist/controls-options-manager/index.js.map +1 -1
  11. package/dist/controls-options-manager/util.js +91 -0
  12. package/dist/controls-options-manager/util.js.map +1 -1
  13. package/dist/hashTree/constants.js +10 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTreeParser.js +550 -346
  16. package/dist/hashTree/hashTreeParser.js.map +1 -1
  17. package/dist/hashTree/utils.js +22 -0
  18. package/dist/hashTree/utils.js.map +1 -1
  19. package/dist/interceptors/locusRetry.js +23 -8
  20. package/dist/interceptors/locusRetry.js.map +1 -1
  21. package/dist/interpretation/index.js +1 -1
  22. package/dist/interpretation/siLanguage.js +1 -1
  23. package/dist/locus-info/index.js +222 -61
  24. package/dist/locus-info/index.js.map +1 -1
  25. package/dist/meeting/index.js +372 -292
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/util.js +1 -0
  28. package/dist/meeting/util.js.map +1 -1
  29. package/dist/meetings/index.js +146 -62
  30. package/dist/meetings/index.js.map +1 -1
  31. package/dist/meetings/util.js +39 -5
  32. package/dist/meetings/util.js.map +1 -1
  33. package/dist/member/index.js +10 -0
  34. package/dist/member/index.js.map +1 -1
  35. package/dist/member/types.js.map +1 -1
  36. package/dist/member/util.js +3 -0
  37. package/dist/member/util.js.map +1 -1
  38. package/dist/metrics/constants.js +5 -1
  39. package/dist/metrics/constants.js.map +1 -1
  40. package/dist/multistream/sendSlotManager.js +116 -2
  41. package/dist/multistream/sendSlotManager.js.map +1 -1
  42. package/dist/types/constants.d.ts +1 -0
  43. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  44. package/dist/types/hashTree/constants.d.ts +1 -0
  45. package/dist/types/hashTree/hashTreeParser.d.ts +53 -15
  46. package/dist/types/hashTree/utils.d.ts +11 -0
  47. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  48. package/dist/types/locus-info/index.d.ts +38 -5
  49. package/dist/types/meeting/index.d.ts +11 -0
  50. package/dist/types/member/index.d.ts +1 -0
  51. package/dist/types/member/types.d.ts +1 -0
  52. package/dist/types/member/util.d.ts +1 -0
  53. package/dist/types/metrics/constants.d.ts +4 -0
  54. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  55. package/dist/webinar/index.js +301 -226
  56. package/dist/webinar/index.js.map +1 -1
  57. package/package.json +16 -16
  58. package/src/constants.ts +1 -0
  59. package/src/controls-options-manager/constants.ts +14 -1
  60. package/src/controls-options-manager/index.ts +26 -19
  61. package/src/controls-options-manager/util.ts +81 -1
  62. package/src/hashTree/constants.ts +9 -0
  63. package/src/hashTree/hashTreeParser.ts +273 -154
  64. package/src/hashTree/utils.ts +17 -0
  65. package/src/interceptors/locusRetry.ts +25 -4
  66. package/src/locus-info/index.ts +233 -79
  67. package/src/meeting/index.ts +98 -11
  68. package/src/meeting/util.ts +1 -0
  69. package/src/meetings/index.ts +58 -34
  70. package/src/meetings/util.ts +44 -1
  71. package/src/member/index.ts +10 -0
  72. package/src/member/types.ts +1 -0
  73. package/src/member/util.ts +3 -0
  74. package/src/metrics/constants.ts +5 -0
  75. package/src/multistream/sendSlotManager.ts +97 -3
  76. package/src/webinar/index.ts +75 -1
  77. package/test/unit/spec/controls-options-manager/index.js +114 -6
  78. package/test/unit/spec/controls-options-manager/util.js +165 -0
  79. package/test/unit/spec/hashTree/hashTreeParser.ts +839 -37
  80. package/test/unit/spec/hashTree/utils.ts +88 -1
  81. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  82. package/test/unit/spec/locus-info/index.js +262 -64
  83. package/test/unit/spec/meeting/index.js +54 -36
  84. package/test/unit/spec/meeting/utils.js +4 -0
  85. package/test/unit/spec/meetings/index.js +190 -8
  86. package/test/unit/spec/meetings/utils.js +124 -0
  87. package/test/unit/spec/member/index.js +7 -0
  88. package/test/unit/spec/member/util.js +24 -0
  89. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  90. package/test/unit/spec/webinar/index.ts +60 -0
@@ -13,6 +13,7 @@ import MediaSharesUtils from '@webex/plugin-meetings/src/locus-info//mediaShares
13
13
  import LocusDeltaParser from '@webex/plugin-meetings/src/locus-info/parser';
14
14
  import Metrics from '@webex/plugin-meetings/src/metrics';
15
15
  import * as HashTreeParserModule from '@webex/plugin-meetings/src/hashTree/hashTreeParser';
16
+ import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
16
17
 
17
18
  import {
18
19
  LOCUSINFO,
@@ -413,7 +414,7 @@ describe('plugin-meetings', () => {
413
414
  };
414
415
 
415
416
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
416
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
417
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
417
418
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
418
419
  });
419
420
 
@@ -440,7 +441,7 @@ describe('plugin-meetings', () => {
440
441
  locusInfo.info.isWebinar = true;
441
442
 
442
443
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
443
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
444
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
444
445
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
445
446
  });
446
447
 
@@ -473,7 +474,7 @@ describe('plugin-meetings', () => {
473
474
  locusInfo.info.isWebinar = true;
474
475
 
475
476
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
476
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
477
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
477
478
  updatedObjects: [{htMeta: {elementId: {type: 'self'}}, data: newSelf}],
478
479
  });
479
480
 
@@ -501,7 +502,7 @@ describe('plugin-meetings', () => {
501
502
  };
502
503
 
503
504
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
504
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
505
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
505
506
  updatedObjects: [{htMeta: {elementId: {type: 'fullState'}}, data: newFullState}],
506
507
  });
507
508
 
@@ -519,7 +520,7 @@ describe('plugin-meetings', () => {
519
520
  };
520
521
 
521
522
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
522
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
523
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
523
524
  updatedObjects: [{htMeta: {elementId: {type: 'info'}}, data: newInfo}],
524
525
  });
525
526
 
@@ -537,7 +538,7 @@ describe('plugin-meetings', () => {
537
538
  };
538
539
 
539
540
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
540
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
541
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
541
542
  updatedObjects: [{htMeta: {elementId: {type: 'links'}}, data: newLinks}],
542
543
  });
543
544
 
@@ -557,7 +558,7 @@ describe('plugin-meetings', () => {
557
558
  };
558
559
 
559
560
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
560
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
561
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
561
562
  updatedObjects: [{htMeta: newLocusHtMeta, data: newLocus}],
562
563
  });
563
564
 
@@ -590,7 +591,7 @@ describe('plugin-meetings', () => {
590
591
  };
591
592
 
592
593
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
593
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
594
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
594
595
  updatedObjects: [
595
596
  {
596
597
  htMeta: newLocusHtMeta,
@@ -637,7 +638,7 @@ describe('plugin-meetings', () => {
637
638
  };
638
639
 
639
640
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
640
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
641
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
641
642
  updatedObjects: [
642
643
  // first, a removal of LOCUS object
643
644
  {htMeta: {elementId: {type: 'locus'}}, data: null},
@@ -671,7 +672,7 @@ describe('plugin-meetings', () => {
671
672
  const newLocusHtMeta = {elementId: {type: 'locus', version: 99}};
672
673
 
673
674
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
674
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
675
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
675
676
  updatedObjects: [
676
677
  // first, an update
677
678
  {htMeta: newLocusHtMeta, data: newLocus},
@@ -700,7 +701,7 @@ describe('plugin-meetings', () => {
700
701
  };
701
702
 
702
703
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
703
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
704
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
704
705
  updatedObjects: [
705
706
  // first, an update
706
707
  {htMeta: {elementId: {type: 'locus'}}, data: newLocus1},
@@ -730,7 +731,7 @@ describe('plugin-meetings', () => {
730
731
  };
731
732
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
732
733
  // with 1 participant added, 1 updated, and 1 removed
733
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
734
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
734
735
  updatedObjects: [
735
736
  {htMeta: {elementId: {type: 'participant', id: 'fake-ht-participant-1'}}, data: null},
736
737
  {
@@ -774,7 +775,7 @@ describe('plugin-meetings', () => {
774
775
  };
775
776
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
776
777
  // with 1 participant added, 1 updated, and 1 removed
777
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
778
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
778
779
  updatedObjects: [
779
780
  {htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-1'}}, data: null},
780
781
  {
@@ -807,7 +808,7 @@ describe('plugin-meetings', () => {
807
808
  };
808
809
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
809
810
  // with 1 embedded app added, 1 updated, and 1 removed
810
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
811
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
811
812
  updatedObjects: [
812
813
  {htMeta: {elementId: {type: 'embeddedapp', id: 'fake-ht-embeddedApp-1'}}, data: null},
813
814
  {
@@ -844,7 +845,7 @@ describe('plugin-meetings', () => {
844
845
  };
845
846
 
846
847
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
847
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
848
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
848
849
  updatedObjects: [
849
850
  {
850
851
  htMeta: {elementId: {type: 'mediashare', id: 'fake-ht-mediaShare-2'}},
@@ -883,7 +884,7 @@ describe('plugin-meetings', () => {
883
884
  };
884
885
 
885
886
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
886
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
887
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
887
888
  updatedObjects: [
888
889
  {
889
890
  htMeta: {elementId: {type: 'controlentry', id: 'control-1'}},
@@ -911,7 +912,7 @@ describe('plugin-meetings', () => {
911
912
 
912
913
  it('should process locus update correctly when CONTROL object is received with no data', () => {
913
914
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
914
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
915
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
915
916
  updatedObjects: [
916
917
  {
917
918
  htMeta: {elementId: {type: 'controlentry', id: 'some-control-id'}},
@@ -935,7 +936,7 @@ describe('plugin-meetings', () => {
935
936
  const destroyStub = sinon.stub(locusInfo.webex.meetings, 'destroy');
936
937
 
937
938
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
938
- locusInfoUpdateCallback(MEETING_ENDED);
939
+ locusInfoUpdateCallback({updateType: MEETING_ENDED});
939
940
 
940
941
  assert.calledOnceWithExactly(collectionGetStub, locusInfo.meetingId);
941
942
  assert.calledOnceWithExactly(
@@ -953,7 +954,7 @@ describe('plugin-meetings', () => {
953
954
  const destroyStub = sinon.stub(locusInfo.webex.meetings, 'destroy');
954
955
 
955
956
  // simulate an update from the HashTreeParser (normally this would be triggered by incoming locus messages)
956
- locusInfoUpdateCallback(MEETING_ENDED);
957
+ locusInfoUpdateCallback({updateType: MEETING_ENDED});
957
958
 
958
959
  assert.calledOnceWithExactly(collectionGetStub, locusInfo.meetingId);
959
960
  assert.notCalled(destroyStub);
@@ -963,7 +964,7 @@ describe('plugin-meetings', () => {
963
964
  const createdHashTreeParser = locusInfo.hashTreeParsers.get('fake-locus-url');
964
965
  createdHashTreeParser.initializedFromHashTree = false;
965
966
 
966
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
967
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
967
968
  updatedObjects: [
968
969
  {
969
970
  htMeta: {elementId: {type: 'self'}},
@@ -978,7 +979,7 @@ describe('plugin-meetings', () => {
978
979
  });
979
980
 
980
981
  it('should set forceReplaceMembers to false on subsequent updates (initializedFromHashTree is true)', () => {
981
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
982
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
982
983
  updatedObjects: [
983
984
  {
984
985
  htMeta: {elementId: {type: 'self'}},
@@ -994,7 +995,7 @@ describe('plugin-meetings', () => {
994
995
  it('should copy participant data to self when participant matches self identity and state is LEFT with reason MOVED', () => {
995
996
  locusInfo.self = {id: 'fake-self', identity: 'user-123'};
996
997
 
997
- locusInfoUpdateCallback(OBJECTS_UPDATED, {
998
+ locusInfoUpdateCallback({updateType: OBJECTS_UPDATED,
998
999
  updatedObjects: [
999
1000
  {
1000
1001
  htMeta: {elementId: {type: 'participant', id: 99}},
@@ -2024,7 +2025,7 @@ describe('plugin-meetings', () => {
2024
2025
  function: 'updateSelf',
2025
2026
  },
2026
2027
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2027
- {muted: true, unmuteAllowed: true}
2028
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2028
2029
  );
2029
2030
 
2030
2031
  // but sometimes "previous self" is defined, but without controls.audio.muted, so we test this here:
@@ -2039,7 +2040,7 @@ describe('plugin-meetings', () => {
2039
2040
  function: 'updateSelf',
2040
2041
  },
2041
2042
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2042
- {muted: true, unmuteAllowed: true}
2043
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2043
2044
  );
2044
2045
  });
2045
2046
 
@@ -2098,7 +2099,7 @@ describe('plugin-meetings', () => {
2098
2099
  function: 'updateSelf',
2099
2100
  },
2100
2101
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2101
- {muted: true, unmuteAllowed: true}
2102
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2102
2103
  );
2103
2104
  });
2104
2105
 
@@ -2237,7 +2238,7 @@ describe('plugin-meetings', () => {
2237
2238
  function: 'updateSelf',
2238
2239
  },
2239
2240
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2240
- {muted: true, unmuteAllowed: false}
2241
+ {muted: true, unmuteAllowed: false, modifiedBy: null}
2241
2242
  );
2242
2243
 
2243
2244
  // now change only disallowUnmute
@@ -2255,7 +2256,28 @@ describe('plugin-meetings', () => {
2255
2256
  function: 'updateSelf',
2256
2257
  },
2257
2258
  LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2258
- {muted: true, unmuteAllowed: true}
2259
+ {muted: true, unmuteAllowed: true, modifiedBy: null}
2260
+ );
2261
+ });
2262
+
2263
+ it('should include modifiedBy in payload when muted by host', () => {
2264
+ locusInfo.webex.internal.device.url = self.deviceUrl;
2265
+ locusInfo.updateSelf(self);
2266
+ const newSelf = cloneDeep(self);
2267
+ newSelf.controls.audio.muted = true;
2268
+ newSelf.controls.audio.meta = {modifiedBy: 'host-uuid-123'};
2269
+
2270
+ locusInfo.emitScoped = sinon.stub();
2271
+ locusInfo.updateSelf(newSelf);
2272
+
2273
+ assert.calledWith(
2274
+ locusInfo.emitScoped,
2275
+ {
2276
+ file: 'locus-info',
2277
+ function: 'updateSelf',
2278
+ },
2279
+ LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
2280
+ {muted: true, unmuteAllowed: true, modifiedBy: 'host-uuid-123'}
2259
2281
  );
2260
2282
  });
2261
2283
 
@@ -3154,13 +3176,14 @@ describe('plugin-meetings', () => {
3154
3176
  const createMockParser = (state = 'active') => ({
3155
3177
  state,
3156
3178
  stop: sinon.stub(),
3157
- resume: sinon.stub(),
3179
+ resumeFromMessage: sinon.stub(),
3158
3180
  handleMessage: sinon.stub(),
3159
3181
  });
3160
3182
 
3161
3183
  const createSelfElementWithReplaces = (replacedLocusUrl, replacedAt) => ({
3162
3184
  htMeta: {elementId: {type: 'Self'}},
3163
3185
  data: {
3186
+ deviceUrl,
3164
3187
  devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
3165
3188
  },
3166
3189
  });
@@ -3236,7 +3259,7 @@ describe('plugin-meetings', () => {
3236
3259
  stateElementsMessage: message,
3237
3260
  });
3238
3261
 
3239
- assert.calledOnce(parserA.resume);
3262
+ assert.calledOnce(parserA.resumeFromMessage);
3240
3263
  assert.calledOnce(parserB.stop);
3241
3264
  });
3242
3265
 
@@ -3259,7 +3282,7 @@ describe('plugin-meetings', () => {
3259
3282
  stateElementsMessage: message,
3260
3283
  });
3261
3284
 
3262
- assert.notCalled(parserA.resume);
3285
+ assert.notCalled(parserA.resumeFromMessage);
3263
3286
  assert.notCalled(parserB.stop);
3264
3287
  });
3265
3288
 
@@ -3278,7 +3301,7 @@ describe('plugin-meetings', () => {
3278
3301
  stateElementsMessage: message,
3279
3302
  });
3280
3303
 
3281
- assert.notCalled(parserA.resume);
3304
+ assert.notCalled(parserA.resumeFromMessage);
3282
3305
  assert.notCalled(parserA.handleMessage);
3283
3306
  });
3284
3307
 
@@ -3431,6 +3454,156 @@ describe('plugin-meetings', () => {
3431
3454
  assert.calledOnce(locusInfo.sendClassicVsHashTreeMismatchMetric);
3432
3455
  assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
3433
3456
  });
3457
+
3458
+ describe('parser switch via API response', () => {
3459
+ const deviceUrl = 'http://device-url.com';
3460
+ const locusUrlA = 'http://locus-url-A.com';
3461
+ const locusUrlB = 'http://locus-url-B.com';
3462
+
3463
+ let HashTreeParserStub;
3464
+
3465
+ const createMockApiParser = (state = 'active') => ({
3466
+ state,
3467
+ stop: sinon.stub(),
3468
+ resumeFromApiResponse: sinon.stub(),
3469
+ handleLocusUpdate: sinon.stub(),
3470
+ initializeFromGetLociResponse: sinon.stub(),
3471
+ });
3472
+
3473
+ const createLocusWithReplaces = (url, replacedLocusUrl, replacedAt) => ({
3474
+ url,
3475
+ self: {
3476
+ devices: [{url: deviceUrl, replaces: [{locusUrl: replacedLocusUrl, replacedAt}]}],
3477
+ },
3478
+ });
3479
+
3480
+ const createLocusWithoutReplaces = (url) => ({
3481
+ url,
3482
+ self: {devices: [{url: deviceUrl}]},
3483
+ });
3484
+
3485
+ beforeEach(() => {
3486
+ locusInfo.webex.internal.device.url = deviceUrl;
3487
+ HashTreeParserStub = sinon
3488
+ .stub(HashTreeParserModule, 'default')
3489
+ .returns(createMockApiParser());
3490
+ });
3491
+
3492
+ it('should create a new parser and initialize it when no entry exists for the locusUrl', () => {
3493
+ // existing parser for a different url so hashTreeParsers.size > 0
3494
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: createMockApiParser(), initializedFromHashTree: true});
3495
+
3496
+ const locus = createLocusWithReplaces(locusUrlB, locusUrlA, '2026-01-01T00:00:00Z');
3497
+ sinon.stub(locusInfo, 'handleLocusDelta');
3498
+
3499
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3500
+
3501
+ assert.isTrue(locusInfo.hashTreeParsers.has(locusUrlB));
3502
+ const newEntry = locusInfo.hashTreeParsers.get(locusUrlB);
3503
+ assert.isFalse(newEntry.initializedFromHashTree);
3504
+
3505
+ // the stub returns the mock, so initializeFromGetLociResponse should be called on it
3506
+ const createdParser = HashTreeParserStub.returnValues[0];
3507
+ assert.calledOnceWithExactly(createdParser.initializeFromGetLociResponse, locus);
3508
+ assert.notCalled(locusInfo.handleLocusDelta);
3509
+ });
3510
+
3511
+ it('should reactivate a stopped parser when replaces info is newer', () => {
3512
+ const parserA = createMockApiParser('stopped');
3513
+ const parserB = createMockApiParser('active');
3514
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-01-01T00:00:00Z', initializedFromHashTree: true});
3515
+ locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
3516
+
3517
+ const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-02-01T00:00:00Z');
3518
+
3519
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3520
+
3521
+ assert.calledOnce(parserA.resumeFromApiResponse);
3522
+ assert.calledWithExactly(parserA.resumeFromApiResponse, locus);
3523
+ assert.calledOnce(parserB.stop);
3524
+ assert.equal(locusInfo.hashTreeParsers.get(locusUrlB).replacedAt, '2026-02-01T00:00:00Z');
3525
+ assert.isFalse(locusInfo.hashTreeParsers.get(locusUrlA).initializedFromHashTree);
3526
+ });
3527
+
3528
+ it('should not reactivate a stopped parser when replaces info is not newer', () => {
3529
+ const parserA = createMockApiParser('stopped');
3530
+ const parserB = createMockApiParser('active');
3531
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, replacedAt: '2026-03-01T00:00:00Z', initializedFromHashTree: true});
3532
+ locusInfo.hashTreeParsers.set(locusUrlB, {parser: parserB, initializedFromHashTree: true});
3533
+
3534
+ const locus = createLocusWithReplaces(locusUrlA, locusUrlB, '2026-01-01T00:00:00Z');
3535
+
3536
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3537
+
3538
+ assert.notCalled(parserA.resumeFromApiResponse);
3539
+ assert.notCalled(parserB.stop);
3540
+ });
3541
+
3542
+ it('should not reactivate a stopped parser when no replaces info is available', () => {
3543
+ const parserA = createMockApiParser('stopped');
3544
+ locusInfo.hashTreeParsers.set(locusUrlA, {parser: parserA, initializedFromHashTree: true});
3545
+
3546
+ const locus = createLocusWithoutReplaces(locusUrlA);
3547
+
3548
+ locusInfo.handleLocusAPIResponse(mockMeeting, {locus});
3549
+
3550
+ assert.notCalled(parserA.resumeFromApiResponse);
3551
+ });
3552
+ });
3553
+ });
3554
+
3555
+ describe('#syncAllHashTreeDatasets', () => {
3556
+ it('should call syncAllDatasets on each parser that has an entry', async () => {
3557
+ const parser1 = {syncAllDatasets: sinon.stub().resolves()};
3558
+ const parser2 = {syncAllDatasets: sinon.stub().resolves()};
3559
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3560
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2});
3561
+
3562
+ await locusInfo.syncAllHashTreeDatasets();
3563
+
3564
+ assert.calledOnce(parser1.syncAllDatasets);
3565
+ assert.calledOnce(parser2.syncAllDatasets);
3566
+ });
3567
+
3568
+ it('should skip parser entries without a parser object', async () => {
3569
+ const parser1 = {syncAllDatasets: sinon.stub().resolves()};
3570
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3571
+ locusInfo.hashTreeParsers.set('url2', {parser: undefined});
3572
+
3573
+ await locusInfo.syncAllHashTreeDatasets();
3574
+
3575
+ assert.calledOnce(parser1.syncAllDatasets);
3576
+ });
3577
+
3578
+ it('should await each parsers syncAllDatasets sequentially', async () => {
3579
+ const callOrder = [];
3580
+ const parser1 = {syncAllDatasets: sinon.stub().callsFake(() => {
3581
+ callOrder.push('start1');
3582
+ return new Promise((resolve) => {
3583
+ setTimeout(() => {
3584
+ callOrder.push('end1');
3585
+ resolve();
3586
+ }, 100);
3587
+ });
3588
+ })};
3589
+ const parser2 = {syncAllDatasets: sinon.stub().callsFake(() => {
3590
+ callOrder.push('start2');
3591
+ return Promise.resolve();
3592
+ })};
3593
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1});
3594
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2});
3595
+
3596
+ const clock = sinon.useFakeTimers();
3597
+ const promise = locusInfo.syncAllHashTreeDatasets();
3598
+ // parser1 started but parser2 not yet
3599
+ assert.deepEqual(callOrder, ['start1']);
3600
+
3601
+ await clock.tickAsync(100);
3602
+ await promise;
3603
+ // parser1 finished, then parser2 started and finished
3604
+ assert.deepEqual(callOrder, ['start1', 'end1', 'start2']);
3605
+ clock.restore();
3606
+ });
3434
3607
  });
3435
3608
 
3436
3609
  describe('#LocusDeltaEvents', () => {
@@ -3521,49 +3694,23 @@ describe('plugin-meetings', () => {
3521
3694
  assert.deepEqual(callOrder, ['updateLocusUrl', 'updateMeetingInfo']);
3522
3695
  });
3523
3696
 
3524
- it('#updateLocusInfo ignores breakout LEFT message', () => {
3525
- const newLocus = {
3526
- self: {
3527
- reason: 'MOVED',
3528
- state: 'LEFT',
3529
- },
3530
- };
3697
+ it('#updateLocusInfo ignores locus when isSelfMovedOrBreakoutEnded returns true', () => {
3698
+ const newLocus = {self: {state: 'JOINED'}};
3699
+
3700
+ sinon.stub(MeetingsUtil, 'isSelfMovedOrBreakoutEnded').returns(true);
3531
3701
 
3532
3702
  locusInfo.updateControls = sinon.stub();
3533
- locusInfo.updateConversationUrl = sinon.stub();
3534
- locusInfo.updateCreated = sinon.stub();
3535
3703
  locusInfo.updateFullState = sinon.stub();
3536
- locusInfo.updateHostInfo = sinon.stub();
3537
- locusInfo.updateMeetingInfo = sinon.stub();
3538
- locusInfo.updateMediaShares = sinon.stub();
3539
- locusInfo.updateReplaces = sinon.stub();
3540
3704
  locusInfo.updateSelf = sinon.stub();
3541
- locusInfo.updateLocusUrl = sinon.stub();
3542
- locusInfo.updateAclUrl = sinon.stub();
3543
- locusInfo.updateBasequence = sinon.stub();
3544
- locusInfo.updateSequence = sinon.stub();
3545
- locusInfo.updateEmbeddedApps = sinon.stub();
3546
- locusInfo.updateLinks = sinon.stub();
3547
- locusInfo.compareAndUpdate = sinon.stub();
3548
3705
 
3549
3706
  locusInfo.updateLocusInfo(newLocus);
3550
3707
 
3708
+ assert.calledOnceWithExactly(MeetingsUtil.isSelfMovedOrBreakoutEnded, newLocus);
3551
3709
  assert.notCalled(locusInfo.updateControls);
3552
- assert.notCalled(locusInfo.updateConversationUrl);
3553
- assert.notCalled(locusInfo.updateCreated);
3554
3710
  assert.notCalled(locusInfo.updateFullState);
3555
- assert.notCalled(locusInfo.updateHostInfo);
3556
- assert.notCalled(locusInfo.updateMeetingInfo);
3557
- assert.notCalled(locusInfo.updateMediaShares);
3558
- assert.notCalled(locusInfo.updateReplaces);
3559
3711
  assert.notCalled(locusInfo.updateSelf);
3560
- assert.notCalled(locusInfo.updateLocusUrl);
3561
- assert.notCalled(locusInfo.updateAclUrl);
3562
- assert.notCalled(locusInfo.updateBasequence);
3563
- assert.notCalled(locusInfo.updateSequence);
3564
- assert.notCalled(locusInfo.updateEmbeddedApps);
3565
- assert.notCalled(locusInfo.updateLinks);
3566
- assert.notCalled(locusInfo.compareAndUpdate);
3712
+
3713
+ MeetingsUtil.isSelfMovedOrBreakoutEnded.restore();
3567
3714
  });
3568
3715
 
3569
3716
  it('#updateLocusInfo puts the Locus DTO top level properties at the right place in LocusInfo class', () => {
@@ -4413,6 +4560,31 @@ describe('plugin-meetings', () => {
4413
4560
  });
4414
4561
  });
4415
4562
 
4563
+ describe('#cleanUp', () => {
4564
+ it('calls cleanUp on all hash tree parsers and clears maps', () => {
4565
+ const parser1 = {cleanUp: sinon.stub()};
4566
+ const parser2 = {cleanUp: sinon.stub()};
4567
+
4568
+ locusInfo.hashTreeParsers.set('url1', {parser: parser1, initializedFromHashTree: true});
4569
+ locusInfo.hashTreeParsers.set('url2', {parser: parser2, initializedFromHashTree: true});
4570
+ locusInfo.hashTreeObjectId2ParticipantId.set(1, 'participant1');
4571
+
4572
+ locusInfo.cleanUp();
4573
+
4574
+ assert.calledOnce(parser1.cleanUp);
4575
+ assert.calledOnce(parser2.cleanUp);
4576
+ assert.equal(locusInfo.hashTreeParsers.size, 0);
4577
+ assert.equal(locusInfo.hashTreeObjectId2ParticipantId.size, 0);
4578
+ });
4579
+
4580
+ it('works when there are no hash tree parsers', () => {
4581
+ locusInfo.cleanUp();
4582
+
4583
+ assert.equal(locusInfo.hashTreeParsers.size, 0);
4584
+ assert.equal(locusInfo.hashTreeObjectId2ParticipantId.size, 0);
4585
+ });
4586
+ });
4587
+
4416
4588
  describe('#handleOneonOneEvent', () => {
4417
4589
  beforeEach(() => {
4418
4590
  locusInfo.emitScoped = sinon.stub();
@@ -4954,6 +5126,31 @@ describe('plugin-meetings', () => {
4954
5126
  );
4955
5127
  assert.notCalled(getTheLocusToUpdateStub);
4956
5128
  });
5129
+
5130
+ it('should call handleLocusAPIResponse for SDK_LOCUS_FROM_SYNC_MEETINGS when hash tree parsers exist', () => {
5131
+ const fakeLocusUrl = 'http://locus-url.com';
5132
+ const fakeLocus = {url: fakeLocusUrl, fullState: {state: 'ACTIVE'}};
5133
+ const mockHashTreeParser = {
5134
+ handleMessage: sinon.stub(),
5135
+ handleLocusUpdate: sinon.stub(),
5136
+ };
5137
+ locusInfo.hashTreeParsers.set(fakeLocusUrl, {
5138
+ parser: mockHashTreeParser,
5139
+ initializedFromHashTree: true,
5140
+ });
5141
+
5142
+ sinon.stub(locusInfo, 'handleLocusDelta');
5143
+
5144
+ locusInfo.parse(mockMeeting, {
5145
+ eventType: LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS,
5146
+ locus: fakeLocus,
5147
+ });
5148
+
5149
+ // should route through handleLocusAPIResponse which passes unwrapped LocusDTO to parser
5150
+ assert.calledOnce(mockHashTreeParser.handleLocusUpdate);
5151
+ assert.notCalled(mockHashTreeParser.handleMessage);
5152
+ assert.notCalled(locusInfo.handleLocusDelta);
5153
+ });
4957
5154
  });
4958
5155
  });
4959
5156
 
@@ -5146,6 +5343,7 @@ describe('plugin-meetings', () => {
5146
5343
  return {
5147
5344
  htMeta: {elementId: {type: 'Self'}},
5148
5345
  data: {
5346
+ deviceUrl,
5149
5347
  devices,
5150
5348
  },
5151
5349
  };