@webex/plugin-meetings 3.10.0-next.9 → 3.10.0-set-bitrate.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 +550 -130
  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 +100 -50
  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 +97 -80
  42. package/dist/types/locus-info/types.d.ts +54 -0
  43. package/dist/types/meeting/index.d.ts +23 -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 +23 -22
  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 +597 -154
  58. package/src/locus-info/types.ts +53 -0
  59. package/src/meeting/index.ts +88 -28
  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 +795 -16
  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,8 @@ import {
17
17
  MEETING_REMOVED_REASON,
18
18
  CALL_REMOVED_REASON,
19
19
  RECORDING_STATE,
20
- BREAKOUTS,
20
+ Enum,
21
+ SELF_ROLES,
21
22
  } from '../constants';
22
23
 
23
24
  import InfoUtils from './infoUtils';
@@ -30,52 +31,55 @@ import MediaSharesUtils from './mediaSharesUtils';
30
31
  import LocusDeltaParser from './parser';
31
32
  import Metrics from '../metrics';
32
33
  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;
34
+ import HashTreeParser, {
35
+ DataSet,
36
+ HashTreeMessage,
37
+ HashTreeObject,
38
+ isSelf,
39
+ LocusInfoUpdateType,
40
+ } from '../hashTree/hashTreeParser';
41
+ import {ObjectType, ObjectTypeToLocusKeyMap} from '../hashTree/types';
42
+ import {Links, LocusDTO, LocusFullState} from './types';
43
+
44
+ export type LocusLLMEvent = {
45
+ data: {
46
+ eventType: typeof LOCUSEVENT.HASH_TREE_DATA_UPDATED;
47
+ stateElementsMessage: HashTreeMessage;
53
48
  };
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
49
  };
74
50
 
51
+ // list of top level keys in Locus DTO relevant for Hash Tree DTOs processing
52
+ // it does not contain fields specific to classic Locus DTOs like sequence or baseSequence
53
+ const LocusDtoTopLevelKeys = [
54
+ 'controls',
55
+ 'fullState',
56
+ 'host',
57
+ 'info',
58
+ 'links',
59
+ 'mediaShares',
60
+ 'meetings',
61
+ 'participants',
62
+ 'replaces',
63
+ 'self',
64
+ 'sequence',
65
+ 'syncUrl',
66
+ 'url',
67
+ 'htMeta', // only exists when hash trees are used
68
+ ];
69
+
75
70
  export type LocusApiResponseBody = {
71
+ dataSets?: DataSet[];
76
72
  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
73
  };
78
74
 
