@webex/plugin-meetings 3.3.1 → 3.4.0

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 (126) 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/constants.js +11 -4
  5. package/dist/constants.js.map +1 -1
  6. package/dist/interpretation/index.js +1 -1
  7. package/dist/interpretation/siLanguage.js +1 -1
  8. package/dist/locus-info/selfUtils.js +0 -5
  9. package/dist/locus-info/selfUtils.js.map +1 -1
  10. package/dist/media/MediaConnectionAwaiter.js +70 -15
  11. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  12. package/dist/media/index.js +12 -0
  13. package/dist/media/index.js.map +1 -1
  14. package/dist/meeting/connectionStateHandler.js +67 -0
  15. package/dist/meeting/connectionStateHandler.js.map +1 -0
  16. package/dist/meeting/index.js +552 -357
  17. package/dist/meeting/index.js.map +1 -1
  18. package/dist/meeting/locusMediaRequest.js +7 -0
  19. package/dist/meeting/locusMediaRequest.js.map +1 -1
  20. package/dist/meeting/muteState.js +6 -1
  21. package/dist/meeting/muteState.js.map +1 -1
  22. package/dist/meeting/util.js +1 -0
  23. package/dist/meeting/util.js.map +1 -1
  24. package/dist/meeting-info/index.js +4 -4
  25. package/dist/meeting-info/index.js.map +1 -1
  26. package/dist/meeting-info/meeting-info-v2.js +2 -2
  27. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  28. package/dist/meeting-info/util.js +17 -17
  29. package/dist/meeting-info/util.js.map +1 -1
  30. package/dist/meeting-info/utilv2.js +16 -16
  31. package/dist/meeting-info/utilv2.js.map +1 -1
  32. package/dist/meetings/collection.js +1 -1
  33. package/dist/meetings/collection.js.map +1 -1
  34. package/dist/meetings/index.js +37 -33
  35. package/dist/meetings/index.js.map +1 -1
  36. package/dist/meetings/meetings.types.js +8 -0
  37. package/dist/meetings/meetings.types.js.map +1 -1
  38. package/dist/meetings/util.js +3 -2
  39. package/dist/meetings/util.js.map +1 -1
  40. package/dist/metrics/constants.js +2 -1
  41. package/dist/metrics/constants.js.map +1 -1
  42. package/dist/metrics/index.js +57 -0
  43. package/dist/metrics/index.js.map +1 -1
  44. package/dist/personal-meeting-room/index.js +1 -1
  45. package/dist/personal-meeting-room/index.js.map +1 -1
  46. package/dist/reachability/clusterReachability.js +108 -53
  47. package/dist/reachability/clusterReachability.js.map +1 -1
  48. package/dist/reachability/index.js +415 -56
  49. package/dist/reachability/index.js.map +1 -1
  50. package/dist/types/constants.d.ts +11 -3
  51. package/dist/types/media/MediaConnectionAwaiter.d.ts +24 -4
  52. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  53. package/dist/types/meeting/index.d.ts +27 -7
  54. package/dist/types/meeting/locusMediaRequest.d.ts +2 -0
  55. package/dist/types/meeting-info/index.d.ts +3 -2
  56. package/dist/types/meeting-info/meeting-info-v2.d.ts +3 -2
  57. package/dist/types/meeting-info/util.d.ts +5 -4
  58. package/dist/types/meeting-info/utilv2.d.ts +3 -2
  59. package/dist/types/meetings/collection.d.ts +3 -2
  60. package/dist/types/meetings/index.d.ts +4 -3
  61. package/dist/types/meetings/meetings.types.d.ts +9 -0
  62. package/dist/types/metrics/constants.d.ts +1 -0
  63. package/dist/types/metrics/index.d.ts +15 -0
  64. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  65. package/dist/types/reachability/index.d.ts +93 -2
  66. package/dist/webinar/index.js +1 -1
  67. package/package.json +23 -23
  68. package/src/breakouts/index.ts +7 -1
  69. package/src/constants.ts +13 -17
  70. package/src/locus-info/selfUtils.ts +0 -5
  71. package/src/media/MediaConnectionAwaiter.ts +89 -14
  72. package/src/media/index.ts +13 -0
  73. package/src/meeting/connectionStateHandler.ts +65 -0
  74. package/src/meeting/index.ts +526 -292
  75. package/src/meeting/locusMediaRequest.ts +5 -0
  76. package/src/meeting/muteState.ts +6 -1
  77. package/src/meeting/util.ts +1 -0
  78. package/src/meeting-info/index.ts +9 -6
  79. package/src/meeting-info/meeting-info-v2.ts +4 -4
  80. package/src/meeting-info/util.ts +23 -28
  81. package/src/meeting-info/utilv2.ts +18 -24
  82. package/src/meetings/collection.ts +3 -3
  83. package/src/meetings/index.ts +39 -40
  84. package/src/meetings/meetings.types.ts +11 -0
  85. package/src/meetings/util.ts +5 -4
  86. package/src/metrics/constants.ts +1 -0
  87. package/src/metrics/index.ts +44 -0
  88. package/src/personal-meeting-room/index.ts +2 -2
  89. package/src/reachability/clusterReachability.ts +86 -25
  90. package/src/reachability/index.ts +316 -27
  91. package/test/unit/spec/breakouts/index.ts +51 -32
  92. package/test/unit/spec/locus-info/selfUtils.js +25 -23
  93. package/test/unit/spec/media/MediaConnectionAwaiter.ts +131 -32
  94. package/test/unit/spec/media/index.ts +42 -27
  95. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  96. package/test/unit/spec/meeting/index.js +758 -179
  97. package/test/unit/spec/meeting/locusMediaRequest.ts +7 -0
  98. package/test/unit/spec/meeting/muteState.js +24 -0
  99. package/test/unit/spec/meeting-info/index.js +4 -4
  100. package/test/unit/spec/meeting-info/meetinginfov2.js +24 -28
  101. package/test/unit/spec/meeting-info/request.js +2 -2
  102. package/test/unit/spec/meeting-info/utilv2.js +41 -49
  103. package/test/unit/spec/meetings/index.js +14 -0
  104. package/test/unit/spec/metrics/index.js +126 -0
  105. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -2
  106. package/test/unit/spec/personal-meeting-room/personal-meeting-room.js +2 -2
  107. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  108. package/test/unit/spec/reachability/index.ts +1153 -84
  109. package/test/unit/spec/rtcMetrics/index.ts +1 -0
  110. package/dist/mediaQualityMetrics/config.js +0 -321
  111. package/dist/mediaQualityMetrics/config.js.map +0 -1
  112. package/dist/statsAnalyzer/global.js +0 -44
  113. package/dist/statsAnalyzer/global.js.map +0 -1
  114. package/dist/statsAnalyzer/index.js +0 -1072
  115. package/dist/statsAnalyzer/index.js.map +0 -1
  116. package/dist/statsAnalyzer/mqaUtil.js +0 -368
  117. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  118. package/dist/types/mediaQualityMetrics/config.d.ts +0 -247
  119. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  120. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  121. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  122. package/src/mediaQualityMetrics/config.ts +0 -255
  123. package/src/statsAnalyzer/global.ts +0 -37
  124. package/src/statsAnalyzer/index.ts +0 -1318
  125. package/src/statsAnalyzer/mqaUtil.ts +0 -463
  126. package/test/unit/spec/stats-analyzer/index.js +0 -1819
