@webex/plugin-meetings 3.12.0-next.5 → 3.12.0-next.51
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 +646 -371
- 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 +289 -86
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +19 -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 +205 -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 +83 -16
- 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 +21 -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 +20 -2
- 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 +362 -174
- 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 +291 -93
- package/src/locus-info/types.ts +25 -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 +109 -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 +1341 -140
- 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 +475 -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 +309 -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
|
-
import {LocusDTO} from '../locus-info/types';
|
|
8
|
-
import {deleteNestedObjectsWithHtMeta, isMetadata} from './utils';
|
|
9
|
+
import {LocusDTO, LocusErrorCodes} from '../locus-info/types';
|
|
10
|
+
import {deleteNestedObjectsWithHtMeta, isMetadata, sortByInitPriority} from './utils';
|
|
9
11
|
|
|
10
12
|
export interface DataSet {
|
|
11
13
|
url: string;
|
|
@@ -54,13 +56,24 @@ type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
|
|
|
54
56
|
export const LocusInfoUpdateType = {
|
|
55
57
|
OBJECTS_UPDATED: 'OBJECTS_UPDATED',
|
|
56
58
|
MEETING_ENDED: 'MEETING_ENDED',
|
|
59
|
+
LOCUS_NOT_FOUND: 'LOCUS_NOT_FOUND',
|
|
57
60
|
} as const;
|
|
58
61
|
|
|
59
62
|
export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
|
|
64
|
+
interface LocusUpdatePayloads {
|
|
65
|
+
[LocusInfoUpdateType.OBJECTS_UPDATED]: {updatedObjects: HashTreeObject[]};
|
|
66
|
+
[LocusInfoUpdateType.MEETING_ENDED]: unknown; // No extra data
|
|
67
|
+
[LocusInfoUpdateType.LOCUS_NOT_FOUND]: unknown; // No extra data
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type LocusInfoUpdate = {
|
|
71
|
+
[K in keyof LocusUpdatePayloads]: {
|
|
72
|
+
updateType: K;
|
|
73
|
+
} & LocusUpdatePayloads[K];
|
|
74
|
+
}[keyof LocusUpdatePayloads];
|
|
75
|
+
|
|
76
|
+
export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
64
77
|
|
|
65
78
|
interface LeafInfo {
|
|
66
79
|
type: ObjectType;
|
|
@@ -75,6 +88,13 @@ interface LeafInfo {
|
|
|
75
88
|
*/
|
|
76
89
|
export class MeetingEndedError extends Error {}
|
|
77
90
|
|
|
91
|
+
/**
|
|
92
|
+
* This error is thrown when a 404 is received from Locus hash tree endpoints, indicating that the locus URL
|
|
93
|
+
* is no longer valid (e.g. participant moved to a breakout room, or meeting ended).
|
|
94
|
+
* It's handled internally by HashTreeParser and results in LOCUS_NOT_FOUND being sent up.
|
|
95
|
+
*/
|
|
96
|
+
export class LocusNotFoundError extends Error {}
|
|
97
|
+
|
|
78
98
|
/* Currently Locus always sends Metadata objects only in the "self" dataset.
|
|
79
99
|
* If this ever changes, update all the code that relies on this constant.
|
|
80
100
|
*/
|
|
@@ -99,6 +119,10 @@ class HashTreeParser {
|
|
|
99
119
|
heartbeatIntervalMs?: number;
|
|
100
120
|
private excludedDataSets: string[];
|
|
101
121
|
state: 'active' | 'stopped';
|
|
122
|
+
private syncQueue: Array<{dataSetName: string; reason: string; isInitialization?: boolean}> = [];
|
|
123
|
+
private isSyncInProgress = false;
|
|
124
|
+
private isSyncAllInProgress = false;
|
|
125
|
+
private syncQueueProcessingPromise: Promise<void> = Promise.resolve();
|
|
102
126
|
|
|
103
127
|
/**
|
|
104
128
|
* Constructor for HashTreeParser
|
|
@@ -224,16 +248,16 @@ class HashTreeParser {
|
|
|
224
248
|
* @param {DataSet} dataSetInfo The new data set to be added
|
|
225
249
|
* @returns {Promise}
|
|
226
250
|
*/
|
|
227
|
-
private initializeNewVisibleDataSet(
|
|
251
|
+
private async initializeNewVisibleDataSet(
|
|
228
252
|
visibleDataSetInfo: VisibleDataSetInfo,
|
|
229
253
|
dataSetInfo: DataSet
|
|
230
|
-
): Promise<
|
|
254
|
+
): Promise<void> {
|
|
231
255
|
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
232
256
|
LoggerProxy.logger.info(
|
|
233
257
|
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
234
258
|
);
|
|
235
259
|
|
|
236
|
-
return
|
|
260
|
+
return;
|
|
237
261
|
}
|
|
238
262
|
|
|
239
263
|
LoggerProxy.logger.info(
|
|
@@ -241,7 +265,7 @@ class HashTreeParser {
|
|
|
241
265
|
);
|
|
242
266
|
|
|
243
267
|
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
244
|
-
return
|
|
268
|
+
return;
|
|
245
269
|
}
|
|
246
270
|
|
|
247
271
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
@@ -251,51 +275,8 @@ class HashTreeParser {
|
|
|
251
275
|
hashTree,
|
|
252
276
|
};
|
|
253
277
|
|
|
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
|
-
);
|
|
278
|
+
this.enqueueSyncForDataset(dataSetInfo.name, 'new visible data set initialization', true);
|
|
279
|
+
await this.syncQueueProcessingPromise;
|
|
299
280
|
}
|
|
300
281
|
|
|
301
282
|
/**
|
|
@@ -382,9 +363,8 @@ class HashTreeParser {
|
|
|
382
363
|
if (this.state === 'stopped') {
|
|
383
364
|
return;
|
|
384
365
|
}
|
|
385
|
-
const updatedObjects: HashTreeObject[] = [];
|
|
386
366
|
|
|
387
|
-
for (const dataSet of visibleDataSets) {
|
|
367
|
+
for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
388
368
|
const {name, leafCount, url} = dataSet;
|
|
389
369
|
|
|
390
370
|
if (!this.dataSets[name]) {
|
|
@@ -420,19 +400,12 @@ class HashTreeParser {
|
|
|
420
400
|
);
|
|
421
401
|
this.dataSets[name].hashTree = new HashTree([], leafCount);
|
|
422
402
|
|
|
423
|
-
|
|
424
|
-
const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
|
|
425
|
-
|
|
426
|
-
if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
427
|
-
updatedObjects.push(...(data.updatedObjects || []));
|
|
428
|
-
}
|
|
403
|
+
this.enqueueSyncForDataset(name, `initialization sync for ${debugText}`, true);
|
|
429
404
|
}
|
|
430
405
|
}
|
|
431
406
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
updatedObjects,
|
|
435
|
-
});
|
|
407
|
+
// wait for all enqueued initialization syncs to complete
|
|
408
|
+
await this.syncQueueProcessingPromise;
|
|
436
409
|
}
|
|
437
410
|
|
|
438
411
|
/**
|
|
@@ -450,7 +423,7 @@ class HashTreeParser {
|
|
|
450
423
|
// object mapping dataset names to arrays of leaf data
|
|
451
424
|
const leafInfo: Record<string, Array<LeafInfo>> = {};
|
|
452
425
|
|
|
453
|
-
const findAndStoreMetaData = (currentLocusPart: any) => {
|
|
426
|
+
const findAndStoreMetaData = (currentLocusPart: any, currentLocusPartName: string) => {
|
|
454
427
|
if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
|
|
455
428
|
return;
|
|
456
429
|
}
|
|
@@ -465,10 +438,18 @@ class HashTreeParser {
|
|
|
465
438
|
};
|
|
466
439
|
|
|
467
440
|
if (copyData) {
|
|
468
|
-
|
|
441
|
+
if ((type as string).toLowerCase() === ObjectType.control) {
|
|
442
|
+
// control entries require special handling, because they are signalled by Locus
|
|
443
|
+
// differently when coming in messages vs API responses
|
|
444
|
+
newLeafInfo.data = {
|
|
445
|
+
[currentLocusPartName]: cloneDeep(currentLocusPart),
|
|
446
|
+
};
|
|
447
|
+
} else {
|
|
448
|
+
newLeafInfo.data = cloneDeep(currentLocusPart);
|
|
469
449
|
|
|
470
|
-
|
|
471
|
-
|
|
450
|
+
// remove any nested other objects that have their own htMeta
|
|
451
|
+
deleteNestedObjectsWithHtMeta(newLeafInfo.data);
|
|
452
|
+
}
|
|
472
453
|
}
|
|
473
454
|
|
|
474
455
|
for (const dataSetName of dataSetNames) {
|
|
@@ -480,19 +461,19 @@ class HashTreeParser {
|
|
|
480
461
|
}
|
|
481
462
|
|
|
482
463
|
if (Array.isArray(currentLocusPart)) {
|
|
483
|
-
for (const item of currentLocusPart) {
|
|
484
|
-
findAndStoreMetaData(item);
|
|
464
|
+
for (const [index, item] of currentLocusPart.entries()) {
|
|
465
|
+
findAndStoreMetaData(item, index.toString());
|
|
485
466
|
}
|
|
486
467
|
} else {
|
|
487
468
|
for (const key of Object.keys(currentLocusPart)) {
|
|
488
469
|
if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
|
|
489
|
-
findAndStoreMetaData(currentLocusPart[key]);
|
|
470
|
+
findAndStoreMetaData(currentLocusPart[key], key);
|
|
490
471
|
}
|
|
491
472
|
}
|
|
492
473
|
}
|
|
493
474
|
};
|
|
494
475
|
|
|
495
|
-
findAndStoreMetaData(locus);
|
|
476
|
+
findAndStoreMetaData(locus, 'locus');
|
|
496
477
|
|
|
497
478
|
return leafInfo;
|
|
498
479
|
}
|
|
@@ -584,6 +565,32 @@ class HashTreeParser {
|
|
|
584
565
|
});
|
|
585
566
|
}
|
|
586
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Handles known errors that can happen during syncs
|
|
570
|
+
*
|
|
571
|
+
* @param {any} error - The error to handle
|
|
572
|
+
* @returns {boolean} true if the error was recognized and handled, false otherwise
|
|
573
|
+
*/
|
|
574
|
+
private handleSyncErrors(error: any) {
|
|
575
|
+
if (error instanceof MeetingEndedError) {
|
|
576
|
+
this.callLocusInfoUpdateCallback({
|
|
577
|
+
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
if (error instanceof LocusNotFoundError) {
|
|
583
|
+
this.callLocusInfoUpdateCallback({
|
|
584
|
+
updateType: LocusInfoUpdateType.LOCUS_NOT_FOUND,
|
|
585
|
+
});
|
|
586
|
+
this.stop();
|
|
587
|
+
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
|
|
587
594
|
/**
|
|
588
595
|
* Asynchronously initializes new visible data sets
|
|
589
596
|
*
|
|
@@ -600,11 +607,7 @@ class HashTreeParser {
|
|
|
600
607
|
);
|
|
601
608
|
queueMicrotask(() => {
|
|
602
609
|
this.initializeNewVisibleDataSets(dataSetsRequiringInitialization).catch((error) => {
|
|
603
|
-
if (error
|
|
604
|
-
this.callLocusInfoUpdateCallback({
|
|
605
|
-
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
606
|
-
});
|
|
607
|
-
} else {
|
|
610
|
+
if (!this.handleSyncErrors(error)) {
|
|
608
611
|
LoggerProxy.logger.warn(
|
|
609
612
|
`HashTreeParser#queueInitForNewVisibleDataSets --> ${
|
|
610
613
|
this.debugId
|
|
@@ -683,6 +686,7 @@ class HashTreeParser {
|
|
|
683
686
|
const {dataSets, locus, metadata} = update;
|
|
684
687
|
|
|
685
688
|
if (!dataSets) {
|
|
689
|
+
// this happens for example when we handle GET /loci response
|
|
686
690
|
LoggerProxy.logger.info(
|
|
687
691
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
688
692
|
);
|
|
@@ -786,6 +790,18 @@ class HashTreeParser {
|
|
|
786
790
|
}
|
|
787
791
|
}
|
|
788
792
|
|
|
793
|
+
/**
|
|
794
|
+
* Updates the leaf count for a data set, resizing its hash tree accordingly.
|
|
795
|
+
*
|
|
796
|
+
* @param {InternalDataSet} dataSet - The data set to update
|
|
797
|
+
* @param {number} newLeafCount - The new leaf count
|
|
798
|
+
* @returns {void}
|
|
799
|
+
*/
|
|
800
|
+
private updateDataSetLeafCount(dataSet: InternalDataSet, newLeafCount: number): void {
|
|
801
|
+
dataSet.hashTree?.resize(newLeafCount);
|
|
802
|
+
dataSet.leafCount = newLeafCount;
|
|
803
|
+
}
|
|
804
|
+
|
|
789
805
|
/**
|
|
790
806
|
* Checks for changes in the visible data sets based on the updated objects.
|
|
791
807
|
* @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects.
|
|
@@ -952,7 +968,7 @@ class HashTreeParser {
|
|
|
952
968
|
}
|
|
953
969
|
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
954
970
|
|
|
955
|
-
for (const ds of addedDataSets) {
|
|
971
|
+
for (const ds of sortByInitPriority(addedDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
956
972
|
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
957
973
|
|
|
958
974
|
LoggerProxy.logger.info(
|
|
@@ -964,12 +980,8 @@ class HashTreeParser {
|
|
|
964
980
|
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
965
981
|
);
|
|
966
982
|
} 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
983
|
// eslint-disable-next-line no-await-in-loop
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
this.callLocusInfoUpdateCallback(updates);
|
|
984
|
+
await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
973
985
|
}
|
|
974
986
|
}
|
|
975
987
|
}
|
|
@@ -1007,7 +1019,7 @@ class HashTreeParser {
|
|
|
1007
1019
|
|
|
1008
1020
|
// when we detect new visible datasets, it may be that the metadata about them is not
|
|
1009
1021
|
// available in the message, they will require separate async initialization
|
|
1010
|
-
let dataSetsRequiringInitialization = [];
|
|
1022
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
1011
1023
|
|
|
1012
1024
|
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
1013
1025
|
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
@@ -1015,7 +1027,7 @@ class HashTreeParser {
|
|
|
1015
1027
|
);
|
|
1016
1028
|
|
|
1017
1029
|
if (metadataUpdates.length > 0) {
|
|
1018
|
-
const updatedMetadataObjects = [];
|
|
1030
|
+
const updatedMetadataObjects: HashTreeObject[] = [];
|
|
1019
1031
|
|
|
1020
1032
|
metadataUpdates.forEach((object) => {
|
|
1021
1033
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
@@ -1044,7 +1056,7 @@ class HashTreeParser {
|
|
|
1044
1056
|
}
|
|
1045
1057
|
}
|
|
1046
1058
|
|
|
1047
|
-
if (message.locusStateElements
|
|
1059
|
+
if (message.locusStateElements && message.locusStateElements.length > 0) {
|
|
1048
1060
|
// by this point we now have this.dataSets setup for data sets from this message
|
|
1049
1061
|
// and hash trees created for the new visible data sets,
|
|
1050
1062
|
// so we can now process all the updates from the message
|
|
@@ -1140,20 +1152,17 @@ class HashTreeParser {
|
|
|
1140
1152
|
* @param {Object} updates parsed from a Locus message
|
|
1141
1153
|
* @returns {void}
|
|
1142
1154
|
*/
|
|
1143
|
-
private callLocusInfoUpdateCallback(updates: {
|
|
1144
|
-
updateType: LocusInfoUpdateType;
|
|
1145
|
-
updatedObjects?: HashTreeObject[];
|
|
1146
|
-
}) {
|
|
1155
|
+
private callLocusInfoUpdateCallback(updates: LocusInfoUpdate) {
|
|
1147
1156
|
if (this.state === 'stopped') {
|
|
1148
1157
|
return;
|
|
1149
1158
|
}
|
|
1150
1159
|
|
|
1151
|
-
const {updateType
|
|
1160
|
+
const {updateType} = updates;
|
|
1152
1161
|
|
|
1153
|
-
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1162
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updates.updatedObjects?.length > 0) {
|
|
1154
1163
|
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1155
1164
|
// or removals for objects that still exist in any of their datasets
|
|
1156
|
-
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1165
|
+
const filteredUpdates = updates.updatedObjects.filter((object) => {
|
|
1157
1166
|
const {elementId} = object.htMeta;
|
|
1158
1167
|
const {type, id, version} = elementId;
|
|
1159
1168
|
|
|
@@ -1190,10 +1199,10 @@ class HashTreeParser {
|
|
|
1190
1199
|
});
|
|
1191
1200
|
|
|
1192
1201
|
if (filteredUpdates.length > 0) {
|
|
1193
|
-
this.locusInfoUpdateCallback(updateType,
|
|
1202
|
+
this.locusInfoUpdateCallback({updateType, updatedObjects: filteredUpdates});
|
|
1194
1203
|
}
|
|
1195
1204
|
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
1196
|
-
this.locusInfoUpdateCallback(updateType
|
|
1205
|
+
this.locusInfoUpdateCallback({updateType});
|
|
1197
1206
|
}
|
|
1198
1207
|
}
|
|
1199
1208
|
|
|
@@ -1215,76 +1224,89 @@ class HashTreeParser {
|
|
|
1215
1224
|
* Performs a sync for the given data set.
|
|
1216
1225
|
*
|
|
1217
1226
|
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1218
|
-
* @param {string} rootHash - Our current root hash for this data set
|
|
1219
1227
|
* @param {string} reason - The reason for the sync (used for logging)
|
|
1228
|
+
* @param {boolean} [isInitialization] - Whether this is an initialization sync (sends empty leaves data instead of comparing hashes)
|
|
1220
1229
|
* @returns {Promise<void>}
|
|
1221
1230
|
*/
|
|
1222
1231
|
private async performSync(
|
|
1223
1232
|
dataSet: InternalDataSet,
|
|
1224
|
-
|
|
1225
|
-
|
|
1233
|
+
reason: string,
|
|
1234
|
+
isInitialization?: boolean
|
|
1226
1235
|
): Promise<void> {
|
|
1227
1236
|
if (!dataSet.hashTree) {
|
|
1228
1237
|
return;
|
|
1229
1238
|
}
|
|
1230
1239
|
|
|
1240
|
+
const {hashTree} = dataSet;
|
|
1241
|
+
const rootHash = hashTree.getRootHash();
|
|
1242
|
+
|
|
1231
1243
|
try {
|
|
1232
1244
|
LoggerProxy.logger.info(
|
|
1233
1245
|
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1234
1246
|
);
|
|
1235
1247
|
|
|
1236
|
-
|
|
1248
|
+
let leavesData: Record<number, LeafDataItem[]> = {};
|
|
1237
1249
|
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1250
|
+
if (!isInitialization) {
|
|
1251
|
+
if (dataSet.leafCount !== 1) {
|
|
1252
|
+
let receivedHashes;
|
|
1240
1253
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
dataSet.name,
|
|
1245
|
-
rootHash
|
|
1246
|
-
);
|
|
1254
|
+
try {
|
|
1255
|
+
// request hashes from sender
|
|
1256
|
+
const hashesResult = await this.getHashesFromLocus(dataSet.name, rootHash);
|
|
1247
1257
|
|
|
1248
|
-
|
|
1258
|
+
if (!hashesResult) {
|
|
1259
|
+
// hashes match, no sync needed
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1249
1262
|
|
|
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
|
-
);
|
|
1263
|
+
receivedHashes = hashesResult.hashes;
|
|
1257
1264
|
|
|
1258
|
-
|
|
1265
|
+
this.updateDataSetLeafCount(dataSet, hashesResult.dataSet.leafCount);
|
|
1266
|
+
} catch (error: any) {
|
|
1267
|
+
if (error?.statusCode === 409) {
|
|
1268
|
+
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1269
|
+
LoggerProxy.logger.info(
|
|
1270
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
throw error;
|
|
1259
1276
|
}
|
|
1260
|
-
throw error;
|
|
1261
|
-
}
|
|
1262
1277
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1278
|
+
// identify mismatched leaves
|
|
1279
|
+
const mismatchedLeaveIndexes = hashTree.diffHashes(receivedHashes);
|
|
1265
1280
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1281
|
+
mismatchedLeaveIndexes.forEach((index) => {
|
|
1282
|
+
leavesData[index] = hashTree.getLeafData(index);
|
|
1283
|
+
});
|
|
1284
|
+
} else {
|
|
1285
|
+
leavesData = {0: hashTree.getLeafData(0)};
|
|
1286
|
+
}
|
|
1271
1287
|
}
|
|
1272
1288
|
// request sync for mismatched leaves
|
|
1273
|
-
|
|
1274
|
-
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1289
|
+
let syncResponse: HashTreeMessage | null = null;
|
|
1275
1290
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1291
|
+
if (isInitialization) {
|
|
1292
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {isInitialization: true});
|
|
1293
|
+
} else if (Object.keys(leavesData).length > 0) {
|
|
1294
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {
|
|
1295
|
+
mismatchedLeavesData: leavesData,
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// sync API may return nothing (in that case data will arrive via messages)
|
|
1300
|
+
// or it may return a response in the same format as messages
|
|
1301
|
+
// We still need to restart the sync timer as a safety net in case the messages don't arrive.
|
|
1302
|
+
this.runSyncAlgorithm(dataSet);
|
|
1303
|
+
|
|
1304
|
+
if (syncResponse) {
|
|
1305
|
+
// the format of sync response is the same as messages, so we can reuse the same handler
|
|
1306
|
+
this.handleMessage(syncResponse, 'via sync API');
|
|
1281
1307
|
}
|
|
1282
1308
|
} catch (error) {
|
|
1283
|
-
if (error
|
|
1284
|
-
this.callLocusInfoUpdateCallback({
|
|
1285
|
-
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
1286
|
-
});
|
|
1287
|
-
} else {
|
|
1309
|
+
if (!this.handleSyncErrors(error)) {
|
|
1288
1310
|
LoggerProxy.logger.warn(
|
|
1289
1311
|
`HashTreeParser#performSync --> ${this.debugId} error during sync for data set "${dataSet.name}":`,
|
|
1290
1312
|
error
|
|
@@ -1293,6 +1315,105 @@ class HashTreeParser {
|
|
|
1293
1315
|
}
|
|
1294
1316
|
}
|
|
1295
1317
|
|
|
1318
|
+
/**
|
|
1319
|
+
* Enqueues a sync for the given data set. If the data set is already in the queue, the request is ignored.
|
|
1320
|
+
* This ensures that all syncs are executed sequentially and no more than 1 sync runs at a time.
|
|
1321
|
+
*
|
|
1322
|
+
* @param {string} dataSetName - The name of the data set to sync
|
|
1323
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1324
|
+
* @param {boolean} [isInitialization=false] - Whether this is an initialization sync (uses empty leaves data instead of hash comparison)
|
|
1325
|
+
* @returns {void}
|
|
1326
|
+
*/
|
|
1327
|
+
private enqueueSyncForDataset(
|
|
1328
|
+
dataSetName: string,
|
|
1329
|
+
reason: string,
|
|
1330
|
+
isInitialization = false
|
|
1331
|
+
): void {
|
|
1332
|
+
if (this.state === 'stopped') return;
|
|
1333
|
+
|
|
1334
|
+
const existingEntry = this.syncQueue.find((entry) => entry.dataSetName === dataSetName);
|
|
1335
|
+
|
|
1336
|
+
if (existingEntry) {
|
|
1337
|
+
if (isInitialization) {
|
|
1338
|
+
existingEntry.isInitialization = true;
|
|
1339
|
+
}
|
|
1340
|
+
LoggerProxy.logger.info(
|
|
1341
|
+
`HashTreeParser#enqueueSyncForDataset --> ${this.debugId} data set "${dataSetName}" already in sync queue, skipping`
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
this.syncQueue.push({dataSetName, reason, isInitialization});
|
|
1348
|
+
|
|
1349
|
+
if (!this.isSyncInProgress) {
|
|
1350
|
+
this.syncQueueProcessingPromise = this.processSyncQueue();
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Processes the sync queue sequentially. Only one instance of this method runs at a time.
|
|
1356
|
+
*
|
|
1357
|
+
* @returns {Promise<void>}
|
|
1358
|
+
*/
|
|
1359
|
+
private async processSyncQueue(): Promise<void> {
|
|
1360
|
+
if (this.isSyncInProgress) return;
|
|
1361
|
+
|
|
1362
|
+
this.isSyncInProgress = true;
|
|
1363
|
+
try {
|
|
1364
|
+
while (this.syncQueue.length > 0 && this.state !== 'stopped') {
|
|
1365
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1366
|
+
const {dataSetName, reason, isInitialization} = this.syncQueue.shift()!;
|
|
1367
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1368
|
+
|
|
1369
|
+
if (!dataSet?.hashTree) {
|
|
1370
|
+
// eslint-disable-next-line no-continue
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1375
|
+
await this.performSync(dataSet, reason, isInitialization);
|
|
1376
|
+
}
|
|
1377
|
+
} finally {
|
|
1378
|
+
this.isSyncInProgress = false;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Syncs all data sets that have hash trees, one by one in sequence, using the priority order
|
|
1384
|
+
* provided by sortByInitPriority(). Does nothing if the parser is stopped or if a syncAllDatasets
|
|
1385
|
+
* call is already in progress.
|
|
1386
|
+
*
|
|
1387
|
+
* @returns {Promise<void>}
|
|
1388
|
+
*/
|
|
1389
|
+
public async syncAllDatasets(): Promise<void> {
|
|
1390
|
+
if (this.state === 'stopped') return;
|
|
1391
|
+
if (this.isSyncAllInProgress) return;
|
|
1392
|
+
|
|
1393
|
+
this.isSyncAllInProgress = true;
|
|
1394
|
+
try {
|
|
1395
|
+
const dataSetsWithHashTrees = Object.values(this.dataSets)
|
|
1396
|
+
.filter((dataSet) => dataSet?.hashTree)
|
|
1397
|
+
.map((dataSet) => ({name: dataSet.name}));
|
|
1398
|
+
|
|
1399
|
+
const sorted = sortByInitPriority(dataSetsWithHashTrees, DATA_SET_INIT_PRIORITY);
|
|
1400
|
+
|
|
1401
|
+
LoggerProxy.logger.info(
|
|
1402
|
+
`HashTreeParser#syncAllDatasets --> ${this.debugId} syncing datasets: ${sorted
|
|
1403
|
+
.map((ds) => ds.name)
|
|
1404
|
+
.join(', ')}`
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
for (const ds of sorted) {
|
|
1408
|
+
this.enqueueSyncForDataset(ds.name, 'syncAllDatasets');
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
await this.syncQueueProcessingPromise;
|
|
1412
|
+
} finally {
|
|
1413
|
+
this.isSyncAllInProgress = false;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1296
1417
|
/**
|
|
1297
1418
|
* Runs the sync algorithm for the given data set.
|
|
1298
1419
|
*
|
|
@@ -1320,12 +1441,6 @@ class HashTreeParser {
|
|
|
1320
1441
|
|
|
1321
1442
|
dataSet.hashTree.resize(receivedDataSet.leafCount);
|
|
1322
1443
|
|
|
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
1444
|
const delay = dataSet.idleMs + this.getWeightedBackoffTime(dataSet.backoff);
|
|
1330
1445
|
|
|
1331
1446
|
if (delay > 0) {
|
|
@@ -1337,7 +1452,7 @@ class HashTreeParser {
|
|
|
1337
1452
|
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} setting "${dataSet.name}" sync timer for ${delay}`
|
|
1338
1453
|
);
|
|
1339
1454
|
|
|
1340
|
-
dataSet.timer = setTimeout(
|
|
1455
|
+
dataSet.timer = setTimeout(() => {
|
|
1341
1456
|
dataSet.timer = undefined;
|
|
1342
1457
|
|
|
1343
1458
|
if (!dataSet.hashTree) {
|
|
@@ -1351,9 +1466,8 @@ class HashTreeParser {
|
|
|
1351
1466
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
1352
1467
|
|
|
1353
1468
|
if (dataSet.root !== rootHash) {
|
|
1354
|
-
|
|
1355
|
-
dataSet,
|
|
1356
|
-
rootHash,
|
|
1469
|
+
this.enqueueSyncForDataset(
|
|
1470
|
+
dataSet.name,
|
|
1357
1471
|
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
1358
1472
|
);
|
|
1359
1473
|
} else {
|
|
@@ -1399,18 +1513,15 @@ class HashTreeParser {
|
|
|
1399
1513
|
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1400
1514
|
const delay = this.heartbeatIntervalMs + backoffTime;
|
|
1401
1515
|
|
|
1402
|
-
dataSet.heartbeatWatchdogTimer = setTimeout(
|
|
1516
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(() => {
|
|
1403
1517
|
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1404
1518
|
|
|
1405
1519
|
LoggerProxy.logger.warn(
|
|
1406
1520
|
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1407
1521
|
);
|
|
1408
1522
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
dataSet.hashTree.getRootHash(),
|
|
1412
|
-
`heartbeat watchdog expired`
|
|
1413
|
-
);
|
|
1523
|
+
this.enqueueSyncForDataset(dataSet.name, `heartbeat watchdog expired`);
|
|
1524
|
+
this.resetHeartbeatWatchdogs([dataSet]);
|
|
1414
1525
|
}, delay);
|
|
1415
1526
|
}
|
|
1416
1527
|
}
|
|
@@ -1443,6 +1554,7 @@ class HashTreeParser {
|
|
|
1443
1554
|
`HashTreeParser#stop --> ${this.debugId} Stopping HashTreeParser, clearing timers and hash trees`
|
|
1444
1555
|
);
|
|
1445
1556
|
this.stopAllTimers();
|
|
1557
|
+
this.syncQueue = [];
|
|
1446
1558
|
Object.values(this.dataSets).forEach((dataSet) => {
|
|
1447
1559
|
dataSet.hashTree = undefined;
|
|
1448
1560
|
});
|
|
@@ -1451,17 +1563,27 @@ class HashTreeParser {
|
|
|
1451
1563
|
}
|
|
1452
1564
|
|
|
1453
1565
|
/**
|
|
1454
|
-
*
|
|
1566
|
+
* Cleans up the HashTreeParser, stopping all timers and clearing all internal state.
|
|
1567
|
+
* After calling this, the parser should not be used anymore.
|
|
1568
|
+
* @returns {void}
|
|
1569
|
+
*/
|
|
1570
|
+
public cleanUp() {
|
|
1571
|
+
this.stop();
|
|
1572
|
+
this.dataSets = {};
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
/**
|
|
1576
|
+
* Resumes the HashTreeParser that was previously stopped, using a hash tree message.
|
|
1455
1577
|
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
1456
1578
|
* @returns {void}
|
|
1457
1579
|
*/
|
|
1458
|
-
public
|
|
1580
|
+
public resumeFromMessage(message: HashTreeMessage) {
|
|
1459
1581
|
// check that message contains metadata with visible data sets - this is essential to be able to resume
|
|
1460
1582
|
const metadataObject = message.locusStateElements?.find((el) => isMetadata(el));
|
|
1461
1583
|
|
|
1462
1584
|
if (!metadataObject?.data?.visibleDataSets) {
|
|
1463
1585
|
LoggerProxy.logger.warn(
|
|
1464
|
-
`HashTreeParser#
|
|
1586
|
+
`HashTreeParser#resumeFromMessage --> ${this.debugId} Cannot resume HashTreeParser because the message is missing metadata with visible data sets info`
|
|
1465
1587
|
);
|
|
1466
1588
|
|
|
1467
1589
|
return;
|
|
@@ -1482,7 +1604,7 @@ class HashTreeParser {
|
|
|
1482
1604
|
};
|
|
1483
1605
|
}
|
|
1484
1606
|
LoggerProxy.logger.info(
|
|
1485
|
-
`HashTreeParser#
|
|
1607
|
+
`HashTreeParser#resumeFromMessage --> ${
|
|
1486
1608
|
this.debugId
|
|
1487
1609
|
} Resuming HashTreeParser with data sets: ${Object.keys(this.dataSets).join(
|
|
1488
1610
|
', '
|
|
@@ -1493,18 +1615,47 @@ class HashTreeParser {
|
|
|
1493
1615
|
this.handleMessage(message, 'on resume');
|
|
1494
1616
|
}
|
|
1495
1617
|
|
|
1618
|
+
/**
|
|
1619
|
+
* Resumes the HashTreeParser that was previously stopped, using a Locus API response.
|
|
1620
|
+
* Unlike resumeFromMessage(), this does not require metadata/dataSets in the input,
|
|
1621
|
+
* as it fetches all necessary information from Locus via initializeFromGetLociResponse.
|
|
1622
|
+
* @param {LocusDTO} locus - locus object from an API response
|
|
1623
|
+
* @returns {Promise}
|
|
1624
|
+
*/
|
|
1625
|
+
public async resumeFromApiResponse(locus: LocusDTO) {
|
|
1626
|
+
this.state = 'active';
|
|
1627
|
+
this.dataSets = {};
|
|
1628
|
+
|
|
1629
|
+
LoggerProxy.logger.info(
|
|
1630
|
+
`HashTreeParser#resumeFromApiResponse --> ${this.debugId} Resuming HashTreeParser from API response`
|
|
1631
|
+
);
|
|
1632
|
+
|
|
1633
|
+
await this.initializeFromGetLociResponse(locus);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1496
1636
|
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1637
|
+
// 404 for any dataset means the locus is no longer available at this URL - could be replaced or ended
|
|
1638
|
+
// if a dataset is just not visible, we would get a 400
|
|
1639
|
+
if (error.statusCode === 404) {
|
|
1640
|
+
LoggerProxy.logger.info(
|
|
1641
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received 404 for data set "${dataSetName}", locus not found`
|
|
1642
|
+
);
|
|
1643
|
+
this.stopAllTimers();
|
|
1644
|
+
|
|
1645
|
+
throw new LocusNotFoundError();
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1497
1648
|
const isValidDataSetForSentinel =
|
|
1498
1649
|
dataSetName === undefined ||
|
|
1499
1650
|
PossibleSentinelMessageDataSetNames.includes(dataSetName.toLowerCase());
|
|
1500
1651
|
|
|
1501
1652
|
if (
|
|
1502
|
-
|
|
1503
|
-
|
|
1653
|
+
error.statusCode === 409 &&
|
|
1654
|
+
error.body?.errorCode === LocusErrorCodes.LOCUS_INACTIVE &&
|
|
1504
1655
|
isValidDataSetForSentinel
|
|
1505
1656
|
) {
|
|
1506
1657
|
LoggerProxy.logger.info(
|
|
1507
|
-
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1658
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode}/${error.body?.errorCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1508
1659
|
);
|
|
1509
1660
|
this.stopAllTimers();
|
|
1510
1661
|
|
|
@@ -1516,7 +1667,7 @@ class HashTreeParser {
|
|
|
1516
1667
|
* Gets the current hashes from the locus for a specific data set.
|
|
1517
1668
|
* @param {string} dataSetName
|
|
1518
1669
|
* @param {string} currentRootHash
|
|
1519
|
-
* @returns {
|
|
1670
|
+
* @returns {Object|null} An object containing the hashes and leaf count, or null if the hashes match and no sync is needed
|
|
1520
1671
|
*/
|
|
1521
1672
|
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1522
1673
|
LoggerProxy.logger.info(
|
|
@@ -1535,6 +1686,15 @@ class HashTreeParser {
|
|
|
1535
1686
|
},
|
|
1536
1687
|
})
|
|
1537
1688
|
.then((response) => {
|
|
1689
|
+
if (!response.body || isEmpty(response.body)) {
|
|
1690
|
+
// 204 with empty body means our hashes match Locus, no sync needed
|
|
1691
|
+
LoggerProxy.logger.info(
|
|
1692
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got ${response.statusCode} with empty body for data set "${dataSetName}", hashes match - no sync needed`
|
|
1693
|
+
);
|
|
1694
|
+
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1538
1698
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
1539
1699
|
const dataSetFromResponse = response.body?.dataSet;
|
|
1540
1700
|
|
|
@@ -1563,6 +1723,13 @@ class HashTreeParser {
|
|
|
1563
1723
|
error
|
|
1564
1724
|
);
|
|
1565
1725
|
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1726
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
1727
|
+
debugId: this.debugId,
|
|
1728
|
+
dataSetName,
|
|
1729
|
+
request: 'GET /hashtree',
|
|
1730
|
+
statusCode: error.statusCode,
|
|
1731
|
+
reason: error.message,
|
|
1732
|
+
});
|
|
1566
1733
|
|
|
1567
1734
|
throw error;
|
|
1568
1735
|
});
|
|
@@ -1572,29 +1739,43 @@ class HashTreeParser {
|
|
|
1572
1739
|
* Sends a sync request to Locus for the specified data set.
|
|
1573
1740
|
*
|
|
1574
1741
|
* @param {InternalDataSet} dataSet The data set to sync.
|
|
1575
|
-
* @param {
|
|
1742
|
+
* @param {Object} options Either `{ isInitialization: true }` for init syncs (uses leafCount=1 with empty leaf data) or `{ mismatchedLeavesData }` for normal syncs.
|
|
1576
1743
|
* @returns {Promise<HashTreeMessage|null>}
|
|
1577
1744
|
*/
|
|
1578
1745
|
private sendSyncRequestToLocus(
|
|
1579
1746
|
dataSet: InternalDataSet,
|
|
1580
|
-
mismatchedLeavesData: Record<number, LeafDataItem[]>
|
|
1747
|
+
options: {isInitialization: true} | {mismatchedLeavesData: Record<number, LeafDataItem[]>}
|
|
1581
1748
|
): Promise<HashTreeMessage | null> {
|
|
1582
1749
|
LoggerProxy.logger.info(
|
|
1583
1750
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sending sync request for data set "${dataSet.name}"`
|
|
1584
1751
|
);
|
|
1585
1752
|
|
|
1753
|
+
const isInitialization = 'isInitialization' in options;
|
|
1754
|
+
|
|
1586
1755
|
const url = `${dataSet.url}/sync`;
|
|
1587
|
-
const body
|
|
1588
|
-
leafCount:
|
|
1756
|
+
const body: {
|
|
1757
|
+
leafCount: number;
|
|
1758
|
+
leafDataEntries: {leafIndex: number; elementIds: LeafDataItem[]}[];
|
|
1759
|
+
} = {
|
|
1760
|
+
leafCount: isInitialization ? 1 : dataSet.leafCount,
|
|
1589
1761
|
leafDataEntries: [],
|
|
1590
1762
|
};
|
|
1591
1763
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1764
|
+
if (isInitialization) {
|
|
1765
|
+
// initialization sync: Locus requires leafCount=1 with a single empty leaf
|
|
1766
|
+
body.leafDataEntries.push({leafIndex: 0, elementIds: []});
|
|
1767
|
+
} else {
|
|
1768
|
+
const {mismatchedLeavesData} = options;
|
|
1769
|
+
|
|
1770
|
+
Object.keys(mismatchedLeavesData).forEach((index) => {
|
|
1771
|
+
const leafIndex = parseInt(index, 10);
|
|
1772
|
+
|
|
1773
|
+
body.leafDataEntries.push({
|
|
1774
|
+
leafIndex,
|
|
1775
|
+
elementIds: mismatchedLeavesData[leafIndex],
|
|
1776
|
+
});
|
|
1596
1777
|
});
|
|
1597
|
-
}
|
|
1778
|
+
}
|
|
1598
1779
|
|
|
1599
1780
|
const ourCurrentRootHash = dataSet.hashTree ? dataSet.hashTree.getRootHash() : EMPTY_HASH;
|
|
1600
1781
|
|
|
@@ -1627,6 +1808,13 @@ class HashTreeParser {
|
|
|
1627
1808
|
error
|
|
1628
1809
|
);
|
|
1629
1810
|
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1811
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
1812
|
+
debugId: this.debugId,
|
|
1813
|
+
dataSetName: dataSet.name,
|
|
1814
|
+
request: 'POST /sync',
|
|
1815
|
+
statusCode: error.statusCode,
|
|
1816
|
+
reason: error.message,
|
|
1817
|
+
});
|
|
1630
1818
|
|
|
1631
1819
|
throw error;
|
|
1632
1820
|
});
|