75
+ const LocusObjectStateAfterUpdates = {
76
+ unchanged: 'unchanged',
77
+ removed: 'removed',
78
+ updated: 'updated',
79
+ } as const;
80
+
81
+ type LocusObjectStateAfterUpdates = Enum<typeof LocusObjectStateAfterUpdates>;
82
+
79
83
  /**
80
84
  * @description LocusInfo extends ChildEmitter to convert locusInfo info a private emitter to parent object
81
85
  * @export
@@ -93,10 +97,7 @@ export default class LocusInfo extends EventsScope {
93
97
  aclUrl: any;
94
98
  baseSequence: any;
95
99
  created: any;
96
- identities: any;
97
- membership: any;
98
100
  participants: any;
99
- participantsUrl: any;
100
101
  replaces: any;
101
102
  scheduledMeeting: any;
102
103
  sequence: any;
@@ -108,12 +109,14 @@ export default class LocusInfo extends EventsScope {
108
109
  info: any;
109
110
  roles: any;
110
111
  mediaShares: any;
111
- replace: any;
112
112
  url: any;
113
- services: any;
114
- resources: any;
113
+ links?: Links;
115
114
  mainSessionLocusCache: any;
116
115
  self: any;
116
+ hashTreeParser?: HashTreeParser;
117
+ hashTreeObjectId2ParticipantId: Map<number, string>; // mapping of hash tree object ids to participant ids
118
+ classicVsHashTreeMismatchMetricCounter = 0;
119
+
117
120
  /**
118
121
  * Constructor
119
122
  * @param {function} updateMeeting callback to update the meeting object from an object
@@ -132,10 +135,12 @@ export default class LocusInfo extends EventsScope {
132
135
  this.meetingId = meetingId;
133
136
  this.updateMeeting = updateMeeting;
134
137
  this.locusParser = new LocusDeltaParser();
138
+ this.hashTreeObjectId2ParticipantId = new Map();
135
139
  }
136
140
 
137
141
  /**
138
142
  * 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.
143
+ * WARNING: This function must not be used for hash tree based Locus meetings.
139
144
  *
140
145
  * @param {Meeting} meeting
141
146
  * @param {boolean} isLocusUrlChanged
@@ -319,13 +324,10 @@ export default class LocusInfo extends EventsScope {
319
324
  init(locus: any = {}) {
320
325
  this.created = locus.created || null;
321
326
  this.scheduledMeeting = locus.meeting || null;
322
- this.participantsUrl = locus.participantsUrl || null;
323
327
  this.replaces = locus.replaces || null;
324
328
  this.aclUrl = locus.aclUrl || null;
325
329
  this.baseSequence = locus.baseSequence || null;
326
330
  this.sequence = locus.sequence || null;
327
- this.membership = locus.membership || null;
328
- this.identities = locus.identities || null;
329
331
  this.participants = locus.participants || null;
330
332
 
331
333
  /**
@@ -351,19 +353,113 @@ export default class LocusInfo extends EventsScope {
351
353
  this.updateSelf(locus.self);
352
354
  this.updateHostInfo(locus.host);
353
355
  this.updateMediaShares(locus.mediaShares);
354
- this.updateServices(locus.links?.services);
355
- this.updateResources(locus.links?.resources);
356
+ this.updateLinks(locus.links);
356
357
  }
357
358
 
358
359
  /**
359
- * @param {Object} locus
360
+ * Creates the HashTreeParser instance.
361
+ * @param {Object} initial locus data
362
+ * @returns {void}
363
+ */
364
+ private createHashTreeParser({
365
+ initialLocus,
366
+ }: {
367
+ initialLocus: {
368
+ dataSets: Array<DataSet>;
369
+ locus: any;
370
+ };
371
+ }) {
372
+ return new HashTreeParser({
373
+ initialLocus,
374
+ webexRequest: this.webex.request.bind(this.webex),
375
+ locusInfoUpdateCallback: this.updateFromHashTree.bind(this),
376
+ debugId: `HT-${this.meetingId.substring(0, 4)}`,
377
+ });
378
+ }
379
+
380
+ /**
381
+ * @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
382
  * @returns {undefined}
361
383
  * @memberof LocusInfo
362
384
  */