@@ -3,8 +3,9 @@
3
3
  */
4
4
 
5
5
  /* eslint-disable class-methods-use-this */
6
- import {mapValues} from 'lodash';
6
+ import {isEqual, mapValues, mean} from 'lodash';
7
7
 
8
+ import {Defer} from '@webex/common';
8
9
  import LoggerProxy from '../common/logs/logger-proxy';
9
10
  import MeetingUtil from '../meeting/util';
10
11
 
@@ -12,10 +13,16 @@ import {REACHABILITY} from '../constants';
12
13
 
13
14
  import ReachabilityRequest, {ClusterList} from './request';
14
15
  import {
16
+ ClientMediaIpsUpdatedEventData,
15
17
  ClusterReachability,
16
18
  ClusterReachabilityResult,
19
+ Events,
20
+ ResultEventData,
17
21
  TransportResult,
18
22
  } from './clusterReachability';
23
+ import EventsScope from '../common/events/events-scope';
24
+ import BEHAVIORAL_METRICS from '../metrics/constants';
25
+ import Metrics from '../metrics';
19
26
 
20
27
  export type ReachabilityMetrics = {
21
28
  reachability_public_udp_success: number;
@@ -60,11 +67,16 @@ export type ReachabilityResults = Record<
60
67
  }
61
68
  >;
