@webex/plugin-meetings 3.10.0-next.9 → 3.10.0-webex-services-ready.2

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 +22 -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 +529 -75
  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 +63 -22
  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 +27 -0
  40. package/dist/types/hashTree/utils.d.ts +9 -0
  41. package/dist/types/locus-info/index.d.ts +95 -56
  42. package/dist/types/locus-info/types.d.ts +54 -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 +32 -0
  56. package/src/hashTree/utils.ts +42 -0
  57. package/src/locus-info/index.ts +571 -106
  58. package/src/locus-info/types.ts +53 -0
  59. package/src/meeting/index.ts +78 -27
  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 +722 -4
  70. package/test/unit/spec/meeting/index.js +120 -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 {Links, 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
@@ -110,10 +113,13 @@ export default class LocusInfo extends EventsScope {
110
113
  mediaShares: any;
111
114
  replace: any;
112
115
  url: any;
113
- services: any;
114
- resources: any;
116
+ links?: Links;
115
117
  mainSessionLocusCache: any;
116
118
  self: any;
119
+ hashTreeParser?: HashTreeParser;
120
+ hashTreeObjectId2ParticipantId: Map<number, string>; // mapping of hash tree object ids to participant ids
121
+ classicVsHashTreeMismatchMetricCounter = 0;
122
+
117
123
  /**
118
124
  * Constructor
119
125
  * @param {function} updateMeeting callback to update the meeting object from an object
@@ -132,10 +138,12 @@ export default class LocusInfo extends EventsScope {
132
138
  this.meetingId = meetingId;
133
139
  this.updateMeeting = updateMeeting;
134
140
  this.locusParser = new LocusDeltaParser();
141
+ this.hashTreeObjectId2ParticipantId = new Map();
135
142
  }
136
143
 
137
144
  /**
138
145
  * 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.
146
+ * WARNING: This function must not be used for hash tree based Locus meetings.
139
147
  *
140
148
  * @param {Meeting} meeting
141
149
  * @param {boolean} isLocusUrlChanged
@@ -351,19 +359,113 @@ export default class LocusInfo extends EventsScope {
351
359
  this.updateSelf(locus.self);
352
360
  this.updateHostInfo(locus.host);
353
361
  this.updateMediaShares(locus.mediaShares);
354
- this.updateServices(locus.links?.services);
355
- this.updateResources(locus.links?.resources);
362
+ this.updateLinks(locus.links);
356
363
  }
357
364
 
358
365
  /**
359
- * @param {Object} locus
366
+ * Creates the HashTreeParser instance.
367
+ * @param {Object} initial locus data
368
+ * @returns {void}
369
+ */
370
+ private createHashTreeParser({
371
+ initialLocus,
372
+ }: {
373
+ initialLocus: {
374
+ dataSets: Array<DataSet>;
375
+ locus: any;
376
+ };
377
+ }) {
378
+ return new HashTreeParser({
379
+ initialLocus,
380
+ webexRequest: this.webex.request.bind(this.webex),
381
+ locusInfoUpdateCallback: this.updateFromHashTree.bind(this),
382
+ debugId: `HT-${this.meetingId.substring(0, 4)}`,
383
+ });
384
+ }
385
+
386
+ /**
387
+ * @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
388
  * @returns {undefined}
361
389
  * @memberof LocusInfo
362
390
  */
363
- initialSetup(locus: object) {
364
- this.updateLocusCache(locus);
365
- this.onFullLocus(locus);
391
+ async initialSetup(
392
+ data:
393
+ | {
394
+ trigger: 'join-response';
395
+ locus: LocusDTO;
396
+ dataSets?: DataSet[];
397
+ }
398
+ | {
399
+ trigger: 'locus-message';
400
+ locus?: LocusDTO;
401
+ hashTreeMessage?: HashTreeMessage;
402
+ }
403
+ | {
404
+ trigger: 'get-loci-response';
405
+ locus?: LocusDTO;
406
+ }
407
+ ) {
408
+ switch (data.trigger) {
409
+ case 'locus-message':
410
+ if (data.hashTreeMessage) {
411
+ // we need the SELF object to be in the received message, because it contains visibleDataSets
412
+ // and these are needed to initialize all the hash trees
413
+ const selfObject = data.hashTreeMessage.locusStateElements?.find((el) => isSelf(el));
414
+
415
+ if (!selfObject?.data?.visibleDataSets) {
416
+ LoggerProxy.logger.warn(
417
+ `Locus-info:index#initialSetup --> cannot initialize HashTreeParser, SELF object with visibleDataSets is missing in the message`
418
+ );
419
+
420
+ throw new Error('SELF object with visibleDataSets is missing in the message');
421
+ }
422
+
423
+ LoggerProxy.logger.info(
424
+ 'Locus-info:index#initialSetup --> creating HashTreeParser from message'
425
+ );
426
+ // first create the HashTreeParser, but don't initialize it with any data yet
427
+ // pass just a fake locus that contains only the visibleDataSets
428
+ this.hashTreeParser = this.createHashTreeParser({
429
+ initialLocus: {
430
+ locus: {self: {visibleDataSets: selfObject.data.visibleDataSets}},
431
+ dataSets: [], // empty, because they will be populated in initializeFromMessage() call // dataSets: data.hashTreeMessage.dataSets,
432
+ },
433
+ });
366
434
 
435
+ // now handle the message - that should populate all the visible datasets
436
+ await this.hashTreeParser.initializeFromMessage(data.hashTreeMessage);
437
+ } else {
438
+ // "classic" Locus case, no hash trees involved
439
+ this.updateLocusCache(data.locus);
440
+ this.onFullLocus(data.locus, undefined);
441
+ }
442
+ break;
443
+ case 'join-response':
444
+ this.updateLocusCache(data.locus);
445
+ this.onFullLocus(data.locus, undefined, data.dataSets);
446
+ break;
447
+ case 'get-loci-response':
448
+ if (data.locus?.links?.resources?.visibleDataSets?.url) {
449
+ LoggerProxy.logger.info(
450
+ 'Locus-info:index#initialSetup --> creating HashTreeParser from get-loci-response'
451
+ );
452
+ // first create the HashTreeParser, but don't initialize it with any data yet
453
+ // pass just a fake locus that contains only the visibleDataSets
454
+ this.hashTreeParser = this.createHashTreeParser({
455
+ initialLocus: {
456
+ locus: {self: {visibleDataSets: data.locus?.self?.visibleDataSets}},
457
+ dataSets: [], // empty, because we don't have them yet
458
+ },
459
+ });
460
+
461
+ // now initialize all the data
462
+ await this.hashTreeParser.initializeFromGetLociResponse(data.locus);
463
+ } else {
464
+ // "classic" Locus case, no hash trees involved
465
+ this.updateLocusCache(data.locus);
466
+ this.onFullLocus(data.locus, undefined);
467
+ }
468
+ }
367
469
  // Change it to true after it receives it first locus object
368
470
  this.emitChange = true;
369
471
  }
@@ -375,7 +477,297 @@ export default class LocusInfo extends EventsScope {
375
477
  * @returns {void}
376
478
  */
377
479
  handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
378
- this.handleLocusDelta(responseBody.locus, meeting);
480
+ if (this.hashTreeParser) {
481
+ if (!responseBody.dataSets) {
482
+ this.sendClassicVsHashTreeMismatchMetric(
483
+ meeting,
484
+ `expected hash tree dataSets in API response but they are missing`
485
+ );
486
+ // continuing as we can still manage without responseBody.dataSets, but this is very suspicious
487
+ }
488
+ LoggerProxy.logger.info(
489
+ 'Locus-info:index#handleLocusAPIResponse --> passing Locus API response to HashTreeParser: ',
490
+ responseBody
491
+ );
492
+ // update the data in our hash trees
493
+ this.hashTreeParser.handleLocusUpdate(responseBody);
494
+ } else {
495
+ if (responseBody.dataSets) {
496
+ this.sendClassicVsHashTreeMismatchMetric(
497
+ meeting,
498
+ `unexpected hash tree dataSets in API response`
499
+ );
500
+ }
501
+ // classic Locus delta
502
+ this.handleLocusDelta(responseBody.locus, meeting);
503
+ }
504
+ }
505
+
506
+ /**
507
+ *
508
+ * @param {HashTreeObject} object data set object
509
+ * @param {any} locus
510
+ * @returns {void}
511
+ */
512
+ updateLocusFromHashTreeObject(object: HashTreeObject, locus: LocusDTO): LocusDTO {
513
+ const type = object.htMeta.elementId.type.toLowerCase();
514
+
515
+ switch (type) {
516
+ case ObjectType.locus: {
517
+ if (!object.data) {
518
+ // not doing anything here, as we need Locus to always be there (at least some fields)
519
+ // and that's already taken care of in updateFromHashTree()
520
+ LoggerProxy.logger.info(
521
+ `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object removed, version=${object.htMeta.elementId.version}`
522
+ );
523
+
524
+ return locus;
525
+ }
526
+ // replace the main locus
527
+
528
+ // The Locus object we receive from backend has empty participants array,
529
+ // and may have (although it shouldn't) other fields that are managed by other ObjectTypes
530
+ // like "fullState" or "info", so we're making sure to delete them here
531
+ const locusObjectFromData = object.data;
532
+
533
+ Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
534
+ delete locusObjectFromData[locusDtoKey];
535
+ });
536
+
537
+ locus = {...locus, ...locusObjectFromData};
538
+ LoggerProxy.logger.info(
539
+ `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object updated to version=${object.htMeta.elementId.version}`
540
+ );
541
+ break;
542
+ }
543
+ case ObjectType.mediaShare:
544
+ if (object.data) {
545
+ LoggerProxy.logger.info(
546
+ `Locus-info:index#updateLocusFromHashTreeObject --> mediaShare id=${
547
+ object.htMeta.elementId.id
548
+ } name='${object.data.name}' updated ${
549
+ object.data.name === 'content'
550
+ ? `floor=${object.data.floor?.disposition}, ${object.data.floor?.beneficiary?.id}`
551
+ : ''
552
+ } version=${object.htMeta.elementId.version}`
553
+ );
554
+ const existingMediaShare = locus.mediaShares?.find(
555
+ (ms) => ms.htMeta.elementId.id === object.htMeta.elementId.id
556
+ );
557
+
558
+ if (existingMediaShare) {
559
+ Object.assign(existingMediaShare, object.data);
560
+ } else {
561
+ locus.mediaShares = locus.mediaShares || [];
562
+ locus.mediaShares.push(object.data);
563
+ }
564
+ } else {
565
+ LoggerProxy.logger.info(
566
+ `Locus-info:index#updateLocusFromHashTreeObject --> mediaShare id=${object.htMeta.elementId.id} removed, version=${object.htMeta.elementId.version}`
567
+ );
568
+ locus.mediaShares = locus.mediaShares?.filter(
569
+ (ms) => ms.htMeta.elementId.id !== object.htMeta.elementId.id
570
+ );
571
+ }
572
+ break;
573
+ case ObjectType.participant:
574
+ LoggerProxy.logger.info(
575
+ `Locus-info:index#updateLocusFromHashTreeObject --> participant id=${
576
+ object.htMeta.elementId.id
577
+ } ${object.data ? 'updated' : 'removed'} version=${object.htMeta.elementId.version}`
578
+ );
579
+ if (object.data) {
580
+ if (!locus.participants) {
581
+ locus.participants = [];
582
+ }
583
+ const participantObject = object.data;
584
+ participantObject.htMeta = object.htMeta;
585
+ locus.participants.push(participantObject);
586
+ this.hashTreeObjectId2ParticipantId.set(object.htMeta.elementId.id, participantObject.id);
587
+ } else {
588
+ const participantId = this.hashTreeObjectId2ParticipantId.get(object.htMeta.elementId.id);
589
+
590
+ if (!locus.jsSdkMeta) {
591
+ locus.jsSdkMeta = {removedParticipantIds: []};
592
+ }
593
+ locus.jsSdkMeta.removedParticipantIds.push(participantId);
594
+ this.hashTreeObjectId2ParticipantId.delete(object.htMeta.elementId.id);
595
+ }
596
+ break;
597
+ case ObjectType.links:
598
+ case ObjectType.info:
599
+ case ObjectType.fullState:
600
+ case ObjectType.self:
601
+ if (!object.data) {
602
+ // self without data is handled inside HashTreeParser and results in LocusInfoUpdateType.MEETING_ENDED, so we should never get here
603
+ // all other types info, fullstate, etc - Locus should never send them without data
604
+ LoggerProxy.logger.warn(
605
+ `Locus-info:index#updateLocusFromHashTreeObject --> received ${type} object without data, this is not expected! version=${object.htMeta.elementId.version}`
606
+ );
607
+ } else {
608
+ LoggerProxy.logger.info(
609
+ `Locus-info:index#updateLocusFromHashTreeObject --> ${type} object updated to version ${object.htMeta.elementId.version}`
610
+ );
611
+ const locusDtoKey = ObjectTypeToLocusKeyMap[type];
612
+ locus[locusDtoKey] = object.data;
613
+ }
614
+ break;
615
+ default:
616
+ LoggerProxy.logger.warn(
617
+ `Locus-info:index#updateLocusFromHashTreeObject --> received unsupported object type ${type}`
618
+ );
619
+ break;
620
+ }
621
+
622
+ return locus;
623
+ }
624
+
625
+ /**
626
+ * Sends a metric when we receive something from Locus that uses hash trees while we
627
+ * expect classic deltas or the other way around.
628
+ * @param {Meeting} meeting
629
+ * @param {string} message
630
+ * @returns {void}
631
+ */
632
+ sendClassicVsHashTreeMismatchMetric(meeting: any, message: string) {
633
+ LoggerProxy.logger.warn(
634
+ `Locus-info:index#sendClassicVsHashTreeMismatchMetric --> classic vs hash tree mismatch! ${message}`
635
+ );
636
+
637
+ // we don't want to flood the metrics system
638
+ if (this.classicVsHashTreeMismatchMetricCounter < 5) {
639
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH, {
640
+ correlationId: meeting.correlationId,
641
+ message,
642
+ });
643
+ this.classicVsHashTreeMismatchMetricCounter += 1;
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Handles a hash tree message received from Locus.
649
+ *
650
+ * @param {Meeting} meeting - The meeting object
651
+ * @param {eventType} eventType - The event type
652
+ * @param {HashTreeMessage} message incoming hash tree message
653
+ * @returns {void}
654
+ */
655
+ private handleHashTreeMessage(meeting: any, eventType: LOCUSEVENT, message: HashTreeMessage) {
656
+ if (eventType !== LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
657
+ this.sendClassicVsHashTreeMismatchMetric(
658
+ meeting,
659
+ `got ${eventType}, expected ${LOCUSEVENT.HASH_TREE_DATA_UPDATED}`
660
+ );
661
+
662
+ return;
663
+ }
664
+
665
+ this.hashTreeParser.handleMessage(message);
666
+ }
667
+
668
+ /**
669
+ * Callback registered with HashTreeParser to receive locus info updates.
670
+ * Updates our locus info based on the data parsed by the hash tree parser.
671
+ *
672
+ * @param {LocusInfoUpdateType} updateType - The type of update received.
673
+ * @param {Object} [data] - Additional data for the update, if applicable.
674
+ * @returns {void}
675
+ */
676
+ private updateFromHashTree(
677
+ updateType: LocusInfoUpdateType,
678
+ data?: {updatedObjects: HashTreeObject[]}
679
+ ) {
680
+ switch (updateType) {
681
+ case LocusInfoUpdateType.OBJECTS_UPDATED: {
682
+ // initialize our new locus
683
+ let locus: LocusDTO = {
684
+ participants: [],
685
+ jsSdkMeta: {removedParticipantIds: []},
686
+ };
687
+
688
+ // first go over all the updates and check what happens with the main locus object
689
+ let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
690
+ LocusObjectStateAfterUpdates.unchanged;
691
+ data.updatedObjects.forEach((object) => {
692
+ if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
693
+ if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
694
+ // this code doesn't supported it right now,
695
+ // cases for "updated" followed by "removed", or multiple "updated" would need more handling
696
+ // but these should never happen
697
+ LoggerProxy.logger.warn(
698
+ `Locus-info:index#updateFromHashTree --> received multiple LOCUS objects in one update, this is unexpected!`
699
+ );
700
+ Metrics.sendBehavioralMetric(
701
+ BEHAVIORAL_METRICS.LOCUS_HASH_TREE_UNSUPPORTED_OPERATION,
702
+ {
703
+ locusUrl: object.data?.url || this.url,
704
+ message: object.data
705
+ ? 'multiple LOCUS object updates'
706
+ : 'LOCUS object update followed by removal',
707
+ }
708
+ );
709
+ }
710
+ if (object.data) {
711
+ locusObjectStateAfterUpdates = LocusObjectStateAfterUpdates.updated;
712
+ } else {
713
+ locusObjectStateAfterUpdates = LocusObjectStateAfterUpdates.removed;
714
+ }
715
+ }
716
+ });
717
+
718
+ // if Locus object is unchanged or removed, we need to keep using the existing locus
719
+ // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
720
+ // if it gets updated, we only need to have the fields that are not part of "locus" object (like "info" or "mediaShares")
721
+ // so that when Locus object gets updated, if the new one is missing some field, that field will
722
+ // be removed from our locusInfo
723
+ if (
724
+ locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.unchanged ||
725
+ locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.removed
726
+ ) {
727
+ // copy over all of existing locus except participants
728
+ LocusDtoTopLevelKeys.forEach((key) => {
729
+ if (key !== 'participants') {
730
+ locus[key] = cloneDeep(this[key]);
731
+ }
732
+ });
733
+ } else {
734
+ // initialize only the fields that are not part of main "Locus" object
735
+ // (except participants, which need to stay empty - that means "no participant changes")
736
+ Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
737
+ if (locusDtoKey !== 'participants') {
738
+ locus[locusDtoKey] = cloneDeep(this[locusDtoKey]);
739
+ }
740
+ });
741
+ }
742
+
743
+ LoggerProxy.logger.info(
744
+ `Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
745
+ data.updatedObjects.map((o) => ({
746
+ type: o.htMeta.elementId.type,
747
+ id: o.htMeta.elementId.id,
748
+ hasData: !!o.data,
749
+ }))
750
+ )}`
751
+ );
752
+ // now apply all the updates from the hash tree onto the locus
753
+ data.updatedObjects.forEach((object) => {
754
+ locus = this.updateLocusFromHashTreeObject(object, locus);
755
+ });
756
+
757
+ // update our locus info with the new locus
758
+ this.onDeltaLocus(locus);
759
+
760
+ break;
761
+ }
762
+
763
+ case LocusInfoUpdateType.MEETING_ENDED: {
764
+ LoggerProxy.logger.info(
765
+ `Locus-info:index#updateFromHashTree --> received signal that meeting ended, destroying meeting ${this.meetingId}`
766
+ );
767
+ const meeting = this.webex.meetings.meetingCollection.get(this.meetingId);
768
+ this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
769
+ }
770
+ }
379
771
  }
