@webex/plugin-meetings 3.12.0-next.7 → 3.12.0-next.71

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 (178) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +30 -7
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +38 -24
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +13 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +880 -382
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +42 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/dataChannelAuthToken.js +75 -15
  27. package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
  28. package/dist/interceptors/locusRetry.js +23 -8
  29. package/dist/interceptors/locusRetry.js.map +1 -1
  30. package/dist/interpretation/index.js +10 -1
  31. package/dist/interpretation/index.js.map +1 -1
  32. package/dist/interpretation/interpretation.types.js +7 -0
  33. package/dist/interpretation/interpretation.types.js.map +1 -0
  34. package/dist/interpretation/siLanguage.js +1 -1
  35. package/dist/locus-info/controlsUtils.js +4 -1
  36. package/dist/locus-info/controlsUtils.js.map +1 -1
  37. package/dist/locus-info/index.js +298 -87
  38. package/dist/locus-info/index.js.map +1 -1
  39. package/dist/locus-info/types.js +19 -0
  40. package/dist/locus-info/types.js.map +1 -1
  41. package/dist/media/index.js +3 -1
  42. package/dist/media/index.js.map +1 -1
  43. package/dist/media/properties.js +1 -0
  44. package/dist/media/properties.js.map +1 -1
  45. package/dist/meeting/in-meeting-actions.js +3 -1
  46. package/dist/meeting/in-meeting-actions.js.map +1 -1
  47. package/dist/meeting/index.js +1046 -689
  48. package/dist/meeting/index.js.map +1 -1
  49. package/dist/meeting/muteState.js +10 -1
  50. package/dist/meeting/muteState.js.map +1 -1
  51. package/dist/meeting/request.js +5 -2
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/util.js +20 -2
  54. package/dist/meeting/util.js.map +1 -1
  55. package/dist/meeting-info/meeting-info-v2.js +2 -2
  56. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  57. package/dist/meetings/index.js +231 -78
  58. package/dist/meetings/index.js.map +1 -1
  59. package/dist/meetings/meetings.types.js +6 -1
  60. package/dist/meetings/meetings.types.js.map +1 -1
  61. package/dist/meetings/request.js +39 -0
  62. package/dist/meetings/request.js.map +1 -1
  63. package/dist/meetings/util.js +79 -5
  64. package/dist/meetings/util.js.map +1 -1
  65. package/dist/member/index.js +10 -0
  66. package/dist/member/index.js.map +1 -1
  67. package/dist/member/types.js.map +1 -1
  68. package/dist/member/util.js +3 -0
  69. package/dist/member/util.js.map +1 -1
  70. package/dist/metrics/constants.js +4 -1
  71. package/dist/metrics/constants.js.map +1 -1
  72. package/dist/multistream/codec/constants.js +63 -0
  73. package/dist/multistream/codec/constants.js.map +1 -0
  74. package/dist/multistream/mediaRequestManager.js +62 -15
  75. package/dist/multistream/mediaRequestManager.js.map +1 -1
  76. package/dist/multistream/receiveSlot.js +9 -0
  77. package/dist/multistream/receiveSlot.js.map +1 -1
  78. package/dist/reactions/reactions.type.js.map +1 -1
  79. package/dist/recording-controller/index.js +1 -3
  80. package/dist/recording-controller/index.js.map +1 -1
  81. package/dist/types/config.d.ts +2 -0
  82. package/dist/types/constants.d.ts +9 -1
  83. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  84. package/dist/types/controls-options-manager/index.d.ts +10 -0
  85. package/dist/types/hashTree/constants.d.ts +2 -0
  86. package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
  87. package/dist/types/hashTree/utils.d.ts +18 -0
  88. package/dist/types/index.d.ts +3 -0
  89. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  90. package/dist/types/interpretation/interpretation.types.d.ts +10 -0
  91. package/dist/types/locus-info/index.d.ts +50 -6
  92. package/dist/types/locus-info/types.d.ts +21 -1
  93. package/dist/types/media/properties.d.ts +1 -0
  94. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  95. package/dist/types/meeting/index.d.ts +78 -5
  96. package/dist/types/meeting/request.d.ts +1 -0
  97. package/dist/types/meeting/util.d.ts +8 -0
  98. package/dist/types/meetings/index.d.ts +30 -2
  99. package/dist/types/meetings/meetings.types.d.ts +15 -0
  100. package/dist/types/meetings/request.d.ts +14 -0
  101. package/dist/types/member/index.d.ts +1 -0
  102. package/dist/types/member/types.d.ts +1 -0
  103. package/dist/types/member/util.d.ts +1 -0
  104. package/dist/types/metrics/constants.d.ts +3 -0
  105. package/dist/types/multistream/codec/constants.d.ts +7 -0
  106. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  107. package/dist/types/reactions/reactions.type.d.ts +3 -0
  108. package/dist/webinar/index.js +305 -159
  109. package/dist/webinar/index.js.map +1 -1
  110. package/package.json +22 -22
  111. package/src/aiEnableRequest/index.ts +16 -0
  112. package/src/breakouts/breakout.ts +3 -1
  113. package/src/breakouts/index.ts +31 -0
  114. package/src/config.ts +2 -0
  115. package/src/constants.ts +13 -2
  116. package/src/controls-options-manager/constants.ts +14 -1
  117. package/src/controls-options-manager/index.ts +47 -24
  118. package/src/controls-options-manager/util.ts +81 -1
  119. package/src/hashTree/constants.ts +16 -0
  120. package/src/hashTree/hashTreeParser.ts +580 -196
  121. package/src/hashTree/utils.ts +36 -0
  122. package/src/index.ts +6 -0
  123. package/src/interceptors/dataChannelAuthToken.ts +88 -12
  124. package/src/interceptors/locusRetry.ts +25 -4
  125. package/src/interpretation/index.ts +27 -9
  126. package/src/interpretation/interpretation.types.ts +11 -0
  127. package/src/locus-info/controlsUtils.ts +3 -1
  128. package/src/locus-info/index.ts +293 -97
  129. package/src/locus-info/types.ts +25 -1
  130. package/src/media/index.ts +3 -0
  131. package/src/media/properties.ts +1 -0
  132. package/src/meeting/in-meeting-actions.ts +4 -0
  133. package/src/meeting/index.ts +386 -48
  134. package/src/meeting/muteState.ts +10 -1
  135. package/src/meeting/request.ts +11 -0
  136. package/src/meeting/util.ts +21 -2
  137. package/src/meeting-info/meeting-info-v2.ts +4 -2
  138. package/src/meetings/index.ts +134 -44
  139. package/src/meetings/meetings.types.ts +19 -0
  140. package/src/meetings/request.ts +43 -0
  141. package/src/meetings/util.ts +97 -1
  142. package/src/member/index.ts +10 -0
  143. package/src/member/types.ts +1 -0
  144. package/src/member/util.ts +3 -0
  145. package/src/metrics/constants.ts +3 -0
  146. package/src/multistream/codec/constants.ts +58 -0
  147. package/src/multistream/mediaRequestManager.ts +119 -28
  148. package/src/multistream/receiveSlot.ts +18 -0
  149. package/src/reactions/reactions.type.ts +3 -0
  150. package/src/recording-controller/index.ts +1 -2
  151. package/src/webinar/index.ts +214 -36
  152. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  153. package/test/unit/spec/breakouts/breakout.ts +9 -3
  154. package/test/unit/spec/breakouts/index.ts +49 -0
  155. package/test/unit/spec/controls-options-manager/index.js +140 -29
  156. package/test/unit/spec/controls-options-manager/util.js +165 -0
  157. package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
  158. package/test/unit/spec/hashTree/utils.ts +125 -1
  159. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
  160. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  161. package/test/unit/spec/interpretation/index.ts +26 -4
  162. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  163. package/test/unit/spec/locus-info/index.js +487 -81
  164. package/test/unit/spec/media/index.ts +31 -0
  165. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  166. package/test/unit/spec/meeting/index.js +1240 -37
  167. package/test/unit/spec/meeting/muteState.js +81 -0
  168. package/test/unit/spec/meeting/request.js +12 -0
  169. package/test/unit/spec/meeting/utils.js +33 -0
  170. package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
  171. package/test/unit/spec/meetings/index.js +360 -10
  172. package/test/unit/spec/meetings/request.js +141 -0
  173. package/test/unit/spec/meetings/utils.js +189 -0
  174. package/test/unit/spec/member/index.js +7 -0
  175. package/test/unit/spec/member/util.js +24 -0
  176. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  177. package/test/unit/spec/recording-controller/index.js +9 -8
  178. package/test/unit/spec/webinar/index.ts +329 -28
