@webex/plugin-meetings 3.10.0-next.14 → 3.10.0-next.16

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 (37) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/hashTree/constants.js +20 -0
  4. package/dist/hashTree/constants.js.map +1 -0
  5. package/dist/hashTree/hashTree.js +515 -0
  6. package/dist/hashTree/hashTree.js.map +1 -0
  7. package/dist/hashTree/hashTreeParser.js +1115 -14
  8. package/dist/hashTree/hashTreeParser.js.map +1 -1
  9. package/dist/hashTree/types.js +10 -3
  10. package/dist/hashTree/types.js.map +1 -1
  11. package/dist/hashTree/utils.js +48 -0
  12. package/dist/hashTree/utils.js.map +1 -0
  13. package/dist/interpretation/index.js +1 -1
  14. package/dist/interpretation/siLanguage.js +1 -1
  15. package/dist/locus-info/index.js +59 -49
  16. package/dist/locus-info/index.js.map +1 -1
  17. package/dist/locus-info/types.js.map +1 -1
  18. package/dist/types/hashTree/constants.d.ts +8 -0
  19. package/dist/types/hashTree/hashTree.d.ts +129 -0
  20. package/dist/types/hashTree/hashTreeParser.d.ts +151 -0
  21. package/dist/types/hashTree/types.d.ts +11 -0
  22. package/dist/types/hashTree/utils.d.ts +9 -0
  23. package/dist/types/locus-info/index.d.ts +5 -15
  24. package/dist/types/locus-info/types.d.ts +21 -12
  25. package/dist/webinar/index.js +1 -1
  26. package/package.json +22 -21
  27. package/src/hashTree/constants.ts +9 -0
  28. package/src/hashTree/hashTree.ts +463 -0
  29. package/src/hashTree/hashTreeParser.ts +1022 -7
  30. package/src/hashTree/types.ts +13 -1
  31. package/src/hashTree/utils.ts +42 -0
  32. package/src/locus-info/index.ts +64 -48
  33. package/src/locus-info/types.ts +19 -12
  34. package/test/unit/spec/hashTree/hashTree.ts +655 -0
  35. package/test/unit/spec/hashTree/hashTreeParser.ts +1532 -0
  36. package/test/unit/spec/hashTree/utils.ts +103 -0
  37. package/test/unit/spec/locus-info/index.js +150 -7
@@ -1,15 +1,27 @@
1
1
  import {Enum} from '../constants';
2
2
 
3
- // todo: Locus docs have now more types like CONTROL_ENTRY, EMBEDDED_APP, FULL_STATE, INFO, MEDIA_SHARE - need to add support for them once Locus implements them
3
+ // todo: Locus docs have now more types like CONTROL_ENTRY, EMBEDDED_APP - need to add support for them once Locus implements them
4
4
  export const ObjectType = {
5
5
  participant: 'participant',
6
6
  self: 'self',
7
7
  locus: 'locus',
8
8
  mediaShare: 'mediashare',
9
+ info: 'info',
10
+ fullState: 'fullstate',
11
+ links: 'links',
9
12
  } as const;
10
13
 
11
14
  export type ObjectType = Enum<typeof ObjectType>;
12
15
 
16
+ // mapping from ObjectType to top level LocusDTO keys
17
+ export const ObjectTypeToLocusKeyMap = {
18
+ [ObjectType.links]: 'links',
19
+ [ObjectType.info]: 'info',
20
+ [ObjectType.fullState]: 'fullState',
21
+ [ObjectType.self]: 'self',
22
+ [ObjectType.participant]: 'participants', // note: each object is a single participant in participants array
23
+ [ObjectType.mediaShare]: 'mediaShares', // note: each object is a single mediaShare in mediaShares array
24
+ };
13
25
  export interface HtMeta {
14
26
  elementId: {
15
27
  type: ObjectType;
@@ -0,0 +1,42 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+
3
+ /**
4
+ * Analyzes given part of Locus DTO recursively and delete any nested objects that have their own htMeta
5
+ *
6
+ * @param {Object} currentLocusPart part of locus DTO to analyze
7
+ * @param {Object} parent parent object
8
+ * @param {string|number} currentKey key of the parent object that currentLocusPart is
9
+ * @returns {void}
10
+ */
11
+ export const deleteNestedObjectsWithHtMeta = (
12
+ currentLocusPart: any,
13
+ parent?: any,
14
+ currentKey?: string | number
15
+ ) => {
16
+ if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
17
+ return;
18
+ }
19
+
20
+ if (parent && currentKey !== undefined && currentLocusPart.htMeta) {
21
+ if (Array.isArray(parent)) {
22
+ parent.splice(Number(currentKey), 1);
23
+ } else {
24
+ delete parent[currentKey];
25
+ }
26
+
27
+ return;
28
+ }
29
+
30
+ if (Array.isArray(currentLocusPart)) {
31
+ // iterate array in reverse, so that indexes remain valid when deleting elements
32
+ for (let i = currentLocusPart.length - 1; i >= 0; i -= 1) {
33
+ deleteNestedObjectsWithHtMeta(currentLocusPart[i], currentLocusPart, i);
34
+ }
35
+ } else {
36
+ for (const key of Object.keys(currentLocusPart)) {
37
+ if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
38
+ deleteNestedObjectsWithHtMeta(currentLocusPart[key], currentLocusPart, key);
39
+ }
40
+ }
41
+ }
42
+ };
@@ -37,8 +37,8 @@ import HashTreeParser, {
37
37
  isSelf,
38
38
  LocusInfoUpdateType,
39
39
  } from '../hashTree/hashTreeParser';