380
772
 
381
773
  /**
@@ -385,38 +777,52 @@ export default class LocusInfo extends EventsScope {
385
777
  * @memberof LocusInfo
386
778
  */
387
779
  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;
780
+ if (this.hashTreeParser) {
781
+ this.handleHashTreeMessage(
782
+ meeting,
783
+ data.eventType,
784
+ data.stateElementsMessage as HashTreeMessage
785
+ );
786
+ } else {
787
+ // eslint-disable-next-line @typescript-eslint/no-shadow
788
+ const {eventType} = data;
789
+ const locus = this.getTheLocusToUpdate(data.locus);
790
+ LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
791
+
792
+ locus.jsSdkMeta = {removedParticipantIds: []};
793
+
794
+ switch (eventType) {
795
+ case LOCUSEVENT.PARTICIPANT_JOIN:
796
+ case LOCUSEVENT.PARTICIPANT_LEFT:
797
+ case LOCUSEVENT.CONTROLS_UPDATED:
798
+ case LOCUSEVENT.PARTICIPANT_AUDIO_MUTED:
799
+ case LOCUSEVENT.PARTICIPANT_AUDIO_UNMUTED:
800
+ case LOCUSEVENT.PARTICIPANT_VIDEO_MUTED:
801
+ case LOCUSEVENT.PARTICIPANT_VIDEO_UNMUTED:
802
+ case LOCUSEVENT.SELF_CHANGED:
803
+ case LOCUSEVENT.PARTICIPANT_UPDATED:
804
+ case LOCUSEVENT.PARTICIPANT_CONTROLS_UPDATED:
805
+ case LOCUSEVENT.PARTICIPANT_ROLES_UPDATED:
806
+ case LOCUSEVENT.PARTICIPANT_DECLINED:
807
+ case LOCUSEVENT.FLOOR_GRANTED:
808
+ case LOCUSEVENT.FLOOR_RELEASED:
809
+ this.onFullLocus(locus, eventType);
810
+ break;
811
+ case LOCUSEVENT.DIFFERENCE:
812
+ this.handleLocusDelta(locus, meeting);
813
+ break;
814
+ case LOCUSEVENT.HASH_TREE_DATA_UPDATED:
815
+ this.sendClassicVsHashTreeMismatchMetric(
816
+ meeting,
817
+ `got ${eventType}, expected classic events`
818
+ );
819
+ break;
415
820
 
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);
821
+ default:
822
+ // Why will there be a event with no eventType ????
823
+ // we may not need this, we can get full locus
824
+ this.handleLocusDelta(locus, meeting);
825
+ }
420
826
  }