62
69
 
70
+ // timeouts in seconds
71
+ const DEFAULT_TIMEOUT = 3;
72
+ const VIDEO_MESH_TIMEOUT = 1;
73
+ const OVERALL_TIMEOUT = 15;
74
+
63
75
  /**
64
76
  * @class Reachability
65
77
  * @export
66
78
  */
67
- export default class Reachability {
79
+ export default class Reachability extends EventsScope {
68
80
  namespace = REACHABILITY.namespace;
69
81
  webex: object;
70
82
  reachabilityRequest: ReachabilityRequest;
@@ -72,12 +84,22 @@ export default class Reachability {
72
84
  [key: string]: ClusterReachability;
73
85
  };
74
86
 
87
+ reachabilityDefer?: Defer;
88
+
89
+ vmnTimer?: ReturnType<typeof setTimeout>;
90
+ publicCloudTimer?: ReturnType<typeof setTimeout>;
91
+ overallTimer?: ReturnType<typeof setTimeout>;
92
+
93
+ expectedResultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}};
94
+ resultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}};
95
+
75
96
  /**
76
97
  * Creates an instance of Reachability.
77
98
  * @param {object} webex
78
99
  * @memberof Reachability
79
100
  */
80
101
  constructor(webex: object) {
102
+ super();
81
103
  this.webex = webex;
82
104
 
83
105
  /**
@@ -105,15 +127,6 @@ export default class Reachability {
105
127
  MeetingUtil.getIpVersion(this.webex)
106
128
  );
107
129
 
108
- // Perform Reachability Check
109
- const results = await this.performReachabilityChecks(clusters);
110
-
111
- // @ts-ignore
112
- await this.webex.boundedStorage.put(
113
- this.namespace,
114
- REACHABILITY.localStorageResult,
115
- JSON.stringify(results)
116
- );
117
130
  // @ts-ignore
118
131
  await this.webex.boundedStorage.put(
119
132
  this.namespace,
@@ -121,11 +134,12 @@ export default class Reachability {
121
134
  JSON.stringify(joinCookie)
122
135
  );
123
136
 
124
- LoggerProxy.logger.log(
125
- 'Reachability:index#gatherReachability --> Reachability checks completed'
126
- );
137
+ this.reachabilityDefer = new Defer();
138
+
139
+ // Perform Reachability Check
140
+ await this.performReachabilityChecks(clusters);
127
141
 
128
- return results;
142
+ return this.reachabilityDefer.promise;
129
143
  } catch (error) {
130
144
  LoggerProxy.logger.error(`Reachability:index#gatherReachability --> Error:`, error);
131
145
 
@@ -395,16 +409,210 @@ export default class Reachability {
395
409
  });
396
410
  }
397
411
 
412
+ /**
413
+ * Returns true if we've obtained all the reachability results for all the public clusters
414
+ * In other words, it means that all public clusters are reachable over each protocol,
415
+ * because we only get a "result" if we managed to reach a cluster
416
+ *
417
+ * @returns {boolean}
418
+ */
419
+ private areAllPublicClusterResultsReady() {
420
+ return isEqual(this.expectedResultsCount.public, this.resultsCount.public);
421
+ }
422
+
423
+ /**
424
+ * Returns true if we've obtained all the reachability results for all the clusters
425
+ *
426
+ * @returns {boolean}
427
+ */
428
+ private areAllResultsReady() {
429
+ return isEqual(this.expectedResultsCount, this.resultsCount);
430
+ }
431
+
432
+ /**
433
+ * Resolves the promise returned by gatherReachability() method
434
+ * @returns {void}
435
+ */
436
+ private resolveReachabilityPromise() {
437
+ if (this.vmnTimer) {
438
+ clearTimeout(this.vmnTimer);
439
+ }
440
+ if (this.publicCloudTimer) {
441
+ clearTimeout(this.publicCloudTimer);
442
+ }
443
+
444
+ this.logUnreachableClusters();
445
+ this.reachabilityDefer?.resolve();
446
+ }
447
+
448
+ /**
449
+ * Aborts all cluster reachability checks that are in progress
450
+ *
451
+ * @returns {void}
452
+ */
453
+ private abortClusterReachability() {
454
+ Object.values(this.clusterReachability).forEach((clusterReachability) => {
455
+ clusterReachability.abort();
456
+ });
457
+ }
458
+
459
+ /**
460
+ * Helper function for calculating min/max/average values of latency
461
+ *
462
+ * @param {Array<any>} results
463
+ * @param {string} protocol
464
+ * @param {boolean} isVideoMesh
465
+ * @returns {{min:number, max: number, average: number}}
466
+ */
467
+ protected getStatistics(
468
+ results: Array<ClusterReachabilityResult & {isVideoMesh: boolean}>,
469
+ protocol: 'udp' | 'tcp' | 'xtls',
470
+ isVideoMesh: boolean
471
+ ) {
472
+ const values = results
473
+ .filter((result) => result.isVideoMesh === isVideoMesh)
474
+ .filter((result) => result[protocol].result === 'reachable')
475
+ .map((result) => result[protocol].latencyInMilliseconds);
476
+
477
+ if (values.length === 0) {
478
+ return {
479
+ min: -1,
480
+ max: -1,
481
+ average: -1,
482
+ };
483
+ }
484
+
485
+ return {
486
+ min: Math.min(...values),
487
+ max: Math.max(...values),
488
+ average: mean(values),
489
+ };
490
+ }
491
+
492
+ /**
493
+ * Sends a metric with all the statistics about how long reachability took
494
+ *
495
+ * @returns {void}
496
+ */
497
+ protected async sendMetric() {
498
+ const results = [];
499
+
500
+ Object.values(this.clusterReachability).forEach((clusterReachability) => {
501
+ results.push({
502
+ ...clusterReachability.getResult(),
503
+ isVideoMesh: clusterReachability.isVideoMesh,
504
+ });
505
+ });
506
+
507
+ const stats = {
508
+ vmn: {
509
+ udp: this.getStatistics(results, 'udp', true),
510
+ },
511
+ public: {
512
+ udp: this.getStatistics(results, 'udp', false),
513
+ tcp: this.getStatistics(results, 'tcp', false),
514
+ xtls: this.getStatistics(results, 'xtls', false),
515
+ },
516
+ };
517
+ Metrics.sendBehavioralMetric(
518
+ BEHAVIORAL_METRICS.REACHABILITY_COMPLETED,
519
+ Metrics.prepareMetricFields(stats)
520
+ );
521
+ }
522
+
523
+ /**
524
+ * Starts all the timers used for various timeouts
525
+ *
526
+ * @returns {void}
527
+ */
528
+ private startTimers() {
529
+ this.vmnTimer = setTimeout(() => {
530
+ this.vmnTimer = undefined;
531
+ // if we are only missing VMN results, then we don't want to wait for them any longer
532
+ // as they are likely to fail if users are not on corporate network
533
+ if (this.areAllPublicClusterResultsReady()) {
534
+ LoggerProxy.logger.log(
535
+ 'Reachability:index#startTimers --> Reachability checks timed out (VMN timeout)'
536
+ );
537
+
538
+ this.resolveReachabilityPromise();
539
+ }
540
+ }, VIDEO_MESH_TIMEOUT * 1000);
541
+
542
+ this.publicCloudTimer = setTimeout(() => {
543
+ this.publicCloudTimer = undefined;
544
+
545
+ LoggerProxy.logger.log(
546
+ `Reachability:index#startTimers --> Reachability checks timed out (${DEFAULT_TIMEOUT}s)`
547
+ );
548
+
549
+ // resolve the promise, so that the client won't be blocked waiting on meetings.register() for too long
550
+ this.resolveReachabilityPromise();
551
+ }, DEFAULT_TIMEOUT * 1000);
552
+
553
+ this.overallTimer = setTimeout(() => {
554
+ this.overallTimer = undefined;
555
+ this.abortClusterReachability();
556
+ this.emit(
557
+ {
558
+ file: 'reachability',
559
+ function: 'overallTimer timeout',
560
+ },
561
+ 'reachability:done',
562
+ {}
563
+ );
564
+ this.sendMetric();
565
+
566
+ LoggerProxy.logger.log(
567
+ `Reachability:index#startTimers --> Reachability checks fully timed out (${OVERALL_TIMEOUT}s)`
568
+ );
569
+ }, OVERALL_TIMEOUT * 1000);
570
+ }
571
+
572
+ /**
573
+ * Stores given reachability results in local storage
574
+ *
575
+ * @param {ReachabilityResults} results
576
+ * @returns {Promise<void>}
577
+ */
578
+ private async storeResults(results: ReachabilityResults) {
579
+ // @ts-ignore
580
+ await this.webex.boundedStorage.put(
581
+ this.namespace,
582
+ REACHABILITY.localStorageResult,
583
+ JSON.stringify(results)
584
+ );
585
+ }
586
+
587
+ /**
588
+ * Resets all the internal counters that keep track of the results
589
+ *
590
+ * @returns {void}
591
+ */
592
+ private resetResultCounters() {
593
+ this.expectedResultsCount.videoMesh.udp = 0;
594
+ this.expectedResultsCount.public.udp = 0;
595
+ this.expectedResultsCount.public.tcp = 0;
596
+ this.expectedResultsCount.public.xtls = 0;
597
+
598
+ this.resultsCount.videoMesh.udp = 0;
599
+ this.resultsCount.public.udp = 0;
600
+ this.resultsCount.public.tcp = 0;
601
+ this.resultsCount.public.xtls = 0;
602
+ }
603
+
398
604
  /**
399
605
  * Performs reachability checks for all clusters
400
606
  * @param {ClusterList} clusterList
401
- * @returns {Promise<ReachabilityResults>} reachability check results
607
+ * @returns {Promise<void>} promise that's resolved as soon as the checks are started
402
608
  */
403
- private async performReachabilityChecks(clusterList: ClusterList): Promise<ReachabilityResults> {
609
+ private async performReachabilityChecks(clusterList: ClusterList) {
404
610
  const results: ReachabilityResults = {};
405
611
 
612
+ this.clusterReachability = {};
613
+
406
614
  if (!clusterList || !Object.keys(clusterList).length) {
407
- return Promise.resolve(results);
615
+ return;
408
616
  }
409
617
 
410
618
  LoggerProxy.logger.log(
@@ -417,7 +625,11 @@ export default class Reachability {
417
625
  } reachability checks`
418
626
  );
419
627
 
420
- const clusterReachabilityChecks = Object.keys(clusterList).map((key) => {
628
+ this.resetResultCounters();
629
+ this.startTimers();
630
+
631
+ // sanitize the urls in the clusterList
632
+ Object.keys(clusterList).forEach((key) => {
421
633
  const cluster = clusterList[key];
422
634
 
423
635
  // Linus doesn't support TCP reachability checks on video mesh nodes
@@ -429,6 +641,7 @@ export default class Reachability {
429
641
  cluster.tcp = [];
430
642
  }
431
643
 
644
+ // Linus doesn't support xTLS reachability checks on video mesh nodes
432
645
  const includeTlsReachability =
433
646
  // @ts-ignore
434
647
  this.webex.config.meetings.experimental.enableTlsReachability && !cluster.isVideoMesh;
@@ -437,18 +650,94 @@ export default class Reachability {
437
650
  cluster.xtls = [];
438
651
  }
439
652
 
440
- this.clusterReachability[key] = new ClusterReachability(key, cluster);
653
+ // initialize the result for this cluster
654
+ results[key] = {
655
+ udp: {result: cluster.udp.length > 0 ? 'unreachable' : 'untested'},
656
+ tcp: {result: cluster.tcp.length > 0 ? 'unreachable' : 'untested'},
657
+ xtls: {result: cluster.xtls.length > 0 ? 'unreachable' : 'untested'},
658
+ isVideoMesh: cluster.isVideoMesh,
659
+ };
660
+
661
+ // update expected results counters to include this cluster
662
+ this.expectedResultsCount[cluster.isVideoMesh ? 'videoMesh' : 'public'].udp +=
663
+ cluster.udp.length;
664
+ if (!cluster.isVideoMesh) {
665
+ this.expectedResultsCount.public.tcp += cluster.tcp.length;
666
+ this.expectedResultsCount.public.xtls += cluster.xtls.length;
667
+ }
668
+ });
669
+
670
+ const isFirstResult = {
671
+ udp: true,
672
+ tcp: true,
673
+ xtls: true,
674
+ };
675
+
676
+ // save the initialized results (in case we don't get any "resultReady" events at all)
677
+ await this.storeResults(results);
441
678
 
442
- return this.clusterReachability[key].start().then((result) => {
443
- results[key] = result;
444
- results[key].isVideoMesh = cluster.isVideoMesh;
679
+ // now start the reachability on all the clusters
680
+ Object.keys(clusterList).forEach((key) => {
681
+ const cluster = clusterList[key];
682
+
683
+ this.clusterReachability[key] = new ClusterReachability(key, cluster);
684
+ this.clusterReachability[key].on(Events.resultReady, async (data: ResultEventData) => {
685
+ const {protocol, result, clientMediaIPs, latencyInMilliseconds} = data;
686
+
687
+ if (isFirstResult[protocol]) {
688
+ this.emit(
689
+ {
690
+ file: 'reachability',
691
+ function: 'resultReady event handler',
692
+ },
693
+ 'reachability:firstResultAvailable',
694
+ {
695
+ protocol,
696
+ }
697
+ );
698
+ isFirstResult[protocol] = false;
699
+ }
700
+ this.resultsCount[cluster.isVideoMesh ? 'videoMesh' : 'public'][protocol] += 1;
701
+
702
+ const areAllResultsReady = this.areAllResultsReady();
703
+
704
+ results[key][protocol].result = result;
705
+ results[key][protocol].clientMediaIPs = clientMediaIPs;
706
+ results[key][protocol].latencyInMilliseconds = latencyInMilliseconds;
707
+
708
+ await this.storeResults(results);
709
+
710
+ if (areAllResultsReady) {
711
+ clearTimeout(this.overallTimer);
712
+ this.overallTimer = undefined;
713
+ this.emit(
714
+ {
715
+ file: 'reachability',
716
+ function: 'performReachabilityChecks',
717
+ },
718
+ 'reachability:done',
719
+ {}
720
+ );
721
+ this.sendMetric();
722
+
723
+ LoggerProxy.logger.log(
724
+ `Reachability:index#gatherReachability --> Reachability checks fully completed`
725
+ );
726
+ this.resolveReachabilityPromise();
727
+ }
445
728
  });
