@webex/plugin-meetings 3.12.0-next.20 → 3.12.0-next.22

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.
@@ -684,11 +684,31 @@ export default class LocusInfo extends EventsScope {
684
684
  * @param {LocusApiResponseBody} responseBody body of the http response from Locus API call
685
685
  * @returns {void}
686
686
  */
687
- handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
687
+ handleLocusAPIResponse(meeting: any, responseBody: LocusApiResponseBody): void {
688
688
  const isWrapped = 'locus' in responseBody;
689
689
  const locusUrl = isWrapped ? responseBody.locus?.url : responseBody.url;
690
690
  const hashTreeParserEntry = locusUrl && this.hashTreeParsers.get(locusUrl);
691
- 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
692
712
  if (isWrapped) {
693
713
  if (!responseBody.dataSets) {
694
714
  this.sendClassicVsHashTreeMismatchMetric(
@@ -704,20 +724,23 @@ export default class LocusInfo extends EventsScope {
704
724
  // update the data in our hash trees
705
725
  hashTreeParserEntry.parser.handleLocusUpdate(responseBody);
706
726
  } else {
707
- // 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
708
728
  hashTreeParserEntry.parser.handleLocusUpdate({locus: responseBody});
709
729
  }
710
- } else {
711
- if (isWrapped && responseBody.dataSets) {
712
- this.sendClassicVsHashTreeMismatchMetric(
713
- meeting,
714
- `unexpected hash tree dataSets in API response`
715
- );
716
- }
717
- // classic Locus delta
718
- const locus = isWrapped ? responseBody.locus : responseBody;
719
- this.handleLocusDelta(locus, meeting);
730
+
731
+ return;
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
+ );
720
740
  }
741
+
742
+ // classic Locus delta
743
+ this.handleLocusDelta(locus, meeting);
721
744
  }
722
745
 
723
746
  /**
@@ -946,6 +969,114 @@ export default class LocusInfo extends EventsScope {
946
969
  }
947
970
  }
948
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
+
949
1080
  /**
950
1081
  * Checks if the hash tree message should trigger a switch to a different HashTreeParser
951
1082
  *
@@ -994,36 +1125,12 @@ export default class LocusInfo extends EventsScope {
994
1125
  if (entry.parser.state === 'stopped') {
995
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
996
1127
  // this happens when you move from breakout A -> breakout B -> back to breakout A
997
- if (replaces) {
998
- if (replaces.replacedAt > (entry.replacedAt || '')) {
999
- LoggerProxy.logger.info(
1000
- `Locus-info:index#handleHashTreeParserSwitch --> resuming a HashTreeParser for locusUrl=${message.locusUrl}, which replaces ${replaces.locusUrl}`
1001
- );
1002
- const replacedEntry = this.hashTreeParsers.get(replaces.locusUrl);
1003
-
1004
- if (replacedEntry) {
1005
- replacedEntry.replacedAt = replaces.replacedAt;
1006
- entry.initializedFromHashTree = false;
1007
- this.hashTreeObjectId2ParticipantId.clear();
1008
-
1009
- replacedEntry.parser.stop();
1010
- entry.parser.resume(message);
1011
- } else {
1012
- LoggerProxy.logger.warn(
1013
- `Locus-info:index#handleHashTreeParserSwitch --> the parser that is supposed to be replaced with the currently resumed parser is not found, locusUrl=${replaces.locusUrl}`
1014
- );
1015
- }
1016
- } else {
1017
- LoggerProxy.logger.info(
1018
- `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`
1019
- );
1020
- }
1021
-
1022
- return true;
1023
- }
1024
-
1025
- LoggerProxy.logger.info(
1026
- `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)
1027
1134
  );
1028
1135
 
1029
1136
  return true;
@@ -1064,6 +1171,21 @@ export default class LocusInfo extends EventsScope {
1064
1171
  }
1065
1172
  }
1066
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
+ }
1187
+ }
1188
+
1067
1189
  /**
1068
1190
  * Callback registered with HashTreeParser to receive locus info updates.
1069
1191
  * Updates our locus info based on the data parsed by the hash tree parser.
@@ -1202,11 +1324,17 @@ export default class LocusInfo extends EventsScope {
1202
1324
  */
1203
1325
  parse(meeting: any, data: any) {
1204
1326
  if (this.hashTreeParsers.size > 0) {
1205
- this.handleHashTreeMessage(
1206
- meeting,
1207
- data.eventType,
1208
- data.stateElementsMessage as HashTreeMessage
1209
- );
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
+ }
1210
1338
  } else {
1211
1339
  const {eventType} = data;
1212
1340
 
@@ -71,6 +71,7 @@ import {HashTreeObject} from '../hashTree/types';
71
71
  import {isSelf} from '../hashTree/utils';
72
72
 
73
73
  import {createLocusFromHashTreeMessage, findMeetingForHashTreeMessage} from '../locus-info';
74
+ import {LocusDTO} from '../locus-info/types';
74
75
 
75
76
  let mediaLogger;
76
77
 
@@ -1886,23 +1887,20 @@ export default class Meetings extends WebexPlugin {
1886
1887
  * @public
1887
1888
  * @memberof Meetings
1888
1889
  */
1889
- public syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1890
+ public async syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1890
1891
  // @ts-ignore
1891
1892
  if (this.webex.credentials.isUnverifiedGuest) {
1892
1893
  LoggerProxy.logger.info(
1893
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1894
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1894
1895
  );
1895
-
1896
- return Promise.resolve();
1897
- }
1898
-
1899
- return this.request
1900
- .getActiveMeetings()
1901
- .then((locusArray: any) => {
1896
+ } else {
1897
+ try {
1898
+ const locusArray = await this.request.getActiveMeetings();
1902
1899
  const activeLocusUrl: string[] = [];
1903
1900
 
1904
1901
  if (locusArray?.loci && locusArray.loci.length > 0) {
1905
1902
  const lociToUpdate = this.sortLocusArrayToUpdate(locusArray.loci);
1903
+
1906
1904
  lociToUpdate.forEach((locus) => {
1907
1905
  activeLocusUrl.push(locus.url);
1908
1906
  this.handleLocusEvent({
@@ -1920,21 +1918,48 @@ export default class Meetings extends WebexPlugin {
1920
1918
  // (they had a locusUrl previously but are no longer active) in the sync
1921
1919
  for (const meeting of Object.values(meetingsCollection)) {
1922
1920
  // @ts-ignore
1923
- const {locusUrl} = meeting;
1921
+ const {locusUrl, locusInfo} = meeting;
1924
1922
  if ((keepOnlyLocusMeetings || locusUrl) && !activeLocusUrl.includes(locusUrl)) {
1925
- // destroy function also uploads logs
1926
- // @ts-ignore
1927
- this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1923
+ const globalMeetingId = locusInfo?.info?.globalMeetingId;
1924
+
1925
+ if (
1926
+ globalMeetingId &&
1927
+ locusArray?.loci?.some(
1928
+ (locus: LocusDTO) => locus.info?.globalMeetingId === globalMeetingId
1929
+ )
1930
+ ) {
1931
+ // don't destroy the meeting as Locus API still returned some Locus that shares
1932
+ // the same globalMeetingId - that happens for example if a webinar user (who hasn't scheduled it)
1933
+ // is in a breakout and gets moved to a different breakout while we were offline
1934
+ } else {
1935
+ // destroy function also uploads logs
1936
+ // @ts-ignore
1937
+ this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1938
+ }
1928
1939
  }
1929
1940
  }
1930
1941
  }
1931
- })
1932
- .catch((error) => {
1942
+ } catch (error) {
1933
1943
  LoggerProxy.logger.error(
1934
1944
  `Meetings:index#syncMeetings --> failed to sync meetings, ${error}`
1935
1945
  );
1936
- throw new Error(error);
1937
- });
1946
+ throw error;
1947
+ }
1948
+ }
1949
+
1950
+ // Trigger hash tree syncs for all remaining meetings
1951
+ const remainingMeetings = this.meetingCollection.getAll();
1952
+ const syncPromises = [];
1953
+
1954
+ for (const meeting of Object.values(remainingMeetings) as any[]) {
1955
+ if (meeting.locusInfo) {
1956
+ syncPromises.push(meeting.locusInfo.syncAllHashTreeDatasets());
1957
+ }
1958
+ }
1959
+
1960
+ if (syncPromises.length > 0) {
1961
+ await Promise.all(syncPromises);
1962
+ }
1938
1963
  }
1939
1964
 
1940
1965
  /**