421
827
  }
422
828
 
@@ -432,19 +838,45 @@ export default class LocusInfo extends EventsScope {
432
838
  }
433
839
 
434
840
  /**
435
- * updates the locus with full locus object
841
+ * Function for handling full locus when it's using hash trees (so not the "classic" one).
842
+ *
436
843
  * @param {object} locus locus object
437
- * @param {string} eventType particulat locus event
438
- * @returns {object} null
439
- * @memberof LocusInfo
844
+ * @param {string} eventType locus event
845
+ * @param {DataSet[]} dataSets
846
+ * @returns {void}
440
847
  */
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.'
848
+ private onFullLocusWithHashTrees(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
849
+ if (!this.hashTreeParser) {
850
+ LoggerProxy.logger.info(`Locus-info:index#onFullLocus --> creating hash tree parser`);
851
+ LoggerProxy.logger.info(
852
+ 'Locus-info:index#onFullLocus --> dataSets:',
853
+ dataSets,
854
+ ' and locus:',
855
+ locus
856
+ );
857
+ this.hashTreeParser = this.createHashTreeParser({
858
+ initialLocus: {locus, dataSets},
859
+ });
860
+ this.onFullLocusCommon(locus, eventType);
861
+ } else {
862
+ // in this case the Locus we're getting is not necessarily the full one
863
+ // so treat it like if we just got it in any api response
864
+
865
+ LoggerProxy.logger.info(
866
+ 'Locus-info:index#onFullLocus --> hash tree parser already exists, handling it like a normal API response'
445
867
  );
868
+ this.handleLocusAPIResponse(undefined, {dataSets, locus});
446
869
  }
870
+ }
447
871
 
