@webex/plugin-meetings 3.10.0-next.9 → 3.10.0-webex-services-ready.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 (73) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +11 -3
  4. package/dist/constants.js.map +1 -1
  5. package/dist/hashTree/constants.js +20 -0
  6. package/dist/hashTree/constants.js.map +1 -0
  7. package/dist/hashTree/hashTree.js +515 -0
  8. package/dist/hashTree/hashTree.js.map +1 -0
  9. package/dist/hashTree/hashTreeParser.js +1266 -0
  10. package/dist/hashTree/hashTreeParser.js.map +1 -0
  11. package/dist/hashTree/types.js +21 -0
  12. package/dist/hashTree/types.js.map +1 -0
  13. package/dist/hashTree/utils.js +48 -0
  14. package/dist/hashTree/utils.js.map +1 -0
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/index.js +511 -48
  18. package/dist/locus-info/index.js.map +1 -1
  19. package/dist/locus-info/types.js +7 -0
  20. package/dist/locus-info/types.js.map +1 -0
  21. package/dist/meeting/index.js +41 -15
  22. package/dist/meeting/index.js.map +1 -1
  23. package/dist/meeting/util.js +1 -0
  24. package/dist/meeting/util.js.map +1 -1
  25. package/dist/meetings/index.js +112 -70
  26. package/dist/meetings/index.js.map +1 -1
  27. package/dist/metrics/constants.js +3 -1
  28. package/dist/metrics/constants.js.map +1 -1
  29. package/dist/reachability/clusterReachability.js +44 -358
  30. package/dist/reachability/clusterReachability.js.map +1 -1
  31. package/dist/reachability/reachability.types.js +14 -1
  32. package/dist/reachability/reachability.types.js.map +1 -1
  33. package/dist/reachability/reachabilityPeerConnection.js +445 -0
  34. package/dist/reachability/reachabilityPeerConnection.js.map +1 -0
  35. package/dist/types/constants.d.ts +26 -21
  36. package/dist/types/hashTree/constants.d.ts +8 -0
  37. package/dist/types/hashTree/hashTree.d.ts +129 -0
  38. package/dist/types/hashTree/hashTreeParser.d.ts +260 -0
  39. package/dist/types/hashTree/types.d.ts +25 -0
  40. package/dist/types/hashTree/utils.d.ts +9 -0
  41. package/dist/types/locus-info/index.d.ts +91 -42
  42. package/dist/types/locus-info/types.d.ts +46 -0
  43. package/dist/types/meeting/index.d.ts +22 -9
  44. package/dist/types/meetings/index.d.ts +9 -2
  45. package/dist/types/metrics/constants.d.ts +2 -0
  46. package/dist/types/reachability/clusterReachability.d.ts +10 -88
  47. package/dist/types/reachability/reachability.types.d.ts +12 -1
  48. package/dist/types/reachability/reachabilityPeerConnection.d.ts +111 -0
  49. package/dist/webinar/index.js +1 -1
  50. package/package.json +22 -21
  51. package/src/constants.ts +13 -1
  52. package/src/hashTree/constants.ts +9 -0
  53. package/src/hashTree/hashTree.ts +463 -0
  54. package/src/hashTree/hashTreeParser.ts +1161 -0
  55. package/src/hashTree/types.ts +30 -0
  56. package/src/hashTree/utils.ts +42 -0
  57. package/src/locus-info/index.ts +556 -85
  58. package/src/locus-info/types.ts +48 -0
  59. package/src/meeting/index.ts +58 -26
  60. package/src/meeting/util.ts +1 -0
  61. package/src/meetings/index.ts +104 -51
  62. package/src/metrics/constants.ts +2 -0
  63. package/src/reachability/clusterReachability.ts +50 -347
  64. package/src/reachability/reachability.types.ts +15 -1
  65. package/src/reachability/reachabilityPeerConnection.ts +416 -0
  66. package/test/unit/spec/hashTree/hashTree.ts +655 -0
  67. package/test/unit/spec/hashTree/hashTreeParser.ts +1532 -0
  68. package/test/unit/spec/hashTree/utils.ts +103 -0
  69. package/test/unit/spec/locus-info/index.js +667 -1
  70. package/test/unit/spec/meeting/index.js +91 -20
  71. package/test/unit/spec/meeting/utils.js +77 -0
  72. package/test/unit/spec/meetings/index.js +71 -26
  73. package/test/unit/spec/reachability/clusterReachability.ts +281 -138