446
- });
447
729
 
448
- await Promise.all(clusterReachabilityChecks);
730
+ // clientMediaIps can be updated independently from the results, so we need to listen for them too
731
+ this.clusterReachability[key].on(
732
+ Events.clientMediaIpsUpdated,
733
+ async (data: ClientMediaIpsUpdatedEventData) => {
734
+ results[key][data.protocol].clientMediaIPs = data.clientMediaIPs;
449
735
 
450
- this.logUnreachableClusters();
736
+ await this.storeResults(results);
737
+ }
738
+ );
451
739
 
452
- return results;
740
+ this.clusterReachability[key].start(); // not awaiting on purpose
741
+ });
453
742
  }
454
743
  }
@@ -87,6 +87,7 @@ describe('plugin-meetings', () => {
87
87
  // @ts-ignore
88
88
  webex = new MockWebex({});
89
89
  webex.internal.llm.on = sinon.stub();
90
+ webex.internal.llm.isConnected = sinon.stub();
90
91
  webex.internal.mercury.on = sinon.stub();
91
92
  breakouts = new Breakouts({}, {parent: webex});
92
93
  breakouts.groupId = 'groupId';
@@ -225,38 +226,6 @@ describe('plugin-meetings', () => {
225
226
  });
226
227
  });
227
228
 