363
- initialSetup(locus: object) {
364
- this.updateLocusCache(locus);
365
- this.onFullLocus(locus);
385
+ async initialSetup(
386
+ data:
387
+ | {
388
+ trigger: 'join-response';
389
+ locus: LocusDTO;
390
+ dataSets?: DataSet[];
391
+ }
392
+ | {
393
+ trigger: 'locus-message';
394
+ locus?: LocusDTO;
395
+ hashTreeMessage?: HashTreeMessage;
396
+ }
397
+ | {
398
+ trigger: 'get-loci-response';
399
+ locus?: LocusDTO;
400
+ }
401
+ ) {
402
+ switch (data.trigger) {
403
+ case 'locus-message':
404
+ if (data.hashTreeMessage) {
405
+ // we need the SELF object to be in the received message, because it contains visibleDataSets
406
+ // and these are needed to initialize all the hash trees
407
+ const selfObject = data.hashTreeMessage.locusStateElements?.find((el) => isSelf(el));
408
+
409
+ if (!selfObject?.data?.visibleDataSets) {
410
+ LoggerProxy.logger.warn(
411
+ `Locus-info:index#initialSetup --> cannot initialize HashTreeParser, SELF object with visibleDataSets is missing in the message`
412
+ );
413
+
414
+ throw new Error('SELF object with visibleDataSets is missing in the message');
415
+ }
366
416
 
417
+ LoggerProxy.logger.info(
418
+ 'Locus-info:index#initialSetup --> creating HashTreeParser from message'
419
+ );
420
+ // first create the HashTreeParser, but don't initialize it with any data yet
421
+ // pass just a fake locus that contains only the visibleDataSets
422
+ this.hashTreeParser = this.createHashTreeParser({
423
+ initialLocus: {
424
+ locus: {self: {visibleDataSets: selfObject.data.visibleDataSets}},
425
+ dataSets: [], // empty, because they will be populated in initializeFromMessage() call // dataSets: data.hashTreeMessage.dataSets,
426
+ },
427
+ });
428
+
429
+ // now handle the message - that should populate all the visible datasets
430
+ await this.hashTreeParser.initializeFromMessage(data.hashTreeMessage);
431
+ } else {
432
+ // "classic" Locus case, no hash trees involved
433
+ this.updateLocusCache(data.locus);
434
+ this.onFullLocus(data.locus, undefined);
435
+ }
436
+ break;
437
+ case 'join-response':
438
+ this.updateLocusCache(data.locus);
439
+ this.onFullLocus(data.locus, undefined, data.dataSets);
440
+ break;
441
+ case 'get-loci-response':
442
+ if (data.locus?.links?.resources?.visibleDataSets?.url) {
443
+ LoggerProxy.logger.info(
444
+ 'Locus-info:index#initialSetup --> creating HashTreeParser from get-loci-response'
445
+ );
446
+ // first create the HashTreeParser, but don't initialize it with any data yet
447
+ // pass just a fake locus that contains only the visibleDataSets
448
+ this.hashTreeParser = this.createHashTreeParser({
449
+ initialLocus: {
450
+ locus: {self: {visibleDataSets: data.locus?.self?.visibleDataSets}},
451
+ dataSets: [], // empty, because we don't have them yet
452
+ },
453
+ });
454
+
455
+ // now initialize all the data
456
+ await this.hashTreeParser.initializeFromGetLociResponse(data.locus);
457
+ } else {
458
+ // "classic" Locus case, no hash trees involved
459
+ this.updateLocusCache(data.locus);
460
+ this.onFullLocus(data.locus, undefined);
461
+ }
462
+ }
367
463
  // Change it to true after it receives it first locus object
368
464
  this.emitChange = true;
369
465
  }
@@ -375,7 +471,317 @@ export default class LocusInfo extends EventsScope {
375
471
  * @returns {void}
376
472
  */
377
473
  handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
378
- this.handleLocusDelta(responseBody.locus, meeting);
474
+ if (this.hashTreeParser) {
475
+ if (!responseBody.dataSets) {
476
+ this.sendClassicVsHashTreeMismatchMetric(
477
+ meeting,
478
+ `expected hash tree dataSets in API response but they are missing`
479
+ );
480
+ // continuing as we can still manage without responseBody.dataSets, but this is very suspicious
481
+ }
482
+ LoggerProxy.logger.info(
483
+ 'Locus-info:index#handleLocusAPIResponse --> passing Locus API response to HashTreeParser: ',
484
+ responseBody
485
+ );
486
+ // update the data in our hash trees
487
+ this.hashTreeParser.handleLocusUpdate(responseBody);
488
+ } else {
489
+ if (responseBody.dataSets) {
490
+ this.sendClassicVsHashTreeMismatchMetric(
491
+ meeting,
492
+ `unexpected hash tree dataSets in API response`
493
+ );
494
+ }
495
+ // classic Locus delta
496
+ this.handleLocusDelta(responseBody.locus, meeting);
497
+ }
498
+ }
499
+
500
+ /**
501
+ *
502
+ * @param {HashTreeObject} object data set object
503
+ * @param {any} locus
504
+ * @returns {void}
505
+ */
506
+ updateLocusFromHashTreeObject(object: HashTreeObject, locus: LocusDTO): LocusDTO {
507
+ const type = object.htMeta.elementId.type.toLowerCase();
508
+
509
+ const addParticipantObject = (obj: HashTreeObject) => {
510
+ if (!locus.participants) {
511
+ locus.participants = [];
512
+ }
513
+ locus.participants.push(obj.data);
514
+ this.hashTreeObjectId2ParticipantId.set(obj.htMeta.elementId.id, obj.data.id);
515
+ };
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
+ addParticipantObject(object);
583
+ } else {
584
+ const participantId = this.hashTreeObjectId2ParticipantId.get(object.htMeta.elementId.id);
585
+
586
+ if (!locus.jsSdkMeta) {
587
+ locus.jsSdkMeta = {removedParticipantIds: []};
588
+ }
589
+ locus.jsSdkMeta.removedParticipantIds.push(participantId);
590
+ this.hashTreeObjectId2ParticipantId.delete(object.htMeta.elementId.id);
591
+ }
592
+ break;
593
+ case ObjectType.links:
594
+ case ObjectType.info:
595
+ case ObjectType.fullState:
596
+ case ObjectType.self:
597
+ if (!object.data) {
598
+ // self without data is handled inside HashTreeParser and results in LocusInfoUpdateType.MEETING_ENDED, so we should never get here
599
+ // all other types info, fullstate, etc - Locus should never send them without data
600
+ LoggerProxy.logger.warn(
601
+ `Locus-info:index#updateLocusFromHashTreeObject --> received ${type} object without data, this is not expected! version=${object.htMeta.elementId.version}`
602
+ );
603
+ } else {
604
+ LoggerProxy.logger.info(
605
+ `Locus-info:index#updateLocusFromHashTreeObject --> ${type} object updated to version ${object.htMeta.elementId.version}`
606
+ );
607
+ const locusDtoKey = ObjectTypeToLocusKeyMap[type];
608
+ locus[locusDtoKey] = object.data;
609
+
610
+ /* Hash tree based webinar attendees don't receive a Participant object for themselves from Locus,
611
+ but a lot of existing code in SDK and web app expects a member object for self to exist,
612
+ so whenever SELF changes for a webinar attendee, we copy it into a participant object.
613
+ We can do it, because SELF has always all the same properties as a participant object.
614
+ */
615
+ if (
616
+ type === ObjectType.self &&
617
+ locus.info?.isWebinar &&
618
+ object.data.controls?.role?.roles?.find(
619
+ (r) => r.type === SELF_ROLES.ATTENDEE && r.hasRole
620
+ )
621
+ ) {
622
+ LoggerProxy.logger.info(
623
+ `Locus-info:index#updateLocusFromHashTreeObject --> webinar attendee: creating participant object from self`
624
+ );
625
+ addParticipantObject(object);
626
+ }
627
+ }
628
+ break;
629
+ default:
630
+ LoggerProxy.logger.warn(
631
+ `Locus-info:index#updateLocusFromHashTreeObject --> received unsupported object type ${type}`
632
+ );
633
+ break;
634
+ }
635
+
636
+ return locus;
637
+ }
638
+
639
+ /**
640
+ * Sends a metric when we receive something from Locus that uses hash trees while we
641
+ * expect classic deltas or the other way around.
642
+ * @param {Meeting} meeting
643
+ * @param {string} message
644
+ * @returns {void}
645
+ */
646
+ sendClassicVsHashTreeMismatchMetric(meeting: any, message: string) {
647
+ LoggerProxy.logger.warn(
648
+ `Locus-info:index#sendClassicVsHashTreeMismatchMetric --> classic vs hash tree mismatch! ${message}`
649
+ );
650
+
651
+ // we don't want to flood the metrics system
652
+ if (this.classicVsHashTreeMismatchMetricCounter < 5) {
653
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH, {
654
+ correlationId: meeting.correlationId,
655
+ message,
656
+ });
657
+ this.classicVsHashTreeMismatchMetricCounter += 1;
658
+ }
659
+ }
660
+
661
+ /**
662
+ * Handles a hash tree message received from Locus.
663
+ *
664
+ * @param {Meeting} meeting - The meeting object
665
+ * @param {eventType} eventType - The event type
666
+ * @param {HashTreeMessage} message incoming hash tree message
667
+ * @returns {void}
668
+ */
669
+ private handleHashTreeMessage(meeting: any, eventType: LOCUSEVENT, message: HashTreeMessage) {
670
+ if (eventType !== LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
671
+ this.sendClassicVsHashTreeMismatchMetric(
672
+ meeting,
673
+ `got ${eventType}, expected ${LOCUSEVENT.HASH_TREE_DATA_UPDATED}`
674
+ );
675
+
676
+ return;
677
+ }
678
+
679
+ this.hashTreeParser.handleMessage(message);
680
+ }
681
+
682
+ /**
683
+ * Callback registered with HashTreeParser to receive locus info updates.
684
+ * Updates our locus info based on the data parsed by the hash tree parser.
685
+ *
686
+ * @param {LocusInfoUpdateType} updateType - The type of update received.
687
+ * @param {Object} [data] - Additional data for the update, if applicable.
688
+ * @returns {void}
689
+ */
690
+ private updateFromHashTree(
691
+ updateType: LocusInfoUpdateType,
692
+ data?: {updatedObjects: HashTreeObject[]}
693
+ ) {
694
+ switch (updateType) {
695
+ case LocusInfoUpdateType.OBJECTS_UPDATED: {
696
+ // initialize our new locus
697
+ let locus: LocusDTO = {
698
+ participants: [],
699
+ jsSdkMeta: {removedParticipantIds: []},
700
+ };
701
+
702
+ // first go over all the updates and check what happens with the main locus object
703
+ let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
704
+ LocusObjectStateAfterUpdates.unchanged;
705
+ data.updatedObjects.forEach((object) => {
706
+ if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
707
+ if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
708
+ // this code doesn't supported it right now,
709
+ // cases for "updated" followed by "removed", or multiple "updated" would need more handling
710
+ // but these should never happen
711
+ LoggerProxy.logger.warn(
712
+ `Locus-info:index#updateFromHashTree --> received multiple LOCUS objects in one update, this is unexpected!`
713
+ );
714
+ Metrics.sendBehavioralMetric(
715
+ BEHAVIORAL_METRICS.LOCUS_HASH_TREE_UNSUPPORTED_OPERATION,
716
+ {
717
+ locusUrl: object.data?.url || this.url,
718
+ message: object.data
719
+ ? 'multiple LOCUS object updates'
720
+ : 'LOCUS object update followed by removal',
721
+ }
722
+ );
723
+ }
724
+ if (object.data) {
725
+ locusObjectStateAfterUpdates = LocusObjectStateAfterUpdates.updated;
726
+ } else {
727
+ locusObjectStateAfterUpdates = LocusObjectStateAfterUpdates.removed;
728
+ }
729
+ }
730
+ });
731
+
732
+ // if Locus object is unchanged or removed, we need to keep using the existing locus
733
+ // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
734
+ // if it gets updated, we only need to have the fields that are not part of "locus" object (like "info" or "mediaShares")
735
+ // so that when Locus object gets updated, if the new one is missing some field, that field will
736
+ // be removed from our locusInfo
737
+ if (
738
+ locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.unchanged ||
739
+ locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.removed
740
+ ) {
741
+ // copy over all of existing locus except participants
742
+ LocusDtoTopLevelKeys.forEach((key) => {
743
+ if (key !== 'participants') {
744
+ locus[key] = cloneDeep(this[key]);
745
+ }
746
+ });
747
+ } else {
748
+ // initialize only the fields that are not part of main "Locus" object
749
+ // (except participants, which need to stay empty - that means "no participant changes")
750
+ Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
751
+ if (locusDtoKey !== 'participants') {
752
+ locus[locusDtoKey] = cloneDeep(this[locusDtoKey]);
753
+ }
754
+ });
755
+ }
756
+
757
+ LoggerProxy.logger.info(
758
+ `Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
759
+ data.updatedObjects.map((o) => ({
760
+ type: o.htMeta.elementId.type,
761
+ id: o.htMeta.elementId.id,
762
+ hasData: !!o.data,
763
+ }))
764
+ )}`
765
+ );
766
+ // now apply all the updates from the hash tree onto the locus
767
+ data.updatedObjects.forEach((object) => {
768
+ locus = this.updateLocusFromHashTreeObject(object, locus);
769
+ });
770
+
771
+ // update our locus info with the new locus
772
+ this.onDeltaLocus(locus);
773
+
774
+ break;
775
+ }
776
+
777
+ case LocusInfoUpdateType.MEETING_ENDED: {
778
+ LoggerProxy.logger.info(
779
+ `Locus-info:index#updateFromHashTree --> received signal that meeting ended, destroying meeting ${this.meetingId}`
780
+ );
781
+ const meeting = this.webex.meetings.meetingCollection.get(this.meetingId);
782
+ this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
783
+ }
784
+ }
379
785
  }