40
- import {ObjectType} from '../hashTree/types';
41
- import {LocusDTO} from './types';
40
+ import {ObjectType, ObjectTypeToLocusKeyMap} from '../hashTree/types';
41
+ import {Links, LocusDTO, LocusFullState} from './types';
42
42
 
43
43
  export type LocusLLMEvent = {
44
44
  data: {
@@ -47,6 +47,8 @@ export type LocusLLMEvent = {
47
47
  };
48
48
  };
49
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
50
52
  const LocusDtoTopLevelKeys = [
51
53
  'controls',
52
54
  'fullState',
@@ -111,8 +113,7 @@ export default class LocusInfo extends EventsScope {
111
113
  mediaShares: any;
112
114
  replace: any;
113
115
  url: any;
114
- services: any;
115
- resources: any;
116
+ links?: Links;
116
117
  mainSessionLocusCache: any;
117
118
  self: any;
118
119
  hashTreeParser?: HashTreeParser;
@@ -358,8 +359,7 @@ export default class LocusInfo extends EventsScope {
358
359
  this.updateSelf(locus.self);
359
360
  this.updateHostInfo(locus.host);
360
361
  this.updateMediaShares(locus.mediaShares);
361
- this.updateServices(locus.links?.services);
362
- this.updateResources(locus.links?.resources);
362
+ this.updateLinks(locus.links);
363
363
  }
364
364
 
365
365
  /**
@@ -478,12 +478,19 @@ export default class LocusInfo extends EventsScope {
478
478
  */
479
479
  handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
480
480
  if (this.hashTreeParser) {
481
- // API responses with hash tree are a bit problematic and not fully confirmed how they will look like
482
- // we don't really need them, because all updates are guaranteed to come via Mercury or LLM messages anyway
483
- // so it's OK to skip them for now
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
+ }
484
488
  LoggerProxy.logger.info(
485
- 'Locus-info:index#handleLocusAPIResponse: skipping handling of API http response with hashTreeParser'
489
+ 'Locus-info:index#handleLocusAPIResponse --> passing Locus API response to HashTreeParser: ',
490
+ responseBody
486
491
  );
492
+ // update the data in our hash trees
493
+ this.hashTreeParser.handleLocusUpdate(responseBody);
487
494
  } else {
488
495
  if (responseBody.dataSets) {
489
496
  this.sendClassicVsHashTreeMismatchMetric(
@@ -511,23 +518,25 @@ export default class LocusInfo extends EventsScope {
511
518
  // not doing anything here, as we need Locus to always be there (at least some fields)
512
519
  // and that's already taken care of in updateFromHashTree()
513
520
  LoggerProxy.logger.info(
514
- `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object removed`
521
+ `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object removed, version=${object.htMeta.elementId.version}`
515
522
  );
516
523
 
517
524
  return locus;
518
525
  }
519
526
  // replace the main locus
520
527
 
521
- // The Locus object we receive from backend has empty participants, so removing them to avoid it overriding the ones in our current locus object
522
- // Also, other fields like mediaShares are managed by other ObjectType updates, so removing them too
523
- // BTW, it also doesn't have "self". That's OK as it won't override existing locus.self and also existing SDK code can handle that missing self in Locus updates
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
524
531
  const locusObjectFromData = object.data;
525
- delete locusObjectFromData.participants;
526
- delete locusObjectFromData.mediaShares;
532
+
533
+ Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
534
+ delete locusObjectFromData[locusDtoKey];
535
+ });
527
536
 
528
537
  locus = {...locus, ...locusObjectFromData};
529
538
  LoggerProxy.logger.info(
530
- `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object updated`
539
+ `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object updated to version=${object.htMeta.elementId.version}`
531
540
  );
532
541
  break;
533
542
  }
@@ -540,7 +549,7 @@ export default class LocusInfo extends EventsScope {
540
549
  object.data.name === 'content'
541
550
  ? `floor=${object.data.floor?.disposition}, ${object.data.floor?.beneficiary?.id}`
542
551
  : ''
543
- }`
552
+ } version=${object.htMeta.elementId.version}`
544
553
  );
545
554
  const existingMediaShare = locus.mediaShares?.find(
546
555
  (ms) => ms.htMeta.elementId.id === object.htMeta.elementId.id
@@ -554,7 +563,7 @@ export default class LocusInfo extends EventsScope {
554
563
  }
555
564
  } else {
556
565
  LoggerProxy.logger.info(
557
- `Locus-info:index#updateLocusFromHashTreeObject --> mediaShare id=${object.htMeta.elementId.id} removed`
566
+ `Locus-info:index#updateLocusFromHashTreeObject --> mediaShare id=${object.htMeta.elementId.id} removed, version=${object.htMeta.elementId.version}`
558
567
  );
559
568
  locus.mediaShares = locus.mediaShares?.filter(
560
569
  (ms) => ms.htMeta.elementId.id !== object.htMeta.elementId.id
@@ -565,7 +574,7 @@ export default class LocusInfo extends EventsScope {
565
574
  LoggerProxy.logger.info(
566
575
  `Locus-info:index#updateLocusFromHashTreeObject --> participant id=${
567
576
  object.htMeta.elementId.id
568
- } ${object.data ? 'updated' : 'removed'}`
577
+ } ${object.data ? 'updated' : 'removed'} version=${object.htMeta.elementId.version}`
569
578
  );
570
579
  if (object.data) {
571
580
  if (!locus.participants) {
@@ -585,19 +594,23 @@ export default class LocusInfo extends EventsScope {
585
594
  this.hashTreeObjectId2ParticipantId.delete(object.htMeta.elementId.id);
586
595
  }
587
596
  break;
597
+ case ObjectType.links:
598
+ case ObjectType.info:
599
+ case ObjectType.fullState:
588
600
  case ObjectType.self:
589
601
  if (!object.data) {
590
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
591
604
  LoggerProxy.logger.warn(
592
- `Locus-info:index#updateLocusFromHashTreeObject --> received SELF object without data, this is not expected!`
605
+ `Locus-info:index#updateLocusFromHashTreeObject --> received ${type} object without data, this is not expected! version=${object.htMeta.elementId.version}`
593
606
  );
594
-
595
- return locus;
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;
596
613
  }
597
- LoggerProxy.logger.info(
598
- `Locus-info:index#updateLocusFromHashTreeObject --> SELF object updated`
599
- );
600
- locus.self = object.data;
601
614
  break;
602
615
  default:
603
616
  LoggerProxy.logger.warn(
@@ -704,19 +717,27 @@ export default class LocusInfo extends EventsScope {
704
717
 
705
718
  // if Locus object is unchanged or removed, we need to keep using the existing locus
706
719
  // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
707
- // if it gets updated, we don't need to do anything and we start with an empty one
708
- // so that when it gets updated, if the new one is missing some field, that field will
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
709
722
  // be removed from our locusInfo
710
723
  if (
711
724
  locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.unchanged ||
712
725
  locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.removed
713
726
  ) {
714
- // copy over existing locus
727
+ // copy over all of existing locus except participants
715
728
  LocusDtoTopLevelKeys.forEach((key) => {
716
729
  if (key !== 'participants') {
717
730
  locus[key] = cloneDeep(this[key]);
718
731
  }
719
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
+ });
720
741
  }
721
742
 
722
743
  LoggerProxy.logger.info(
@@ -724,7 +745,7 @@ export default class LocusInfo extends EventsScope {
724
745
  data.updatedObjects.map((o) => ({
725
746
  type: o.htMeta.elementId.type,
726
747
  id: o.htMeta.elementId.id,
727
- hasData: o.data !== undefined,
748
+ hasData: !!o.data,
728
749
  }))
729
750
  )}`
730
751
  );
@@ -1010,8 +1031,7 @@ export default class LocusInfo extends EventsScope {
1010
1031
  this.updateMemberShip(locus.membership);
1011
1032
  this.updateIdentifiers(locus.identities);
1012
1033
  this.updateEmbeddedApps(locus.embeddedApps);
1013
- this.updateServices(locus.links?.services);
1014
- this.updateResources(locus.links?.resources);
1034
+ this.updateLinks(locus.links);
1015
1035
  this.compareAndUpdate();
1016
1036
  // update which required to compare different objects from locus
1017
1037
  }
@@ -1663,17 +1683,19 @@ export default class LocusInfo extends EventsScope {
1663
1683
  }
1664
1684
 
1665
1685
  /**
1666
- * @param {Object} services
1686
+ * Updates links and emits appropriate events if services or resources have changed
1687
+ * @param {Object} links
1667
1688
  * @returns {undefined}
1668
1689
  * @memberof LocusInfo
1669
1690
  */
1670
- updateServices(services: Record<'breakout' | 'record', {url: string}>) {
1671
- if (services && !isEqual(this.services, services)) {
1672
- this.services = services;
1691
+ updateLinks(links?: Links) {
1692
+ const {services, resources} = links || {};
1693
+
1694
+ if (services && !isEqual(this.links?.services, services)) {
1673
1695
  this.emitScoped(
1674
1696
  {
1675
1697
  file: 'locus-info',
1676
- function: 'updateServices',
1698
+ function: 'updateLinks',
1677
1699
  },
1678
1700
  LOCUSINFO.EVENTS.LINKS_SERVICES,
1679
1701
  {
@@ -1681,20 +1703,12 @@ export default class LocusInfo extends EventsScope {
1681
1703
  }
1682
1704
  );
1683
1705
  }
1684
- }
1685
1706
 
1686
- /**
1687
- * @param {Object} resources
1688
- * @returns {undefined}
1689
- * @memberof LocusInfo
1690
- */
1691
- updateResources(resources: Record<'webcastInstance', {url: string}>) {
1692
- if (resources && !isEqual(this.resources, resources)) {
1693
- this.resources = resources;
1707
+ if (resources && !isEqual(this.links?.resources, resources)) {
1694
1708
  this.emitScoped(
1695
1709
  {
1696
1710
  file: 'locus-info',
1697
- function: 'updateResources',
1711
+ function: 'updateLinks',
1698
1712
  },
1699
1713
  LOCUSINFO.EVENTS.LINKS_RESOURCES,
1700
1714
  {
@@ -1702,6 +1716,8 @@ export default class LocusInfo extends EventsScope {
1702
1716
  }
1703
1717
  );
1704
1718
  }
1719
+
1720
+ this.links = links;
1705
1721
  }
1706
1722
 
1707
1723
  /**
@@ -1,18 +1,25 @@
1
1
  import {HtMeta} from '../hashTree/types';
2
2
 
3
+ export type LocusFullState = {
4
+ active: boolean;
5
+ count: number;
6
+ lastActive: string;
7
+ locked: boolean;
8
+ sessionId: string;
9
+ seessionIds: string[];
10
+ startTime: number;
11
+ state: string;
12
+ type: string;
13
+ };
14
+
15
+ export type Links = {
16
+ services: Record<'breakout' | 'record', {url: string}>; // there exist also other services, but these are the ones we currently use
17
+ resources: Record<'webcastInstance' | 'visibleDataSets', {url: string}>; // there exist also other resources, but these are the ones we currently use
18
+ };
19
+
3
20
  export type LocusDTO = {
4
21
  controls?: any;
5
- fullState?: {
6
- active: boolean;
7
- count: number;
8
- lastActive: string;
9
- locked: boolean;
10
- sessionId: string;
11
- seessionIds: string[];
12
- startTime: number;
13
- state: string;
14
- type: string;
15
- };
22
+ fullState?: LocusFullState;
16
23
  host?: {
17
24
  id: string;
18
25
  incomingCallProtocols: any[];
@@ -25,7 +32,7 @@ export type LocusDTO = {
25
32
  jsSdkMeta?: {
26
33
  removedParticipantIds: string[]; // list of ids of participants that are removed in the last update
27
34
  };
28
- links?: any;
35
+ links?: Links;
29
36
  mediaShares?: any[];
30
37
  meetings?: any[];
31
38
  participants: any[];