@webex/plugin-meetings 3.12.0-next.3 → 3.12.0-next.31

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 (90) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/constants.js +3 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/controls-options-manager/constants.js +11 -1
  8. package/dist/controls-options-manager/constants.js.map +1 -1
  9. package/dist/controls-options-manager/index.js +23 -21
  10. package/dist/controls-options-manager/index.js.map +1 -1
  11. package/dist/controls-options-manager/util.js +91 -0
  12. package/dist/controls-options-manager/util.js.map +1 -1
  13. package/dist/hashTree/constants.js +10 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTreeParser.js +550 -346
  16. package/dist/hashTree/hashTreeParser.js.map +1 -1
  17. package/dist/hashTree/utils.js +22 -0
  18. package/dist/hashTree/utils.js.map +1 -1
  19. package/dist/interceptors/locusRetry.js +23 -8
  20. package/dist/interceptors/locusRetry.js.map +1 -1
  21. package/dist/interpretation/index.js +1 -1
  22. package/dist/interpretation/siLanguage.js +1 -1
  23. package/dist/locus-info/index.js +222 -61
  24. package/dist/locus-info/index.js.map +1 -1
  25. package/dist/meeting/index.js +372 -292
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/util.js +1 -0
  28. package/dist/meeting/util.js.map +1 -1
  29. package/dist/meetings/index.js +146 -62
  30. package/dist/meetings/index.js.map +1 -1
  31. package/dist/meetings/util.js +39 -5
  32. package/dist/meetings/util.js.map +1 -1
  33. package/dist/member/index.js +10 -0
  34. package/dist/member/index.js.map +1 -1
  35. package/dist/member/types.js.map +1 -1
  36. package/dist/member/util.js +3 -0
  37. package/dist/member/util.js.map +1 -1
  38. package/dist/metrics/constants.js +5 -1
  39. package/dist/metrics/constants.js.map +1 -1
  40. package/dist/multistream/sendSlotManager.js +116 -2
  41. package/dist/multistream/sendSlotManager.js.map +1 -1
  42. package/dist/types/constants.d.ts +1 -0
  43. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  44. package/dist/types/hashTree/constants.d.ts +1 -0
  45. package/dist/types/hashTree/hashTreeParser.d.ts +53 -15
  46. package/dist/types/hashTree/utils.d.ts +11 -0
  47. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  48. package/dist/types/locus-info/index.d.ts +38 -5
  49. package/dist/types/meeting/index.d.ts +11 -0
  50. package/dist/types/member/index.d.ts +1 -0
  51. package/dist/types/member/types.d.ts +1 -0
  52. package/dist/types/member/util.d.ts +1 -0
  53. package/dist/types/metrics/constants.d.ts +4 -0
  54. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  55. package/dist/webinar/index.js +301 -226
  56. package/dist/webinar/index.js.map +1 -1
  57. package/package.json +16 -16
  58. package/src/constants.ts +1 -0
  59. package/src/controls-options-manager/constants.ts +14 -1
  60. package/src/controls-options-manager/index.ts +26 -19
  61. package/src/controls-options-manager/util.ts +81 -1
  62. package/src/hashTree/constants.ts +9 -0
  63. package/src/hashTree/hashTreeParser.ts +273 -154
  64. package/src/hashTree/utils.ts +17 -0
  65. package/src/interceptors/locusRetry.ts +25 -4
  66. package/src/locus-info/index.ts +233 -79
  67. package/src/meeting/index.ts +98 -11
  68. package/src/meeting/util.ts +1 -0
  69. package/src/meetings/index.ts +58 -34
  70. package/src/meetings/util.ts +44 -1
  71. package/src/member/index.ts +10 -0
  72. package/src/member/types.ts +1 -0
  73. package/src/member/util.ts +3 -0
  74. package/src/metrics/constants.ts +5 -0
  75. package/src/multistream/sendSlotManager.ts +97 -3
  76. package/src/webinar/index.ts +75 -1
  77. package/test/unit/spec/controls-options-manager/index.js +114 -6
  78. package/test/unit/spec/controls-options-manager/util.js +165 -0
  79. package/test/unit/spec/hashTree/hashTreeParser.ts +839 -37
  80. package/test/unit/spec/hashTree/utils.ts +88 -1
  81. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  82. package/test/unit/spec/locus-info/index.js +262 -64
  83. package/test/unit/spec/meeting/index.js +54 -36
  84. package/test/unit/spec/meeting/utils.js +4 -0
  85. package/test/unit/spec/meetings/index.js +190 -8
  86. package/test/unit/spec/meetings/utils.js +124 -0
  87. package/test/unit/spec/member/index.js +7 -0
  88. package/test/unit/spec/member/util.js +24 -0
  89. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  90. package/test/unit/spec/webinar/index.ts +60 -0