380
786
 
381
787
  /**
@@ -385,38 +791,52 @@ export default class LocusInfo extends EventsScope {
385
791
  * @memberof LocusInfo
386
792
  */
387
793
  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;
794
+ if (this.hashTreeParser) {
795
+ this.handleHashTreeMessage(
796
+ meeting,
797
+ data.eventType,
798
+ data.stateElementsMessage as HashTreeMessage
799
+ );
800
+ } else {
801
+ // eslint-disable-next-line @typescript-eslint/no-shadow
802
+ const {eventType} = data;
803
+ const locus = this.getTheLocusToUpdate(data.locus);
804
+ LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
805
+
806
+ locus.jsSdkMeta = {removedParticipantIds: []};
807
+
808
+ switch (eventType) {
809
+ case LOCUSEVENT.PARTICIPANT_JOIN:
810
+ case LOCUSEVENT.PARTICIPANT_LEFT:
811
+ case LOCUSEVENT.CONTROLS_UPDATED:
812
+ case LOCUSEVENT.PARTICIPANT_AUDIO_MUTED:
813
+ case LOCUSEVENT.PARTICIPANT_AUDIO_UNMUTED:
814
+ case LOCUSEVENT.PARTICIPANT_VIDEO_MUTED:
815
+ case LOCUSEVENT.PARTICIPANT_VIDEO_UNMUTED:
816
+ case LOCUSEVENT.SELF_CHANGED:
817
+ case LOCUSEVENT.PARTICIPANT_UPDATED:
818
+ case LOCUSEVENT.PARTICIPANT_CONTROLS_UPDATED:
819
+ case LOCUSEVENT.PARTICIPANT_ROLES_UPDATED:
820
+ case LOCUSEVENT.PARTICIPANT_DECLINED:
821
+ case LOCUSEVENT.FLOOR_GRANTED:
822
+ case LOCUSEVENT.FLOOR_RELEASED:
823
+ this.onFullLocus(locus, eventType);
824
+ break;
825
+ case LOCUSEVENT.DIFFERENCE:
826
+ this.handleLocusDelta(locus, meeting);
827
+ break;
828
+ case LOCUSEVENT.HASH_TREE_DATA_UPDATED:
829
+ this.sendClassicVsHashTreeMismatchMetric(
830
+ meeting,
831
+ `got ${eventType}, expected classic events`
832
+ );
833
+ break;
415
834
 
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);
835
+ default:
836
+ // Why will there be a event with no eventType ????
837
+ // we may not need this, we can get full locus
838
+ this.handleLocusDelta(locus, meeting);
839
+ }
420
840
  }
