@webex/plugin-meetings 3.12.0-next.5 → 3.12.0-next.50
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/AGENTS.md +9 -0
- package/dist/aiEnableRequest/index.js +15 -2
- package/dist/aiEnableRequest/index.js.map +1 -1
- package/dist/breakouts/breakout.js +6 -2
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +6 -3
- package/dist/constants.js.map +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +38 -24
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/hashTree/constants.js +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +593 -358
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +4 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +277 -86
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +16 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/properties.js +1 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +842 -521
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +19 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +199 -77
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +6 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/request.js +39 -0
- package/dist/meetings/request.js.map +1 -1
- package/dist/meetings/util.js +67 -5
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +2 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +61 -15
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/locus-info/index.d.ts +46 -6
- package/dist/types/locus-info/types.d.ts +17 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +70 -1
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +18 -1
- package/dist/types/meetings/meetings.types.d.ts +15 -0
- package/dist/types/meetings/request.d.ts +14 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/member/util.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/webinar/index.js +361 -235
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/index.ts +16 -0
- package/src/breakouts/breakout.ts +2 -1
- package/src/config.ts +1 -0
- package/src/constants.ts +5 -1
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +47 -24
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +306 -160
- package/src/hashTree/utils.ts +17 -0
- package/src/index.ts +5 -0
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +25 -8
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +276 -93
- package/src/locus-info/types.ts +19 -1
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +315 -26
- package/src/meeting/util.ts +20 -2
- package/src/meetings/index.ts +104 -43
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +80 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +1 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +162 -21
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +7 -3
- package/test/unit/spec/controls-options-manager/index.js +140 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1294 -191
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/locus-info/controlsUtils.js +172 -57
- package/test/unit/spec/locus-info/index.js +443 -81
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +836 -41
- package/test/unit/spec/meeting/muteState.js +3 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meetings/index.js +275 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +161 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/recording-controller/index.js +9 -8
- package/test/unit/spec/webinar/index.ts +141 -16
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {cloneDeep, isEmpty, zip} from 'lodash';
|
|
2
2
|
import HashTree, {LeafDataItem} from './hashTree';
|
|
3
3
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
4
|
+
import Metrics from '../metrics';
|
|
5
|
+
import BEHAVIORAL_METRICS from '../metrics/constants';
|
|
4
6
|
import {Enum, HTTP_VERBS} from '../constants';
|
|
5
|
-
import {DataSetNames, EMPTY_HASH} from './constants';
|
|
7
|
+
import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH} from './constants';
|
|
6
8
|
import {ObjectType, HtMeta, HashTreeObject} from './types';
|
|
7
9
|
import {LocusDTO} from '../locus-info/types';
|
|
8
|
-
import {deleteNestedObjectsWithHtMeta, isMetadata} from './utils';
|
|
10
|
+
import {deleteNestedObjectsWithHtMeta, isMetadata, sortByInitPriority} from './utils';
|
|
9
11
|
|
|
10
12
|
export interface DataSet {
|
|
11
13
|
url: string;
|
|
@@ -57,10 +59,15 @@ export const LocusInfoUpdateType = {
|
|
|
57
59
|
} as const;
|
|
58
60
|
|
|
59
61
|
export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
|
|
60
|
-
export type
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
export type LocusInfoUpdate =
|
|
63
|
+
| {
|
|
64
|
+
updateType: typeof LocusInfoUpdateType.OBJECTS_UPDATED;
|
|
65
|
+
updatedObjects: HashTreeObject[];
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
updateType: typeof LocusInfoUpdateType.MEETING_ENDED;
|
|
69
|
+
};
|
|
70
|
+
export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
64
71
|
|
|
65
72
|
interface LeafInfo {
|
|
66
73
|
type: ObjectType;
|
|
@@ -99,6 +106,10 @@ class HashTreeParser {
|
|
|
99
106
|
heartbeatIntervalMs?: number;
|
|
100
107
|
private excludedDataSets: string[];
|
|
101
108
|
state: 'active' | 'stopped';
|
|
109
|
+
private syncQueue: Array<{dataSetName: string; reason: string; isInitialization?: boolean}> = [];
|
|
110
|
+
private isSyncInProgress = false;
|
|
111
|
+
private isSyncAllInProgress = false;
|
|
112
|
+
private syncQueueProcessingPromise: Promise<void> = Promise.resolve();
|
|
102
113
|
|
|
103
114
|
/**
|
|
104
115
|
* Constructor for HashTreeParser
|
|
@@ -224,16 +235,16 @@ class HashTreeParser {
|
|
|
224
235
|
* @param {DataSet} dataSetInfo The new data set to be added
|
|
225
236
|
* @returns {Promise}
|
|
226
237
|
*/
|
|
227
|
-
private initializeNewVisibleDataSet(
|
|
238
|
+
private async initializeNewVisibleDataSet(
|
|
228
239
|
visibleDataSetInfo: VisibleDataSetInfo,
|
|
229
240
|
dataSetInfo: DataSet
|
|
230
|
-
): Promise<
|
|
241
|
+
): Promise<void> {
|
|
231
242
|
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
232
243
|
LoggerProxy.logger.info(
|
|
233
244
|
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
234
245
|
);
|
|
235
246
|
|
|
236
|
-
return
|
|
247
|
+
return;
|
|
237
248
|
}
|
|
238
249
|
|
|
239
250
|
LoggerProxy.logger.info(
|
|
@@ -241,7 +252,7 @@ class HashTreeParser {
|
|
|
241
252
|
);
|
|
242
253
|
|
|
243
254
|
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
244
|
-
return
|
|
255
|
+
return;
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
@@ -251,51 +262,8 @@ class HashTreeParser {
|
|
|
251
262
|
hashTree,
|
|
252
263
|
};
|
|
253
264
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Sends a special sync request to Locus with all leaves empty - this is a way to get all the data for a given dataset.
|
|
259
|
-
*
|
|
260
|
-
* @param {string} datasetName - name of the dataset for which to send the request
|
|
261
|
-
* @param {string} debugText - text to include in logs
|
|
262
|
-
* @returns {Promise}
|
|
263
|
-
*/
|
|
264
|
-
private sendInitializationSyncRequestToLocus(
|
|
265
|
-
datasetName: string,
|
|
266
|
-
debugText: string
|
|
267
|
-
): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
|
|
268
|
-
const dataset = this.dataSets[datasetName];
|
|
269
|
-
|
|
270
|
-
if (!dataset) {
|
|
271
|
-
LoggerProxy.logger.warn(
|
|
272
|
-
`HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} No data set found for ${datasetName}, cannot send the request for leaf data`
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
return Promise.resolve(null);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const emptyLeavesData = new Array(dataset.leafCount).fill([]);
|
|
279
|
-
|
|
280
|
-
LoggerProxy.logger.info(
|
|
281
|
-
`HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} Sending initial sync request to Locus for data set "${datasetName}" with empty leaf data`
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
return this.sendSyncRequestToLocus(this.dataSets[datasetName], emptyLeavesData).then(
|
|
285
|
-
(syncResponse) => {
|
|
286
|
-
if (syncResponse) {
|
|
287
|
-
return {
|
|
288
|
-
updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
289
|
-
updatedObjects: this.parseMessage(
|
|
290
|
-
syncResponse,
|
|
291
|
-
`via empty leaves /sync API call for ${debugText}`
|
|
292
|
-
),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return {updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []};
|
|
297
|
-
}
|
|
298
|
-
);
|
|
265
|
+
this.enqueueSyncForDataset(dataSetInfo.name, 'new visible data set initialization', true);
|
|
266
|
+
await this.syncQueueProcessingPromise;
|
|
299
267
|
}
|
|
300
268
|
|
|
301
269
|
/**
|
|
@@ -382,9 +350,8 @@ class HashTreeParser {
|
|
|
382
350
|
if (this.state === 'stopped') {
|
|
383
351
|
return;
|
|
384
352
|
}
|
|
385
|
-
const updatedObjects: HashTreeObject[] = [];
|
|
386
353
|
|
|
387
|
-
for (const dataSet of visibleDataSets) {
|
|
354
|
+
for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
388
355
|
const {name, leafCount, url} = dataSet;
|
|
389
356
|
|
|
390
357
|
if (!this.dataSets[name]) {
|
|
@@ -420,19 +387,12 @@ class HashTreeParser {
|
|
|
420
387
|
);
|
|
421
388
|
this.dataSets[name].hashTree = new HashTree([], leafCount);
|
|
422
389
|
|
|
423
|
-
|
|
424
|
-
const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
|
|
425
|
-
|
|
426
|
-
if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
427
|
-
updatedObjects.push(...(data.updatedObjects || []));
|
|
428
|
-
}
|
|
390
|
+
this.enqueueSyncForDataset(name, `initialization sync for ${debugText}`, true);
|
|
429
391
|
}
|
|
430
392
|
}
|
|
431
393
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
updatedObjects,
|
|
435
|
-
});
|
|
394
|
+
// wait for all enqueued initialization syncs to complete
|
|
395
|
+
await this.syncQueueProcessingPromise;
|
|
436
396
|
}
|
|
437
397
|
|
|
438
398
|
/**
|
|
@@ -450,7 +410,7 @@ class HashTreeParser {
|
|
|
450
410
|
// object mapping dataset names to arrays of leaf data
|
|
451
411
|
const leafInfo: Record<string, Array<LeafInfo>> = {};
|
|
452
412
|
|
|
453
|
-
const findAndStoreMetaData = (currentLocusPart: any) => {
|
|
413
|
+
const findAndStoreMetaData = (currentLocusPart: any, currentLocusPartName: string) => {
|
|
454
414
|
if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
|
|
455
415
|
return;
|
|
456
416
|
}
|
|
@@ -465,10 +425,18 @@ class HashTreeParser {
|
|
|
465
425
|
};
|
|
466
426
|
|
|
467
427
|
if (copyData) {
|
|
468
|
-
|
|
428
|
+
if ((type as string).toLowerCase() === ObjectType.control) {
|
|
429
|
+
// control entries require special handling, because they are signalled by Locus
|
|
430
|
+
// differently when coming in messages vs API responses
|
|
431
|
+
newLeafInfo.data = {
|
|
432
|
+
[currentLocusPartName]: cloneDeep(currentLocusPart),
|
|
433
|
+
};
|
|
434
|
+
} else {
|
|
435
|
+
newLeafInfo.data = cloneDeep(currentLocusPart);
|
|
469
436
|
|
|
470
|
-
|
|
471
|
-
|
|
437
|
+
// remove any nested other objects that have their own htMeta
|
|
438
|
+
deleteNestedObjectsWithHtMeta(newLeafInfo.data);
|
|
439
|
+
}
|
|
472
440
|
}
|
|
473
441
|
|
|
474
442
|
for (const dataSetName of dataSetNames) {
|
|
@@ -480,19 +448,19 @@ class HashTreeParser {
|
|
|
480
448
|
}
|
|
481
449
|
|
|
482
450
|
if (Array.isArray(currentLocusPart)) {
|
|
483
|
-
for (const item of currentLocusPart) {
|
|
484
|
-
findAndStoreMetaData(item);
|
|
451
|
+
for (const [index, item] of currentLocusPart.entries()) {
|
|
452
|
+
findAndStoreMetaData(item, index.toString());
|
|
485
453
|
}
|
|
486
454
|
} else {
|
|
487
455
|
for (const key of Object.keys(currentLocusPart)) {
|
|
488
456
|
if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
|
|
489
|
-
findAndStoreMetaData(currentLocusPart[key]);
|
|
457
|
+
findAndStoreMetaData(currentLocusPart[key], key);
|
|
490
458
|
}
|
|
491
459
|
}
|
|
492
460
|
}
|
|
493
461
|
};
|
|
494
462
|
|
|
495
|
-
findAndStoreMetaData(locus);
|
|
463
|
+
findAndStoreMetaData(locus, 'locus');
|
|
496
464
|
|
|
497
465
|
return leafInfo;
|
|
498
466
|
}
|
|
@@ -683,6 +651,7 @@ class HashTreeParser {
|
|
|
683
651
|
const {dataSets, locus, metadata} = update;
|
|
684
652
|
|
|
685
653
|
if (!dataSets) {
|
|
654
|
+
// this happens for example when we handle GET /loci response
|
|
686
655
|
LoggerProxy.logger.info(
|
|
687
656
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
688
657
|
);
|
|
@@ -786,6 +755,18 @@ class HashTreeParser {
|
|
|
786
755
|
}
|
|
787
756
|
}
|
|
788
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Updates the leaf count for a data set, resizing its hash tree accordingly.
|
|
760
|
+
*
|
|
761
|
+
* @param {InternalDataSet} dataSet - The data set to update
|
|
762
|
+
* @param {number} newLeafCount - The new leaf count
|
|
763
|
+
* @returns {void}
|
|
764
|
+
*/
|
|
765
|
+
private updateDataSetLeafCount(dataSet: InternalDataSet, newLeafCount: number): void {
|
|
766
|
+
dataSet.hashTree?.resize(newLeafCount);
|
|
767
|
+
dataSet.leafCount = newLeafCount;
|
|
768
|
+
}
|
|
769
|
+
|
|
789
770
|
/**
|
|
790
771
|
* Checks for changes in the visible data sets based on the updated objects.
|
|
791
772
|
* @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects.
|
|
@@ -952,7 +933,7 @@ class HashTreeParser {
|
|
|
952
933
|
}
|
|
953
934
|
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
954
935
|
|
|
955
|
-
for (const ds of addedDataSets) {
|
|
936
|
+
for (const ds of sortByInitPriority(addedDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
956
937
|
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
957
938
|
|
|
958
939
|
LoggerProxy.logger.info(
|
|
@@ -964,12 +945,8 @@ class HashTreeParser {
|
|
|
964
945
|
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
965
946
|
);
|
|
966
947
|
} else {
|
|
967
|
-
// we're awaiting in a loop, because in practice there will be only one new data set at a time,
|
|
968
|
-
// so no point in trying to parallelize this
|
|
969
948
|
// eslint-disable-next-line no-await-in-loop
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
this.callLocusInfoUpdateCallback(updates);
|
|
949
|
+
await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
973
950
|
}
|
|
974
951
|
}
|
|
975
952
|
}
|
|
@@ -1007,7 +984,7 @@ class HashTreeParser {
|
|
|
1007
984
|
|
|
1008
985
|
// when we detect new visible datasets, it may be that the metadata about them is not
|
|
1009
986
|
// available in the message, they will require separate async initialization
|
|
1010
|
-
let dataSetsRequiringInitialization = [];
|
|
987
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
1011
988
|
|
|
1012
989
|
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
1013
990
|
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
@@ -1015,7 +992,7 @@ class HashTreeParser {
|
|
|
1015
992
|
);
|
|
1016
993
|
|
|
1017
994
|
if (metadataUpdates.length > 0) {
|
|
1018
|
-
const updatedMetadataObjects = [];
|
|
995
|
+
const updatedMetadataObjects: HashTreeObject[] = [];
|
|
1019
996
|
|
|
1020
997
|
metadataUpdates.forEach((object) => {
|
|
1021
998
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
@@ -1044,7 +1021,7 @@ class HashTreeParser {
|
|
|
1044
1021
|
}
|
|
1045
1022
|
}
|
|
1046
1023
|
|
|
1047
|
-
if (message.locusStateElements
|
|
1024
|
+
if (message.locusStateElements && message.locusStateElements.length > 0) {
|
|
1048
1025
|
// by this point we now have this.dataSets setup for data sets from this message
|
|
1049
1026
|
// and hash trees created for the new visible data sets,
|
|
1050
1027
|
// so we can now process all the updates from the message
|
|
@@ -1140,20 +1117,17 @@ class HashTreeParser {
|
|
|
1140
1117
|
* @param {Object} updates parsed from a Locus message
|
|
1141
1118
|
* @returns {void}
|
|
1142
1119
|
*/
|
|
1143
|
-
private callLocusInfoUpdateCallback(updates: {
|
|
1144
|
-
updateType: LocusInfoUpdateType;
|
|
1145
|
-
updatedObjects?: HashTreeObject[];
|
|
1146
|
-
}) {
|
|
1120
|
+
private callLocusInfoUpdateCallback(updates: LocusInfoUpdate) {
|
|
1147
1121
|
if (this.state === 'stopped') {
|
|
1148
1122
|
return;
|
|
1149
1123
|
}
|
|
1150
1124
|
|
|
1151
|
-
const {updateType
|
|
1125
|
+
const {updateType} = updates;
|
|
1152
1126
|
|
|
1153
|
-
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1127
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updates.updatedObjects?.length > 0) {
|
|
1154
1128
|
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1155
1129
|
// or removals for objects that still exist in any of their datasets
|
|
1156
|
-
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1130
|
+
const filteredUpdates = updates.updatedObjects.filter((object) => {
|
|
1157
1131
|
const {elementId} = object.htMeta;
|
|
1158
1132
|
const {type, id, version} = elementId;
|
|
1159
1133
|
|
|
@@ -1190,10 +1164,10 @@ class HashTreeParser {
|
|
|
1190
1164
|
});
|
|
1191
1165
|
|
|
1192
1166
|
if (filteredUpdates.length > 0) {
|
|
1193
|
-
this.locusInfoUpdateCallback(updateType,
|
|
1167
|
+
this.locusInfoUpdateCallback({updateType, updatedObjects: filteredUpdates});
|
|
1194
1168
|
}
|
|
1195
1169
|
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
1196
|
-
this.locusInfoUpdateCallback(updateType
|
|
1170
|
+
this.locusInfoUpdateCallback({updateType});
|
|
1197
1171
|
}
|
|
1198
1172
|
}
|
|
1199
1173
|
|
|
@@ -1215,69 +1189,86 @@ class HashTreeParser {
|
|
|
1215
1189
|
* Performs a sync for the given data set.
|
|
1216
1190
|
*
|
|
1217
1191
|
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1218
|
-
* @param {string} rootHash - Our current root hash for this data set
|
|
1219
1192
|
* @param {string} reason - The reason for the sync (used for logging)
|
|
1193
|
+
* @param {boolean} [isInitialization] - Whether this is an initialization sync (sends empty leaves data instead of comparing hashes)
|
|
1220
1194
|
* @returns {Promise<void>}
|
|
1221
1195
|
*/
|
|
1222
1196
|
private async performSync(
|
|
1223
1197
|
dataSet: InternalDataSet,
|
|
1224
|
-
|
|
1225
|
-
|
|
1198
|
+
reason: string,
|
|
1199
|
+
isInitialization?: boolean
|
|
1226
1200
|
): Promise<void> {
|
|
1227
1201
|
if (!dataSet.hashTree) {
|
|
1228
1202
|
return;
|
|
1229
1203
|
}
|
|
1230
1204
|
|
|
1205
|
+
const {hashTree} = dataSet;
|
|
1206
|
+
const rootHash = hashTree.getRootHash();
|
|
1207
|
+
|
|
1231
1208
|
try {
|
|
1232
1209
|
LoggerProxy.logger.info(
|
|
1233
1210
|
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1234
1211
|
);
|
|
1235
1212
|
|
|
1236
|
-
|
|
1213
|
+
let leavesData: Record<number, LeafDataItem[]> = {};
|
|
1237
1214
|
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1215
|
+
if (!isInitialization) {
|
|
1216
|
+
if (dataSet.leafCount !== 1) {
|
|
1217
|
+
let receivedHashes;
|
|
1240
1218
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
dataSet.name,
|
|
1245
|
-
rootHash
|
|
1246
|
-
);
|
|
1219
|
+
try {
|
|
1220
|
+
// request hashes from sender
|
|
1221
|
+
const hashesResult = await this.getHashesFromLocus(dataSet.name, rootHash);
|
|
1247
1222
|
|
|
1248
|
-
|
|
1223
|
+
if (!hashesResult) {
|
|
1224
|
+
// hashes match, no sync needed
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1249
1227
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1228
|
+
receivedHashes = hashesResult.hashes;
|
|
1229
|
+
|
|
1230
|
+
this.updateDataSetLeafCount(dataSet, hashesResult.dataSet.leafCount);
|
|
1231
|
+
} catch (error: any) {
|
|
1232
|
+
if (error?.statusCode === 409) {
|
|
1233
|
+
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1234
|
+
LoggerProxy.logger.info(
|
|
1235
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1236
|
+
);
|
|
1257
1237
|
|
|
1258
|
-
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
throw error;
|
|
1259
1241
|
}
|
|
1260
|
-
throw error;
|
|
1261
|
-
}
|
|
1262
1242
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1243
|
+
// identify mismatched leaves
|
|
1244
|
+
const mismatchedLeaveIndexes = hashTree.diffHashes(receivedHashes);
|
|
1265
1245
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1246
|
+
mismatchedLeaveIndexes.forEach((index) => {
|
|
1247
|
+
leavesData[index] = hashTree.getLeafData(index);
|
|
1248
|
+
});
|
|
1249
|
+
} else {
|
|
1250
|
+
leavesData = {0: hashTree.getLeafData(0)};
|
|
1251
|
+
}
|
|
1271
1252
|
}
|
|
1272
1253
|
// request sync for mismatched leaves
|
|
1273
|
-
|
|
1274
|
-
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1254
|
+
let syncResponse: HashTreeMessage | null = null;
|
|
1275
1255
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1256
|
+
if (isInitialization) {
|
|
1257
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {isInitialization: true});
|
|
1258
|
+
} else if (Object.keys(leavesData).length > 0) {
|
|
1259
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {
|
|
1260
|
+
mismatchedLeavesData: leavesData,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// sync API may return nothing (in that case data will arrive via messages)
|
|
1265
|
+
// or it may return a response in the same format as messages
|
|
1266
|
+
// We still need to restart the sync timer as a safety net in case the messages don't arrive.
|
|
1267
|
+
this.runSyncAlgorithm(dataSet);
|
|
1268
|
+
|
|
1269
|
+
if (syncResponse) {
|
|
1270
|
+
// the format of sync response is the same as messages, so we can reuse the same handler
|
|
1271
|
+
this.handleMessage(syncResponse, 'via sync API');
|
|
1281
1272
|
}
|
|
1282
1273
|
} catch (error) {
|
|
1283
1274
|
if (error instanceof MeetingEndedError) {
|
|
@@ -1293,6 +1284,105 @@ class HashTreeParser {
|
|
|
1293
1284
|
}
|
|
1294
1285
|
}
|
|
1295
1286
|
|
|
1287
|
+
/**
|
|
1288
|
+
* Enqueues a sync for the given data set. If the data set is already in the queue, the request is ignored.
|
|
1289
|
+
* This ensures that all syncs are executed sequentially and no more than 1 sync runs at a time.
|
|
1290
|
+
*
|
|
1291
|
+
* @param {string} dataSetName - The name of the data set to sync
|
|
1292
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1293
|
+
* @param {boolean} [isInitialization=false] - Whether this is an initialization sync (uses empty leaves data instead of hash comparison)
|
|
1294
|
+
* @returns {void}
|
|
1295
|
+
*/
|
|
1296
|
+
private enqueueSyncForDataset(
|
|
1297
|
+
dataSetName: string,
|
|
1298
|
+
reason: string,
|
|
1299
|
+
isInitialization = false
|
|
1300
|
+
): void {
|
|
1301
|
+
if (this.state === 'stopped') return;
|
|
1302
|
+
|
|
1303
|
+
const existingEntry = this.syncQueue.find((entry) => entry.dataSetName === dataSetName);
|
|
1304
|
+
|
|
1305
|
+
if (existingEntry) {
|
|
1306
|
+
if (isInitialization) {
|
|
1307
|
+
existingEntry.isInitialization = true;
|
|
1308
|
+
}
|
|
1309
|
+
LoggerProxy.logger.info(
|
|
1310
|
+
`HashTreeParser#enqueueSyncForDataset --> ${this.debugId} data set "${dataSetName}" already in sync queue, skipping`
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
this.syncQueue.push({dataSetName, reason, isInitialization});
|
|
1317
|
+
|
|
1318
|
+
if (!this.isSyncInProgress) {
|
|
1319
|
+
this.syncQueueProcessingPromise = this.processSyncQueue();
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Processes the sync queue sequentially. Only one instance of this method runs at a time.
|
|
1325
|
+
*
|
|
1326
|
+
* @returns {Promise<void>}
|
|
1327
|
+
*/
|
|
1328
|
+
private async processSyncQueue(): Promise<void> {
|
|
1329
|
+
if (this.isSyncInProgress) return;
|
|
1330
|
+
|
|
1331
|
+
this.isSyncInProgress = true;
|
|
1332
|
+
try {
|
|
1333
|
+
while (this.syncQueue.length > 0 && this.state !== 'stopped') {
|
|
1334
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1335
|
+
const {dataSetName, reason, isInitialization} = this.syncQueue.shift()!;
|
|
1336
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1337
|
+
|
|
1338
|
+
if (!dataSet?.hashTree) {
|
|
1339
|
+
// eslint-disable-next-line no-continue
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1344
|
+
await this.performSync(dataSet, reason, isInitialization);
|
|
1345
|
+
}
|
|
1346
|
+
} finally {
|
|
1347
|
+
this.isSyncInProgress = false;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Syncs all data sets that have hash trees, one by one in sequence, using the priority order
|
|
1353
|
+
* provided by sortByInitPriority(). Does nothing if the parser is stopped or if a syncAllDatasets
|
|
1354
|
+
* call is already in progress.
|
|
1355
|
+
*
|
|
1356
|
+
* @returns {Promise<void>}
|
|
1357
|
+
*/
|
|
1358
|
+
public async syncAllDatasets(): Promise<void> {
|
|
1359
|
+
if (this.state === 'stopped') return;
|
|
1360
|
+
if (this.isSyncAllInProgress) return;
|
|
1361
|
+
|
|
1362
|
+
this.isSyncAllInProgress = true;
|
|
1363
|
+
try {
|
|
1364
|
+
const dataSetsWithHashTrees = Object.values(this.dataSets)
|
|
1365
|
+
.filter((dataSet) => dataSet?.hashTree)
|
|
1366
|
+
.map((dataSet) => ({name: dataSet.name}));
|
|
1367
|
+
|
|
1368
|
+
const sorted = sortByInitPriority(dataSetsWithHashTrees, DATA_SET_INIT_PRIORITY);
|
|
1369
|
+
|
|
1370
|
+
LoggerProxy.logger.info(
|
|
1371
|
+
`HashTreeParser#syncAllDatasets --> ${this.debugId} syncing datasets: ${sorted
|
|
1372
|
+
.map((ds) => ds.name)
|
|
1373
|
+
.join(', ')}`
|
|
1374
|
+
);
|
|
1375
|
+
|
|
1376
|
+
for (const ds of sorted) {
|
|
1377
|
+
this.enqueueSyncForDataset(ds.name, 'syncAllDatasets');
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
await this.syncQueueProcessingPromise;
|
|
1381
|
+
} finally {
|
|
1382
|
+
this.isSyncAllInProgress = false;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1296
1386
|
/**
|
|
1297
1387
|
* Runs the sync algorithm for the given data set.
|
|
1298
1388
|
*
|
|
@@ -1320,12 +1410,6 @@ class HashTreeParser {
|
|
|
1320
1410
|
|
|
1321
1411
|
dataSet.hashTree.resize(receivedDataSet.leafCount);
|
|
1322
1412
|
|
|
1323
|
-
// temporary log for the workshop // todo: remove
|
|
1324
|
-
const ourCurrentRootHash = dataSet.hashTree.getRootHash();
|
|
1325
|
-
LoggerProxy.logger.info(
|
|
1326
|
-
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} dataSet="${dataSet.name}" version=${dataSet.version} hashes before starting timer: ours=${ourCurrentRootHash} Locus=${dataSet.root}`
|
|
1327
|
-
);
|
|
1328
|
-
|
|
1329
1413
|
const delay = dataSet.idleMs + this.getWeightedBackoffTime(dataSet.backoff);
|
|
1330
1414
|
|
|
1331
1415
|
if (delay > 0) {
|
|
@@ -1337,7 +1421,7 @@ class HashTreeParser {
|
|
|
1337
1421
|
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} setting "${dataSet.name}" sync timer for ${delay}`
|
|
1338
1422
|
);
|
|
1339
1423
|
|
|
1340
|
-
dataSet.timer = setTimeout(
|
|
1424
|
+
dataSet.timer = setTimeout(() => {
|
|
1341
1425
|
dataSet.timer = undefined;
|
|
1342
1426
|
|
|
1343
1427
|
if (!dataSet.hashTree) {
|
|
@@ -1351,9 +1435,8 @@ class HashTreeParser {
|
|
|
1351
1435
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
1352
1436
|
|
|
1353
1437
|
if (dataSet.root !== rootHash) {
|
|
1354
|
-
|
|
1355
|
-
dataSet,
|
|
1356
|
-
rootHash,
|
|
1438
|
+
this.enqueueSyncForDataset(
|
|
1439
|
+
dataSet.name,
|
|
1357
1440
|
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
1358
1441
|
);
|
|
1359
1442
|
} else {
|
|
@@ -1399,18 +1482,15 @@ class HashTreeParser {
|
|
|
1399
1482
|
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1400
1483
|
const delay = this.heartbeatIntervalMs + backoffTime;
|
|
1401
1484
|
|
|
1402
|
-
dataSet.heartbeatWatchdogTimer = setTimeout(
|
|
1485
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(() => {
|
|
1403
1486
|
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1404
1487
|
|
|
1405
1488
|
LoggerProxy.logger.warn(
|
|
1406
1489
|
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1407
1490
|
);
|
|
1408
1491
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
dataSet.hashTree.getRootHash(),
|
|
1412
|
-
`heartbeat watchdog expired`
|
|
1413
|
-
);
|
|
1492
|
+
this.enqueueSyncForDataset(dataSet.name, `heartbeat watchdog expired`);
|
|
1493
|
+
this.resetHeartbeatWatchdogs([dataSet]);
|
|
1414
1494
|
}, delay);
|
|
1415
1495
|
}
|
|
1416
1496
|
}
|
|
@@ -1443,6 +1523,7 @@ class HashTreeParser {
|
|
|
1443
1523
|
`HashTreeParser#stop --> ${this.debugId} Stopping HashTreeParser, clearing timers and hash trees`
|
|
1444
1524
|
);
|
|
1445
1525
|
this.stopAllTimers();
|
|
1526
|
+
this.syncQueue = [];
|
|
1446
1527
|
Object.values(this.dataSets).forEach((dataSet) => {
|
|
1447
1528
|
dataSet.hashTree = undefined;
|
|
1448
1529
|
});
|
|
@@ -1451,17 +1532,27 @@ class HashTreeParser {
|
|
|
1451
1532
|
}
|
|
1452
1533
|
|
|
1453
1534
|
/**
|
|
1454
|
-
*
|
|
1535
|
+
* Cleans up the HashTreeParser, stopping all timers and clearing all internal state.
|
|
1536
|
+
* After calling this, the parser should not be used anymore.
|
|
1537
|
+
* @returns {void}
|
|
1538
|
+
*/
|
|
1539
|
+
public cleanUp() {
|
|
1540
|
+
this.stop();
|
|
1541
|
+
this.dataSets = {};
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Resumes the HashTreeParser that was previously stopped, using a hash tree message.
|
|
1455
1546
|
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
1456
1547
|
* @returns {void}
|
|
1457
1548
|
*/
|
|
1458
|
-
public
|
|
1549
|
+
public resumeFromMessage(message: HashTreeMessage) {
|
|
1459
1550
|
// check that message contains metadata with visible data sets - this is essential to be able to resume
|
|
1460
1551
|
const metadataObject = message.locusStateElements?.find((el) => isMetadata(el));
|
|
1461
1552
|
|
|
1462
1553
|
if (!metadataObject?.data?.visibleDataSets) {
|
|
1463
1554
|
LoggerProxy.logger.warn(
|
|
1464
|
-
`HashTreeParser#
|
|
1555
|
+
`HashTreeParser#resumeFromMessage --> ${this.debugId} Cannot resume HashTreeParser because the message is missing metadata with visible data sets info`
|
|
1465
1556
|
);
|
|
1466
1557
|
|
|
1467
1558
|
return;
|
|
@@ -1482,7 +1573,7 @@ class HashTreeParser {
|
|
|
1482
1573
|
};
|
|
1483
1574
|
}
|
|
1484
1575
|
LoggerProxy.logger.info(
|
|
1485
|
-
`HashTreeParser#
|
|
1576
|
+
`HashTreeParser#resumeFromMessage --> ${
|
|
1486
1577
|
this.debugId
|
|
1487
1578
|
} Resuming HashTreeParser with data sets: ${Object.keys(this.dataSets).join(
|
|
1488
1579
|
', '
|
|
@@ -1493,6 +1584,24 @@ class HashTreeParser {
|
|
|
1493
1584
|
this.handleMessage(message, 'on resume');
|
|
1494
1585
|
}
|
|
1495
1586
|
|
|
1587
|
+
/**
|
|
1588
|
+
* Resumes the HashTreeParser that was previously stopped, using a Locus API response.
|
|
1589
|
+
* Unlike resumeFromMessage(), this does not require metadata/dataSets in the input,
|
|
1590
|
+
* as it fetches all necessary information from Locus via initializeFromGetLociResponse.
|
|
1591
|
+
* @param {LocusDTO} locus - locus object from an API response
|
|
1592
|
+
* @returns {Promise}
|
|
1593
|
+
*/
|
|
1594
|
+
public async resumeFromApiResponse(locus: LocusDTO) {
|
|
1595
|
+
this.state = 'active';
|
|
1596
|
+
this.dataSets = {};
|
|
1597
|
+
|
|
1598
|
+
LoggerProxy.logger.info(
|
|
1599
|
+
`HashTreeParser#resumeFromApiResponse --> ${this.debugId} Resuming HashTreeParser from API response`
|
|
1600
|
+
);
|
|
1601
|
+
|
|
1602
|
+
await this.initializeFromGetLociResponse(locus);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1496
1605
|
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1497
1606
|
const isValidDataSetForSentinel =
|
|
1498
1607
|
dataSetName === undefined ||
|
|
@@ -1516,7 +1625,7 @@ class HashTreeParser {
|
|
|
1516
1625
|
* Gets the current hashes from the locus for a specific data set.
|
|
1517
1626
|
* @param {string} dataSetName
|
|
1518
1627
|
* @param {string} currentRootHash
|
|
1519
|
-
* @returns {
|
|
1628
|
+
* @returns {Object|null} An object containing the hashes and leaf count, or null if the hashes match and no sync is needed
|
|
1520
1629
|
*/
|
|
1521
1630
|
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1522
1631
|
LoggerProxy.logger.info(
|
|
@@ -1535,6 +1644,15 @@ class HashTreeParser {
|
|
|
1535
1644
|
},
|
|
1536
1645
|
})
|
|
1537
1646
|
.then((response) => {
|
|
1647
|
+
if (!response.body || isEmpty(response.body)) {
|
|
1648
|
+
// 204 with empty body means our hashes match Locus, no sync needed
|
|
1649
|
+
LoggerProxy.logger.info(
|
|
1650
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got ${response.statusCode} with empty body for data set "${dataSetName}", hashes match - no sync needed`
|
|
1651
|
+
);
|
|
1652
|
+
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1538
1656
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
1539
1657
|
const dataSetFromResponse = response.body?.dataSet;
|
|
1540
1658
|
|
|
@@ -1563,6 +1681,13 @@ class HashTreeParser {
|
|
|
1563
1681
|
error
|
|
1564
1682
|
);
|
|
1565
1683
|
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1684
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
1685
|
+
debugId: this.debugId,
|
|
1686
|
+
dataSetName,
|
|
1687
|
+
request: 'GET /hashtree',
|
|
1688
|
+
statusCode: error.statusCode,
|
|
1689
|
+
reason: error.message,
|
|
1690
|
+
});
|
|
1566
1691
|
|
|
1567
1692
|
throw error;
|
|
1568
1693
|
});
|
|
@@ -1572,29 +1697,43 @@ class HashTreeParser {
|
|
|
1572
1697
|
* Sends a sync request to Locus for the specified data set.
|
|
1573
1698
|
*
|
|
1574
1699
|
* @param {InternalDataSet} dataSet The data set to sync.
|
|
1575
|
-
* @param {
|
|
1700
|
+
* @param {Object} options Either `{ isInitialization: true }` for init syncs (uses leafCount=1 with empty leaf data) or `{ mismatchedLeavesData }` for normal syncs.
|
|
1576
1701
|
* @returns {Promise<HashTreeMessage|null>}
|
|
1577
1702
|
*/
|
|
1578
1703
|
private sendSyncRequestToLocus(
|
|
1579
1704
|
dataSet: InternalDataSet,
|
|
1580
|
-
mismatchedLeavesData: Record<number, LeafDataItem[]>
|
|
1705
|
+
options: {isInitialization: true} | {mismatchedLeavesData: Record<number, LeafDataItem[]>}
|
|
1581
1706
|
): Promise<HashTreeMessage | null> {
|
|
1582
1707
|
LoggerProxy.logger.info(
|
|
1583
1708
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sending sync request for data set "${dataSet.name}"`
|
|
1584
1709
|
);
|
|
1585
1710
|
|
|
1711
|
+
const isInitialization = 'isInitialization' in options;
|
|
1712
|
+
|
|
1586
1713
|
const url = `${dataSet.url}/sync`;
|
|
1587
|
-
const body
|
|
1588
|
-
leafCount:
|
|
1714
|
+
const body: {
|
|
1715
|
+
leafCount: number;
|
|
1716
|
+
leafDataEntries: {leafIndex: number; elementIds: LeafDataItem[]}[];
|
|
1717
|
+
} = {
|
|
1718
|
+
leafCount: isInitialization ? 1 : dataSet.leafCount,
|
|
1589
1719
|
leafDataEntries: [],
|
|
1590
1720
|
};
|
|
1591
1721
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1722
|
+
if (isInitialization) {
|
|
1723
|
+
// initialization sync: Locus requires leafCount=1 with a single empty leaf
|
|
1724
|
+
body.leafDataEntries.push({leafIndex: 0, elementIds: []});
|
|
1725
|
+
} else {
|
|
1726
|
+
const {mismatchedLeavesData} = options;
|
|
1727
|
+
|
|
1728
|
+
Object.keys(mismatchedLeavesData).forEach((index) => {
|
|
1729
|
+
const leafIndex = parseInt(index, 10);
|
|
1730
|
+
|
|
1731
|
+
body.leafDataEntries.push({
|
|
1732
|
+
leafIndex,
|
|
1733
|
+
elementIds: mismatchedLeavesData[leafIndex],
|
|
1734
|
+
});
|
|
1596
1735
|
});
|
|
1597
|
-
}
|
|
1736
|
+
}
|
|
1598
1737
|
|
|
1599
1738
|
const ourCurrentRootHash = dataSet.hashTree ? dataSet.hashTree.getRootHash() : EMPTY_HASH;
|
|
1600
1739
|
|
|
@@ -1627,6 +1766,13 @@ class HashTreeParser {
|
|
|
1627
1766
|
error
|
|
1628
1767
|
);
|
|
1629
1768
|
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1769
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
1770
|
+
debugId: this.debugId,
|
|
1771
|
+
dataSetName: dataSet.name,
|
|
1772
|
+
request: 'POST /sync',
|
|
1773
|
+
statusCode: error.statusCode,
|
|
1774
|
+
reason: error.message,
|
|
1775
|
+
});
|
|
1630
1776
|
|
|
1631
1777
|
throw error;
|
|
1632
1778
|
});
|