@@ -60,3 +60,20 @@ export const deleteNestedObjectsWithHtMeta = (
60
60
  }
61
61
  }
62
62
  };
63
+
64
+ /**
65
+ * Reorders items so that those matching the given priority list come first (in priority order),
66
+ * followed by everything else in their original order.
67
+ *
68
+ * @param {Array<T>} items - The items to reorder
69
+ * @param {string[]} priority - Ordered list of names that should come first
70
+ * @returns {Array<T>} A new array with prioritized items first
71
+ */
72
+ export function sortByInitPriority<T extends {name: string}>(items: T[], priority: string[]): T[] {
73
+ const prioritized = priority
74
+ .map((name) => items.find((item) => item.name === name))
75
+ .filter(Boolean) as T[];
76
+ const rest = items.filter((item) => !priority.includes(item.name));
77
+
78
+ return [...prioritized, ...rest];
79
+ }
@@ -18,12 +18,33 @@ export default class LocusRetryStatusInterceptor extends Interceptor {
18
18
  }
19
19
 
20
20
  /**
21
- * Handle response errors
22
- * @param {Object} options
23
- * @param {WebexHttpError} reason
24
- * @returns {Promise<WebexHttpError>}
21
+ * Check whether a URI is a Locus /hashtree or /sync endpoint.
22
+ * @param {string} uri
23
+ * @returns {boolean}
25
24
  */