421
841
  }
422
842
 
@@ -432,19 +852,45 @@ export default class LocusInfo extends EventsScope {
432
852
  }
433
853
 
434
854
  /**
435
- * updates the locus with full locus object
855
+ * Function for handling full locus when it's using hash trees (so not the "classic" one).
856
+ *
436
857
  * @param {object} locus locus object
437
- * @param {string} eventType particulat locus event
438
- * @returns {object} null
439
- * @memberof LocusInfo
858
+ * @param {string} eventType locus event
859
+ * @param {DataSet[]} dataSets
860
+ * @returns {void}
440
861
  */
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.'
862
+ private onFullLocusWithHashTrees(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
863
+ if (!this.hashTreeParser) {
864
+ LoggerProxy.logger.info(`Locus-info:index#onFullLocus --> creating hash tree parser`);
865
+ LoggerProxy.logger.info(
866
+ 'Locus-info:index#onFullLocus --> dataSets:',
867
+ dataSets,
868
+ ' and locus:',
869
+ locus
445
870
  );
871
+ this.hashTreeParser = this.createHashTreeParser({
872
+ initialLocus: {locus, dataSets},
873
+ });
874
+ this.onFullLocusCommon(locus, eventType);
875
+ } else {
876
+ // in this case the Locus we're getting is not necessarily the full one
877
+ // so treat it like if we just got it in any api response
878
+
879
+ LoggerProxy.logger.info(
880
+ 'Locus-info:index#onFullLocus --> hash tree parser already exists, handling it like a normal API response'
881
+ );
882
+ this.handleLocusAPIResponse(undefined, {dataSets, locus});
446
883
  }
884
+ }
447
885
 
