@webex/plugin-meetings 3.11.0-next.23 → 3.11.0-next.25

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.
@@ -29,6 +29,7 @@ export interface HashTreeMessage {
29
29
  locusStateElements?: Array<HashTreeObject>;
30
30
  locusSessionId?: string;
31
31
  locusUrl: string;
32
+ heartbeatIntervalMs?: number;
32
33
  }
33
34
 
34
35
  export interface VisibleDataSetInfo {
@@ -45,6 +46,7 @@ export interface Metadata {
45
46
  interface InternalDataSet extends DataSet {
46
47
  hashTree?: HashTree; // set only for visible data sets
47
48
  timer?: ReturnType<typeof setTimeout>;
49
+ heartbeatWatchdogTimer?: ReturnType<typeof setTimeout>;
48
50
  }
49
51
 
50
52
  type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
@@ -88,6 +90,7 @@ class HashTreeParser {
88
90
  locusInfoUpdateCallback: LocusInfoUpdateCallback;
89
91
  visibleDataSets: VisibleDataSetInfo[];
90
92
  debugId: string;
93
+ heartbeatIntervalMs?: number;
91
94
 
92
95
  /**
93
96
  * Constructor for HashTreeParser
@@ -726,11 +729,15 @@ class HashTreeParser {
726
729
  private deleteHashTree(dataSetName: string) {
727
730
  this.dataSets[dataSetName].hashTree = undefined;
728
731
 
729
- // we also need to stop the timer as there is no hash tree anymore to sync
732
+ // we also need to stop the timers as there is no hash tree anymore to sync
730
733
  if (this.dataSets[dataSetName].timer) {
731
734
  clearTimeout(this.dataSets[dataSetName].timer);
732
735
  this.dataSets[dataSetName].timer = undefined;
733
736
  }
737
+ if (this.dataSets[dataSetName].heartbeatWatchdogTimer) {
738
+ clearTimeout(this.dataSets[dataSetName].heartbeatWatchdogTimer);
739
+ this.dataSets[dataSetName].heartbeatWatchdogTimer = undefined;
740
+ }
734
741
  }
735
742
 
736
743
  /**
@@ -928,48 +935,50 @@ class HashTreeParser {
928
935
  }
929
936
  }
930
937
 
931
- // by this point we now have this.dataSets setup for data sets from this message
932
- // and hash trees created for the new visible data sets,
933
- // so we can now process all the updates from the message
934
- dataSets.forEach((dataSet) => {
935
- if (this.dataSets[dataSet.name]) {
936
- const {hashTree} = this.dataSets[dataSet.name];
937
-
938
- if (hashTree) {
939
- const locusStateElementsForThisSet = message.locusStateElements.filter((object) =>
940
- object.htMeta.dataSetNames.includes(dataSet.name)
941
- );
942
-
943
- const appliedChangesList = hashTree.updateItems(
944
- locusStateElementsForThisSet.map((object) =>
945
- object.data
946
- ? {operation: 'update', item: object.htMeta.elementId}
947
- : {operation: 'remove', item: object.htMeta.elementId}
948
- )
949
- );
950
-
951
- zip(appliedChangesList, locusStateElementsForThisSet).forEach(
952
- ([changeApplied, object]) => {
953
- if (changeApplied) {
954
- if (isSelf(object) && !object.data) {
955
- isRosterDropped = true;
938
+ if (message.locusStateElements?.length > 0) {
939
+ // by this point we now have this.dataSets setup for data sets from this message
940
+ // and hash trees created for the new visible data sets,
941
+ // so we can now process all the updates from the message
942
+ dataSets.forEach((dataSet) => {
943
+ if (this.dataSets[dataSet.name]) {
944
+ const {hashTree} = this.dataSets[dataSet.name];
945
+
946
+ if (hashTree) {
947
+ const locusStateElementsForThisSet = message.locusStateElements.filter((object) =>
948
+ object.htMeta.dataSetNames.includes(dataSet.name)
949
+ );
950
+
951
+ const appliedChangesList = hashTree.updateItems(
952
+ locusStateElementsForThisSet.map((object) =>
953
+ object.data
954
+ ? {operation: 'update', item: object.htMeta.elementId}
955
+ : {operation: 'remove', item: object.htMeta.elementId}
956
+ )
957
+ );
958
+
959
+ zip(appliedChangesList, locusStateElementsForThisSet).forEach(
960
+ ([changeApplied, object]) => {
961
+ if (changeApplied) {
962
+ if (isSelf(object) && !object.data) {
963
+ isRosterDropped = true;
964
+ }
965
+ // add to updatedObjects so that our locus DTO will get updated with the new object
966
+ updatedObjects.push(object);
956
967
  }
957
- // add to updatedObjects so that our locus DTO will get updated with the new object
958
- updatedObjects.push(object);
959
968
  }
960
- }
961
- );
962
- } else {
963
- LoggerProxy.logger.info(
964
- `Locus-info:index#parseMessage --> ${this.debugId} unexpected (not visible) dataSet ${dataSet.name} received in hash tree message`
965
- );
969
+ );
970
+ } else {
971
+ LoggerProxy.logger.info(
972
+ `Locus-info:index#parseMessage --> ${this.debugId} unexpected (not visible) dataSet ${dataSet.name} received in hash tree message`
973
+ );
974
+ }
966
975
  }
967
- }
968
976
 
969
- if (!isRosterDropped) {
970
- this.runSyncAlgorithm(dataSet);
971
- }
972
- });
977
+ if (!isRosterDropped) {
978
+ this.runSyncAlgorithm(dataSet);
979
+ }
980
+ });
981
+ }
973
982
 
974
983
  if (isRosterDropped) {
975
984
  LoggerProxy.logger.info(
@@ -1005,11 +1014,20 @@ class HashTreeParser {
1005
1014
  * @returns {void}
1006
1015
  */
1007
1016
  async handleMessage(message: HashTreeMessage, debugText?: string): Promise<void> {
1017
+ if (message.heartbeatIntervalMs) {
1018
+ this.heartbeatIntervalMs = message.heartbeatIntervalMs;
1019
+ }
1008
1020
  if (message.locusStateElements === undefined) {
1009
1021
  this.handleRootHashHeartBeatMessage(message);
1022
+ this.resetHeartbeatWatchdogs(message.dataSets);
1010
1023
  } else {
1011
1024
  const updates = await this.parseMessage(message, debugText);
1012
1025
 
1026
+ // Only reset watchdogs if the meeting hasn't ended
1027
+ if (updates.updateType !== LocusInfoUpdateType.MEETING_ENDED) {
1028
+ this.resetHeartbeatWatchdogs(message.dataSets);
1029
+ }
1030
+
1013
1031
  this.callLocusInfoUpdateCallback(updates);
1014
1032
  }
1015
1033
  }
@@ -1087,6 +1105,75 @@ class HashTreeParser {
1087
1105
  return Math.round(randomValue ** exponent * maxMs);
1088
1106
  }
1089
1107
 
1108
+ /**
1109
+ * Performs a sync for the given data set.
1110
+ *
1111
+ * @param {InternalDataSet} dataSet - The data set to sync
1112
+ * @param {string} rootHash - Our current root hash for this data set
1113
+ * @param {string} reason - The reason for the sync (used for logging)
1114
+ * @returns {Promise<void>}
1115
+ */
1116
+ private async performSync(
1117
+ dataSet: InternalDataSet,
1118
+ rootHash: string,
1119
+ reason: string
1120
+ ): Promise<void> {
1121
+ if (!dataSet.hashTree) {
1122
+ return;
1123
+ }
1124
+
1125
+ LoggerProxy.logger.info(
1126
+ `HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
1127
+ );
1128
+
1129
+ const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
1130
+
1131
+ if (dataSet.leafCount !== 1) {
1132
+ let receivedHashes;
1133
+
1134
+ try {
1135
+ // request hashes from sender
1136
+ const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
1137
+ dataSet.name,
1138
+ rootHash
1139
+ );
1140
+
1141
+ receivedHashes = hashes;
1142
+
1143
+ dataSet.hashTree.resize(latestDataSetInfo.leafCount);
1144
+ } catch (error) {
1145
+ if (error.statusCode === 409) {
1146
+ // this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
1147
+ LoggerProxy.logger.info(
1148
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
1149
+ );
1150
+
1151
+ return;
1152
+ }
1153
+ throw error;
1154
+ }
1155
+
1156
+ // identify mismatched leaves
1157
+ const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
1158
+
1159
+ mismatchedLeaveIndexes.forEach((index) => {
1160
+ mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
1161
+ });
1162
+ } else {
1163
+ mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
1164
+ }
1165
+ // request sync for mismatched leaves
1166
+ if (Object.keys(mismatchedLeavesData).length > 0) {
1167
+ const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
1168
+
1169
+ // sync API may return nothing (in that case data will arrive via messages)
1170
+ // or it may return a response in the same format as messages
1171
+ if (syncResponse) {
1172
+ this.handleMessage(syncResponse, 'via sync API');
1173
+ }
1174
+ }
1175
+ }
1176
+
1090
1177
  /**
1091
1178
  * Runs the sync algorithm for the given data set.
1092
1179
  *
@@ -1145,56 +1232,11 @@ class HashTreeParser {
1145
1232
  const rootHash = dataSet.hashTree.getRootHash();
1146
1233
 
1147
1234
  if (dataSet.root !== rootHash) {
1148
- LoggerProxy.logger.info(
1149
- `HashTreeParser#runSyncAlgorithm --> ${this.debugId} Root hash mismatch: received=${dataSet.root}, ours=${rootHash}, syncing data set "${dataSet.name}"`
1235
+ await this.performSync(
1236
+ dataSet,
1237
+ rootHash,
1238
+ `Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
1150
1239
  );
1151
-
1152
- const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
1153
-
1154
- if (dataSet.leafCount !== 1) {
1155
- let receivedHashes;
1156
-
1157
- try {
1158
- // request hashes from sender
1159
- const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
1160
- dataSet.name,
1161
- rootHash
1162
- );
1163
-
1164
- receivedHashes = hashes;
1165
-
1166
- dataSet.hashTree.resize(latestDataSetInfo.leafCount);
1167
- } catch (error) {
1168
- if (error.statusCode === 409) {
1169
- // this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
1170
- LoggerProxy.logger.info(
1171
- `HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
1172
- );
1173
-
1174
- return;
1175
- }
1176
- throw error;
1177
- }
1178
-
1179
- // identify mismatched leaves
1180
- const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
1181
-
1182
- mismatchedLeaveIndexes.forEach((index) => {
1183
- mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
1184
- });
1185
- } else {
1186
- mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
1187
- }
1188
- // request sync for mismatched leaves
1189
- if (Object.keys(mismatchedLeavesData).length > 0) {
1190
- const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
1191
-
1192
- // sync API may return nothing (in that case data will arrive via messages)
1193
- // or it may return a response in the same format as messages
1194
- if (syncResponse) {
1195
- this.handleMessage(syncResponse, 'via sync API');
1196
- }
1197
- }
1198
1240
  } else {
1199
1241
  LoggerProxy.logger.info(
1200
1242
  `HashTreeParser#runSyncAlgorithm --> ${this.debugId} "${dataSet.name}" root hash matching: ${rootHash}, version=${dataSet.version}`
@@ -1208,6 +1250,52 @@ class HashTreeParser {
1208
1250
  }
1209
1251
  }
1210
1252
 
1253
+ /**
1254
+ * Resets the heartbeat watchdog timers for the specified data sets. Each data set has its own
1255
+ * watchdog timer that monitors whether heartbeats are being received within the expected interval.
1256
+ * If a heartbeat is not received for a specific data set within heartbeatIntervalMs plus
1257
+ * a backoff-calculated time, the sync algorithm is initiated for that data set
1258
+ *
1259
+ * @param {Array<DataSet>} receivedDataSets - The data sets from the received message for which watchdog timers should be reset
1260
+ * @returns {void}
1261
+ */
1262
+ private resetHeartbeatWatchdogs(receivedDataSets: Array<DataSet>): void {
1263
+ if (!this.heartbeatIntervalMs) {
1264
+ return;
1265
+ }
1266
+
1267
+ for (const receivedDataSet of receivedDataSets) {
1268
+ const dataSet = this.dataSets[receivedDataSet.name];
1269
+
1270
+ if (!dataSet?.hashTree) {
1271
+ // eslint-disable-next-line no-continue
1272
+ continue;
1273
+ }
1274
+
1275
+ if (dataSet.heartbeatWatchdogTimer) {
1276
+ clearTimeout(dataSet.heartbeatWatchdogTimer);
1277
+ dataSet.heartbeatWatchdogTimer = undefined;
1278
+ }
1279
+
1280
+ const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
1281
+ const delay = this.heartbeatIntervalMs + backoffTime;
1282
+
1283
+ dataSet.heartbeatWatchdogTimer = setTimeout(async () => {
1284
+ dataSet.heartbeatWatchdogTimer = undefined;
1285
+
1286
+ LoggerProxy.logger.warn(
1287
+ `HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
1288
+ );
1289
+
1290
+ await this.performSync(
1291
+ dataSet,
1292
+ dataSet.hashTree.getRootHash(),
1293
+ `heartbeat watchdog expired`
1294
+ );
1295
+ }, delay);
1296
+ }
1297
+ }
1298
+
1211
1299
  /**
1212
1300
  * Stops all timers for the data sets to prevent any further sync attempts.
1213
1301
  * @returns {void}
@@ -1218,6 +1306,10 @@ class HashTreeParser {
1218
1306
  clearTimeout(dataSet.timer);
1219
1307
  dataSet.timer = undefined;
1220
1308
  }
1309
+ if (dataSet.heartbeatWatchdogTimer) {
1310
+ clearTimeout(dataSet.heartbeatWatchdogTimer);
1311
+ dataSet.heartbeatWatchdogTimer = undefined;
1312
+ }
1221
1313
  });
1222
1314
  }
1223
1315