@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.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/hashTree/hashTreeParser.js +226 -114
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/types/hashTree/hashTreeParser.d.ts +22 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +1 -1
- package/src/hashTree/hashTreeParser.ts +180 -88
- package/test/unit/spec/hashTree/hashTreeParser.ts +477 -4
|
@@ -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
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
object
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
(
|
|
953
|
-
|
|
954
|
-
if (
|
|
955
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
970
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
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
|
|