@@ -34,6 +34,7 @@ import BEHAVIORAL_METRICS from '../metrics/constants';
34
34
  import HashTreeParser, {
35
35
  DataSet,
36
36
  HashTreeMessage,
37
+ LocusInfoUpdate,
37
38
  LocusInfoUpdateType,
38
39
  Metadata,
39
40
  } from '../hashTree/hashTreeParser';
@@ -97,7 +98,7 @@ export type HashTreeParserEntry = {
97
98
  * Gets the replacement information
98
99
  *
99
100
  * @param {any} self - "self" object from Locus DTO
100
- * @param {string} deviceUrl - The URL of the user's device
101
+ * @param {string} deviceUrl - The URL of the specified device
101
102
  * @returns {any} The replace information if available, otherwise undefined
102
103
  */
103
104
  function getReplaceInfoFromSelf(self: any, deviceUrl: string): ReplacesInfo | undefined {
@@ -137,14 +138,15 @@ function findLocusUrlInAnyHashTreeParser(
137
138
  *
138
139
  * @param {HashTreeMessage} message - The hash tree message to find the meeting for
139
140
  * @param {MeetingCollection} meetingCollection - The collection of meetings to search
140
- * @param {string} deviceUrl - The URL of the user's device
141
141
  * @returns {any} The meeting if found, otherwise undefined
142
142
  */
143
143
  export function findMeetingForHashTreeMessage(
144
- message: HashTreeMessage,
145
- meetingCollection: MeetingCollection,
146
- deviceUrl: string
144
+ message: HashTreeMessage | undefined,
145
+ meetingCollection: MeetingCollection
147
146
  ): any {
147
+ if (!message) {
148
+ return undefined;
149
+ }
148
150
  let foundMeeting = findLocusUrlInAnyHashTreeParser(meetingCollection, message.locusUrl);
149
151
 
150
152
  if (foundMeeting) {
@@ -154,7 +156,7 @@ export function findMeetingForHashTreeMessage(
154
156
  // if we haven't found anything, it may mean that message has a new locusUrl
155
157
  // check if it indicates that it replaces some existing current locusUrl (this is indicated in "self")
156
158
  const self = message.locusStateElements?.find((el) => isSelf(el))?.data;
157
- const replaces = getReplaceInfoFromSelf(self, deviceUrl);
159
+ const replaces = getReplaceInfoFromSelf(self, self?.deviceUrl);
158
160
 
159
161
  if (replaces?.locusUrl) {
160
162
  foundMeeting = findLocusUrlInAnyHashTreeParser(meetingCollection, replaces.locusUrl);
@@ -545,7 +547,7 @@ export default class LocusInfo extends EventsScope {
545
547
  dataSets: Array<DataSet>;
546
548
  locus: any;
547
549
  };
548
- metadata: Metadata;
550
+ metadata: Metadata | null;
549
551
  replacedAt?: string;
550
552
  }): HashTreeParser {
551
553
  const parser = new HashTreeParser({
@@ -553,7 +555,7 @@ export default class LocusInfo extends EventsScope {
553
555
  metadata,
554
556
  webexRequest: this.webex.request.bind(this.webex),
555
557
  locusInfoUpdateCallback: this.updateFromHashTree.bind(this, locusUrl),
556
- debugId: `HT-${locusUrl.split('/').pop().substring(0, 4)}`,
558
+ debugId: `HT-${locusUrl.split('/')?.pop()?.substring(0, 4)}`,
557
559
  excludedDataSets: this.webex.config.meetings.locus?.excludedDataSets,
558
560
  });
559
561
 
@@ -580,6 +582,7 @@ export default class LocusInfo extends EventsScope {
580
582
 
581
583
  /**
582
584
  * @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
585
+ * @param {Function} [onLocusSynced] - optional callback that will be called at the end of initial setup, when locus info is fully synced. It will be called with the full locus snapshot as an argument (which may be null if we haven't received any full locus DTOs during the initial setup, for example in case we receive only hash tree messages without full locus DTOs)
583
586
  * @returns {undefined}
584
587
  * @memberof LocusInfo
585
588
  */
@@ -599,8 +602,10 @@ export default class LocusInfo extends EventsScope {
599
602
  | {
600
603
  trigger: 'get-loci-response';
601
604
  locus?: LocusDTO;
602
- }
605
+ },
606
+ onLocusSynced?: (locus: LocusDTO) => void
603
607
  ) {
608
+ let initialFullLocus: LocusDTO | null = null;
604
609
  switch (data.trigger) {
605
610
  case 'locus-message':
606
611
  if (data.hashTreeMessage) {
@@ -648,6 +653,7 @@ export default class LocusInfo extends EventsScope {
648
653
  case 'join-response':
649
654
  this.updateLocusCache(data.locus);
650
655
  this.onFullLocus('join response', data.locus, undefined, data.dataSets, data.metadata);
656
+ initialFullLocus = data.locus;
651
657
  break;
652
658
  case 'get-loci-response':
653
659
  if (data.locus?.links?.resources?.visibleDataSets?.url) {
@@ -656,7 +662,7 @@ export default class LocusInfo extends EventsScope {
656
662
  );
657
663
  // first create the HashTreeParser, but don't initialize it with any data yet
658
664
  const hashTreeParser = this.createHashTreeParser({
659
- locusUrl: data.locus.url,
665
+ locusUrl: data.locus.url as string,
660
666
  initialLocus: {
661
667
  locus: null,
662
668
  dataSets: [], // empty, because we don't have them yet
@@ -670,52 +676,99 @@ export default class LocusInfo extends EventsScope {
670
676
  // "classic" Locus case, no hash trees involved
671
677
  this.updateLocusCache(data.locus);
672
678
  this.onFullLocus('classic get-loci-response', data.locus, undefined);
679
+ initialFullLocus = data.locus || null;
673
680
  }
674
681
  }
682
+
683
+ if (onLocusSynced) {
684
+ try {
685
+ onLocusSynced(initialFullLocus || this.getCurrentLocusSnapshot());
686
+ } catch (error) {
687
+ LoggerProxy.logger.warn(
688
+ `Locus-info:index#initialSetup --> onLocusSynced callback failed: ${error}`
689
+ );
690
+ }
691
+ }
692
+
675
693
  // Change it to true after it receives it first locus object
676
694
  this.emitChange = true;
677
695
  }
678
696
 
697
+ /**
698
+ * Builds a full locus DTO snapshot from current internal locus state.
699
+ *
700
+ * @returns {LocusDTO}
701
+ */
702
+ private getCurrentLocusSnapshot(): LocusDTO {
703
+ const locus: Record<string, any> = {};
704
+
705
+ LocusDtoTopLevelKeys.forEach((key) => {
706
+ const value = (this as Record<string, any>)[key];
707
+
708
+ if (value !== undefined && value !== null) {
709
+ locus[key] = cloneDeep(value);
710
+ }
711
+ });
712
+
713
+ if (!Array.isArray(locus.participants)) {
714
+ locus.participants = [];
715
+ }
716
+
717
+ return locus as LocusDTO;
718
+ }
719
+
679
720
  /**
680
721
  * Handles HTTP response from Locus API call.
681
722
  * @param {Meeting} meeting meeting object
682
723
  * @param {LocusApiResponseBody} responseBody body of the http response from Locus API call
683
724
  * @returns {void}
684
725
  */
685
- handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
726
+ handleLocusAPIResponse(meeting: any, responseBody: LocusApiResponseBody): void {
686
727
  const isWrapped = 'locus' in responseBody;
687
728
  const locusUrl = isWrapped ? responseBody.locus?.url : responseBody.url;
688
729
  const hashTreeParserEntry = locusUrl && this.hashTreeParsers.get(locusUrl);
689
- if (hashTreeParserEntry) {
690
- if (isWrapped) {
691
- if (!responseBody.dataSets) {
692
- this.sendClassicVsHashTreeMismatchMetric(
693
- meeting,
694
- `expected hash tree dataSets in API response but they are missing`
730
+ const locus = isWrapped
731
+ ? (responseBody as {locus: LocusDTO}).locus
732
+ : (responseBody as LocusDTO);
733
+
734
+ if (this.hashTreeParsers.size > 0) {
735
+ // We are in hash tree mode. Check if we need to create/reactivate a parser for this locusUrl.
736
+ if (!hashTreeParserEntry || hashTreeParserEntry.parser.state === 'stopped') {
737
+ if (!locusUrl) {
738
+ LoggerProxy.logger.warn(
739
+ 'Locus-info:index#handleLocusAPIResponse --> API response has no locusUrl, cannot handle hash tree parser switch'
695
740
  );
696
- // continuing as we can still manage without responseBody.dataSets, but this is very suspicious
741
+
742
+ return;
697
743
  }
698
- LoggerProxy.logger.info(
699
- 'Locus-info:index#handleLocusAPIResponse --> passing Locus API response to HashTreeParser: ',
700
- responseBody
701
- );
744
+
745
+ this.handleHashTreeParserSwitchForAPIResponse(locusUrl, locus);
746
+
747
+ return;
748
+ }
749
+
750
+ // Active parser found - pass the API response to it
751
+ if (isWrapped) {
702
752
  // update the data in our hash trees
703
753
  hashTreeParserEntry.parser.handleLocusUpdate(responseBody);
704
754
  } else {
705
- // LocusDTO without wrapper - pass it through as if it had no dataSets
755
+ // LocusDTO without wrapper - pass it through as if it had no dataSets nor metadata
706
756
  hashTreeParserEntry.parser.handleLocusUpdate({locus: responseBody});
707
757
  }
708
- } else {
709
- if (isWrapped && responseBody.dataSets) {
710
- this.sendClassicVsHashTreeMismatchMetric(
711
- meeting,
712
- `unexpected hash tree dataSets in API response`
713
- );
714
- }
715
- // classic Locus delta
716
- const locus = isWrapped ? responseBody.locus : responseBody;
717
- this.handleLocusDelta(locus, meeting);
758
+
759
+ return;
718
760
  }
761
+
762
+ // No hash tree parsers - classic Locus mode
763
+ if (isWrapped && responseBody.dataSets) {
764
+ this.sendClassicVsHashTreeMismatchMetric(
765
+ meeting,
766
+ `unexpected hash tree dataSets in API response`
767
+ );
768
+ }
769
+
770
+ // classic Locus delta
771
+ this.handleLocusDelta(locus, meeting);
719
772
  }
720
773
 
721
774
  /**
@@ -944,6 +997,114 @@ export default class LocusInfo extends EventsScope {
944
997
  }
945
998
  }
946
999
 
1000
+ /**
1001
+ * Helper that handles the common logic for reactivating a stopped HashTreeParser when
1002
+ * a newer "replaces" is detected. Used by both the message and API response parser switch methods.
1003
+ *
1004
+ * @param {string} callerName - name of the calling method, used in log messages
1005
+ * @param {string} locusUrl - the locus URL of the stopped parser
1006
+ * @param {HashTreeParserEntry} stoppedEntry - the stopped parser entry
1007
+ * @param {ReplacesInfo} replaces - replacement info extracted from self
1008
+ * @param {Function} resumeCallback - callback to invoke after reactivation to resume the parser
1009
+ * @returns {void}
1010
+ */
1011
+ private resumeStoppedParser(
1012
+ callerName: string,
1013
+ locusUrl: string,
1014
+ stoppedEntry: HashTreeParserEntry,
1015
+ replaces: ReplacesInfo | undefined,
1016
+ resumeCallback: () => void
1017
+ ): void {
1018
+ // this check is just for typescript, it should never happen, replaces should always be defined
1019
+ if (!replaces) {
1020
+ LoggerProxy.logger.info(
1021
+ `Locus-info:index#${callerName} --> received data for stopped HashTreeParser with locusUrl ${locusUrl}, but no replaces info provided, so not re-activating the parser`
1022
+ );
1023
+
1024
+ return;
1025
+ }
1026
+
1027
+ if (replaces.replacedAt <= (stoppedEntry.replacedAt || '')) {
1028
+ LoggerProxy.logger.info(
1029
+ `Locus-info:index#${callerName} --> received data for stopped HashTreeParser with locusUrl ${locusUrl}, but replaces info provided is not newer, so not re-activating the parser`
1030
+ );
1031
+
1032
+ return;
1033
+ }
1034
+
1035
+ LoggerProxy.logger.info(
1036
+ `Locus-info:index#${callerName} --> reactivating HashTreeParser for locusUrl=${locusUrl}, which replaces ${replaces.locusUrl}`
1037
+ );
1038
+
1039
+ const replacedEntry = this.hashTreeParsers.get(replaces.locusUrl);
1040
+
1041
+ if (replacedEntry) {
1042
+ replacedEntry.replacedAt = replaces.replacedAt;
1043
+ replacedEntry.parser.stop();
1044
+ } else {
1045
+ LoggerProxy.logger.warn(
1046
+ `Locus-info:index#${callerName} --> the parser that is supposed to be replaced with the currently reactivated parser is not found, locusUrl=${replaces.locusUrl}`
1047
+ );
1048
+ }
1049
+
1050
+ stoppedEntry.initializedFromHashTree = false;
1051
+ this.hashTreeObjectId2ParticipantId.clear();
1052
+
1053
+ resumeCallback();
1054
+ }
1055
+
1056
+ /**
1057
+ * Handles an API response whose locusUrl doesn't match any active HashTreeParser
1058
+ * (either no entry exists, or the existing entry is stopped).
1059
+ * Creates a new parser or reactivates a stopped one using initializeFromGetLociResponse.
1060
+ *
1061
+ * @param {string} locusUrl - the locus URL from the API response
1062
+ * @param {LocusDTO} locus - the locus DTO from the API response
1063
+ * @returns {void}
1064
+ */
1065
+ private handleHashTreeParserSwitchForAPIResponse(locusUrl: string, locus: LocusDTO): void {
1066
+ const entry = this.hashTreeParsers.get(locusUrl);
1067
+
1068
+ const replaces = getReplaceInfoFromSelf(
1069
+ locus.self,
1070
+ // @ts-ignore
1071
+ this.webex.internal.device.url
1072
+ );
1073
+
1074
+ if (!entry) {
1075
+ LoggerProxy.logger.info(
1076
+ `Locus-info:index#handleHashTreeParserSwitchForAPIResponse --> no parser for locusUrl ${locusUrl}, creating a new one`
1077
+ );
1078
+
1079
+ const parser = this.createHashTreeParser({
1080
+ locusUrl,
1081
+ initialLocus: {locus: null, dataSets: []},
1082
+ metadata: null,
1083
+ replacedAt: replaces?.replacedAt,
1084
+ });
1085
+
1086
+ parser.initializeFromGetLociResponse(locus);
1087
+
1088
+ return;
1089
+ }
1090
+
1091
+ if (entry.parser.state !== 'stopped') {
1092
+ LoggerProxy.logger.warn(
1093
+ `Locus-info:index#handleHashTreeParserSwitchForAPIResponse --> unexpected parser state "${entry.parser.state}" for locusUrl ${locusUrl}`
1094
+ );
1095
+
1096
+ return;
1097
+ }
1098
+
1099
+ this.resumeStoppedParser(
1100
+ 'handleHashTreeParserSwitchForAPIResponse',
1101
+ locusUrl,
1102
+ entry,
1103
+ replaces,
1104
+ () => entry.parser.resumeFromApiResponse(locus)
1105
+ );
1106
+ }
1107
+
947
1108
  /**
948
1109
  * Checks if the hash tree message should trigger a switch to a different HashTreeParser
949
1110
  *
@@ -965,7 +1126,7 @@ export default class LocusInfo extends EventsScope {
965
1126
  // but it's buried inside the message, we need to find it and pass it to HashTreeParser constructor
966
1127
  const metadata = message.locusStateElements?.find((el) => isMetadata(el));
967
1128
 
968
- if (metadata?.data?.visibleDataSets?.length > 0) {
1129
+ if (metadata && metadata.data?.visibleDataSets?.length > 0) {
969
1130
  LoggerProxy.logger.info(
970
1131
  `Locus-info:index#handleHashTreeParserSwitch --> no hash tree parser found for locusUrl ${message.locusUrl}, creating a new one`
971
1132
  );
@@ -992,36 +1153,12 @@ export default class LocusInfo extends EventsScope {
992
1153
  if (entry.parser.state === 'stopped') {
993
1154
  // the message matches a stopped parser, we need to check if maybe this is a new "replacement" and we need to re-activate the parser
994
1155
  // this happens when you move from breakout A -> breakout B -> back to breakout A
995
- if (replaces) {
996
- if (replaces.replacedAt > (entry.replacedAt || '')) {
997
- LoggerProxy.logger.info(
998
- `Locus-info:index#handleHashTreeParserSwitch --> resuming a HashTreeParser for locusUrl=${message.locusUrl}, which replaces ${replaces.locusUrl}`
999
- );
1000
- const replacedEntry = this.hashTreeParsers.get(replaces.locusUrl);
1001
-
1002
- if (replacedEntry) {
1003
- replacedEntry.replacedAt = replaces.replacedAt;
1004
- entry.initializedFromHashTree = false;
1005
- this.hashTreeObjectId2ParticipantId.clear();
1006
-
1007
- replacedEntry.parser.stop();
1008
- entry.parser.resume(message);
1009
- } else {
1010
- LoggerProxy.logger.warn(
1011
- `Locus-info:index#handleHashTreeParserSwitch --> the parser that is supposed to be replaced with the currently resumed parser is not found, locusUrl=${replaces.locusUrl}`
1012
- );
1013
- }
1014
- } else {
1015
- LoggerProxy.logger.info(
1016
- `Locus-info:index#handleHashTreeParserSwitch --> received message for stopped HashTreeParser with locusUrl ${message.locusUrl}, but replaces info provided is not newer, so not re-activating the parser`
1017
- );
1018
- }
1019
-
1020
- return true;
1021
- }
1022
-
1023
- LoggerProxy.logger.info(
1024
- `Locus-info:index#handleHashTreeParserSwitch --> received message for stopped HashTreeParser with locusUrl ${message.locusUrl}, but no replaces info provided, so not re-activating the parser`
1156
+ this.resumeStoppedParser(
1157
+ 'handleHashTreeParserSwitch',
1158
+ message.locusUrl,
1159
+ entry,
1160
+ replaces,
1161
+ () => entry.parser.resumeFromMessage(message)
1025
1162
  );
1026
1163
 
1027
1164
  return true;
@@ -1056,7 +1193,27 @@ export default class LocusInfo extends EventsScope {
1056
1193
 
1057
1194
  const entry = this.hashTreeParsers.get(message.locusUrl);
1058
1195
 
1059
- entry.parser.handleMessage(message);
1196
+ // the check is just for typescript, the case of no entry in hashTreeParsers is handled in handleHashTreeParserSwitch() above
1197
+ if (entry) {
1198
+ entry.parser.handleMessage(message);
1199
+ }
1200
+ }
1201
+
1202
+ /**
1203
+ * Triggers a sync of all hash tree datasets for all hash tree parsers associated with this meeting.
1204
+ * The syncs are executed sequentially within each parser.
1205
+ *
1206
+ * @param {Object} [options={}] - Options for syncing
1207
+ * @param {boolean} [options.onlyLLM=false] - Whether to sync only LLM based data sets
1208
+ * @returns {Promise<void>}
1209
+ */
1210
+ async syncAllHashTreeDatasets(options: {onlyLLM?: boolean} = {}): Promise<void> {
1211
+ for (const [, entry] of this.hashTreeParsers) {
1212
+ if (entry.parser) {
1213
+ // eslint-disable-next-line no-await-in-loop
1214
+ await entry.parser.syncAllDatasets(options);
1215
+ }
1216
+ }
1060
1217
  }
1061
1218
 
1062
1219
  /**
@@ -1064,16 +1221,11 @@ export default class LocusInfo extends EventsScope {
1064
1221
  * Updates our locus info based on the data parsed by the hash tree parser.
1065
1222
  *
1066
1223
  * @param {string} locusUrl - the locus URL for which the update is received
1067
- * @param {LocusInfoUpdateType} updateType - The type of update received.
1068
- * @param {Object} [data] - Additional data for the update, if applicable.
1224
+ * @param {LocusInfoUpdate} update - Details about the update.
1069
1225
  * @returns {void}
1070
1226
  */
1071
- private updateFromHashTree(
1072
- locusUrl: string,
1073
- updateType: LocusInfoUpdateType,
1074
- data?: {updatedObjects: HashTreeObject[]}
1075
- ) {
1076
- switch (updateType) {
1227
+ private updateFromHashTree(locusUrl: string, update: LocusInfoUpdate) {
1228
+ switch (update.updateType) {
1077
1229
  case LocusInfoUpdateType.OBJECTS_UPDATED: {
1078
1230
  // initialize our new locus
1079
1231
  let locus: LocusDTO = {
@@ -1087,7 +1239,7 @@ export default class LocusInfo extends EventsScope {
1087
1239
  // first go over all the updates and check what happens with the main locus object
1088
1240
  let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
1089
1241
  LocusObjectStateAfterUpdates.unchanged;
1090
- data.updatedObjects.forEach((object) => {
1242
+ update.updatedObjects.forEach((object) => {
1091
1243
  if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
1092
1244
  if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
1093
1245
  // this code doesn't supported it right now,
@@ -1116,6 +1268,14 @@ export default class LocusInfo extends EventsScope {
1116
1268
 
1117
1269
  const hashTreeParserEntry = this.hashTreeParsers.get(locusUrl);
1118
1270
 
1271
+ if (!hashTreeParserEntry) {
1272
+ LoggerProxy.logger.warn(
1273
+ `Locus-info:index#updateFromHashTree --> no HashTreeParser found for locusUrl ${locusUrl} when trying to apply updates from hash tree`
1274
+ );
1275
+
1276
+ return;
1277
+ }
1278
+
1119
1279
  if (!hashTreeParserEntry.initializedFromHashTree) {
1120
1280
  // this is the first time we're getting an update for this locusUrl,
1121
1281
  // so it's probably a move to/from breakout. We need to start from a clean state,
@@ -1124,7 +1284,8 @@ export default class LocusInfo extends EventsScope {
1124
1284
  `Locus-info:index#updateFromHashTree --> first INITIAL update for locusUrl ${locusUrl}, starting from empty state`
1125
1285
  );
1126
1286
  hashTreeParserEntry.initializedFromHashTree = true;
1127
- locus.jsSdkMeta.forceReplaceMembers = true;
1287
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1288
+ locus.jsSdkMeta!.forceReplaceMembers = true;
1128
1289
  } else if (
1129
1290
  // if Locus object is unchanged or removed, we need to keep using the existing locus
1130
1291
  // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
@@ -1137,7 +1298,7 @@ export default class LocusInfo extends EventsScope {
1137
1298
  // copy over all of existing locus except participants
1138
1299
  LocusDtoTopLevelKeys.forEach((key) => {
1139
1300
  if (key !== 'participants') {
1140
- locus[key] = cloneDeep(this[key]);
1301
+ (locus as Record<string, any>)[key] = cloneDeep((this as Record<string, any>)[key]);
1141
1302
  }
1142
1303
  });
1143
1304
  } else {
@@ -1145,14 +1306,16 @@ export default class LocusInfo extends EventsScope {
1145
1306
  // (except participants, which need to stay empty - that means "no participant changes")
1146
1307
  Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
1147
1308
  if (locusDtoKey !== 'participants') {
1148
- locus[locusDtoKey] = cloneDeep(this[locusDtoKey]);
1309
+ (locus as Record<string, any>)[locusDtoKey] = cloneDeep(
1310
+ (this as Record<string, any>)[locusDtoKey]
1311
+ );
1149
1312
  }
1150
1313
  });
1151
1314
  }
1152
1315
 
1153
1316
  LoggerProxy.logger.info(
1154
1317
  `Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
1155
- data.updatedObjects.map((o) => ({
1318
+ update.updatedObjects.map((o) => ({
1156
1319
  type: o.htMeta.elementId.type,
1157
1320
  id: o.htMeta.elementId.id,
1158
1321
  hasData: !!o.data,
@@ -1160,7 +1323,7 @@ export default class LocusInfo extends EventsScope {
1160
1323
  )}`
1161
1324
  );
1162
1325
  // now apply all the updates from the hash tree onto the locus
1163
- data.updatedObjects.forEach((object) => {
1326
+ update.updatedObjects.forEach((object) => {
1164
1327
  locus = this.updateLocusFromHashTreeObject(object, locus);
1165
1328
  });
1166
1329
 
@@ -1179,6 +1342,21 @@ export default class LocusInfo extends EventsScope {
1179
1342
  );
1180
1343
  this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
1181
1344
  }
1345
+ break;
1346
+ }
1347
+
1348
+ case LocusInfoUpdateType.LOCUS_NOT_FOUND: {
1349
+ LoggerProxy.logger.info(
1350
+ `Locus-info:index#updateFromHashTree --> received LOCUS_NOT_FOUND for ${locusUrl}, triggering syncMeetings`
1351
+ );
1352
+ this.webex.meetings
1353
+ .syncMeetings({keepOnlyLocusMeetings: false, skipHashTreeSync: true})
1354
+ .catch((syncError) => {
1355
+ LoggerProxy.logger.error(
1356
+ `Locus-info:index#updateFromHashTree --> syncMeetings failed after LOCUS_NOT_FOUND: ${syncError}`
1357
+ );
1358
+ });
1359
+ break;
1182
1360
  }
1183
1361
  }
1184
1362
  }
@@ -1191,11 +1369,17 @@ export default class LocusInfo extends EventsScope {
1191
1369
  */
1192
1370
  parse(meeting: any, data: any) {
1193
1371
  if (this.hashTreeParsers.size > 0) {
1194
- this.handleHashTreeMessage(
1195
- meeting,
1196
- data.eventType,
1197
- data.stateElementsMessage as HashTreeMessage
1198
- );
1372
+ if (data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS) {
1373
+ // sync meetings response follows the format of "not wrapped" locus API responses,
1374
+ // so has no dataSets nor Metadata
1375
+ this.handleLocusAPIResponse(meeting, {...data.locus});
1376
+ } else {
1377
+ this.handleHashTreeMessage(
1378
+ meeting,
1379
+ data.eventType,
1380
+ data.stateElementsMessage as HashTreeMessage
1381
+ );
1382
+ }
1199
1383
  } else {
1200
1384
  const {eventType} = data;
1201
1385
 
@@ -1260,16 +1444,16 @@ export default class LocusInfo extends EventsScope {
1260
1444
  * @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
1261
1445
  * @param {object} locus locus object
1262
1446
  * @param {object} metadata locus hash trees metadata
1263
- * @param {string} eventType locus event
1264
1447
  * @param {DataSet[]} dataSets
1448
+ * @param {string} eventType locus event
1265
1449
  * @returns {void}
1266
1450
  */
1267
1451
  private onFullLocusWithHashTrees(
1268
1452
  debugText: string,
1269
1453
  locus: any,
1270
1454
  metadata: Metadata,
1271
- eventType?: string,
1272
- dataSets?: Array<DataSet>
1455
+ dataSets: Array<DataSet>,
1456
+ eventType?: string
1273
1457
  ) {
1274
1458
  if (!this.hashTreeParsers.has(locus.url)) {
1275
1459
  LoggerProxy.logger.info(
@@ -1289,7 +1473,8 @@ export default class LocusInfo extends EventsScope {
1289
1473
  metadata,
1290
1474
  });
1291
1475
  // we have a full locus to start with, so we consider Locus info to be "initialized"
1292
- this.hashTreeParsers.get(locus.url).initializedFromHashTree = true;
1476
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1477
+ this.hashTreeParsers.get(locus.url)!.initializedFromHashTree = true;
1293
1478
  this.onFullLocusCommon(locus, eventType);
1294
1479
  } else {
1295
1480
  // in this case the Locus we're getting is not necessarily the full one
@@ -1351,7 +1536,7 @@ export default class LocusInfo extends EventsScope {
1351
1536
  );
1352
1537
  }
1353
1538
  // this is the new hashmap Locus DTO format (only applicable to webinars for now)
1354
- this.onFullLocusWithHashTrees(debugText, locus, metadata, eventType, dataSets);
1539
+ this.onFullLocusWithHashTrees(debugText, locus, metadata, dataSets, eventType);
1355
1540
  } else {
1356
1541
  this.onFullLocusClassic(debugText, locus, eventType);
1357
1542
  }
@@ -1495,7 +1680,7 @@ export default class LocusInfo extends EventsScope {
1495
1680
  * @memberof LocusInfo
1496
1681
  */
1497
1682
  updateLocusInfo(locus) {
1498
- if (locus.self?.reason === 'MOVED' && locus.self?.state === 'LEFT') {
1683
+ if (MeetingsUtil.isSelfMovedOrBreakoutEnded(locus)) {
1499
1684
  // When moved to a breakout session locus sends a message for the previous locus
1500
1685
  // indicating that we have been moved. It isn't helpful to continue parsing this
1501
1686
  // as it gets interpreted as if we have left the call
@@ -1648,14 +1833,9 @@ export default class LocusInfo extends EventsScope {
1648
1833
  );
1649
1834
  }
1650
1835
  } else if (this.parsedLocus.fullState?.type === _MEETING_) {
1651
- if (
1652
- this.fullState &&
1653
- (this.fullState.state === LOCUS.STATE.INACTIVE ||
1654
- // @ts-ignore
1655
- this.fullState.state === LOCUS.STATE.TERMINATING)
1656
- ) {
1836
+ if (this.fullState && MeetingsUtil.isWholeMeetingEnded(this.fullState)) {
1657
1837
  LoggerProxy.logger.warn(
1658
- 'Locus-info:index#isMeetingActive --> Meeting is ending due to inactive or terminating'
1838
+ 'Locus-info:index#isMeetingActive --> Meeting is ending due to inactive'
1659
1839
  );
1660
1840
 
1661
1841
  // @ts-ignore
@@ -1918,6 +2098,8 @@ export default class LocusInfo extends EventsScope {
1918
2098
  state,
1919
2099
  modifiedBy: current.record.modifiedBy,
1920
2100
  lastModified: current.record.lastModified,
2101
+ modifiedByServiceAppName: current.record.modifiedByServiceAppName,
2102
+ modifiedByServiceAppId: current.record.modifiedByServiceAppId,
1921
2103
  }
1922
2104
  );
1923
2105
  }
@@ -2587,6 +2769,7 @@ export default class LocusInfo extends EventsScope {
2587
2769
  {
2588
2770
  muted: parsedSelves.current.remoteMuted,
2589
2771
  unmuteAllowed: parsedSelves.current.unmuteAllowed,
2772
+ modifiedBy: parsedSelves.current.modifiedBy ?? null,
2590
2773
  }
2591
2774
  );
2592
2775
  }
@@ -2859,4 +3042,17 @@ export default class LocusInfo extends EventsScope {
2859
3042
  clearMainSessionLocusCache() {
2860
3043
  this.mainSessionLocusCache = null;
2861
3044
  }
3045
+
3046
+ /**
3047
+ * Cleans up all hash tree parsers and clears internal maps.
3048
+ * @returns {void}
3049
+ * @memberof LocusInfo
3050
+ */
3051
+ cleanUp() {
3052
+ this.hashTreeParsers.forEach((entry) => {
3053
+ entry.parser.cleanUp();
3054
+ });
3055
+ this.hashTreeParsers.clear();
3056
+ this.hashTreeObjectId2ParticipantId.clear();
3057
+ }
2862
3058
  }
@@ -1,15 +1,33 @@
1
+ import {Enum} from '../constants';
1
2
  import {HtMeta} from '../hashTree/types';
2
3
 
4
+ export const EndMeetingReason = {
5
+ maxMeetingDuration: 'MAX_MEETING_DURATION',
6
+ allParticipantsLeft: 'ALL_PARTICIPANTS_LEFT',
7
+ sipHostLeft: 'SIP_HOST_LEFT',
8
+ noHost: 'NO_HOST',
9
+ waitingForMpsEndMeetingTimeout: 'WAITING_FOR_MPS_END_MEETING_TIMEOUT',
10
+ fraudDetection: 'FRAUD_DETECTION',
11
+ meetingEndedByHost: 'MEETING_ENDED_BY_HOST',
12
+ meetingUpdated: 'MEETING_UPDATED', // Locus code has comment about EndMeetingIfPossible reason for this one
13
+ meetingCancelled: 'MEETING_CANCELLED', // Locus code has comment about EndMeetingIfPossible reason for this one
14
+ autoEndWithSingleParticipant: 'AUTO_END_WITH_SINGLE_PARTICIPANT',
15
+ breakoutEnded: 'BREAKOUT_ENDED', // indicates that only a breakout session ended, not the whole meeting
16
+ } as const;
17
+
18
+ export type EndMeetingReason = Enum<typeof EndMeetingReason>;
19
+
3
20
  export type LocusFullState = {
4
21
  active: boolean;
5
22
  count: number;
6
23
  lastActive: string;
7
24
  locked: boolean;
8
25
  sessionId: string;
9
- seessionIds: string[];
26
+ sessionIds: string[];
10
27
  startTime: number;
11
28
  state: string;
12
29
  type: string;
30
+ endMeetingReason?: EndMeetingReason;
13
31
  };
14
32
 
15
33
  export type Links = {
@@ -59,3 +77,9 @@ export type ReplacesInfo = {
59
77
  replacedAt: string;
60
78
  sessionId: string;
61
79
  };
80
+
81
+ export const LocusErrorCodes = {
82
+ LOCUS_INACTIVE: 2403004,
83
+ } as const;
84
+
85
+ export type LocusErrorCodes = Enum<typeof LocusErrorCodes>;