872
+ /**
873
+ * Function for handling full locus when it's the "classic" one (not hash trees)
874
+ *
875
+ * @param {object} locus locus object
876
+ * @param {string} eventType locus event
877
+ * @returns {void}
878
+ */
879
+ private onFullLocusClassic(locus: any, eventType?: string) {
448
880
  if (!this.locusParser.isNewFullLocus(locus)) {
449
881
  LoggerProxy.logger.info(
450
882
  `Locus-info:index#onFullLocus --> ignoring old full locus DTO, eventType=${eventType}`
@@ -452,9 +884,47 @@ export default class LocusInfo extends EventsScope {
452
884
 
453
885
  return;
454
886
  }
887
+ this.onFullLocusCommon(locus, eventType);
888
+ }
455
889
 
890
+ /**
891
+ * updates the locus with full locus object
892
+ * @param {object} locus locus object
893
+ * @param {string} eventType locus event
894
+ * @param {DataSet[]} dataSets
895
+ * @returns {object} null
896
+ * @memberof LocusInfo
897
+ */
898
+ onFullLocus(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
899
+ if (!locus) {
900
+ LoggerProxy.logger.error(
901
+ 'Locus-info:index#onFullLocus --> object passed as argument was invalid, continuing.'
902
+ );
903
+ }
904
+
905
+ if (dataSets) {
906
+ // this is the new hashmap Locus DTO format (only applicable to webinars for now)
907
+ this.onFullLocusWithHashTrees(locus, eventType, dataSets);
908
+ } else {
909
+ this.onFullLocusClassic(locus, eventType);
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Common part of handling full locus, used by both classic and hash tree based locus handling
915
+ * @param {object} locus locus object
916
+ * @param {string} eventType locus event
917
+ * @returns {void}
918
+ */
919
+ private onFullLocusCommon(locus: any, eventType?: string) {
456
920
  this.scheduledMeeting = locus.meeting || null;
457
921
  this.participants = locus.participants;
922
+ this.participants?.forEach((participant) => {
923
+ // participant.htMeta is set only for hash tree based locus
924
+ if (participant.htMeta?.elementId.id) {
925
+ this.hashTreeObjectId2ParticipantId.set(participant.htMeta.elementId.id, participant.id);
926
+ }
927
+ });
458
928
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
459
929
  this.updateLocusInfo(locus);
460
930
  this.updateParticipants(
@@ -561,8 +1031,7 @@ export default class LocusInfo extends EventsScope {
561
1031
  this.updateMemberShip(locus.membership);
562
1032
  this.updateIdentifiers(locus.identities);
563
1033
  this.updateEmbeddedApps(locus.embeddedApps);
564
- this.updateServices(locus.links?.services);
565
- this.updateResources(locus.links?.resources);
1034
+ this.updateLinks(locus.links);
566
1035
  this.compareAndUpdate();
567
1036
  // update which required to compare different objects from locus
568
1037
  }
@@ -1214,17 +1683,19 @@ export default class LocusInfo extends EventsScope {
1214
1683
  }
1215
1684
 
1216
1685
  /**
1217
- * @param {Object} services
1686
+ * Updates links and emits appropriate events if services or resources have changed
1687
+ * @param {Object} links
1218
1688
  * @returns {undefined}
1219
1689
  * @memberof LocusInfo
1220
1690
  */
1221
- updateServices(services: Record<'breakout' | 'record', {url: string}>) {
1222
- if (services && !isEqual(this.services, services)) {
1223
- this.services = services;
1691
+ updateLinks(links?: Links) {
1692
+ const {services, resources} = links || {};
1693
+
1694
+ if (services && !isEqual(this.links?.services, services)) {
1224
1695
  this.emitScoped(
1225
1696
  {
1226
1697
  file: 'locus-info',
1227
- function: 'updateServices',
1698
+ function: 'updateLinks',
1228
1699
  },
1229
1700
  LOCUSINFO.EVENTS.LINKS_SERVICES,
1230
1701
  {
@@ -1232,20 +1703,12 @@ export default class LocusInfo extends EventsScope {
1232
1703
  }
1233
1704
  );
1234
1705
  }
1235
- }
1236
1706
 
1237
- /**
1238
- * @param {Object} resources
1239
- * @returns {undefined}
1240
- * @memberof LocusInfo
1241
- */
1242
- updateResources(resources: Record<'webcastInstance', {url: string}>) {
1243
- if (resources && !isEqual(this.resources, resources)) {
1244
- this.resources = resources;
1707
+ if (resources && !isEqual(this.links?.resources, resources)) {
1245
1708
  this.emitScoped(
1246
1709
  {
1247
1710
  file: 'locus-info',
1248
- function: 'updateResources',
1711
+ function: 'updateLinks',
1249
1712
  },
1250
1713
  LOCUSINFO.EVENTS.LINKS_RESOURCES,
1251
1714
  {
@@ -1253,6 +1716,8 @@ export default class LocusInfo extends EventsScope {
1253
1716
  }
1254
1717
  );
1255
1718
  }
1719
+
1720
+ this.links = links;
1256
1721
  }
1257
1722
 
1258
1723
  /**