@@ -1,4 +1,4 @@
1
- import {isEqual, assignWith, cloneDeep, isEmpty, forEach} from 'lodash';
1
+ import {isEqual, assignWith, cloneDeep, isEmpty} from 'lodash';
2
2
 
3
3
  import LoggerProxy from '../common/logs/logger-proxy';
4
4
  import EventsScope from '../common/events/events-scope';
@@ -17,7 +17,7 @@ import {
17
17
  MEETING_REMOVED_REASON,
18
18
  CALL_REMOVED_REASON,
19
19
  RECORDING_STATE,
20
- BREAKOUTS,
20
+ Enum,
21
21
  } from '../constants';
22
22
 
23
23
  import InfoUtils from './infoUtils';
@@ -30,52 +30,55 @@ import MediaSharesUtils from './mediaSharesUtils';
30
30
  import LocusDeltaParser from './parser';
31
31
  import Metrics from '../metrics';
32
32
  import BEHAVIORAL_METRICS from '../metrics/constants';
33
-
34
- export type LocusDTO = {
35
- controls?: any;
36
- fullState?: {
37
- active: boolean;
38
- count: number;
39
- lastActive: string;
40
- locked: boolean;
41
- sessionId: string;
42
- seessionIds: string[];
43
- startTime: number;
44
- state: string;
45
- type: string;
46
- };
47
- host?: {
48
- id: string;
49
- incomingCallProtocols: any[];
50
- isExternal: boolean;
51
- name: string;
52
- orgId: string;
33
+ import HashTreeParser, {
34
+ DataSet,
35
+ HashTreeMessage,
36
+ HashTreeObject,
37
+ isSelf,
38
+ LocusInfoUpdateType,
39
+ } from '../hashTree/hashTreeParser';
40
+ import {ObjectType, ObjectTypeToLocusKeyMap} from '../hashTree/types';
41
+ import {LocusDTO, LocusFullState} from './types';
42
+
43
+ export type LocusLLMEvent = {
44
+ data: {
45
+ eventType: typeof LOCUSEVENT.HASH_TREE_DATA_UPDATED;
46
+ stateElementsMessage: HashTreeMessage;
53
47
  };
54
- info?: any;
55
- links?: any;
56
- mediaShares?: any[];
57
- meetings?: any[];
58
- participants: any[];
59
- replaces?: any[];
60
- self?: any;
61
- sequence?: {
62
- dirtyParticipants: number;
63
- entries: number[];
64
- rangeEnd: number;
65
- rangeStart: number;
66
- sequenceHash: number;
67
- sessionToken: string;
68
- since: string;
69
- totalParticipants: number;
70
- };
71
- syncUrl?: string;
72
- url?: string;
73
48
  };
74
49
 
50
+ // list of top level keys in Locus DTO relevant for Hash Tree DTOs processing
51
+ // it does not contain fields specific to classic Locus DTOs like sequence or baseSequence
52
+ const LocusDtoTopLevelKeys = [
53
+ 'controls',
54
+ 'fullState',
55
+ 'host',
56
+ 'info',
57
+ 'links',
58
+ 'mediaShares',
59
+ 'meetings',
60
+ 'participants',
61
+ 'replaces',
62
+ 'self',
63
+ 'sequence',
64
+ 'syncUrl',
65
+ 'url',
66
+ 'htMeta', // only exists when hash trees are used
67
+ ];
68
+
75
69
  export type LocusApiResponseBody = {
70
+ dataSets?: DataSet[];
76
71
  locus: LocusDTO; // this LocusDTO here might not be the full one (for example it won't have all the participants, but it should have self)
77
72
  };
78
73
 
74
+ const LocusObjectStateAfterUpdates = {
75
+ unchanged: 'unchanged',
76
+ removed: 'removed',
77
+ updated: 'updated',
78
+ } as const;
79
+
80
+ type LocusObjectStateAfterUpdates = Enum<typeof LocusObjectStateAfterUpdates>;
81
+
79
82
  /**
80
83
  * @description LocusInfo extends ChildEmitter to convert locusInfo info a private emitter to parent object
81
84
  * @export
@@ -114,6 +117,10 @@ export default class LocusInfo extends EventsScope {
114
117
  resources: any;
115
118
  mainSessionLocusCache: any;
116
119
  self: any;
120
+ hashTreeParser?: HashTreeParser;
121
+ hashTreeObjectId2ParticipantId: Map<number, string>; // mapping of hash tree object ids to participant ids
122
+ classicVsHashTreeMismatchMetricCounter = 0;
123
+
117
124
  /**
118
125
  * Constructor
119
126
  * @param {function} updateMeeting callback to update the meeting object from an object
@@ -132,10 +139,12 @@ export default class LocusInfo extends EventsScope {
132
139
  this.meetingId = meetingId;
133
140
  this.updateMeeting = updateMeeting;
134
141
  this.locusParser = new LocusDeltaParser();
142
+ this.hashTreeObjectId2ParticipantId = new Map();
135
143
  }
136
144
 
137
145
  /**
138
146
  * Does a Locus sync. It tries to get the latest delta DTO or if it can't, it falls back to getting the full Locus DTO.
147
+ * WARNING: This function must not be used for hash tree based Locus meetings.
139
148
  *
140
149
  * @param {Meeting} meeting
141
150
  * @param {boolean} isLocusUrlChanged
@@ -356,14 +365,109 @@ export default class LocusInfo extends EventsScope {
356
365
  }
357
366
 
358
367
  /**
359
- * @param {Object} locus
368
+ * Creates the HashTreeParser instance.
369
+ * @param {Object} initial locus data
370
+ * @returns {void}
371
+ */
372
+ private createHashTreeParser({
373
+ initialLocus,
374
+ }: {
375
+ initialLocus: {
376
+ dataSets: Array<DataSet>;
377
+ locus: any;
378
+ };
379
+ }) {
380
+ return new HashTreeParser({
381
+ initialLocus,
382
+ webexRequest: this.webex.request.bind(this.webex),
383
+ locusInfoUpdateCallback: this.updateFromHashTree.bind(this),
384
+ debugId: `HT-${this.meetingId.substring(0, 4)}`,
385
+ });
386
+ }
387
+
388
+ /**
389
+ * @param {Object} data - data to initialize locus info with. It may be from a join or GET /loci response or from a Mercury event that triggers a creation of meeting object
360
390
  * @returns {undefined}
361
391
  * @memberof LocusInfo
362
392
  */
363
- initialSetup(locus: object) {
364
- this.updateLocusCache(locus);
365
- this.onFullLocus(locus);
393
+ async initialSetup(
394
+ data:
395
+ | {
396
+ trigger: 'join-response';
397
+ locus: LocusDTO;
398
+ dataSets?: DataSet[];
399
+ }
400
+ | {
401
+ trigger: 'locus-message';
402
+ locus?: LocusDTO;
403
+ hashTreeMessage?: HashTreeMessage;
404
+ }
405
+ | {
406
+ trigger: 'get-loci-response';
407
+ locus?: LocusDTO;
408
+ }
409
+ ) {
410
+ switch (data.trigger) {
411
+ case 'locus-message':
412
+ if (data.hashTreeMessage) {
413
+ // we need the SELF object to be in the received message, because it contains visibleDataSets
414
+ // and these are needed to initialize all the hash trees
415
+ const selfObject = data.hashTreeMessage.locusStateElements?.find((el) => isSelf(el));
416
+
417
+ if (!selfObject?.data?.visibleDataSets) {
418
+ LoggerProxy.logger.warn(
419
+ `Locus-info:index#initialSetup --> cannot initialize HashTreeParser, SELF object with visibleDataSets is missing in the message`
420
+ );
421
+
422
+ throw new Error('SELF object with visibleDataSets is missing in the message');
423
+ }
424
+
425
+ LoggerProxy.logger.info(
426
+ 'Locus-info:index#initialSetup --> creating HashTreeParser from message'
427
+ );
428
+ // first create the HashTreeParser, but don't initialize it with any data yet
429
+ // pass just a fake locus that contains only the visibleDataSets
430
+ this.hashTreeParser = this.createHashTreeParser({
431
+ initialLocus: {
432
+ locus: {self: {visibleDataSets: selfObject.data.visibleDataSets}},
433
+ dataSets: [], // empty, because they will be populated in initializeFromMessage() call // dataSets: data.hashTreeMessage.dataSets,
434
+ },
435
+ });
366
436
 
437
+ // now handle the message - that should populate all the visible datasets
438
+ await this.hashTreeParser.initializeFromMessage(data.hashTreeMessage);
439
+ } else {
440
+ // "classic" Locus case, no hash trees involved
441
+ this.updateLocusCache(data.locus);
442
+ this.onFullLocus(data.locus, undefined);
443
+ }
444
+ break;
445
+ case 'join-response':
446
+ this.updateLocusCache(data.locus);
447
+ this.onFullLocus(data.locus, undefined, data.dataSets);
448
+ break;
449
+ case 'get-loci-response':
450
+ if (data.locus?.links?.resources?.visibleDataSets?.url) {
451
+ LoggerProxy.logger.info(
452
+ 'Locus-info:index#initialSetup --> creating HashTreeParser from get-loci-response'
453
+ );
454
+ // first create the HashTreeParser, but don't initialize it with any data yet
455
+ // pass just a fake locus that contains only the visibleDataSets
456
+ this.hashTreeParser = this.createHashTreeParser({
457
+ initialLocus: {
458
+ locus: {self: {visibleDataSets: data.locus?.self?.visibleDataSets}},
459
+ dataSets: [], // empty, because we don't have them yet
460
+ },
461
+ });
462
+
463
+ // now initialize all the data
464
+ await this.hashTreeParser.initializeFromGetLociResponse(data.locus);
465
+ } else {
466
+ // "classic" Locus case, no hash trees involved
467
+ this.updateLocusCache(data.locus);
468
+ this.onFullLocus(data.locus, undefined);
469
+ }
470
+ }
367
471
  // Change it to true after it receives it first locus object
368
472
  this.emitChange = true;
369
473
  }
@@ -375,7 +479,296 @@ export default class LocusInfo extends EventsScope {
375
479
  * @returns {void}
376
480
  */
377
481
  handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
378
- this.handleLocusDelta(responseBody.locus, meeting);
482
+ if (this.hashTreeParser) {
483
+ if (!responseBody.dataSets) {
484
+ this.sendClassicVsHashTreeMismatchMetric(
485
+ meeting,
486
+ `expected hash tree dataSets in API response but they are missing`
487
+ );
488
+ // continuing as we can still manage without responseBody.dataSets, but this is very suspicious
489
+ }
490
+ LoggerProxy.logger.info(
491
+ 'Locus-info:index#handleLocusAPIResponse --> passing Locus API response to HashTreeParser: ',
492
+ responseBody
493
+ );
494
+ // update the data in our hash trees
495
+ this.hashTreeParser.handleLocusUpdate(responseBody);
496
+ } else {
497
+ if (responseBody.dataSets) {
498
+ this.sendClassicVsHashTreeMismatchMetric(
499
+ meeting,
500
+ `unexpected hash tree dataSets in API response`
501
+ );
502
+ }
503
+ // classic Locus delta
504
+ this.handleLocusDelta(responseBody.locus, meeting);
505
+ }
506
+ }
507
+
508
+ /**
509
+ *
510
+ * @param {HashTreeObject} object data set object
511
+ * @param {any} locus
512
+ * @returns {void}
513
+ */
514
+ updateLocusFromHashTreeObject(object: HashTreeObject, locus: LocusDTO): LocusDTO {
515
+ const type = object.htMeta.elementId.type.toLowerCase();
516
+
517
+ switch (type) {
518
+ case ObjectType.locus: {
519
+ if (!object.data) {
520
+ // not doing anything here, as we need Locus to always be there (at least some fields)
521
+ // and that's already taken care of in updateFromHashTree()
522
+ LoggerProxy.logger.info(
523
+ `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object removed, version=${object.htMeta.elementId.version}`
524
+ );
525
+
526
+ return locus;
527
+ }
528
+ // replace the main locus
529
+
530
+ // The Locus object we receive from backend has empty participants array,
531
+ // and may have (although it shouldn't) other fields that are managed by other ObjectTypes
532
+ // like "fullState" or "info", so we're making sure to delete them here
533
+ const locusObjectFromData = object.data;
534
+
535
+ Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
536
+ delete locusObjectFromData[locusDtoKey];
537
+ });
538
+
539
+ locus = {...locus, ...locusObjectFromData};
540
+ LoggerProxy.logger.info(
541
+ `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object updated to version=${object.htMeta.elementId.version}`
542
+ );
543
+ break;
544
+ }
545
+ case ObjectType.mediaShare:
546
+ if (object.data) {
547
+ LoggerProxy.logger.info(
548
+ `Locus-info:index#updateLocusFromHashTreeObject --> mediaShare id=${
549
+ object.htMeta.elementId.id
550
+ } name='${object.data.name}' updated ${
551
+ object.data.name === 'content'
552
+ ? `floor=${object.data.floor?.disposition}, ${object.data.floor?.beneficiary?.id}`
553
+ : ''
554
+ } version=${object.htMeta.elementId.version}`
555
+ );
556
+ const existingMediaShare = locus.mediaShares?.find(
557
+ (ms) => ms.htMeta.elementId.id === object.htMeta.elementId.id
558
+ );
559
+
560
+ if (existingMediaShare) {
561
+ Object.assign(existingMediaShare, object.data);
562
+ } else {
563
+ locus.mediaShares = locus.mediaShares || [];
564
+ locus.mediaShares.push(object.data);
565
+ }
566
+ } else {
567
+ LoggerProxy.logger.info(
568
+ `Locus-info:index#updateLocusFromHashTreeObject --> mediaShare id=${object.htMeta.elementId.id} removed, version=${object.htMeta.elementId.version}`
569
+ );
570
+ locus.mediaShares = locus.mediaShares?.filter(
571
+ (ms) => ms.htMeta.elementId.id !== object.htMeta.elementId.id
572
+ );
573
+ }
574
+ break;
575
+ case ObjectType.participant:
576
+ LoggerProxy.logger.info(
577
+ `Locus-info:index#updateLocusFromHashTreeObject --> participant id=${
578
+ object.htMeta.elementId.id
579
+ } ${object.data ? 'updated' : 'removed'} version=${object.htMeta.elementId.version}`
580
+ );
581
+ if (object.data) {
582
+ if (!locus.participants) {
583
+ locus.participants = [];
584
+ }
585
+ const participantObject = object.data;
586
+ participantObject.htMeta = object.htMeta;
587
+ locus.participants.push(participantObject);
588
+ this.hashTreeObjectId2ParticipantId.set(object.htMeta.elementId.id, participantObject.id);
589
+ } else {
590
+ const participantId = this.hashTreeObjectId2ParticipantId.get(object.htMeta.elementId.id);
591
+
592
+ if (!locus.jsSdkMeta) {
593
+ locus.jsSdkMeta = {removedParticipantIds: []};
594
+ }
595
+ locus.jsSdkMeta.removedParticipantIds.push(participantId);
596
+ this.hashTreeObjectId2ParticipantId.delete(object.htMeta.elementId.id);
597
+ }
598
+ break;
599
+ case ObjectType.info:
600
+ case ObjectType.fullState:
601
+ case ObjectType.self:
602
+ if (!object.data) {
603
+ // self without data is handled inside HashTreeParser and results in LocusInfoUpdateType.MEETING_ENDED, so we should never get here
604
+ // other types like info or fullstate - Locus should never send them without data
605
+ LoggerProxy.logger.warn(
606
+ `Locus-info:index#updateLocusFromHashTreeObject --> received ${type} object without data, this is not expected! version=${object.htMeta.elementId.version}`
607
+ );
608
+ } else {
609
+ LoggerProxy.logger.info(
610
+ `Locus-info:index#updateLocusFromHashTreeObject --> ${type} object updated to version ${object.htMeta.elementId.version}`
611
+ );
612
+ const locusDtoKey = ObjectTypeToLocusKeyMap[type];
613
+ locus[locusDtoKey] = object.data;
614
+ }
615
+ break;
616
+ default:
617
+ LoggerProxy.logger.warn(
618
+ `Locus-info:index#updateLocusFromHashTreeObject --> received unsupported object type ${type}`
619
+ );
620
+ break;
621
+ }
622
+
623
+ return locus;
624
+ }
625
+
626
+ /**
627
+ * Sends a metric when we receive something from Locus that uses hash trees while we
628
+ * expect classic deltas or the other way around.
629
+ * @param {Meeting} meeting
630
+ * @param {string} message
631
+ * @returns {void}
632
+ */
633
+ sendClassicVsHashTreeMismatchMetric(meeting: any, message: string) {
634
+ LoggerProxy.logger.warn(
635
+ `Locus-info:index#sendClassicVsHashTreeMismatchMetric --> classic vs hash tree mismatch! ${message}`
636
+ );
637
+
638
+ // we don't want to flood the metrics system
639
+ if (this.classicVsHashTreeMismatchMetricCounter < 5) {
640
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH, {
641
+ correlationId: meeting.correlationId,
642
+ message,
643
+ });
644
+ this.classicVsHashTreeMismatchMetricCounter += 1;
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Handles a hash tree message received from Locus.
650
+ *
651
+ * @param {Meeting} meeting - The meeting object
652
+ * @param {eventType} eventType - The event type
653
+ * @param {HashTreeMessage} message incoming hash tree message
654
+ * @returns {void}
655
+ */
656
+ private handleHashTreeMessage(meeting: any, eventType: LOCUSEVENT, message: HashTreeMessage) {
657
+ if (eventType !== LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
658
+ this.sendClassicVsHashTreeMismatchMetric(
659
+ meeting,
660
+ `got ${eventType}, expected ${LOCUSEVENT.HASH_TREE_DATA_UPDATED}`
661
+ );
662
+
663
+ return;
664
+ }
665
+
666
+ this.hashTreeParser.handleMessage(message);
667
+ }
668
+
669
+ /**
670
+ * Callback registered with HashTreeParser to receive locus info updates.
671
+ * Updates our locus info based on the data parsed by the hash tree parser.
672
+ *
673
+ * @param {LocusInfoUpdateType} updateType - The type of update received.
674
+ * @param {Object} [data] - Additional data for the update, if applicable.
675
+ * @returns {void}
676
+ */
677
+ private updateFromHashTree(
678
+ updateType: LocusInfoUpdateType,
679
+ data?: {updatedObjects: HashTreeObject[]}
680
+ ) {
681
+ switch (updateType) {
682
+ case LocusInfoUpdateType.OBJECTS_UPDATED: {
683
+ // initialize our new locus
684
+ let locus: LocusDTO = {
685
+ participants: [],
686
+ jsSdkMeta: {removedParticipantIds: []},
687
+ };
688
+
689
+ // first go over all the updates and check what happens with the main locus object
690
+ let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
691
+ LocusObjectStateAfterUpdates.unchanged;
692
+ data.updatedObjects.forEach((object) => {
693
+ if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
694
+ if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
695
+ // this code doesn't supported it right now,
696
+ // cases for "updated" followed by "removed", or multiple "updated" would need more handling
697
+ // but these should never happen
698
+ LoggerProxy.logger.warn(
699
+ `Locus-info:index#updateFromHashTree --> received multiple LOCUS objects in one update, this is unexpected!`
700
+ );
701
+ Metrics.sendBehavioralMetric(
702
+ BEHAVIORAL_METRICS.LOCUS_HASH_TREE_UNSUPPORTED_OPERATION,
703
+ {
704
+ locusUrl: object.data?.url || this.url,
705
+ message: object.data
706
+ ? 'multiple LOCUS object updates'
707
+ : 'LOCUS object update followed by removal',
708
+ }
709
+ );
710
+ }
711
+ if (object.data) {
712
+ locusObjectStateAfterUpdates = LocusObjectStateAfterUpdates.updated;
713
+ } else {
714
+ locusObjectStateAfterUpdates = LocusObjectStateAfterUpdates.removed;
715
+ }
716
+ }
717
+ });
718
+
719
+ // if Locus object is unchanged or removed, we need to keep using the existing locus
720
+ // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
721
+ // if it gets updated, we only need to have the fields that are not part of "locus" object (like "info" or "mediaShares")
722
+ // so that when Locus object gets updated, if the new one is missing some field, that field will
723
+ // be removed from our locusInfo
724
+ if (
725
+ locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.unchanged ||
726
+ locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.removed
727
+ ) {
728
+ // copy over all of existing locus except participants
729
+ LocusDtoTopLevelKeys.forEach((key) => {
730
+ if (key !== 'participants') {
731
+ locus[key] = cloneDeep(this[key]);
732
+ }
733
+ });
734
+ } else {
735
+ // initialize only the fields that are not part of main "Locus" object
736
+ // (except participants, which need to stay empty - that means "no participant changes")
737
+ Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
738
+ if (locusDtoKey !== 'participants') {
739
+ locus[locusDtoKey] = cloneDeep(this[locusDtoKey]);
740
+ }
741
+ });
742
+ }
743
+
744
+ LoggerProxy.logger.info(
745
+ `Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
746
+ data.updatedObjects.map((o) => ({
747
+ type: o.htMeta.elementId.type,
748
+ id: o.htMeta.elementId.id,
749
+ hasData: !!o.data,
750
+ }))
751
+ )}`
752
+ );
753
+ // now apply all the updates from the hash tree onto the locus
754
+ data.updatedObjects.forEach((object) => {
755
+ locus = this.updateLocusFromHashTreeObject(object, locus);
756
+ });
757
+
758
+ // update our locus info with the new locus
759
+ this.onDeltaLocus(locus);
760
+
761
+ break;
762
+ }
763
+
764
+ case LocusInfoUpdateType.MEETING_ENDED: {
765
+ LoggerProxy.logger.info(
766
+ `Locus-info:index#updateFromHashTree --> received signal that meeting ended, destroying meeting ${this.meetingId}`
767
+ );
768
+ const meeting = this.webex.meetings.meetingCollection.get(this.meetingId);
769
+ this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
770
+ }
771
+ }
379
772
  }