886
+ /**
887
+ * Function for handling full locus when it's the "classic" one (not hash trees)
888
+ *
889
+ * @param {object} locus locus object
890
+ * @param {string} eventType locus event
891
+ * @returns {void}
892
+ */
893
+ private onFullLocusClassic(locus: any, eventType?: string) {
448
894
  if (!this.locusParser.isNewFullLocus(locus)) {
449
895
  LoggerProxy.logger.info(
450
896
  `Locus-info:index#onFullLocus --> ignoring old full locus DTO, eventType=${eventType}`
@@ -452,9 +898,47 @@ export default class LocusInfo extends EventsScope {
452
898
 
453
899
  return;
454
900
  }
901
+ this.onFullLocusCommon(locus, eventType);
902
+ }
455
903
 
904
+ /**
905
+ * updates the locus with full locus object
906
+ * @param {object} locus locus object
907
+ * @param {string} eventType locus event
908
+ * @param {DataSet[]} dataSets
909
+ * @returns {object} null
910
+ * @memberof LocusInfo
911
+ */
912
+ onFullLocus(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
913
+ if (!locus) {
914
+ LoggerProxy.logger.error(
915
+ 'Locus-info:index#onFullLocus --> object passed as argument was invalid, continuing.'
916
+ );
917
+ }
918
+
919
+ if (dataSets) {
920
+ // this is the new hashmap Locus DTO format (only applicable to webinars for now)
921
+ this.onFullLocusWithHashTrees(locus, eventType, dataSets);
922
+ } else {
923
+ this.onFullLocusClassic(locus, eventType);
924
+ }
925
+ }
926
+
927
+ /**
928
+ * Common part of handling full locus, used by both classic and hash tree based locus handling
929
+ * @param {object} locus locus object
930
+ * @param {string} eventType locus event
931
+ * @returns {void}
932
+ */
933
+ private onFullLocusCommon(locus: any, eventType?: string) {
456
934
  this.scheduledMeeting = locus.meeting || null;
457
935
  this.participants = locus.participants;
936
+ this.participants?.forEach((participant) => {
937
+ // participant.htMeta is set only for hash tree based locus
938
+ if (participant.htMeta?.elementId.id) {
939
+ this.hashTreeObjectId2ParticipantId.set(participant.htMeta.elementId.id, participant.id);
940
+ }
941
+ });
458
942
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
459
943
  this.updateLocusInfo(locus);
460
944
  this.updateParticipants(
@@ -552,17 +1036,13 @@ export default class LocusInfo extends EventsScope {
552
1036
  this.updateLocusUrl(locus.url, ControlsUtils.isMainSessionDTO(locus));
553
1037
  this.updateMeetingInfo(locus.info, locus.self);
554
1038
  this.updateMediaShares(locus.mediaShares);
555
- this.updateParticipantsUrl(locus.participantsUrl);
556
- this.updateReplace(locus.replace);
1039
+ this.updateReplaces(locus.replaces);
557
1040
  this.updateSelf(locus.self);
558
1041
  this.updateAclUrl(locus.aclUrl);
559
1042
  this.updateBasequence(locus.baseSequence);
560
1043
  this.updateSequence(locus.sequence);
561
- this.updateMemberShip(locus.membership);
562
- this.updateIdentifiers(locus.identities);
563
1044
  this.updateEmbeddedApps(locus.embeddedApps);
564
- this.updateServices(locus.links?.services);
565
- this.updateResources(locus.links?.resources);
1045
+ this.updateLinks(locus.links);
566
1046
  this.compareAndUpdate();
567
1047
  // update which required to compare different objects from locus
568
1048
  }
@@ -1214,17 +1694,19 @@ export default class LocusInfo extends EventsScope {
1214
1694
  }
1215
1695
 
1216
1696
  /**
1217
- * @param {Object} services
1697
+ * Updates links and emits appropriate events if services or resources have changed
1698
+ * @param {Object} links
1218
1699
  * @returns {undefined}
1219
1700
  * @memberof LocusInfo
1220
1701
  */
1221
- updateServices(services: Record<'breakout' | 'record', {url: string}>) {
1222
- if (services && !isEqual(this.services, services)) {
1223
- this.services = services;
1702
+ updateLinks(links?: Links) {
1703
+ const {services, resources} = links || {};
1704
+
1705
+ if (services && !isEqual(this.links?.services, services)) {
1224
1706
  this.emitScoped(
1225
1707
  {
1226
1708
  file: 'locus-info',
1227
- function: 'updateServices',
1709
+ function: 'updateLinks',
1228
1710
  },
1229
1711
  LOCUSINFO.EVENTS.LINKS_SERVICES,
1230
1712
  {
@@ -1232,20 +1714,12 @@ export default class LocusInfo extends EventsScope {
1232
1714
  }
1233
1715
  );
1234
1716
  }
1235
- }
1236
1717
 
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;
1718
+ if (resources && !isEqual(this.links?.resources, resources)) {
1245
1719
  this.emitScoped(
1246
1720
  {
1247
1721
  file: 'locus-info',
1248
- function: 'updateResources',
1722
+ function: 'updateLinks',
1249
1723
  },
1250
1724
  LOCUSINFO.EVENTS.LINKS_RESOURCES,
1251
1725
  {
@@ -1253,6 +1727,8 @@ export default class LocusInfo extends EventsScope {
1253
1727
  }
1254
1728
  );
1255
1729
  }
1730
+
1731
+ this.links = links;
1256
1732
  }
1257
1733
 
1258
1734
  /**
@@ -1439,24 +1915,13 @@ export default class LocusInfo extends EventsScope {
1439
1915
  }
1440
1916
 
1441
1917
  /**
1442
- * @param {String} participantsUrl
1443
- * @returns {undefined}
1444
- * @memberof LocusInfo
1445
- */
1446
- updateParticipantsUrl(participantsUrl: string) {
1447
- if (participantsUrl && !isEqual(this.participantsUrl, participantsUrl)) {
1448
- this.participantsUrl = participantsUrl;
1449
- }
1450
- }
1451
-
1452
- /**
1453
- * @param {Object} replace
1918
+ * @param {Object} replaces
1454
1919
  * @returns {undefined}
1455
1920
  * @memberof LocusInfo
1456
1921
  */
1457
- updateReplace(replace: object) {
1458
- if (replace && !isEqual(this.replace, replace)) {
1459
- this.replace = replace;
1922
+ updateReplaces(replaces: object) {
1923
+ if (replaces && !isEqual(this.replaces, replaces)) {
1924
+ this.replaces = replaces;
1460
1925
  }
1461
1926
  }
1462
1927
 
@@ -1793,28 +2258,6 @@ export default class LocusInfo extends EventsScope {
1793
2258
  }
1794
2259
  }
1795
2260
 
1796
- /**
1797
- * @param {Object} membership
1798
- * @returns {undefined}
1799
- * @memberof LocusInfo
1800
- */
1801
- updateMemberShip(membership: object) {
1802
- if (membership && !isEqual(this.membership, membership)) {
1803
- this.membership = membership;
1804
- }
1805
- }
1806
-
1807
- /**
1808
- * @param {Array} identities
1809
- * @returns {undefined}
1810
- * @memberof LocusInfo
1811
- */
1812
- updateIdentifiers(identities: Array<any>) {
1813
- if (identities && !isEqual(this.identities, identities)) {
1814
- this.identities = identities;
1815
- }
1816
- }
1817
-
1818
2261
  /**
1819
2262
  * check the locus is main session's one or not, if is main session's, update main session cache
1820
2263
  * @param {Object} locus