@webex/plugin-meetings 3.12.0-next.21 → 3.12.0-next.23
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/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/hashTree/hashTreeParser.js +480 -320
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +170 -36
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meetings/index.js +130 -45
- package/dist/meetings/index.js.map +1 -1
- package/dist/types/hashTree/hashTreeParser.d.ts +40 -12
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/locus-info/index.d.ts +29 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +1 -1
- package/src/hashTree/hashTreeParser.ts +182 -97
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/locus-info/index.ts +176 -48
- package/src/meetings/index.ts +42 -17
- package/test/unit/spec/hashTree/hashTreeParser.ts +372 -7
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/locus-info/index.js +179 -4
- package/test/unit/spec/meetings/index.js +127 -5
|
@@ -71,6 +71,10 @@ declare class HashTreeParser {
|
|
|
71
71
|
heartbeatIntervalMs?: number;
|
|
72
72
|
private excludedDataSets;
|
|
73
73
|
state: 'active' | 'stopped';
|
|
74
|
+
private syncQueue;
|
|
75
|
+
private isSyncInProgress;
|
|
76
|
+
private isSyncAllInProgress;
|
|
77
|
+
private syncQueueProcessingPromise;
|
|
74
78
|
/**
|
|
75
79
|
* Constructor for HashTreeParser
|
|
76
80
|
* @param {Object} options
|
|
@@ -124,14 +128,6 @@ declare class HashTreeParser {
|
|
|
124
128
|
* @returns {Promise}
|
|
125
129
|
*/
|
|
126
130
|
private initializeNewVisibleDataSet;
|
|
127
|
-
/**
|
|
128
|
-
* Sends a special sync request to Locus with all leaves empty - this is a way to get all the data for a given dataset.
|
|
129
|
-
*
|
|
130
|
-
* @param {string} datasetName - name of the dataset for which to send the request
|
|
131
|
-
* @param {string} debugText - text to include in logs
|
|
132
|
-
* @returns {Promise}
|
|
133
|
-
*/
|
|
134
|
-
private sendInitializationSyncRequestToLocus;
|
|
135
131
|
/**
|
|
136
132
|
* Queries Locus for all up-to-date information about all visible data sets
|
|
137
133
|
*
|
|
@@ -302,11 +298,35 @@ declare class HashTreeParser {
|
|
|
302
298
|
* Performs a sync for the given data set.
|
|
303
299
|
*
|
|
304
300
|
* @param {InternalDataSet} dataSet - The data set to sync
|
|
305
|
-
* @param {string} rootHash - Our current root hash for this data set
|
|
306
301
|
* @param {string} reason - The reason for the sync (used for logging)
|
|
302
|
+
* @param {boolean} [isInitialization] - Whether this is an initialization sync (sends empty leaves data instead of comparing hashes)
|
|
307
303
|
* @returns {Promise<void>}
|
|
308
304
|
*/
|
|
309
305
|
private performSync;
|
|
306
|
+
/**
|
|
307
|
+
* Enqueues a sync for the given data set. If the data set is already in the queue, the request is ignored.
|
|
308
|
+
* This ensures that all syncs are executed sequentially and no more than 1 sync runs at a time.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} dataSetName - The name of the data set to sync
|
|
311
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
312
|
+
* @param {boolean} [isInitialization=false] - Whether this is an initialization sync (uses empty leaves data instead of hash comparison)
|
|
313
|
+
* @returns {void}
|
|
314
|
+
*/
|
|
315
|
+
private enqueueSyncForDataset;
|
|
316
|
+
/**
|
|
317
|
+
* Processes the sync queue sequentially. Only one instance of this method runs at a time.
|
|
318
|
+
*
|
|
319
|
+
* @returns {Promise<void>}
|
|
320
|
+
*/
|
|
321
|
+
private processSyncQueue;
|
|
322
|
+
/**
|
|
323
|
+
* Syncs all data sets that have hash trees, one by one in sequence, using the priority order
|
|
324
|
+
* provided by sortByInitPriority(). Does nothing if the parser is stopped or if a syncAllDatasets
|
|
325
|
+
* call is already in progress.
|
|
326
|
+
*
|
|
327
|
+
* @returns {Promise<void>}
|
|
328
|
+
*/
|
|
329
|
+
syncAllDatasets(): Promise<void>;
|
|
310
330
|
/**
|
|
311
331
|
* Runs the sync algorithm for the given data set.
|
|
312
332
|
*
|
|
@@ -343,17 +363,25 @@ declare class HashTreeParser {
|
|
|
343
363
|
*/
|
|
344
364
|
cleanUp(): void;
|
|
345
365
|
/**
|
|
346
|
-
* Resumes the HashTreeParser that was previously stopped.
|
|
366
|
+
* Resumes the HashTreeParser that was previously stopped, using a hash tree message.
|
|
347
367
|
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
348
368
|
* @returns {void}
|
|
349
369
|
*/
|
|
350
|
-
|
|
370
|
+
resumeFromMessage(message: HashTreeMessage): void;
|
|
371
|
+
/**
|
|
372
|
+
* Resumes the HashTreeParser that was previously stopped, using a Locus API response.
|
|
373
|
+
* Unlike resumeFromMessage(), this does not require metadata/dataSets in the input,
|
|
374
|
+
* as it fetches all necessary information from Locus via initializeFromGetLociResponse.
|
|
375
|
+
* @param {LocusDTO} locus - locus object from an API response
|
|
376
|
+
* @returns {Promise}
|
|
377
|
+
*/
|
|
378
|
+
resumeFromApiResponse(locus: LocusDTO): Promise<void>;
|
|
351
379
|
private checkForSentinelHttpResponse;
|
|
352
380
|
/**
|
|
353
381
|
* Gets the current hashes from the locus for a specific data set.
|
|
354
382
|
* @param {string} dataSetName
|
|
355
383
|
* @param {string} currentRootHash
|
|
356
|
-
* @returns {
|
|
384
|
+
* @returns {Object|null} An object containing the hashes and leaf count, or null if the hashes match and no sync is needed
|
|
357
385
|
*/
|
|
358
386
|
private getHashesFromLocus;
|
|
359
387
|
/**
|
|
@@ -11,11 +11,11 @@ export default class LocusRetryStatusInterceptor extends Interceptor {
|
|
|
11
11
|
*/
|
|
12
12
|
static create(): LocusRetryStatusInterceptor;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @param {
|
|
16
|
-
* @
|
|
17
|
-
* @returns {Promise<WebexHttpError>}
|
|
14
|
+
* Check whether a URI is a Locus /hashtree or /sync endpoint.
|
|
15
|
+
* @param {string} uri
|
|
16
|
+
* @returns {boolean}
|
|
18
17
|
*/
|
|
18
|
+
private static isLocusHashtreeOrSync;
|
|
19
19
|
onResponseError(options: any, reason: any): Promise<unknown>;
|
|
20
20
|
/**
|
|
21
21
|
* Handle retries for locus service unavailable errors
|
|
@@ -166,6 +166,28 @@ export default class LocusInfo extends EventsScope {
|
|
|
166
166
|
* @returns {void}
|
|
167
167
|
*/
|
|
168
168
|
sendClassicVsHashTreeMismatchMetric(meeting: any, message: string): void;
|
|
169
|
+
/**
|
|
170
|
+
* Helper that handles the common logic for reactivating a stopped HashTreeParser when
|
|
171
|
+
* a newer "replaces" is detected. Used by both the message and API response parser switch methods.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} callerName - name of the calling method, used in log messages
|
|
174
|
+
* @param {string} locusUrl - the locus URL of the stopped parser
|
|
175
|
+
* @param {HashTreeParserEntry} stoppedEntry - the stopped parser entry
|
|
176
|
+
* @param {ReplacesInfo} replaces - replacement info extracted from self
|
|
177
|
+
* @param {Function} resumeCallback - callback to invoke after reactivation to resume the parser
|
|
178
|
+
* @returns {void}
|
|
179
|
+
*/
|
|
180
|
+
private resumeStoppedParser;
|
|
181
|
+
/**
|
|
182
|
+
* Handles an API response whose locusUrl doesn't match any active HashTreeParser
|
|
183
|
+
* (either no entry exists, or the existing entry is stopped).
|
|
184
|
+
* Creates a new parser or reactivates a stopped one using initializeFromGetLociResponse.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} locusUrl - the locus URL from the API response
|
|
187
|
+
* @param {LocusDTO} locus - the locus DTO from the API response
|
|
188
|
+
* @returns {void}
|
|
189
|
+
*/
|
|
190
|
+
private handleHashTreeParserSwitchForAPIResponse;
|
|
169
191
|
/**
|
|
170
192
|
* Checks if the hash tree message should trigger a switch to a different HashTreeParser
|
|
171
193
|
*
|
|
@@ -182,6 +204,13 @@ export default class LocusInfo extends EventsScope {
|
|
|
182
204
|
* @returns {void}
|
|
183
205
|
*/
|
|
184
206
|
private handleHashTreeMessage;
|
|
207
|
+
/**
|
|
208
|
+
* Triggers a sync of all hash tree datasets for all hash tree parsers associated with this meeting.
|
|
209
|
+
* The syncs are executed sequentially within each parser.
|
|
210
|
+
*
|
|
211
|
+
* @returns {Promise<void>}
|
|
212
|
+
*/
|
|
213
|
+
syncAllHashTreeDatasets(): Promise<void>;
|
|
185
214
|
/**
|
|
186
215
|
* Callback registered with HashTreeParser to receive locus info updates.
|
|
187
216
|
* Updates our locus info based on the data parsed by the hash tree parser.
|
package/dist/webinar/index.js
CHANGED
package/package.json
CHANGED
|
@@ -104,6 +104,10 @@ class HashTreeParser {
|
|
|
104
104
|
heartbeatIntervalMs?: number;
|
|
105
105
|
private excludedDataSets: string[];
|
|
106
106
|
state: 'active' | 'stopped';
|
|
107
|
+
private syncQueue: Array<{dataSetName: string; reason: string; isInitialization?: boolean}> = [];
|
|
108
|
+
private isSyncInProgress = false;
|
|
109
|
+
private isSyncAllInProgress = false;
|
|
110
|
+
private syncQueueProcessingPromise: Promise<void> = Promise.resolve();
|
|
107
111
|
|
|
108
112
|
/**
|
|
109
113
|
* Constructor for HashTreeParser
|
|
@@ -229,16 +233,16 @@ class HashTreeParser {
|
|
|
229
233
|
* @param {DataSet} dataSetInfo The new data set to be added
|
|
230
234
|
* @returns {Promise}
|
|
231
235
|
*/
|
|
232
|
-
private initializeNewVisibleDataSet(
|
|
236
|
+
private async initializeNewVisibleDataSet(
|
|
233
237
|
visibleDataSetInfo: VisibleDataSetInfo,
|
|
234
238
|
dataSetInfo: DataSet
|
|
235
|
-
): Promise<
|
|
239
|
+
): Promise<void> {
|
|
236
240
|
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
237
241
|
LoggerProxy.logger.info(
|
|
238
242
|
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
239
243
|
);
|
|
240
244
|
|
|
241
|
-
return
|
|
245
|
+
return;
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
LoggerProxy.logger.info(
|
|
@@ -246,7 +250,7 @@ class HashTreeParser {
|
|
|
246
250
|
);
|
|
247
251
|
|
|
248
252
|
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
249
|
-
return
|
|
253
|
+
return;
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
@@ -256,51 +260,8 @@ class HashTreeParser {
|
|
|
256
260
|
hashTree,
|
|
257
261
|
};
|
|
258
262
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Sends a special sync request to Locus with all leaves empty - this is a way to get all the data for a given dataset.
|
|
264
|
-
*
|
|
265
|
-
* @param {string} datasetName - name of the dataset for which to send the request
|
|
266
|
-
* @param {string} debugText - text to include in logs
|
|
267
|
-
* @returns {Promise}
|
|
268
|
-
*/
|
|
269
|
-
private sendInitializationSyncRequestToLocus(
|
|
270
|
-
datasetName: string,
|
|
271
|
-
debugText: string
|
|
272
|
-
): Promise<LocusInfoUpdate> {
|
|
273
|
-
const dataset = this.dataSets[datasetName];
|
|
274
|
-
|
|
275
|
-
if (!dataset) {
|
|
276
|
-
LoggerProxy.logger.warn(
|
|
277
|
-
`HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} No data set found for ${datasetName}, cannot send the request for leaf data`
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const emptyLeavesData = new Array(dataset.leafCount).fill([]);
|
|
284
|
-
|
|
285
|
-
LoggerProxy.logger.info(
|
|
286
|
-
`HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} Sending initial sync request to Locus for data set "${datasetName}" with empty leaf data`
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
return this.sendSyncRequestToLocus(this.dataSets[datasetName], emptyLeavesData).then(
|
|
290
|
-
(syncResponse) => {
|
|
291
|
-
if (syncResponse) {
|
|
292
|
-
return {
|
|
293
|
-
updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
294
|
-
updatedObjects: this.parseMessage(
|
|
295
|
-
syncResponse,
|
|
296
|
-
`via empty leaves /sync API call for ${debugText}`
|
|
297
|
-
),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return {updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []};
|
|
302
|
-
}
|
|
303
|
-
);
|
|
263
|
+
this.enqueueSyncForDataset(dataSetInfo.name, 'new visible data set initialization', true);
|
|
264
|
+
await this.syncQueueProcessingPromise;
|
|
304
265
|
}
|
|
305
266
|
|
|
306
267
|
/**
|
|
@@ -388,8 +349,6 @@ class HashTreeParser {
|
|
|
388
349
|
return;
|
|
389
350
|
}
|
|
390
351
|
|
|
391
|
-
const updatedObjects: HashTreeObject[] = [];
|
|
392
|
-
|
|
393
352
|
for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
394
353
|
const {name, leafCount, url} = dataSet;
|
|
395
354
|
|
|
@@ -426,19 +385,12 @@ class HashTreeParser {
|
|
|
426
385
|
);
|
|
427
386
|
this.dataSets[name].hashTree = new HashTree([], leafCount);
|
|
428
387
|
|
|
429
|
-
|
|
430
|
-
const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
|
|
431
|
-
|
|
432
|
-
if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
433
|
-
updatedObjects.push(...(data.updatedObjects || []));
|
|
434
|
-
}
|
|
388
|
+
this.enqueueSyncForDataset(name, `initialization sync for ${debugText}`, true);
|
|
435
389
|
}
|
|
436
390
|
}
|
|
437
391
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
updatedObjects,
|
|
441
|
-
});
|
|
392
|
+
// wait for all enqueued initialization syncs to complete
|
|
393
|
+
await this.syncQueueProcessingPromise;
|
|
442
394
|
}
|
|
443
395
|
|
|
444
396
|
/**
|
|
@@ -697,6 +649,7 @@ class HashTreeParser {
|
|
|
697
649
|
const {dataSets, locus, metadata} = update;
|
|
698
650
|
|
|
699
651
|
if (!dataSets) {
|
|
652
|
+
// this happens for example when we handle GET /loci response
|
|
700
653
|
LoggerProxy.logger.info(
|
|
701
654
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
702
655
|
);
|
|
@@ -979,9 +932,7 @@ class HashTreeParser {
|
|
|
979
932
|
);
|
|
980
933
|
} else {
|
|
981
934
|
// eslint-disable-next-line no-await-in-loop
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
this.callLocusInfoUpdateCallback(updates);
|
|
935
|
+
await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
985
936
|
}
|
|
986
937
|
}
|
|
987
938
|
}
|
|
@@ -1224,41 +1175,53 @@ class HashTreeParser {
|
|
|
1224
1175
|
* Performs a sync for the given data set.
|
|
1225
1176
|
*
|
|
1226
1177
|
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1227
|
-
* @param {string} rootHash - Our current root hash for this data set
|
|
1228
1178
|
* @param {string} reason - The reason for the sync (used for logging)
|
|
1179
|
+
* @param {boolean} [isInitialization] - Whether this is an initialization sync (sends empty leaves data instead of comparing hashes)
|
|
1229
1180
|
* @returns {Promise<void>}
|
|
1230
1181
|
*/
|
|
1231
1182
|
private async performSync(
|
|
1232
1183
|
dataSet: InternalDataSet,
|
|
1233
|
-
|
|
1234
|
-
|
|
1184
|
+
reason: string,
|
|
1185
|
+
isInitialization?: boolean
|
|
1235
1186
|
): Promise<void> {
|
|
1236
1187
|
if (!dataSet.hashTree) {
|
|
1237
1188
|
return;
|
|
1238
1189
|
}
|
|
1239
1190
|
|
|
1191
|
+
const {hashTree} = dataSet;
|
|
1192
|
+
const rootHash = hashTree.getRootHash();
|
|
1193
|
+
|
|
1240
1194
|
try {
|
|
1241
1195
|
LoggerProxy.logger.info(
|
|
1242
1196
|
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1243
1197
|
);
|
|
1244
1198
|
|
|
1245
|
-
|
|
1199
|
+
let leavesData: Record<number, LeafDataItem[]>;
|
|
1246
1200
|
|
|
1247
|
-
if (
|
|
1201
|
+
if (isInitialization) {
|
|
1202
|
+
// initialization sync: send all leaves as empty to get all data from Locus
|
|
1203
|
+
leavesData = {};
|
|
1204
|
+
for (let i = 0; i < dataSet.leafCount; i += 1) {
|
|
1205
|
+
leavesData[i] = [];
|
|
1206
|
+
}
|
|
1207
|
+
} else if (dataSet.leafCount !== 1) {
|
|
1208
|
+
leavesData = {};
|
|
1248
1209
|
let receivedHashes;
|
|
1249
1210
|
|
|
1250
1211
|
try {
|
|
1251
1212
|
// request hashes from sender
|
|
1252
|
-
const
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1213
|
+
const hashesResult = await this.getHashesFromLocus(dataSet.name, rootHash);
|
|
1214
|
+
|
|
1215
|
+
if (!hashesResult) {
|
|
1216
|
+
// hashes match, no sync needed
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1256
1219
|
|
|
1257
|
-
receivedHashes = hashes;
|
|
1220
|
+
receivedHashes = hashesResult.hashes;
|
|
1258
1221
|
|
|
1259
|
-
|
|
1260
|
-
} catch (error) {
|
|
1261
|
-
if (error
|
|
1222
|
+
hashTree.resize(hashesResult.dataSet.leafCount);
|
|
1223
|
+
} catch (error: any) {
|
|
1224
|
+
if (error?.statusCode === 409) {
|
|
1262
1225
|
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1263
1226
|
LoggerProxy.logger.info(
|
|
1264
1227
|
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
@@ -1270,17 +1233,17 @@ class HashTreeParser {
|
|
|
1270
1233
|
}
|
|
1271
1234
|
|
|
1272
1235
|
// identify mismatched leaves
|
|
1273
|
-
const mismatchedLeaveIndexes =
|
|
1236
|
+
const mismatchedLeaveIndexes = hashTree.diffHashes(receivedHashes);
|
|
1274
1237
|
|
|
1275
1238
|
mismatchedLeaveIndexes.forEach((index) => {
|
|
1276
|
-
|
|
1239
|
+
leavesData[index] = hashTree.getLeafData(index);
|
|
1277
1240
|
});
|
|
1278
1241
|
} else {
|
|
1279
|
-
|
|
1242
|
+
leavesData = {0: hashTree.getLeafData(0)};
|
|
1280
1243
|
}
|
|
1281
1244
|
// request sync for mismatched leaves
|
|
1282
|
-
if (Object.keys(
|
|
1283
|
-
const syncResponse = await this.sendSyncRequestToLocus(dataSet,
|
|
1245
|
+
if (Object.keys(leavesData).length > 0) {
|
|
1246
|
+
const syncResponse = await this.sendSyncRequestToLocus(dataSet, leavesData);
|
|
1284
1247
|
|
|
1285
1248
|
// sync API may return nothing (in that case data will arrive via messages)
|
|
1286
1249
|
// or it may return a response in the same format as messages
|
|
@@ -1302,6 +1265,105 @@ class HashTreeParser {
|
|
|
1302
1265
|
}
|
|
1303
1266
|
}
|
|
1304
1267
|
|
|
1268
|
+
/**
|
|
1269
|
+
* Enqueues a sync for the given data set. If the data set is already in the queue, the request is ignored.
|
|
1270
|
+
* This ensures that all syncs are executed sequentially and no more than 1 sync runs at a time.
|
|
1271
|
+
*
|
|
1272
|
+
* @param {string} dataSetName - The name of the data set to sync
|
|
1273
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1274
|
+
* @param {boolean} [isInitialization=false] - Whether this is an initialization sync (uses empty leaves data instead of hash comparison)
|
|
1275
|
+
* @returns {void}
|
|
1276
|
+
*/
|
|
1277
|
+
private enqueueSyncForDataset(
|
|
1278
|
+
dataSetName: string,
|
|
1279
|
+
reason: string,
|
|
1280
|
+
isInitialization = false
|
|
1281
|
+
): void {
|
|
1282
|
+
if (this.state === 'stopped') return;
|
|
1283
|
+
|
|
1284
|
+
const existingEntry = this.syncQueue.find((entry) => entry.dataSetName === dataSetName);
|
|
1285
|
+
|
|
1286
|
+
if (existingEntry) {
|
|
1287
|
+
if (isInitialization) {
|
|
1288
|
+
existingEntry.isInitialization = true;
|
|
1289
|
+
}
|
|
1290
|
+
LoggerProxy.logger.info(
|
|
1291
|
+
`HashTreeParser#enqueueSyncForDataset --> ${this.debugId} data set "${dataSetName}" already in sync queue, skipping`
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
this.syncQueue.push({dataSetName, reason, isInitialization});
|
|
1298
|
+
|
|
1299
|
+
if (!this.isSyncInProgress) {
|
|
1300
|
+
this.syncQueueProcessingPromise = this.processSyncQueue();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Processes the sync queue sequentially. Only one instance of this method runs at a time.
|
|
1306
|
+
*
|
|
1307
|
+
* @returns {Promise<void>}
|
|
1308
|
+
*/
|
|
1309
|
+
private async processSyncQueue(): Promise<void> {
|
|
1310
|
+
if (this.isSyncInProgress) return;
|
|
1311
|
+
|
|
1312
|
+
this.isSyncInProgress = true;
|
|
1313
|
+
try {
|
|
1314
|
+
while (this.syncQueue.length > 0 && this.state !== 'stopped') {
|
|
1315
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1316
|
+
const {dataSetName, reason, isInitialization} = this.syncQueue.shift()!;
|
|
1317
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1318
|
+
|
|
1319
|
+
if (!dataSet?.hashTree) {
|
|
1320
|
+
// eslint-disable-next-line no-continue
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1325
|
+
await this.performSync(dataSet, reason, isInitialization);
|
|
1326
|
+
}
|
|
1327
|
+
} finally {
|
|
1328
|
+
this.isSyncInProgress = false;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Syncs all data sets that have hash trees, one by one in sequence, using the priority order
|
|
1334
|
+
* provided by sortByInitPriority(). Does nothing if the parser is stopped or if a syncAllDatasets
|
|
1335
|
+
* call is already in progress.
|
|
1336
|
+
*
|
|
1337
|
+
* @returns {Promise<void>}
|
|
1338
|
+
*/
|
|
1339
|
+
public async syncAllDatasets(): Promise<void> {
|
|
1340
|
+
if (this.state === 'stopped') return;
|
|
1341
|
+
if (this.isSyncAllInProgress) return;
|
|
1342
|
+
|
|
1343
|
+
this.isSyncAllInProgress = true;
|
|
1344
|
+
try {
|
|
1345
|
+
const dataSetsWithHashTrees = Object.values(this.dataSets)
|
|
1346
|
+
.filter((dataSet) => dataSet?.hashTree)
|
|
1347
|
+
.map((dataSet) => ({name: dataSet.name}));
|
|
1348
|
+
|
|
1349
|
+
const sorted = sortByInitPriority(dataSetsWithHashTrees, DATA_SET_INIT_PRIORITY);
|
|
1350
|
+
|
|
1351
|
+
LoggerProxy.logger.info(
|
|
1352
|
+
`HashTreeParser#syncAllDatasets --> ${this.debugId} syncing datasets: ${sorted
|
|
1353
|
+
.map((ds) => ds.name)
|
|
1354
|
+
.join(', ')}`
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
for (const ds of sorted) {
|
|
1358
|
+
this.enqueueSyncForDataset(ds.name, 'syncAllDatasets');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
await this.syncQueueProcessingPromise;
|
|
1362
|
+
} finally {
|
|
1363
|
+
this.isSyncAllInProgress = false;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1305
1367
|
/**
|
|
1306
1368
|
* Runs the sync algorithm for the given data set.
|
|
1307
1369
|
*
|
|
@@ -1346,7 +1408,7 @@ class HashTreeParser {
|
|
|
1346
1408
|
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} setting "${dataSet.name}" sync timer for ${delay}`
|
|
1347
1409
|
);
|
|
1348
1410
|
|
|
1349
|
-
dataSet.timer = setTimeout(
|
|
1411
|
+
dataSet.timer = setTimeout(() => {
|
|
1350
1412
|
dataSet.timer = undefined;
|
|
1351
1413
|
|
|
1352
1414
|
if (!dataSet.hashTree) {
|
|
@@ -1360,9 +1422,8 @@ class HashTreeParser {
|
|
|
1360
1422
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
1361
1423
|
|
|
1362
1424
|
if (dataSet.root !== rootHash) {
|
|
1363
|
-
|
|
1364
|
-
dataSet,
|
|
1365
|
-
rootHash,
|
|
1425
|
+
this.enqueueSyncForDataset(
|
|
1426
|
+
dataSet.name,
|
|
1366
1427
|
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
1367
1428
|
);
|
|
1368
1429
|
} else {
|
|
@@ -1408,18 +1469,14 @@ class HashTreeParser {
|
|
|
1408
1469
|
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1409
1470
|
const delay = this.heartbeatIntervalMs + backoffTime;
|
|
1410
1471
|
|
|
1411
|
-
dataSet.heartbeatWatchdogTimer = setTimeout(
|
|
1472
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(() => {
|
|
1412
1473
|
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1413
1474
|
|
|
1414
1475
|
LoggerProxy.logger.warn(
|
|
1415
1476
|
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1416
1477
|
);
|
|
1417
1478
|
|
|
1418
|
-
|
|
1419
|
-
dataSet,
|
|
1420
|
-
dataSet.hashTree.getRootHash(),
|
|
1421
|
-
`heartbeat watchdog expired`
|
|
1422
|
-
);
|
|
1479
|
+
this.enqueueSyncForDataset(dataSet.name, `heartbeat watchdog expired`);
|
|
1423
1480
|
}, delay);
|
|
1424
1481
|
}
|
|
1425
1482
|
}
|
|
@@ -1452,6 +1509,7 @@ class HashTreeParser {
|
|
|
1452
1509
|
`HashTreeParser#stop --> ${this.debugId} Stopping HashTreeParser, clearing timers and hash trees`
|
|
1453
1510
|
);
|
|
1454
1511
|
this.stopAllTimers();
|
|
1512
|
+
this.syncQueue = [];
|
|
1455
1513
|
Object.values(this.dataSets).forEach((dataSet) => {
|
|
1456
1514
|
dataSet.hashTree = undefined;
|
|
1457
1515
|
});
|
|
@@ -1470,17 +1528,17 @@ class HashTreeParser {
|
|
|
1470
1528
|
}
|
|
1471
1529
|
|
|
1472
1530
|
/**
|
|
1473
|
-
* Resumes the HashTreeParser that was previously stopped.
|
|
1531
|
+
* Resumes the HashTreeParser that was previously stopped, using a hash tree message.
|
|
1474
1532
|
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
1475
1533
|
* @returns {void}
|
|
1476
1534
|
*/
|
|
1477
|
-
public
|
|
1535
|
+
public resumeFromMessage(message: HashTreeMessage) {
|
|
1478
1536
|
// check that message contains metadata with visible data sets - this is essential to be able to resume
|
|
1479
1537
|
const metadataObject = message.locusStateElements?.find((el) => isMetadata(el));
|
|
1480
1538
|
|
|
1481
1539
|
if (!metadataObject?.data?.visibleDataSets) {
|
|
1482
1540
|
LoggerProxy.logger.warn(
|
|
1483
|
-
`HashTreeParser#
|
|
1541
|
+
`HashTreeParser#resumeFromMessage --> ${this.debugId} Cannot resume HashTreeParser because the message is missing metadata with visible data sets info`
|
|
1484
1542
|
);
|
|
1485
1543
|
|
|
1486
1544
|
return;
|
|
@@ -1501,7 +1559,7 @@ class HashTreeParser {
|
|
|
1501
1559
|
};
|
|
1502
1560
|
}
|
|
1503
1561
|
LoggerProxy.logger.info(
|
|
1504
|
-
`HashTreeParser#
|
|
1562
|
+
`HashTreeParser#resumeFromMessage --> ${
|
|
1505
1563
|
this.debugId
|
|
1506
1564
|
} Resuming HashTreeParser with data sets: ${Object.keys(this.dataSets).join(
|
|
1507
1565
|
', '
|
|
@@ -1512,6 +1570,24 @@ class HashTreeParser {
|
|
|
1512
1570
|
this.handleMessage(message, 'on resume');
|
|
1513
1571
|
}
|
|
1514
1572
|
|
|
1573
|
+
/**
|
|
1574
|
+
* Resumes the HashTreeParser that was previously stopped, using a Locus API response.
|
|
1575
|
+
* Unlike resumeFromMessage(), this does not require metadata/dataSets in the input,
|
|
1576
|
+
* as it fetches all necessary information from Locus via initializeFromGetLociResponse.
|
|
1577
|
+
* @param {LocusDTO} locus - locus object from an API response
|
|
1578
|
+
* @returns {Promise}
|
|
1579
|
+
*/
|
|
1580
|
+
public async resumeFromApiResponse(locus: LocusDTO) {
|
|
1581
|
+
this.state = 'active';
|
|
1582
|
+
this.dataSets = {};
|
|
1583
|
+
|
|
1584
|
+
LoggerProxy.logger.info(
|
|
1585
|
+
`HashTreeParser#resumeFromApiResponse --> ${this.debugId} Resuming HashTreeParser from API response`
|
|
1586
|
+
);
|
|
1587
|
+
|
|
1588
|
+
await this.initializeFromGetLociResponse(locus);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1515
1591
|
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1516
1592
|
const isValidDataSetForSentinel =
|
|
1517
1593
|
dataSetName === undefined ||
|
|
@@ -1535,7 +1611,7 @@ class HashTreeParser {
|
|
|
1535
1611
|
* Gets the current hashes from the locus for a specific data set.
|
|
1536
1612
|
* @param {string} dataSetName
|
|
1537
1613
|
* @param {string} currentRootHash
|
|
1538
|
-
* @returns {
|
|
1614
|
+
* @returns {Object|null} An object containing the hashes and leaf count, or null if the hashes match and no sync is needed
|
|
1539
1615
|
*/
|
|
1540
1616
|
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1541
1617
|
LoggerProxy.logger.info(
|
|
@@ -1554,6 +1630,15 @@ class HashTreeParser {
|
|
|
1554
1630
|
},
|
|
1555
1631
|
})
|
|
1556
1632
|
.then((response) => {
|
|
1633
|
+
if (!response.body || isEmpty(response.body)) {
|
|
1634
|
+
// 204 with empty body means our hashes match Locus, no sync needed
|
|
1635
|
+
LoggerProxy.logger.info(
|
|
1636
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got ${response.statusCode} with empty body for data set "${dataSetName}", hashes match - no sync needed`
|
|
1637
|
+
);
|
|
1638
|
+
|
|
1639
|
+
return null;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1557
1642
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
1558
1643
|
const dataSetFromResponse = response.body?.dataSet;
|
|
1559
1644
|
|
|
@@ -18,12 +18,33 @@ export default class LocusRetryStatusInterceptor extends Interceptor {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
* @param {
|
|
23
|
-
* @
|
|
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;
|