380
773
 
381
774
  /**
@@ -385,38 +778,52 @@ export default class LocusInfo extends EventsScope {
385
778
  * @memberof LocusInfo
386
779
  */
387
780
  parse(meeting: any, data: any) {
388
- // eslint-disable-next-line @typescript-eslint/no-shadow
389
- const {eventType} = data;
390
- const locus = this.getTheLocusToUpdate(data.locus);
391
- LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
392
-
393
- locus.jsSdkMeta = {removedParticipantIds: []};
394
-
395
- switch (eventType) {
396
- case LOCUSEVENT.PARTICIPANT_JOIN:
397
- case LOCUSEVENT.PARTICIPANT_LEFT:
398
- case LOCUSEVENT.CONTROLS_UPDATED:
399
- case LOCUSEVENT.PARTICIPANT_AUDIO_MUTED:
400
- case LOCUSEVENT.PARTICIPANT_AUDIO_UNMUTED:
401
- case LOCUSEVENT.PARTICIPANT_VIDEO_MUTED:
402
- case LOCUSEVENT.PARTICIPANT_VIDEO_UNMUTED:
403
- case LOCUSEVENT.SELF_CHANGED:
404
- case LOCUSEVENT.PARTICIPANT_UPDATED:
405
- case LOCUSEVENT.PARTICIPANT_CONTROLS_UPDATED:
406
- case LOCUSEVENT.PARTICIPANT_ROLES_UPDATED:
407
- case LOCUSEVENT.PARTICIPANT_DECLINED:
408
- case LOCUSEVENT.FLOOR_GRANTED:
409
- case LOCUSEVENT.FLOOR_RELEASED:
410
- this.onFullLocus(locus, eventType);
411
- break;
412
- case LOCUSEVENT.DIFFERENCE:
413
- this.handleLocusDelta(locus, meeting);
414
- break;
781
+ if (this.hashTreeParser) {
782
+ this.handleHashTreeMessage(
783
+ meeting,
784
+ data.eventType,
785
+ data.stateElementsMessage as HashTreeMessage
786
+ );
787
+ } else {
788
+ // eslint-disable-next-line @typescript-eslint/no-shadow
789
+ const {eventType} = data;
790
+ const locus = this.getTheLocusToUpdate(data.locus);
791
+ LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
792
+
793
+ locus.jsSdkMeta = {removedParticipantIds: []};
794
+
795
+ switch (eventType) {
796
+ case LOCUSEVENT.PARTICIPANT_JOIN:
797
+ case LOCUSEVENT.PARTICIPANT_LEFT:
798
+ case LOCUSEVENT.CONTROLS_UPDATED:
799
+ case LOCUSEVENT.PARTICIPANT_AUDIO_MUTED:
800
+ case LOCUSEVENT.PARTICIPANT_AUDIO_UNMUTED:
801
+ case LOCUSEVENT.PARTICIPANT_VIDEO_MUTED:
802
+ case LOCUSEVENT.PARTICIPANT_VIDEO_UNMUTED:
803
+ case LOCUSEVENT.SELF_CHANGED:
804
+ case LOCUSEVENT.PARTICIPANT_UPDATED:
805
+ case LOCUSEVENT.PARTICIPANT_CONTROLS_UPDATED:
806
+ case LOCUSEVENT.PARTICIPANT_ROLES_UPDATED:
807
+ case LOCUSEVENT.PARTICIPANT_DECLINED:
808
+ case LOCUSEVENT.FLOOR_GRANTED:
809
+ case LOCUSEVENT.FLOOR_RELEASED:
810
+ this.onFullLocus(locus, eventType);
811
+ break;
812
+ case LOCUSEVENT.DIFFERENCE:
813
+ this.handleLocusDelta(locus, meeting);
814
+ break;
815
+ case LOCUSEVENT.HASH_TREE_DATA_UPDATED:
816
+ this.sendClassicVsHashTreeMismatchMetric(
817
+ meeting,
818
+ `got ${eventType}, expected classic events`
819
+ );
820
+ break;
415
821
 
416
- default:
417
- // Why will there be a event with no eventType ????
418
- // we may not need this, we can get full locus
419
- this.handleLocusDelta(locus, meeting);
822
+ default:
823
+ // Why will there be a event with no eventType ????
824
+ // we may not need this, we can get full locus
825
+ this.handleLocusDelta(locus, meeting);
826
+ }
420
827
  }
421
828
  }
422
829
 
@@ -432,19 +839,45 @@ export default class LocusInfo extends EventsScope {
432
839
  }
433
840
 
434
841
  /**
435
- * updates the locus with full locus object
842
+ * Function for handling full locus when it's using hash trees (so not the "classic" one).
843
+ *
436
844
  * @param {object} locus locus object
437
- * @param {string} eventType particulat locus event
438
- * @returns {object} null
439
- * @memberof LocusInfo
845
+ * @param {string} eventType locus event
846
+ * @param {DataSet[]} dataSets
847
+ * @returns {void}
440
848
  */
441
- onFullLocus(locus: any, eventType?: string) {
442
- if (!locus) {
443
- LoggerProxy.logger.error(
444
- 'Locus-info:index#onFullLocus --> object passed as argument was invalid, continuing.'
849
+ private onFullLocusWithHashTrees(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
850
+ if (!this.hashTreeParser) {
851
+ LoggerProxy.logger.info(`Locus-info:index#onFullLocus --> creating hash tree parser`);
852
+ LoggerProxy.logger.info(
853
+ 'Locus-info:index#onFullLocus --> dataSets:',
854
+ dataSets,
855
+ ' and locus:',
856
+ locus
857
+ );
858
+ this.hashTreeParser = this.createHashTreeParser({
859
+ initialLocus: {locus, dataSets},
860
+ });
861
+ this.onFullLocusCommon(locus, eventType);
862
+ } else {
863
+ // in this case the Locus we're getting is not necessarily the full one
864
+ // so treat it like if we just got it in any api response
865
+
866
+ LoggerProxy.logger.info(
867
+ 'Locus-info:index#onFullLocus --> hash tree parser already exists, handling it like a normal API response'
445
868
  );
869
+ this.handleLocusAPIResponse(undefined, {dataSets, locus});
446
870
  }
871
+ }
447
872
 
873
+ /**
874
+ * Function for handling full locus when it's the "classic" one (not hash trees)
875
+ *
876
+ * @param {object} locus locus object
877
+ * @param {string} eventType locus event
878
+ * @returns {void}
879
+ */
880
+ private onFullLocusClassic(locus: any, eventType?: string) {
448
881
  if (!this.locusParser.isNewFullLocus(locus)) {
449
882
  LoggerProxy.logger.info(
450
883
  `Locus-info:index#onFullLocus --> ignoring old full locus DTO, eventType=${eventType}`
@@ -452,9 +885,47 @@ export default class LocusInfo extends EventsScope {
452
885
 
453
886
  return;
454
887
  }
888
+ this.onFullLocusCommon(locus, eventType);
889
+ }
455
890
 
891
+ /**
892
+ * updates the locus with full locus object
893
+ * @param {object} locus locus object
894
+ * @param {string} eventType locus event
895
+ * @param {DataSet[]} dataSets
896
+ * @returns {object} null
897
+ * @memberof LocusInfo
898
+ */
899
+ onFullLocus(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
900
+ if (!locus) {
901
+ LoggerProxy.logger.error(
902
+ 'Locus-info:index#onFullLocus --> object passed as argument was invalid, continuing.'
903
+ );
904
+ }
905
+
906
+ if (dataSets) {
907
+ // this is the new hashmap Locus DTO format (only applicable to webinars for now)
908
+ this.onFullLocusWithHashTrees(locus, eventType, dataSets);
909
+ } else {
910
+ this.onFullLocusClassic(locus, eventType);
911
+ }
912
+ }
913
+
914
+ /**
915
+ * Common part of handling full locus, used by both classic and hash tree based locus handling
916
+ * @param {object} locus locus object
917
+ * @param {string} eventType locus event
918
+ * @returns {void}
919
+ */
920
+ private onFullLocusCommon(locus: any, eventType?: string) {
456
921
  this.scheduledMeeting = locus.meeting || null;
457
922
  this.participants = locus.participants;
923
+ this.participants?.forEach((participant) => {
924
+ // participant.htMeta is set only for hash tree based locus
925
+ if (participant.htMeta?.elementId.id) {
926
+ this.hashTreeObjectId2ParticipantId.set(participant.htMeta.elementId.id, participant.id);
927
+ }
928
+ });
458
929
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
459
930
  this.updateLocusInfo(locus);
460
931
  this.updateParticipants(