@webex/plugin-meetings 3.12.0-next.4 → 3.12.0-next.40
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/constants.js +1 -1
- 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 +23 -21
- 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 +554 -350
- 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/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 +274 -85
- 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/meeting/index.js +710 -499
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +1 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +174 -77
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/util.js +49 -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/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +53 -15
- package/dist/types/hashTree/utils.d.ts +11 -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/meeting/index.d.ts +64 -1
- 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/webinar/index.js +301 -226
- 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/constants.ts +1 -1
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +26 -19
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +278 -160
- package/src/hashTree/utils.ts +17 -0
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/locus-info/index.ts +274 -93
- package/src/locus-info/types.ts +19 -1
- package/src/meeting/index.ts +206 -22
- package/src/meeting/util.ts +1 -0
- package/src/meetings/index.ts +77 -43
- package/src/meetings/util.ts +56 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/webinar/index.ts +75 -1
- 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 +114 -6
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +996 -51
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/locus-info/index.js +397 -81
- package/test/unit/spec/meeting/index.js +271 -44
- package/test/unit/spec/meeting/utils.js +4 -0
- package/test/unit/spec/meetings/index.js +195 -13
- package/test/unit/spec/meetings/utils.js +137 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/webinar/index.ts +60 -0
|
@@ -2,10 +2,10 @@ import {cloneDeep, isEmpty, zip} from 'lodash';
|
|
|
2
2
|
import HashTree, {LeafDataItem} from './hashTree';
|
|
3
3
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
4
4
|
import {Enum, HTTP_VERBS} from '../constants';
|
|
5
|
-
import {DataSetNames, EMPTY_HASH} from './constants';
|
|
5
|
+
import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH} from './constants';
|
|
6
6
|
import {ObjectType, HtMeta, HashTreeObject} from './types';
|
|
7
7
|
import {LocusDTO} from '../locus-info/types';
|
|
8
|
-
import {deleteNestedObjectsWithHtMeta, isMetadata} from './utils';
|
|
8
|
+
import {deleteNestedObjectsWithHtMeta, isMetadata, sortByInitPriority} from './utils';
|
|
9
9
|
|
|
10
10
|
export interface DataSet {
|
|
11
11
|
url: string;
|
|
@@ -57,10 +57,15 @@ export const LocusInfoUpdateType = {
|
|
|
57
57
|
} as const;
|
|
58
58
|
|
|
59
59
|
export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
|
|
60
|
-
export type
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
export type LocusInfoUpdate =
|
|
61
|
+
| {
|
|
62
|
+
updateType: typeof LocusInfoUpdateType.OBJECTS_UPDATED;
|
|
63
|
+
updatedObjects: HashTreeObject[];
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
updateType: typeof LocusInfoUpdateType.MEETING_ENDED;
|
|
67
|
+
};
|
|
68
|
+
export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
64
69
|
|
|
65
70
|
interface LeafInfo {
|
|
66
71
|
type: ObjectType;
|
|
@@ -99,6 +104,10 @@ class HashTreeParser {
|
|
|
99
104
|
heartbeatIntervalMs?: number;
|
|
100
105
|
private excludedDataSets: string[];
|
|
101
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();
|
|
102
111
|
|
|
103
112
|
/**
|
|
104
113
|
* Constructor for HashTreeParser
|
|
@@ -224,16 +233,16 @@ class HashTreeParser {
|
|
|
224
233
|
* @param {DataSet} dataSetInfo The new data set to be added
|
|
225
234
|
* @returns {Promise}
|
|
226
235
|
*/
|
|
227
|
-
private initializeNewVisibleDataSet(
|
|
236
|
+
private async initializeNewVisibleDataSet(
|
|
228
237
|
visibleDataSetInfo: VisibleDataSetInfo,
|
|
229
238
|
dataSetInfo: DataSet
|
|
230
|
-
): Promise<
|
|
239
|
+
): Promise<void> {
|
|
231
240
|
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
232
241
|
LoggerProxy.logger.info(
|
|
233
242
|
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
234
243
|
);
|
|
235
244
|
|
|
236
|
-
return
|
|
245
|
+
return;
|
|
237
246
|
}
|
|
238
247
|
|
|
239
248
|
LoggerProxy.logger.info(
|
|
@@ -241,7 +250,7 @@ class HashTreeParser {
|
|
|
241
250
|
);
|
|
242
251
|
|
|
243
252
|
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
244
|
-
return
|
|
253
|
+
return;
|
|
245
254
|
}
|
|
246
255
|
|
|
247
256
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
@@ -251,51 +260,8 @@ class HashTreeParser {
|
|
|
251
260
|
hashTree,
|
|
252
261
|
};
|
|
253
262
|
|
|
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
|
-
);
|
|
263
|
+
this.enqueueSyncForDataset(dataSetInfo.name, 'new visible data set initialization', true);
|
|
264
|
+
await this.syncQueueProcessingPromise;
|
|
299
265
|
}
|
|
300
266
|
|
|
301
267
|
/**
|
|
@@ -382,9 +348,8 @@ class HashTreeParser {
|
|
|
382
348
|
if (this.state === 'stopped') {
|
|
383
349
|
return;
|
|
384
350
|
}
|
|
385
|
-
const updatedObjects: HashTreeObject[] = [];
|
|
386
351
|
|
|
387
|
-
for (const dataSet of visibleDataSets) {
|
|
352
|
+
for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
388
353
|
const {name, leafCount, url} = dataSet;
|
|
389
354
|
|
|
390
355
|
if (!this.dataSets[name]) {
|
|
@@ -420,19 +385,12 @@ class HashTreeParser {
|
|
|
420
385
|
);
|
|
421
386
|
this.dataSets[name].hashTree = new HashTree([], leafCount);
|
|
422
387
|
|
|
423
|
-
|
|
424
|
-
const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
|
|
425
|
-
|
|
426
|
-
if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
427
|
-
updatedObjects.push(...(data.updatedObjects || []));
|
|
428
|
-
}
|
|
388
|
+
this.enqueueSyncForDataset(name, `initialization sync for ${debugText}`, true);
|
|
429
389
|
}
|
|
430
390
|
}
|
|
431
391
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
updatedObjects,
|
|
435
|
-
});
|
|
392
|
+
// wait for all enqueued initialization syncs to complete
|
|
393
|
+
await this.syncQueueProcessingPromise;
|
|
436
394
|
}
|
|
437
395
|
|
|
438
396
|
/**
|
|
@@ -450,7 +408,7 @@ class HashTreeParser {
|
|
|
450
408
|
// object mapping dataset names to arrays of leaf data
|
|
451
409
|
const leafInfo: Record<string, Array<LeafInfo>> = {};
|
|
452
410
|
|
|
453
|
-
const findAndStoreMetaData = (currentLocusPart: any) => {
|
|
411
|
+
const findAndStoreMetaData = (currentLocusPart: any, currentLocusPartName: string) => {
|
|
454
412
|
if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
|
|
455
413
|
return;
|
|
456
414
|
}
|
|
@@ -465,10 +423,18 @@ class HashTreeParser {
|
|
|
465
423
|
};
|
|
466
424
|
|
|
467
425
|
if (copyData) {
|
|
468
|
-
|
|
426
|
+
if ((type as string).toLowerCase() === ObjectType.control) {
|
|
427
|
+
// control entries require special handling, because they are signalled by Locus
|
|
428
|
+
// differently when coming in messages vs API responses
|
|
429
|
+
newLeafInfo.data = {
|
|
430
|
+
[currentLocusPartName]: cloneDeep(currentLocusPart),
|
|
431
|
+
};
|
|
432
|
+
} else {
|
|
433
|
+
newLeafInfo.data = cloneDeep(currentLocusPart);
|
|
469
434
|
|
|
470
|
-
|
|
471
|
-
|
|
435
|
+
// remove any nested other objects that have their own htMeta
|
|
436
|
+
deleteNestedObjectsWithHtMeta(newLeafInfo.data);
|
|
437
|
+
}
|
|
472
438
|
}
|
|
473
439
|
|
|
474
440
|
for (const dataSetName of dataSetNames) {
|
|
@@ -480,19 +446,19 @@ class HashTreeParser {
|
|
|
480
446
|
}
|
|
481
447
|
|
|
482
448
|
if (Array.isArray(currentLocusPart)) {
|
|
483
|
-
for (const item of currentLocusPart) {
|
|
484
|
-
findAndStoreMetaData(item);
|
|
449
|
+
for (const [index, item] of currentLocusPart.entries()) {
|
|
450
|
+
findAndStoreMetaData(item, index.toString());
|
|
485
451
|
}
|
|
486
452
|
} else {
|
|
487
453
|
for (const key of Object.keys(currentLocusPart)) {
|
|
488
454
|
if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
|
|
489
|
-
findAndStoreMetaData(currentLocusPart[key]);
|
|
455
|
+
findAndStoreMetaData(currentLocusPart[key], key);
|
|
490
456
|
}
|
|
491
457
|
}
|
|
492
458
|
}
|
|
493
459
|
};
|
|
494
460
|
|
|
495
|
-
findAndStoreMetaData(locus);
|
|
461
|
+
findAndStoreMetaData(locus, 'locus');
|
|
496
462
|
|
|
497
463
|
return leafInfo;
|
|
498
464
|
}
|
|
@@ -683,6 +649,7 @@ class HashTreeParser {
|
|
|
683
649
|
const {dataSets, locus, metadata} = update;
|
|
684
650
|
|
|
685
651
|
if (!dataSets) {
|
|
652
|
+
// this happens for example when we handle GET /loci response
|
|
686
653
|
LoggerProxy.logger.info(
|
|
687
654
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
688
655
|
);
|
|
@@ -952,7 +919,7 @@ class HashTreeParser {
|
|
|
952
919
|
}
|
|
953
920
|
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
954
921
|
|
|
955
|
-
for (const ds of addedDataSets) {
|
|
922
|
+
for (const ds of sortByInitPriority(addedDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
956
923
|
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
957
924
|
|
|
958
925
|
LoggerProxy.logger.info(
|
|
@@ -964,12 +931,8 @@ class HashTreeParser {
|
|
|
964
931
|
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
965
932
|
);
|
|
966
933
|
} 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
934
|
// eslint-disable-next-line no-await-in-loop
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
this.callLocusInfoUpdateCallback(updates);
|
|
935
|
+
await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
973
936
|
}
|
|
974
937
|
}
|
|
975
938
|
}
|
|
@@ -1007,7 +970,7 @@ class HashTreeParser {
|
|
|
1007
970
|
|
|
1008
971
|
// when we detect new visible datasets, it may be that the metadata about them is not
|
|
1009
972
|
// available in the message, they will require separate async initialization
|
|
1010
|
-
let dataSetsRequiringInitialization = [];
|
|
973
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
1011
974
|
|
|
1012
975
|
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
1013
976
|
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
@@ -1015,7 +978,7 @@ class HashTreeParser {
|
|
|
1015
978
|
);
|
|
1016
979
|
|
|
1017
980
|
if (metadataUpdates.length > 0) {
|
|
1018
|
-
const updatedMetadataObjects = [];
|
|
981
|
+
const updatedMetadataObjects: HashTreeObject[] = [];
|
|
1019
982
|
|
|
1020
983
|
metadataUpdates.forEach((object) => {
|
|
1021
984
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
@@ -1044,7 +1007,7 @@ class HashTreeParser {
|
|
|
1044
1007
|
}
|
|
1045
1008
|
}
|
|
1046
1009
|
|
|
1047
|
-
if (message.locusStateElements
|
|
1010
|
+
if (message.locusStateElements && message.locusStateElements.length > 0) {
|
|
1048
1011
|
// by this point we now have this.dataSets setup for data sets from this message
|
|
1049
1012
|
// and hash trees created for the new visible data sets,
|
|
1050
1013
|
// so we can now process all the updates from the message
|
|
@@ -1140,20 +1103,17 @@ class HashTreeParser {
|
|
|
1140
1103
|
* @param {Object} updates parsed from a Locus message
|
|
1141
1104
|
* @returns {void}
|
|
1142
1105
|
*/
|
|
1143
|
-
private callLocusInfoUpdateCallback(updates: {
|
|
1144
|
-
updateType: LocusInfoUpdateType;
|
|
1145
|
-
updatedObjects?: HashTreeObject[];
|
|
1146
|
-
}) {
|
|
1106
|
+
private callLocusInfoUpdateCallback(updates: LocusInfoUpdate) {
|
|
1147
1107
|
if (this.state === 'stopped') {
|
|
1148
1108
|
return;
|
|
1149
1109
|
}
|
|
1150
1110
|
|
|
1151
|
-
const {updateType
|
|
1111
|
+
const {updateType} = updates;
|
|
1152
1112
|
|
|
1153
|
-
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1113
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updates.updatedObjects?.length > 0) {
|
|
1154
1114
|
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1155
1115
|
// or removals for objects that still exist in any of their datasets
|
|
1156
|
-
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1116
|
+
const filteredUpdates = updates.updatedObjects.filter((object) => {
|
|
1157
1117
|
const {elementId} = object.htMeta;
|
|
1158
1118
|
const {type, id, version} = elementId;
|
|
1159
1119
|
|
|
@@ -1190,10 +1150,10 @@ class HashTreeParser {
|
|
|
1190
1150
|
});
|
|
1191
1151
|
|
|
1192
1152
|
if (filteredUpdates.length > 0) {
|
|
1193
|
-
this.locusInfoUpdateCallback(updateType,
|
|
1153
|
+
this.locusInfoUpdateCallback({updateType, updatedObjects: filteredUpdates});
|
|
1194
1154
|
}
|
|
1195
1155
|
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
1196
|
-
this.locusInfoUpdateCallback(updateType
|
|
1156
|
+
this.locusInfoUpdateCallback({updateType});
|
|
1197
1157
|
}
|
|
1198
1158
|
}
|
|
1199
1159
|
|
|
@@ -1215,69 +1175,86 @@ class HashTreeParser {
|
|
|
1215
1175
|
* Performs a sync for the given data set.
|
|
1216
1176
|
*
|
|
1217
1177
|
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1218
|
-
* @param {string} rootHash - Our current root hash for this data set
|
|
1219
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)
|
|
1220
1180
|
* @returns {Promise<void>}
|
|
1221
1181
|
*/
|
|
1222
1182
|
private async performSync(
|
|
1223
1183
|
dataSet: InternalDataSet,
|
|
1224
|
-
|
|
1225
|
-
|
|
1184
|
+
reason: string,
|
|
1185
|
+
isInitialization?: boolean
|
|
1226
1186
|
): Promise<void> {
|
|
1227
1187
|
if (!dataSet.hashTree) {
|
|
1228
1188
|
return;
|
|
1229
1189
|
}
|
|
1230
1190
|
|
|
1191
|
+
const {hashTree} = dataSet;
|
|
1192
|
+
const rootHash = hashTree.getRootHash();
|
|
1193
|
+
|
|
1231
1194
|
try {
|
|
1232
1195
|
LoggerProxy.logger.info(
|
|
1233
1196
|
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1234
1197
|
);
|
|
1235
1198
|
|
|
1236
|
-
|
|
1199
|
+
let leavesData: Record<number, LeafDataItem[]> = {};
|
|
1237
1200
|
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1201
|
+
if (!isInitialization) {
|
|
1202
|
+
if (dataSet.leafCount !== 1) {
|
|
1203
|
+
let receivedHashes;
|
|
1240
1204
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
dataSet.name,
|
|
1245
|
-
rootHash
|
|
1246
|
-
);
|
|
1205
|
+
try {
|
|
1206
|
+
// request hashes from sender
|
|
1207
|
+
const hashesResult = await this.getHashesFromLocus(dataSet.name, rootHash);
|
|
1247
1208
|
|
|
1248
|
-
|
|
1209
|
+
if (!hashesResult) {
|
|
1210
|
+
// hashes match, no sync needed
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1249
1213
|
|
|
1250
|
-
|
|
1251
|
-
} catch (error) {
|
|
1252
|
-
if (error.statusCode === 409) {
|
|
1253
|
-
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1254
|
-
LoggerProxy.logger.info(
|
|
1255
|
-
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1256
|
-
);
|
|
1214
|
+
receivedHashes = hashesResult.hashes;
|
|
1257
1215
|
|
|
1258
|
-
|
|
1216
|
+
hashTree.resize(hashesResult.dataSet.leafCount);
|
|
1217
|
+
} catch (error: any) {
|
|
1218
|
+
if (error?.statusCode === 409) {
|
|
1219
|
+
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1220
|
+
LoggerProxy.logger.info(
|
|
1221
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
throw error;
|
|
1259
1227
|
}
|
|
1260
|
-
throw error;
|
|
1261
|
-
}
|
|
1262
1228
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1229
|
+
// identify mismatched leaves
|
|
1230
|
+
const mismatchedLeaveIndexes = hashTree.diffHashes(receivedHashes);
|
|
1265
1231
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1232
|
+
mismatchedLeaveIndexes.forEach((index) => {
|
|
1233
|
+
leavesData[index] = hashTree.getLeafData(index);
|
|
1234
|
+
});
|
|
1235
|
+
} else {
|
|
1236
|
+
leavesData = {0: hashTree.getLeafData(0)};
|
|
1237
|
+
}
|
|
1271
1238
|
}
|
|
1272
1239
|
// request sync for mismatched leaves
|
|
1273
|
-
|
|
1274
|
-
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1240
|
+
let syncResponse: HashTreeMessage | null = null;
|
|
1275
1241
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1242
|
+
if (isInitialization) {
|
|
1243
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {isInitialization: true});
|
|
1244
|
+
} else if (Object.keys(leavesData).length > 0) {
|
|
1245
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {
|
|
1246
|
+
mismatchedLeavesData: leavesData,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// sync API may return nothing (in that case data will arrive via messages)
|
|
1251
|
+
// or it may return a response in the same format as messages
|
|
1252
|
+
// We still need to restart the sync timer as a safety net in case the messages don't arrive.
|
|
1253
|
+
this.runSyncAlgorithm(dataSet);
|
|
1254
|
+
|
|
1255
|
+
if (syncResponse) {
|
|
1256
|
+
// the format of sync response is the same as messages, so we can reuse the same handler
|
|
1257
|
+
this.handleMessage(syncResponse, 'via sync API');
|
|
1281
1258
|
}
|
|
1282
1259
|
} catch (error) {
|
|
1283
1260
|
if (error instanceof MeetingEndedError) {
|
|
@@ -1293,6 +1270,105 @@ class HashTreeParser {
|
|
|
1293
1270
|
}
|
|
1294
1271
|
}
|
|
1295
1272
|
|
|
1273
|
+
/**
|
|
1274
|
+
* Enqueues a sync for the given data set. If the data set is already in the queue, the request is ignored.
|
|
1275
|
+
* This ensures that all syncs are executed sequentially and no more than 1 sync runs at a time.
|
|
1276
|
+
*
|
|
1277
|
+
* @param {string} dataSetName - The name of the data set to sync
|
|
1278
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1279
|
+
* @param {boolean} [isInitialization=false] - Whether this is an initialization sync (uses empty leaves data instead of hash comparison)
|
|
1280
|
+
* @returns {void}
|
|
1281
|
+
*/
|
|
1282
|
+
private enqueueSyncForDataset(
|
|
1283
|
+
dataSetName: string,
|
|
1284
|
+
reason: string,
|
|
1285
|
+
isInitialization = false
|
|
1286
|
+
): void {
|
|
1287
|
+
if (this.state === 'stopped') return;
|
|
1288
|
+
|
|
1289
|
+
const existingEntry = this.syncQueue.find((entry) => entry.dataSetName === dataSetName);
|
|
1290
|
+
|
|
1291
|
+
if (existingEntry) {
|
|
1292
|
+
if (isInitialization) {
|
|
1293
|
+
existingEntry.isInitialization = true;
|
|
1294
|
+
}
|
|
1295
|
+
LoggerProxy.logger.info(
|
|
1296
|
+
`HashTreeParser#enqueueSyncForDataset --> ${this.debugId} data set "${dataSetName}" already in sync queue, skipping`
|
|
1297
|
+
);
|
|
1298
|
+
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
this.syncQueue.push({dataSetName, reason, isInitialization});
|
|
1303
|
+
|
|
1304
|
+
if (!this.isSyncInProgress) {
|
|
1305
|
+
this.syncQueueProcessingPromise = this.processSyncQueue();
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Processes the sync queue sequentially. Only one instance of this method runs at a time.
|
|
1311
|
+
*
|
|
1312
|
+
* @returns {Promise<void>}
|
|
1313
|
+
*/
|
|
1314
|
+
private async processSyncQueue(): Promise<void> {
|
|
1315
|
+
if (this.isSyncInProgress) return;
|
|
1316
|
+
|
|
1317
|
+
this.isSyncInProgress = true;
|
|
1318
|
+
try {
|
|
1319
|
+
while (this.syncQueue.length > 0 && this.state !== 'stopped') {
|
|
1320
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1321
|
+
const {dataSetName, reason, isInitialization} = this.syncQueue.shift()!;
|
|
1322
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1323
|
+
|
|
1324
|
+
if (!dataSet?.hashTree) {
|
|
1325
|
+
// eslint-disable-next-line no-continue
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1330
|
+
await this.performSync(dataSet, reason, isInitialization);
|
|
1331
|
+
}
|
|
1332
|
+
} finally {
|
|
1333
|
+
this.isSyncInProgress = false;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Syncs all data sets that have hash trees, one by one in sequence, using the priority order
|
|
1339
|
+
* provided by sortByInitPriority(). Does nothing if the parser is stopped or if a syncAllDatasets
|
|
1340
|
+
* call is already in progress.
|
|
1341
|
+
*
|
|
1342
|
+
* @returns {Promise<void>}
|
|
1343
|
+
*/
|
|
1344
|
+
public async syncAllDatasets(): Promise<void> {
|
|
1345
|
+
if (this.state === 'stopped') return;
|
|
1346
|
+
if (this.isSyncAllInProgress) return;
|
|
1347
|
+
|
|
1348
|
+
this.isSyncAllInProgress = true;
|
|
1349
|
+
try {
|
|
1350
|
+
const dataSetsWithHashTrees = Object.values(this.dataSets)
|
|
1351
|
+
.filter((dataSet) => dataSet?.hashTree)
|
|
1352
|
+
.map((dataSet) => ({name: dataSet.name}));
|
|
1353
|
+
|
|
1354
|
+
const sorted = sortByInitPriority(dataSetsWithHashTrees, DATA_SET_INIT_PRIORITY);
|
|
1355
|
+
|
|
1356
|
+
LoggerProxy.logger.info(
|
|
1357
|
+
`HashTreeParser#syncAllDatasets --> ${this.debugId} syncing datasets: ${sorted
|
|
1358
|
+
.map((ds) => ds.name)
|
|
1359
|
+
.join(', ')}`
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
for (const ds of sorted) {
|
|
1363
|
+
this.enqueueSyncForDataset(ds.name, 'syncAllDatasets');
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
await this.syncQueueProcessingPromise;
|
|
1367
|
+
} finally {
|
|
1368
|
+
this.isSyncAllInProgress = false;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1296
1372
|
/**
|
|
1297
1373
|
* Runs the sync algorithm for the given data set.
|
|
1298
1374
|
*
|
|
@@ -1320,12 +1396,6 @@ class HashTreeParser {
|
|
|
1320
1396
|
|
|
1321
1397
|
dataSet.hashTree.resize(receivedDataSet.leafCount);
|
|
1322
1398
|
|
|
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
1399
|
const delay = dataSet.idleMs + this.getWeightedBackoffTime(dataSet.backoff);
|
|
1330
1400
|
|
|
1331
1401
|
if (delay > 0) {
|
|
@@ -1337,7 +1407,7 @@ class HashTreeParser {
|
|
|
1337
1407
|
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} setting "${dataSet.name}" sync timer for ${delay}`
|
|
1338
1408
|
);
|
|
1339
1409
|
|
|
1340
|
-
dataSet.timer = setTimeout(
|
|
1410
|
+
dataSet.timer = setTimeout(() => {
|
|
1341
1411
|
dataSet.timer = undefined;
|
|
1342
1412
|
|
|
1343
1413
|
if (!dataSet.hashTree) {
|
|
@@ -1351,9 +1421,8 @@ class HashTreeParser {
|
|
|
1351
1421
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
1352
1422
|
|
|
1353
1423
|
if (dataSet.root !== rootHash) {
|
|
1354
|
-
|
|
1355
|
-
dataSet,
|
|
1356
|
-
rootHash,
|
|
1424
|
+
this.enqueueSyncForDataset(
|
|
1425
|
+
dataSet.name,
|
|
1357
1426
|
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
1358
1427
|
);
|
|
1359
1428
|
} else {
|
|
@@ -1399,18 +1468,15 @@ class HashTreeParser {
|
|
|
1399
1468
|
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1400
1469
|
const delay = this.heartbeatIntervalMs + backoffTime;
|
|
1401
1470
|
|
|
1402
|
-
dataSet.heartbeatWatchdogTimer = setTimeout(
|
|
1471
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(() => {
|
|
1403
1472
|
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1404
1473
|
|
|
1405
1474
|
LoggerProxy.logger.warn(
|
|
1406
1475
|
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1407
1476
|
);
|
|
1408
1477
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
dataSet.hashTree.getRootHash(),
|
|
1412
|
-
`heartbeat watchdog expired`
|
|
1413
|
-
);
|
|
1478
|
+
this.enqueueSyncForDataset(dataSet.name, `heartbeat watchdog expired`);
|
|
1479
|
+
this.resetHeartbeatWatchdogs([dataSet]);
|
|
1414
1480
|
}, delay);
|
|
1415
1481
|
}
|
|
1416
1482
|
}
|
|
@@ -1443,6 +1509,7 @@ class HashTreeParser {
|
|
|
1443
1509
|
`HashTreeParser#stop --> ${this.debugId} Stopping HashTreeParser, clearing timers and hash trees`
|
|
1444
1510
|
);
|
|
1445
1511
|
this.stopAllTimers();
|
|
1512
|
+
this.syncQueue = [];
|
|
1446
1513
|
Object.values(this.dataSets).forEach((dataSet) => {
|
|
1447
1514
|
dataSet.hashTree = undefined;
|
|
1448
1515
|
});
|
|
@@ -1451,17 +1518,27 @@ class HashTreeParser {
|
|
|
1451
1518
|
}
|
|
1452
1519
|
|
|
1453
1520
|
/**
|
|
1454
|
-
*
|
|
1521
|
+
* Cleans up the HashTreeParser, stopping all timers and clearing all internal state.
|
|
1522
|
+
* After calling this, the parser should not be used anymore.
|
|
1523
|
+
* @returns {void}
|
|
1524
|
+
*/
|
|
1525
|
+
public cleanUp() {
|
|
1526
|
+
this.stop();
|
|
1527
|
+
this.dataSets = {};
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Resumes the HashTreeParser that was previously stopped, using a hash tree message.
|
|
1455
1532
|
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
1456
1533
|
* @returns {void}
|
|
1457
1534
|
*/
|
|
1458
|
-
public
|
|
1535
|
+
public resumeFromMessage(message: HashTreeMessage) {
|
|
1459
1536
|
// check that message contains metadata with visible data sets - this is essential to be able to resume
|
|
1460
1537
|
const metadataObject = message.locusStateElements?.find((el) => isMetadata(el));
|
|
1461
1538
|
|
|
1462
1539
|
if (!metadataObject?.data?.visibleDataSets) {
|
|
1463
1540
|
LoggerProxy.logger.warn(
|
|
1464
|
-
`HashTreeParser#
|
|
1541
|
+
`HashTreeParser#resumeFromMessage --> ${this.debugId} Cannot resume HashTreeParser because the message is missing metadata with visible data sets info`
|
|
1465
1542
|
);
|
|
1466
1543
|
|
|
1467
1544
|
return;
|
|
@@ -1482,7 +1559,7 @@ class HashTreeParser {
|
|
|
1482
1559
|
};
|
|
1483
1560
|
}
|
|
1484
1561
|
LoggerProxy.logger.info(
|
|
1485
|
-
`HashTreeParser#
|
|
1562
|
+
`HashTreeParser#resumeFromMessage --> ${
|
|
1486
1563
|
this.debugId
|
|
1487
1564
|
} Resuming HashTreeParser with data sets: ${Object.keys(this.dataSets).join(
|
|
1488
1565
|
', '
|
|
@@ -1493,6 +1570,24 @@ class HashTreeParser {
|
|
|
1493
1570
|
this.handleMessage(message, 'on resume');
|
|
1494
1571
|
}
|
|
1495
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
|
+
|
|
1496
1591
|
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1497
1592
|
const isValidDataSetForSentinel =
|
|
1498
1593
|
dataSetName === undefined ||
|
|
@@ -1516,7 +1611,7 @@ class HashTreeParser {
|
|
|
1516
1611
|
* Gets the current hashes from the locus for a specific data set.
|
|
1517
1612
|
* @param {string} dataSetName
|
|
1518
1613
|
* @param {string} currentRootHash
|
|
1519
|
-
* @returns {
|
|
1614
|
+
* @returns {Object|null} An object containing the hashes and leaf count, or null if the hashes match and no sync is needed
|
|
1520
1615
|
*/
|
|
1521
1616
|
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1522
1617
|
LoggerProxy.logger.info(
|
|
@@ -1535,6 +1630,15 @@ class HashTreeParser {
|
|
|
1535
1630
|
},
|
|
1536
1631
|
})
|
|
1537
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
|
+
|
|
1538
1642
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
1539
1643
|
const dataSetFromResponse = response.body?.dataSet;
|
|
1540
1644
|
|
|
@@ -1572,29 +1676,43 @@ class HashTreeParser {
|
|
|
1572
1676
|
* Sends a sync request to Locus for the specified data set.
|
|
1573
1677
|
*
|
|
1574
1678
|
* @param {InternalDataSet} dataSet The data set to sync.
|
|
1575
|
-
* @param {
|
|
1679
|
+
* @param {Object} options Either `{ isInitialization: true }` for init syncs (uses leafCount=1 with empty leaf data) or `{ mismatchedLeavesData }` for normal syncs.
|
|
1576
1680
|
* @returns {Promise<HashTreeMessage|null>}
|
|
1577
1681
|
*/
|
|
1578
1682
|
private sendSyncRequestToLocus(
|
|
1579
1683
|
dataSet: InternalDataSet,
|
|
1580
|
-
mismatchedLeavesData: Record<number, LeafDataItem[]>
|
|
1684
|
+
options: {isInitialization: true} | {mismatchedLeavesData: Record<number, LeafDataItem[]>}
|
|
1581
1685
|
): Promise<HashTreeMessage | null> {
|
|
1582
1686
|
LoggerProxy.logger.info(
|
|
1583
1687
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sending sync request for data set "${dataSet.name}"`
|
|
1584
1688
|
);
|
|
1585
1689
|
|
|
1690
|
+
const isInitialization = 'isInitialization' in options;
|
|
1691
|
+
|
|
1586
1692
|
const url = `${dataSet.url}/sync`;
|
|
1587
|
-
const body
|
|
1588
|
-
leafCount:
|
|
1693
|
+
const body: {
|
|
1694
|
+
leafCount: number;
|
|
1695
|
+
leafDataEntries: {leafIndex: number; elementIds: LeafDataItem[]}[];
|
|
1696
|
+
} = {
|
|
1697
|
+
leafCount: isInitialization ? 1 : dataSet.leafCount,
|
|
1589
1698
|
leafDataEntries: [],
|
|
1590
1699
|
};
|
|
1591
1700
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1701
|
+
if (isInitialization) {
|
|
1702
|
+
// initialization sync: Locus requires leafCount=1 with a single empty leaf
|
|
1703
|
+
body.leafDataEntries.push({leafIndex: 0, elementIds: []});
|
|
1704
|
+
} else {
|
|
1705
|
+
const {mismatchedLeavesData} = options;
|
|
1706
|
+
|
|
1707
|
+
Object.keys(mismatchedLeavesData).forEach((index) => {
|
|
1708
|
+
const leafIndex = parseInt(index, 10);
|
|
1709
|
+
|
|
1710
|
+
body.leafDataEntries.push({
|
|
1711
|
+
leafIndex,
|
|
1712
|
+
elementIds: mismatchedLeavesData[leafIndex],
|
|
1713
|
+
});
|
|
1596
1714
|
});
|
|
1597
|
-
}
|
|
1715
|
+
}
|
|
1598
1716
|
|
|
1599
1717
|
const ourCurrentRootHash = dataSet.hashTree ? dataSet.hashTree.getRootHash() : EMPTY_HASH;
|
|
1600
1718
|
|