25
+ private static isLocusHashtreeOrSync(uri: string): boolean {
26
+ try {
27
+ const {pathname} = new URL(uri);
28
+
29
+ return (
30
+ pathname.includes('/locus/') &&
31
+ (pathname.endsWith('/hashtree') || pathname.endsWith('/sync'))
32
+ );
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
26
38
  onResponseError(options, reason) {
39
+ // Don't retry /hashtree or /sync calls for 429 or any 5xx — during a sync storm retries
40
+ // make things worse. The normal sync timers will handle recovery for these endpoints.
41
+ if (
42
+ (reason.statusCode === 429 || reason.statusCode >= 500) &&
43
+ LocusRetryStatusInterceptor.isLocusHashtreeOrSync(options.uri)
44
+ ) {
45
+ return Promise.reject(reason);
46
+ }
47
+
27
48
  if ((reason.statusCode === 503 || reason.statusCode === 429) && options.uri.includes('locus')) {
28
49
  const hasRetriedLocusRequest = rateLimitExpiryTime.get(this);
29
50
  const retryAfterTime = options.headers['retry-after'] || 2000;
@@ -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
 
@@ -656,7 +658,7 @@ export default class LocusInfo extends EventsScope {
656
658
  );
657
659
  // first create the HashTreeParser, but don't initialize it with any data yet
658
660
  const hashTreeParser = this.createHashTreeParser({
659
- locusUrl: data.locus.url,
661
+ locusUrl: data.locus.url as string,
660
662
  initialLocus: {
661
663
  locus: null,
662
664
  dataSets: [], // empty, because we don't have them yet
@@ -682,11 +684,31 @@ export default class LocusInfo extends EventsScope {
682
684
  * @param {LocusApiResponseBody} responseBody body of the http response from Locus API call
683
685
  * @returns {void}
684
686
  */
685
- handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
687
+ handleLocusAPIResponse(meeting: any, responseBody: LocusApiResponseBody): void {
686
688
  const isWrapped = 'locus' in responseBody;
687
689
  const locusUrl = isWrapped ? responseBody.locus?.url : responseBody.url;
688
690
  const hashTreeParserEntry = locusUrl && this.hashTreeParsers.get(locusUrl);
689
- if (hashTreeParserEntry) {
691
+ const locus = isWrapped
692
+ ? (responseBody as {locus: LocusDTO}).locus
693
+ : (responseBody as LocusDTO);
694
+
695
+ if (this.hashTreeParsers.size > 0) {
696
+ // We are in hash tree mode. Check if we need to create/reactivate a parser for this locusUrl.
697
+ if (!hashTreeParserEntry || hashTreeParserEntry.parser.state === 'stopped') {
698
+ if (!locusUrl) {
699
+ LoggerProxy.logger.warn(
700
+ 'Locus-info:index#handleLocusAPIResponse --> API response has no locusUrl, cannot handle hash tree parser switch'
701
+ );
702
+
703
+ return;
704
+ }
705
+
706
+ this.handleHashTreeParserSwitchForAPIResponse(locusUrl, locus);
707
+
708
+ return;
709
+ }
710
+
711
+ // Active parser found - pass the API response to it
690
712
  if (isWrapped) {
691
713
  if (!responseBody.dataSets) {
692
714
  this.sendClassicVsHashTreeMismatchMetric(
@@ -702,20 +724,23 @@ export default class LocusInfo extends EventsScope {
702
724
  // update the data in our hash trees
703
725
  hashTreeParserEntry.parser.handleLocusUpdate(responseBody);
704
726
  } else {
705
- // LocusDTO without wrapper - pass it through as if it had no dataSets
727
+ // LocusDTO without wrapper - pass it through as if it had no dataSets nor metadata
706
728
  hashTreeParserEntry.parser.handleLocusUpdate({locus: responseBody});
707
729
  }
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);
730
+
731
+ return;
718
732
  }
733
+
734
+ // No hash tree parsers - classic Locus mode
735
+ if (isWrapped && responseBody.dataSets) {
736
+ this.sendClassicVsHashTreeMismatchMetric(
737
+ meeting,
738
+ `unexpected hash tree dataSets in API response`
739
+ );
740
+ }
741
+
742
+ // classic Locus delta
743
+ this.handleLocusDelta(locus, meeting);
719
744
  }
720
745
 
721
746
  /**
@@ -944,6 +969,114 @@ export default class LocusInfo extends EventsScope {
944
969
  }
945
970
  }
946
971
 
972
+ /**
973
+ * Helper that handles the common logic for reactivating a stopped HashTreeParser when
974
+ * a newer "replaces" is detected. Used by both the message and API response parser switch methods.
975
+ *
976
+ * @param {string} callerName - name of the calling method, used in log messages
977
+ * @param {string} locusUrl - the locus URL of the stopped parser
978
+ * @param {HashTreeParserEntry} stoppedEntry - the stopped parser entry
979
+ * @param {ReplacesInfo} replaces - replacement info extracted from self
980
+ * @param {Function} resumeCallback - callback to invoke after reactivation to resume the parser
981
+ * @returns {void}
982
+ */
983
+ private resumeStoppedParser(
984
+ callerName: string,
985
+ locusUrl: string,
986
+ stoppedEntry: HashTreeParserEntry,
987
+ replaces: ReplacesInfo | undefined,
988
+ resumeCallback: () => void
989
+ ): void {
990
+ // this check is just for typescript, it should never happen, replaces should always be defined
991
+ if (!replaces) {
992
+ LoggerProxy.logger.info(
993
+ `Locus-info:index#${callerName} --> received data for stopped HashTreeParser with locusUrl ${locusUrl}, but no replaces info provided, so not re-activating the parser`
994
+ );
995
+
996
+ return;
997
+ }
998
+
999
+ if (replaces.replacedAt <= (stoppedEntry.replacedAt || '')) {
1000
+ LoggerProxy.logger.info(
1001
+ `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`
1002
+ );
1003
+
1004
+ return;
1005
+ }
1006
+
1007
+ LoggerProxy.logger.info(
1008
+ `Locus-info:index#${callerName} --> reactivating HashTreeParser for locusUrl=${locusUrl}, which replaces ${replaces.locusUrl}`
1009
+ );
1010
+
1011
+ const replacedEntry = this.hashTreeParsers.get(replaces.locusUrl);
1012
+
1013
+ if (replacedEntry) {
1014
+ replacedEntry.replacedAt = replaces.replacedAt;
1015
+ replacedEntry.parser.stop();
1016
+ } else {
1017
+ LoggerProxy.logger.warn(
1018
+ `Locus-info:index#${callerName} --> the parser that is supposed to be replaced with the currently reactivated parser is not found, locusUrl=${replaces.locusUrl}`
1019
+ );
1020
+ }
1021
+
1022
+ stoppedEntry.initializedFromHashTree = false;
1023
+ this.hashTreeObjectId2ParticipantId.clear();
1024
+
1025
+ resumeCallback();
1026
+ }
1027
+
1028
+ /**
1029
+ * Handles an API response whose locusUrl doesn't match any active HashTreeParser
1030
+ * (either no entry exists, or the existing entry is stopped).
1031
+ * Creates a new parser or reactivates a stopped one using initializeFromGetLociResponse.
1032
+ *
1033
+ * @param {string} locusUrl - the locus URL from the API response
1034
+ * @param {LocusDTO} locus - the locus DTO from the API response
1035
+ * @returns {void}
1036
+ */
1037
+ private handleHashTreeParserSwitchForAPIResponse(locusUrl: string, locus: LocusDTO): void {
1038
+ const entry = this.hashTreeParsers.get(locusUrl);
1039
+
1040
+ const replaces = getReplaceInfoFromSelf(
1041
+ locus.self,
1042
+ // @ts-ignore
1043
+ this.webex.internal.device.url
1044
+ );
1045
+
1046
+ if (!entry) {
1047
+ LoggerProxy.logger.info(
1048
+ `Locus-info:index#handleHashTreeParserSwitchForAPIResponse --> no parser for locusUrl ${locusUrl}, creating a new one`
1049
+ );
1050
+
1051
+ const parser = this.createHashTreeParser({
1052
+ locusUrl,
1053
+ initialLocus: {locus: null, dataSets: []},
1054
+ metadata: null,
1055
+ replacedAt: replaces?.replacedAt,
1056
+ });
1057
+
1058
+ parser.initializeFromGetLociResponse(locus);
1059
+
1060
+ return;
1061
+ }
1062
+
1063
+ if (entry.parser.state !== 'stopped') {
1064
+ LoggerProxy.logger.warn(
1065
+ `Locus-info:index#handleHashTreeParserSwitchForAPIResponse --> unexpected parser state "${entry.parser.state}" for locusUrl ${locusUrl}`
1066
+ );
1067
+
1068
+ return;
1069
+ }
1070
+
1071
+ this.resumeStoppedParser(
1072
+ 'handleHashTreeParserSwitchForAPIResponse',
1073
+ locusUrl,
1074
+ entry,
1075
+ replaces,
1076
+ () => entry.parser.resumeFromApiResponse(locus)
1077
+ );
1078
+ }
1079
+
947
1080
  /**
948
1081
  * Checks if the hash tree message should trigger a switch to a different HashTreeParser
949
1082
  *
@@ -965,7 +1098,7 @@ export default class LocusInfo extends EventsScope {
965
1098
  // but it's buried inside the message, we need to find it and pass it to HashTreeParser constructor
966
1099
  const metadata = message.locusStateElements?.find((el) => isMetadata(el));
967
1100
 
968
- if (metadata?.data?.visibleDataSets?.length > 0) {
1101
+ if (metadata && metadata.data?.visibleDataSets?.length > 0) {
969
1102
  LoggerProxy.logger.info(
970
1103
  `Locus-info:index#handleHashTreeParserSwitch --> no hash tree parser found for locusUrl ${message.locusUrl}, creating a new one`
971
1104
  );
@@ -992,36 +1125,12 @@ export default class LocusInfo extends EventsScope {
992
1125
  if (entry.parser.state === 'stopped') {
993
1126
  // 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
1127
  // 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`
1128
+ this.resumeStoppedParser(
1129
+ 'handleHashTreeParserSwitch',
1130
+ message.locusUrl,
1131
+ entry,
1132
+ replaces,
1133
+ () => entry.parser.resumeFromMessage(message)
1025
1134
  );
1026
1135
 
1027
1136
  return true;
@@ -1056,7 +1165,25 @@ export default class LocusInfo extends EventsScope {
1056
1165
 
1057
1166
  const entry = this.hashTreeParsers.get(message.locusUrl);
1058
1167
 
1059
- entry.parser.handleMessage(message);
1168
+ // the check is just for typescript, the case of no entry in hashTreeParsers is handled in handleHashTreeParserSwitch() above
1169
+ if (entry) {
1170
+ entry.parser.handleMessage(message);
1171
+ }
1172
+ }
1173
+
1174
+ /**
1175
+ * Triggers a sync of all hash tree datasets for all hash tree parsers associated with this meeting.
1176
+ * The syncs are executed sequentially within each parser.
1177
+ *
1178
+ * @returns {Promise<void>}
1179
+ */
1180
+ async syncAllHashTreeDatasets(): Promise<void> {
1181
+ for (const [, entry] of this.hashTreeParsers) {
1182
+ if (entry.parser) {
1183
+ // eslint-disable-next-line no-await-in-loop
1184
+ await entry.parser.syncAllDatasets();
1185
+ }
1186
+ }
1060
1187
  }
1061
1188
 
1062
1189
  /**
@@ -1064,16 +1191,11 @@ export default class LocusInfo extends EventsScope {
1064
1191
  * Updates our locus info based on the data parsed by the hash tree parser.
1065
1192
  *
1066
1193
  * @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.
1194
+ * @param {LocusInfoUpdate} update - Details about the update.
1069
1195
  * @returns {void}
1070
1196
  */
1071
- private updateFromHashTree(
1072
- locusUrl: string,
1073
- updateType: LocusInfoUpdateType,
1074
- data?: {updatedObjects: HashTreeObject[]}
1075
- ) {
1076
- switch (updateType) {
1197
+ private updateFromHashTree(locusUrl: string, update: LocusInfoUpdate) {
1198
+ switch (update.updateType) {
1077
1199
  case LocusInfoUpdateType.OBJECTS_UPDATED: {
1078
1200
  // initialize our new locus
1079
1201
  let locus: LocusDTO = {
@@ -1087,7 +1209,7 @@ export default class LocusInfo extends EventsScope {
1087
1209
  // first go over all the updates and check what happens with the main locus object
1088
1210
  let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
1089
1211
  LocusObjectStateAfterUpdates.unchanged;
1090
- data.updatedObjects.forEach((object) => {
1212
+ update.updatedObjects.forEach((object) => {
1091
1213
  if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
1092
1214
  if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
1093
1215
  // this code doesn't supported it right now,
@@ -1116,6 +1238,14 @@ export default class LocusInfo extends EventsScope {
1116
1238
 
1117
1239
  const hashTreeParserEntry = this.hashTreeParsers.get(locusUrl);
1118
1240
 
1241
+ if (!hashTreeParserEntry) {
1242
+ LoggerProxy.logger.warn(
1243
+ `Locus-info:index#updateFromHashTree --> no HashTreeParser found for locusUrl ${locusUrl} when trying to apply updates from hash tree`
1244
+ );
1245
+
1246
+ return;
1247
+ }
1248
+
1119
1249
  if (!hashTreeParserEntry.initializedFromHashTree) {
1120
1250
  // this is the first time we're getting an update for this locusUrl,
1121
1251
  // so it's probably a move to/from breakout. We need to start from a clean state,
@@ -1124,7 +1254,8 @@ export default class LocusInfo extends EventsScope {
1124
1254
  `Locus-info:index#updateFromHashTree --> first INITIAL update for locusUrl ${locusUrl}, starting from empty state`
1125
1255
  );
1126
1256
  hashTreeParserEntry.initializedFromHashTree = true;
1127
- locus.jsSdkMeta.forceReplaceMembers = true;
1257
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1258
+ locus.jsSdkMeta!.forceReplaceMembers = true;
1128
1259
  } else if (
1129
1260
  // if Locus object is unchanged or removed, we need to keep using the existing locus
1130
1261
  // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
@@ -1137,7 +1268,7 @@ export default class LocusInfo extends EventsScope {
1137
1268
  // copy over all of existing locus except participants
1138
1269
  LocusDtoTopLevelKeys.forEach((key) => {
1139
1270
  if (key !== 'participants') {
1140
- locus[key] = cloneDeep(this[key]);
1271
+ (locus as Record<string, any>)[key] = cloneDeep((this as Record<string, any>)[key]);
1141
1272
  }
1142
1273
  });
1143
1274
  } else {
@@ -1145,14 +1276,16 @@ export default class LocusInfo extends EventsScope {
1145
1276
  // (except participants, which need to stay empty - that means "no participant changes")
1146
1277
  Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
1147
1278
  if (locusDtoKey !== 'participants') {
1148
- locus[locusDtoKey] = cloneDeep(this[locusDtoKey]);
1279
+ (locus as Record<string, any>)[locusDtoKey] = cloneDeep(
1280
+ (this as Record<string, any>)[locusDtoKey]
1281
+ );
1149
1282
  }
1150
1283
  });
1151
1284
  }
1152
1285
 
1153
1286
  LoggerProxy.logger.info(
1154
1287
  `Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
1155
- data.updatedObjects.map((o) => ({
1288
+ update.updatedObjects.map((o) => ({
1156
1289
  type: o.htMeta.elementId.type,
1157
1290
  id: o.htMeta.elementId.id,
1158
1291
  hasData: !!o.data,
@@ -1160,7 +1293,7 @@ export default class LocusInfo extends EventsScope {
1160
1293
  )}`
1161
1294
  );
1162
1295
  // now apply all the updates from the hash tree onto the locus
1163
- data.updatedObjects.forEach((object) => {
1296
+ update.updatedObjects.forEach((object) => {
1164
1297
  locus = this.updateLocusFromHashTreeObject(object, locus);
1165
1298
  });
1166
1299
 
@@ -1191,11 +1324,17 @@ export default class LocusInfo extends EventsScope {
1191
1324
  */
1192
1325
  parse(meeting: any, data: any) {
1193
1326
  if (this.hashTreeParsers.size > 0) {
1194
- this.handleHashTreeMessage(
1195
- meeting,
1196
- data.eventType,
1197
- data.stateElementsMessage as HashTreeMessage
1198
- );
1327
+ if (data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS) {
1328
+ // sync meetings response follows the format of "not wrapped" locus API responses,
1329
+ // so has no dataSets nor Metadata
1330
+ this.handleLocusAPIResponse(meeting, {...data.locus});
1331
+ } else {
1332
+ this.handleHashTreeMessage(
1333
+ meeting,
1334
+ data.eventType,
1335
+ data.stateElementsMessage as HashTreeMessage
1336
+ );
1337
+ }
1199
1338
  } else {
1200
1339
  const {eventType} = data;
1201
1340
 
@@ -1260,16 +1399,16 @@ export default class LocusInfo extends EventsScope {
1260
1399
  * @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
1261
1400
  * @param {object} locus locus object
1262
1401
  * @param {object} metadata locus hash trees metadata
1263
- * @param {string} eventType locus event
1264
1402
  * @param {DataSet[]} dataSets
1403
+ * @param {string} eventType locus event
1265
1404
  * @returns {void}
1266
1405
  */
1267
1406
  private onFullLocusWithHashTrees(
1268
1407
  debugText: string,
1269
1408
  locus: any,
1270
1409
  metadata: Metadata,
1271
- eventType?: string,
1272
- dataSets?: Array<DataSet>
1410
+ dataSets: Array<DataSet>,
1411
+ eventType?: string
1273
1412
  ) {
1274
1413
  if (!this.hashTreeParsers.has(locus.url)) {
1275
1414
  LoggerProxy.logger.info(
@@ -1289,7 +1428,8 @@ export default class LocusInfo extends EventsScope {
1289
1428
  metadata,
1290
1429
  });
1291
1430
  // we have a full locus to start with, so we consider Locus info to be "initialized"
1292
- this.hashTreeParsers.get(locus.url).initializedFromHashTree = true;
1431
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1432
+ this.hashTreeParsers.get(locus.url)!.initializedFromHashTree = true;
1293
1433
  this.onFullLocusCommon(locus, eventType);
1294
1434
  } else {
1295
1435
  // in this case the Locus we're getting is not necessarily the full one
@@ -1351,7 +1491,7 @@ export default class LocusInfo extends EventsScope {
1351
1491
  );
1352
1492
  }
1353
1493
  // this is the new hashmap Locus DTO format (only applicable to webinars for now)
1354
- this.onFullLocusWithHashTrees(debugText, locus, metadata, eventType, dataSets);
1494
+ this.onFullLocusWithHashTrees(debugText, locus, metadata, dataSets, eventType);
1355
1495
  } else {
1356
1496
  this.onFullLocusClassic(debugText, locus, eventType);
1357
1497
  }
@@ -1495,7 +1635,7 @@ export default class LocusInfo extends EventsScope {
1495
1635
  * @memberof LocusInfo
1496
1636
  */
1497
1637
  updateLocusInfo(locus) {
1498
- if (locus.self?.reason === 'MOVED' && locus.self?.state === 'LEFT') {
1638
+ if (MeetingsUtil.isSelfMovedOrBreakoutEnded(locus)) {
1499
1639
  // When moved to a breakout session locus sends a message for the previous locus
1500
1640
  // indicating that we have been moved. It isn't helpful to continue parsing this
1501
1641
  // as it gets interpreted as if we have left the call
@@ -2587,6 +2727,7 @@ export default class LocusInfo extends EventsScope {
2587
2727
  {
2588
2728
  muted: parsedSelves.current.remoteMuted,
2589
2729
  unmuteAllowed: parsedSelves.current.unmuteAllowed,
2730
+ modifiedBy: parsedSelves.current.modifiedBy ?? null,
2590
2731
  }
2591
2732
  );
2592
2733
  }
@@ -2859,4 +3000,17 @@ export default class LocusInfo extends EventsScope {
2859
3000
  clearMainSessionLocusCache() {
2860
3001
  this.mainSessionLocusCache = null;
2861
3002
  }
3003
+
3004
+ /**
3005
+ * Cleans up all hash tree parsers and clears internal maps.
3006
+ * @returns {void}
3007
+ * @memberof LocusInfo
3008
+ */
3009
+ cleanUp() {
3010
+ this.hashTreeParsers.forEach((entry) => {
3011
+ entry.parser.cleanUp();
3012
+ });
3013
+ this.hashTreeParsers.clear();
3014
+ this.hashTreeObjectId2ParticipantId.clear();
3015
+ }
2862
3016
  }