228
- describe('#listenToBroadcastMessages', () => {
229
- it('triggers message event when a message received', () => {
230
- const call = webex.internal.llm.on.getCall(0);
231
- const callback = call.args[1];
232
-
233
- assert.equal(call.args[0], 'event:breakout.message');
234
-
235
- let message;
236
-
237
- breakouts.listenTo(breakouts, BREAKOUTS.EVENTS.MESSAGE, (event) => {
238
- message = event;
239
- });
240
-
241
- breakouts.currentBreakoutSession.sessionId = 'sessionId';
242
-
243
- callback({
244
- data: {
245
- senderUserId: 'senderUserId',
246
- sentTime: 'sentTime',
247
- message: 'message',
248
- },
249
- });
250
-
251
- assert.deepEqual(message, {
252
- senderUserId: 'senderUserId',
253
- sentTime: 'sentTime',
254
- message: 'message',
255
- sessionId: 'sessionId',
256
- });
257
- });
258
- });
259
-
260
229
  describe('#listenToBreakoutRosters', () => {
261
230
  it('triggers member update event when a roster received', () => {
262
231
  const call = webex.internal.mercury.on.getCall(0);
@@ -496,8 +465,58 @@ describe('plugin-meetings', () => {
496
465
  describe('#locusUrlUpdate', () => {
497
466
  it('sets the locus url', () => {
498
467
  breakouts.locusUrlUpdate('newUrl');
468
+ assert.equal(breakouts.locusUrl, 'newUrl');
469
+ });
470
+ });
471
+
472
+ describe('#listenToBroadcastMessages', () => {
473
+ it('do not subscribe message if llm not connected', () => {
474
+ webex.internal.llm.isConnected = sinon.stub().returns(false);
475
+ breakouts.listenTo = sinon.stub();
476
+ breakouts.locusUrlUpdate('newUrl');
477
+ assert.equal(breakouts.locusUrl, 'newUrl');
478
+ assert.notCalled(breakouts.listenTo);
479
+ });
499
480
 
481
+ it('do not subscribe message if already done', () => {
482
+ webex.internal.llm.isConnected = sinon.stub().returns(true);
483
+ breakouts.hasSubscribedToMessage = true;
484
+ breakouts.listenTo = sinon.stub();
485
+ breakouts.locusUrlUpdate('newUrl');
500
486
  assert.equal(breakouts.locusUrl, 'newUrl');
487
+ assert.notCalled(breakouts.listenTo);
488
+ });
489
+
490
+ it('triggers message event when a message received', () => {
491
+ webex.internal.llm.isConnected = sinon.stub().returns(true);
492
+ breakouts.locusUrlUpdate('newUrl');
493
+ const call = webex.internal.llm.on.getCall(0);
494
+ const callback = call.args[1];
495
+
496
+ assert.equal(call.args[0], 'event:breakout.message');
497
+
498
+ let message;
499
+
500
+ breakouts.listenTo(breakouts, BREAKOUTS.EVENTS.MESSAGE, (event) => {
501
+ message = event;
502
+ });
503
+
504
+ breakouts.currentBreakoutSession.sessionId = 'sessionId';
505
+
506
+ callback({
507
+ data: {
508
+ senderUserId: 'senderUserId',
509
+ sentTime: 'sentTime',
510
+ message: 'message',
511
+ },
512
+ });
513
+
514
+ assert.deepEqual(message, {
515
+ senderUserId: 'senderUserId',
516
+ sentTime: 'sentTime',
517
+ message: 'message',
518
+ sessionId: 'sessionId',
519
+ });
501
520
  });
502
521
  });
503
522
 
@@ -345,37 +345,39 @@ describe('plugin-meetings', () => {
345
345
  });
346
346
 
347
347
  describe('mutedByOthersChanged', () => {
348
- it('throws an error if changedSelf is not provided', function() {
349
- assert.throws(() => SelfUtils.mutedByOthersChanged({}, null), 'New self must be defined to determine if self was muted by others.');
350
- });
351
-
352
- it('return false when oldSelf is not defined', function() {
353
- assert.equal(SelfUtils.mutedByOthersChanged(null, { remoteMuted: false }), false);
348
+ it('throws an error if changedSelf is not provided', function () {
349
+ assert.throws(
350
+ () => SelfUtils.mutedByOthersChanged({}, null),
351
+ 'New self must be defined to determine if self was muted by others.'
352
+ );
354
353
  });
355
354
 
356
- it('should return true when remoteMuted is true on entry', function() {
357
- assert.equal(SelfUtils.mutedByOthersChanged(null, { remoteMuted: true }), true);
355
+ it('return false when oldSelf is not defined', function () {
356
+ assert.equal(SelfUtils.mutedByOthersChanged(null, {remoteMuted: false}), false);
358
357
  });
359
358
 
360
- it('should return false when selfIdentity and modifiedBy are the same', function() {
361
- assert.equal(SelfUtils.mutedByOthersChanged(
362
- { remoteMuted: false },
363
- { remoteMuted: true, selfIdentity: 'user1', modifiedBy: 'user1' }
364
- ), false);
359
+ it('should return true when remoteMuted is true on entry', function () {
360
+ assert.equal(SelfUtils.mutedByOthersChanged(null, {remoteMuted: true}), true);
365
361
  });
366
362
 
367
- it('should return true when remoteMuted values are different', function() {
368
- assert.equal(SelfUtils.mutedByOthersChanged(
369
- { remoteMuted: false },
370
- { remoteMuted: true, selfIdentity: 'user1', modifiedBy: 'user2' }
371
- ), true);
363
+ it('should return true when remoteMuted values are different', function () {
364
+ assert.equal(
365
+ SelfUtils.mutedByOthersChanged(
366
+ {remoteMuted: false},
367
+ {remoteMuted: true, selfIdentity: 'user1', modifiedBy: 'user2'}
368
+ ),
369
+ true
370
+ );
372
371
  });
373
372
 
374
- it('should return true when remoteMuted is true and unmuteAllowed has changed', function() {
375
- assert.equal(SelfUtils.mutedByOthersChanged(
376
- { remoteMuted: true, unmuteAllowed: false },
377
- { remoteMuted: true, unmuteAllowed: true, selfIdentity: 'user1', modifiedBy: 'user2' }
378
- ), true);
373
+ it('should return true when remoteMuted is true and unmuteAllowed has changed', function () {
374
+ assert.equal(
375
+ SelfUtils.mutedByOthersChanged(
376
+ {remoteMuted: true, unmuteAllowed: false},
377
+ {remoteMuted: true, unmuteAllowed: true, selfIdentity: 'user1', modifiedBy: 'user2'}
378
+ ),
379
+ true
380
+ );
379
381
  });
380
382
  });
381
383