@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.61
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 +8 -3
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +26 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/config.js +2 -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 +716 -370
- 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 -87
- 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/index.js +3 -1
- package/dist/media/index.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 +907 -535
- 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 +231 -78
- 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 +79 -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 +4 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/receiveSlot.js +9 -0
- package/dist/multistream/receiveSlot.js.map +1 -1
- package/dist/reactions/reactions.type.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 +2 -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 +92 -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 +87 -3
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +30 -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 +3 -0
- package/dist/types/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/types/reactions/reactions.type.d.ts +3 -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 +3 -1
- package/src/breakouts/index.ts +31 -0
- package/src/config.ts +2 -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 +429 -183
- 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 -97
- package/src/locus-info/types.ts +25 -1
- package/src/media/index.ts +3 -0
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +388 -33
- package/src/meeting/util.ts +20 -2
- package/src/meetings/index.ts +134 -44
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +97 -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 +3 -0
- package/src/multistream/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- package/src/multistream/receiveSlot.ts +18 -0
- package/src/reactions/reactions.type.ts +3 -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 +9 -3
- package/test/unit/spec/breakouts/index.ts +49 -0
- 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 +1508 -149
- 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/media/index.ts +31 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +1131 -49
- 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 +360 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +189 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
- 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;
|
|
@@ -47,6 +49,7 @@ interface InternalDataSet extends DataSet {
|
|
|
47
49
|
hashTree?: HashTree; // set only for visible data sets
|
|
48
50
|
timer?: ReturnType<typeof setTimeout>;
|
|
49
51
|
heartbeatWatchdogTimer?: ReturnType<typeof setTimeout>;
|
|
52
|
+
syncAbortController?: AbortController;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
|
|
@@ -54,13 +57,24 @@ type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
|
|
|
54
57
|
export const LocusInfoUpdateType = {
|
|
55
58
|
OBJECTS_UPDATED: 'OBJECTS_UPDATED',
|
|
56
59
|
MEETING_ENDED: 'MEETING_ENDED',
|
|
60
|
+
LOCUS_NOT_FOUND: 'LOCUS_NOT_FOUND',
|
|
57
61
|
} as const;
|
|
58
62
|
|
|
59
63
|
export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
|
|
65
|
+
interface LocusUpdatePayloads {
|
|
66
|
+
[LocusInfoUpdateType.OBJECTS_UPDATED]: {updatedObjects: HashTreeObject[]};
|
|
67
|
+
[LocusInfoUpdateType.MEETING_ENDED]: unknown; // No extra data
|
|
68
|
+
[LocusInfoUpdateType.LOCUS_NOT_FOUND]: unknown; // No extra data
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type LocusInfoUpdate = {
|
|
72
|
+
[K in keyof LocusUpdatePayloads]: {
|
|
73
|
+
updateType: K;
|
|
74
|
+
} & LocusUpdatePayloads[K];
|
|
75
|
+
}[keyof LocusUpdatePayloads];
|
|
76
|
+
|
|
77
|
+
export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
|
|
64
78
|
|
|
65
79
|
interface LeafInfo {
|
|
66
80
|
type: ObjectType;
|
|
@@ -75,6 +89,13 @@ interface LeafInfo {
|
|
|
75
89
|
*/
|
|
76
90
|
export class MeetingEndedError extends Error {}
|
|
77
91
|
|
|
92
|
+
/**
|
|
93
|
+
* This error is thrown when a 404 is received from Locus hash tree endpoints, indicating that the locus URL
|
|
94
|
+
* is no longer valid (e.g. participant moved to a breakout room, or meeting ended).
|
|
95
|
+
* It's handled internally by HashTreeParser and results in LOCUS_NOT_FOUND being sent up.
|
|
96
|
+
*/
|
|
97
|
+
export class LocusNotFoundError extends Error {}
|
|
98
|
+
|
|
78
99
|
/* Currently Locus always sends Metadata objects only in the "self" dataset.
|
|
79
100
|
* If this ever changes, update all the code that relies on this constant.
|
|
80
101
|
*/
|
|
@@ -99,6 +120,10 @@ class HashTreeParser {
|
|
|
99
120
|
heartbeatIntervalMs?: number;
|
|
100
121
|
private excludedDataSets: string[];
|
|
101
122
|
state: 'active' | 'stopped';
|
|
123
|
+
private syncQueue: Array<{dataSetName: string; reason: string; isInitialization?: boolean}> = [];
|
|
124
|
+
private isSyncInProgress = false;
|
|
125
|
+
private isSyncAllInProgress = false;
|
|
126
|
+
private syncQueueProcessingPromise: Promise<void> = Promise.resolve();
|
|
102
127
|
|
|
103
128
|
/**
|
|
104
129
|
* Constructor for HashTreeParser
|
|
@@ -224,16 +249,16 @@ class HashTreeParser {
|
|
|
224
249
|
* @param {DataSet} dataSetInfo The new data set to be added
|
|
225
250
|
* @returns {Promise}
|
|
226
251
|
*/
|
|
227
|
-
private initializeNewVisibleDataSet(
|
|
252
|
+
private async initializeNewVisibleDataSet(
|
|
228
253
|
visibleDataSetInfo: VisibleDataSetInfo,
|
|
229
254
|
dataSetInfo: DataSet
|
|
230
|
-
): Promise<
|
|
255
|
+
): Promise<void> {
|
|
231
256
|
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
232
257
|
LoggerProxy.logger.info(
|
|
233
258
|
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
234
259
|
);
|
|
235
260
|
|
|
236
|
-
return
|
|
261
|
+
return;
|
|
237
262
|
}
|
|
238
263
|
|
|
239
264
|
LoggerProxy.logger.info(
|
|
@@ -241,7 +266,7 @@ class HashTreeParser {
|
|
|
241
266
|
);
|
|
242
267
|
|
|
243
268
|
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
244
|
-
return
|
|
269
|
+
return;
|
|
245
270
|
}
|
|
246
271
|
|
|
247
272
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
@@ -251,51 +276,8 @@ class HashTreeParser {
|
|
|
251
276
|
hashTree,
|
|
252
277
|
};
|
|
253
278
|
|
|
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
|
-
);
|
|
279
|
+
this.enqueueSyncForDataset(dataSetInfo.name, 'new visible data set initialization', true);
|
|
280
|
+
await this.syncQueueProcessingPromise;
|
|
299
281
|
}
|
|
300
282
|
|
|
301
283
|
/**
|
|
@@ -382,9 +364,8 @@ class HashTreeParser {
|
|
|
382
364
|
if (this.state === 'stopped') {
|
|
383
365
|
return;
|
|
384
366
|
}
|
|
385
|
-
const updatedObjects: HashTreeObject[] = [];
|
|
386
367
|
|
|
387
|
-
for (const dataSet of visibleDataSets) {
|
|
368
|
+
for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
388
369
|
const {name, leafCount, url} = dataSet;
|
|
389
370
|
|
|
390
371
|
if (!this.dataSets[name]) {
|
|
@@ -420,19 +401,12 @@ class HashTreeParser {
|
|
|
420
401
|
);
|
|
421
402
|
this.dataSets[name].hashTree = new HashTree([], leafCount);
|
|
422
403
|
|
|
423
|
-
|
|
424
|
-
const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
|
|
425
|
-
|
|
426
|
-
if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
427
|
-
updatedObjects.push(...(data.updatedObjects || []));
|
|
428
|
-
}
|
|
404
|
+
this.enqueueSyncForDataset(name, `initialization sync for ${debugText}`, true);
|
|
429
405
|
}
|
|
430
406
|
}
|
|
431
407
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
updatedObjects,
|
|
435
|
-
});
|
|
408
|
+
// wait for all enqueued initialization syncs to complete
|
|
409
|
+
await this.syncQueueProcessingPromise;
|
|
436
410
|
}
|
|
437
411
|
|
|
438
412
|
/**
|
|
@@ -586,12 +560,40 @@ class HashTreeParser {
|
|
|
586
560
|
)}`
|
|
587
561
|
);
|
|
588
562
|
|
|
563
|
+
this.cancelPendingSyncsForDataSets(dataSets.map((ds) => ds.name));
|
|
564
|
+
|
|
589
565
|
dataSets.forEach((dataSet) => {
|
|
590
566
|
this.updateDataSetInfo(dataSet);
|
|
591
567
|
this.runSyncAlgorithm(dataSet);
|
|
592
568
|
});
|
|
593
569
|
}
|
|
594
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Handles known errors that can happen during syncs
|
|
573
|
+
*
|
|
574
|
+
* @param {any} error - The error to handle
|
|
575
|
+
* @returns {boolean} true if the error was recognized and handled, false otherwise
|
|
576
|
+
*/
|
|
577
|
+
private handleSyncErrors(error: any) {
|
|
578
|
+
if (error instanceof MeetingEndedError) {
|
|
579
|
+
this.callLocusInfoUpdateCallback({
|
|
580
|
+
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
if (error instanceof LocusNotFoundError) {
|
|
586
|
+
this.callLocusInfoUpdateCallback({
|
|
587
|
+
updateType: LocusInfoUpdateType.LOCUS_NOT_FOUND,
|
|
588
|
+
});
|
|
589
|
+
this.stop();
|
|
590
|
+
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
|
|
595
597
|
/**
|
|
596
598
|
* Asynchronously initializes new visible data sets
|
|
597
599
|
*
|
|
@@ -608,11 +610,7 @@ class HashTreeParser {
|
|
|
608
610
|
);
|
|
609
611
|
queueMicrotask(() => {
|
|
610
612
|
this.initializeNewVisibleDataSets(dataSetsRequiringInitialization).catch((error) => {
|
|
611
|
-
if (error
|
|
612
|
-
this.callLocusInfoUpdateCallback({
|
|
613
|
-
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
614
|
-
});
|
|
615
|
-
} else {
|
|
613
|
+
if (!this.handleSyncErrors(error)) {
|
|
616
614
|
LoggerProxy.logger.warn(
|
|
617
615
|
`HashTreeParser#queueInitForNewVisibleDataSets --> ${
|
|
618
616
|
this.debugId
|
|
@@ -690,7 +688,14 @@ class HashTreeParser {
|
|
|
690
688
|
|
|
691
689
|
const {dataSets, locus, metadata} = update;
|
|
692
690
|
|
|
691
|
+
LoggerProxy.logger.info(
|
|
692
|
+
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received update with dataSets=${dataSets
|
|
693
|
+
?.map((ds) => ds.name)
|
|
694
|
+
.join(',')} metadata=${metadata ? 'yes' : 'no'}`
|
|
695
|
+
);
|
|
696
|
+
|
|
693
697
|
if (!dataSets) {
|
|
698
|
+
// this happens for example when we handle GET /loci response
|
|
694
699
|
LoggerProxy.logger.info(
|
|
695
700
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
696
701
|
);
|
|
@@ -794,6 +799,18 @@ class HashTreeParser {
|
|
|
794
799
|
}
|
|
795
800
|
}
|
|
796
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Updates the leaf count for a data set, resizing its hash tree accordingly.
|
|
804
|
+
*
|
|
805
|
+
* @param {InternalDataSet} dataSet - The data set to update
|
|
806
|
+
* @param {number} newLeafCount - The new leaf count
|
|
807
|
+
* @returns {void}
|
|
808
|
+
*/
|
|
809
|
+
private updateDataSetLeafCount(dataSet: InternalDataSet, newLeafCount: number): void {
|
|
810
|
+
dataSet.hashTree?.resize(newLeafCount);
|
|
811
|
+
dataSet.leafCount = newLeafCount;
|
|
812
|
+
}
|
|
813
|
+
|
|
797
814
|
/**
|
|
798
815
|
* Checks for changes in the visible data sets based on the updated objects.
|
|
799
816
|
* @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects.
|
|
@@ -844,6 +861,8 @@ class HashTreeParser {
|
|
|
844
861
|
*/
|
|
845
862
|
private deleteHashTree(dataSetName: string) {
|
|
846
863
|
this.dataSets[dataSetName].hashTree = undefined;
|
|
864
|
+
this.dataSets[dataSetName].syncAbortController?.abort();
|
|
865
|
+
this.dataSets[dataSetName].syncAbortController = undefined;
|
|
847
866
|
|
|
848
867
|
// we also need to stop the timers as there is no hash tree anymore to sync
|
|
849
868
|
if (this.dataSets[dataSetName].timer) {
|
|
@@ -960,7 +979,7 @@ class HashTreeParser {
|
|
|
960
979
|
}
|
|
961
980
|
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
962
981
|
|
|
963
|
-
for (const ds of addedDataSets) {
|
|
982
|
+
for (const ds of sortByInitPriority(addedDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
964
983
|
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
965
984
|
|
|
966
985
|
LoggerProxy.logger.info(
|
|
@@ -972,12 +991,8 @@ class HashTreeParser {
|
|
|
972
991
|
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
973
992
|
);
|
|
974
993
|
} else {
|
|
975
|
-
// we're awaiting in a loop, because in practice there will be only one new data set at a time,
|
|
976
|
-
// so no point in trying to parallelize this
|
|
977
994
|
// eslint-disable-next-line no-await-in-loop
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
this.callLocusInfoUpdateCallback(updates);
|
|
995
|
+
await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
981
996
|
}
|
|
982
997
|
}
|
|
983
998
|
}
|
|
@@ -997,25 +1012,39 @@ class HashTreeParser {
|
|
|
997
1012
|
const {dataSets, visibleDataSetsUrl} = message;
|
|
998
1013
|
|
|
999
1014
|
LoggerProxy.logger.info(
|
|
1000
|
-
`HashTreeParser#parseMessage --> ${this.debugId}
|
|
1001
|
-
|
|
1015
|
+
`HashTreeParser#parseMessage --> ${this.debugId} ${
|
|
1016
|
+
debugText || ''
|
|
1017
|
+
} dataSets: ${message.dataSets
|
|
1018
|
+
?.map(({name, version}) => `${name}:${version}`)
|
|
1019
|
+
.join(',')}, elements: ${message.locusStateElements
|
|
1020
|
+
?.map(
|
|
1021
|
+
(el) =>
|
|
1022
|
+
`${el.htMeta.elementId.type}:${el.htMeta.elementId.id}:${el.htMeta.elementId.version}${
|
|
1023
|
+
el.data ? '+' : '-'
|
|
1024
|
+
}`
|
|
1025
|
+
)
|
|
1026
|
+
.join(',')}`
|
|
1002
1027
|
);
|
|
1028
|
+
|
|
1003
1029
|
if (message.locusStateElements?.length === 0) {
|
|
1004
1030
|
LoggerProxy.logger.warn(
|
|
1005
1031
|
`HashTreeParser#parseMessage --> ${this.debugId} got empty locusStateElements!!!`
|
|
1006
1032
|
);
|
|
1007
|
-
|
|
1033
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_EMPTY_LOCUS_STATE_ELEMENTS, {
|
|
1034
|
+
debugId: this.debugId,
|
|
1035
|
+
});
|
|
1008
1036
|
}
|
|
1009
1037
|
|
|
1010
1038
|
// first, update our metadata about the datasets with info from the message
|
|
1011
1039
|
this.visibleDataSetsUrl = visibleDataSetsUrl;
|
|
1012
1040
|
dataSets.forEach((dataSet) => this.updateDataSetInfo(dataSet));
|
|
1041
|
+
this.cancelPendingSyncsForDataSets(dataSets.map((ds) => ds.name));
|
|
1013
1042
|
|
|
1014
1043
|
const updatedObjects: HashTreeObject[] = [];
|
|
1015
1044
|
|
|
1016
1045
|
// when we detect new visible datasets, it may be that the metadata about them is not
|
|
1017
1046
|
// available in the message, they will require separate async initialization
|
|
1018
|
-
let dataSetsRequiringInitialization = [];
|
|
1047
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
1019
1048
|
|
|
1020
1049
|
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
1021
1050
|
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
@@ -1023,7 +1052,7 @@ class HashTreeParser {
|
|
|
1023
1052
|
);
|
|
1024
1053
|
|
|
1025
1054
|
if (metadataUpdates.length > 0) {
|
|
1026
|
-
const updatedMetadataObjects = [];
|
|
1055
|
+
const updatedMetadataObjects: HashTreeObject[] = [];
|
|
1027
1056
|
|
|
1028
1057
|
metadataUpdates.forEach((object) => {
|
|
1029
1058
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
@@ -1052,7 +1081,7 @@ class HashTreeParser {
|
|
|
1052
1081
|
}
|
|
1053
1082
|
}
|
|
1054
1083
|
|
|
1055
|
-
if (message.locusStateElements
|
|
1084
|
+
if (message.locusStateElements && message.locusStateElements.length > 0) {
|
|
1056
1085
|
// by this point we now have this.dataSets setup for data sets from this message
|
|
1057
1086
|
// and hash trees created for the new visible data sets,
|
|
1058
1087
|
// so we can now process all the updates from the message
|
|
@@ -1148,20 +1177,17 @@ class HashTreeParser {
|
|
|
1148
1177
|
* @param {Object} updates parsed from a Locus message
|
|
1149
1178
|
* @returns {void}
|
|
1150
1179
|
*/
|
|
1151
|
-
private callLocusInfoUpdateCallback(updates: {
|
|
1152
|
-
updateType: LocusInfoUpdateType;
|
|
1153
|
-
updatedObjects?: HashTreeObject[];
|
|
1154
|
-
}) {
|
|
1180
|
+
private callLocusInfoUpdateCallback(updates: LocusInfoUpdate) {
|
|
1155
1181
|
if (this.state === 'stopped') {
|
|
1156
1182
|
return;
|
|
1157
1183
|
}
|
|
1158
1184
|
|
|
1159
|
-
const {updateType
|
|
1185
|
+
const {updateType} = updates;
|
|
1160
1186
|
|
|
1161
|
-
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1187
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updates.updatedObjects?.length > 0) {
|
|
1162
1188
|
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1163
1189
|
// or removals for objects that still exist in any of their datasets
|
|
1164
|
-
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1190
|
+
const filteredUpdates = updates.updatedObjects.filter((object) => {
|
|
1165
1191
|
const {elementId} = object.htMeta;
|
|
1166
1192
|
const {type, id, version} = elementId;
|
|
1167
1193
|
|
|
@@ -1198,10 +1224,10 @@ class HashTreeParser {
|
|
|
1198
1224
|
});
|
|
1199
1225
|
|
|
1200
1226
|
if (filteredUpdates.length > 0) {
|
|
1201
|
-
this.locusInfoUpdateCallback(updateType,
|
|
1227
|
+
this.locusInfoUpdateCallback({updateType, updatedObjects: filteredUpdates});
|
|
1202
1228
|
}
|
|
1203
1229
|
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
1204
|
-
this.locusInfoUpdateCallback(updateType
|
|
1230
|
+
this.locusInfoUpdateCallback({updateType});
|
|
1205
1231
|
}
|
|
1206
1232
|
}
|
|
1207
1233
|
|
|
@@ -1223,81 +1249,240 @@ class HashTreeParser {
|
|
|
1223
1249
|
* Performs a sync for the given data set.
|
|
1224
1250
|
*
|
|
1225
1251
|
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1226
|
-
* @param {string} rootHash - Our current root hash for this data set
|
|
1227
1252
|
* @param {string} reason - The reason for the sync (used for logging)
|
|
1253
|
+
* @param {boolean} [isInitialization] - Whether this is an initialization sync (sends empty leaves data instead of comparing hashes)
|
|
1228
1254
|
* @returns {Promise<void>}
|
|
1229
1255
|
*/
|
|
1230
1256
|
private async performSync(
|
|
1231
1257
|
dataSet: InternalDataSet,
|
|
1232
|
-
|
|
1233
|
-
|
|
1258
|
+
reason: string,
|
|
1259
|
+
isInitialization?: boolean
|
|
1234
1260
|
): Promise<void> {
|
|
1235
1261
|
if (!dataSet.hashTree) {
|
|
1236
1262
|
return;
|
|
1237
1263
|
}
|
|
1238
1264
|
|
|
1265
|
+
const abortController = dataSet.syncAbortController ?? new AbortController();
|
|
1266
|
+
dataSet.syncAbortController = abortController;
|
|
1267
|
+
|
|
1268
|
+
const {hashTree} = dataSet;
|
|
1269
|
+
const rootHash = hashTree.getRootHash();
|
|
1270
|
+
|
|
1239
1271
|
try {
|
|
1240
1272
|
LoggerProxy.logger.info(
|
|
1241
1273
|
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1242
1274
|
);
|
|
1243
1275
|
|
|
1244
|
-
|
|
1276
|
+
let leavesData: Record<number, LeafDataItem[]> = {};
|
|
1245
1277
|
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1278
|
+
if (!isInitialization) {
|
|
1279
|
+
if (dataSet.leafCount !== 1) {
|
|
1280
|
+
let receivedHashes;
|
|
1248
1281
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
dataSet.name,
|
|
1253
|
-
rootHash
|
|
1254
|
-
);
|
|
1282
|
+
try {
|
|
1283
|
+
// request hashes from sender
|
|
1284
|
+
const hashesResult = await this.getHashesFromLocus(dataSet.name, rootHash);
|
|
1255
1285
|
|
|
1256
|
-
|
|
1286
|
+
if (!hashesResult) {
|
|
1287
|
+
// hashes match, no sync needed
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1257
1290
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1291
|
+
receivedHashes = hashesResult.hashes;
|
|
1292
|
+
|
|
1293
|
+
this.updateDataSetLeafCount(dataSet, hashesResult.dataSet.leafCount);
|
|
1294
|
+
} catch (error: any) {
|
|
1295
|
+
if (error?.statusCode === 409) {
|
|
1296
|
+
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1297
|
+
LoggerProxy.logger.info(
|
|
1298
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1299
|
+
);
|
|
1265
1300
|
|
|
1266
|
-
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
throw error;
|
|
1267
1304
|
}
|
|
1268
|
-
|
|
1305
|
+
|
|
1306
|
+
// identify mismatched leaves
|
|
1307
|
+
const mismatchedLeaveIndexes = hashTree.diffHashes(receivedHashes);
|
|
1308
|
+
|
|
1309
|
+
mismatchedLeaveIndexes.forEach((index) => {
|
|
1310
|
+
leavesData[index] = hashTree.getLeafData(index);
|
|
1311
|
+
});
|
|
1312
|
+
} else {
|
|
1313
|
+
leavesData = {0: hashTree.getLeafData(0)};
|
|
1269
1314
|
}
|
|
1315
|
+
}
|
|
1270
1316
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1317
|
+
if (abortController.signal.aborted) {
|
|
1318
|
+
LoggerProxy.logger.info(
|
|
1319
|
+
`HashTreeParser#performSync --> ${this.debugId} abandoning sync for "${dataSet.name}" before /sync - message received during sync`
|
|
1320
|
+
);
|
|
1273
1321
|
|
|
1274
|
-
|
|
1275
|
-
mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
|
|
1276
|
-
});
|
|
1277
|
-
} else {
|
|
1278
|
-
mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
|
|
1322
|
+
return;
|
|
1279
1323
|
}
|
|
1280
1324
|
// request sync for mismatched leaves
|
|
1281
|
-
|
|
1282
|
-
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1325
|
+
let syncResponse: HashTreeMessage | null = null;
|
|
1283
1326
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1327
|
+
if (isInitialization) {
|
|
1328
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {isInitialization: true});
|
|
1329
|
+
} else if (Object.keys(leavesData).length > 0) {
|
|
1330
|
+
syncResponse = await this.sendSyncRequestToLocus(dataSet, {
|
|
1331
|
+
mismatchedLeavesData: leavesData,
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// sync API may return nothing (in that case data will arrive via messages)
|
|
1336
|
+
// or it may return a response in the same format as messages
|
|
1337
|
+
// We still need to restart the sync timer as a safety net in case the messages don't arrive.
|
|
1338
|
+
this.runSyncAlgorithm(dataSet);
|
|
1339
|
+
|
|
1340
|
+
if (syncResponse) {
|
|
1341
|
+
// clear the abort controller before processing the response so that
|
|
1342
|
+
// parseMessage() -> cancelPendingSyncsForDataSets() doesn't log a
|
|
1343
|
+
// misleading "aborting sync" message for this already-completed sync
|
|
1344
|
+
dataSet.syncAbortController = undefined;
|
|
1345
|
+
// the format of sync response is the same as messages, so we can reuse the same handler
|
|
1346
|
+
this.handleMessage(syncResponse, 'via sync API');
|
|
1289
1347
|
}
|
|
1290
1348
|
} catch (error) {
|
|
1291
|
-
if (error
|
|
1292
|
-
this.callLocusInfoUpdateCallback({
|
|
1293
|
-
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
1294
|
-
});
|
|
1295
|
-
} else {
|
|
1349
|
+
if (!this.handleSyncErrors(error)) {
|
|
1296
1350
|
LoggerProxy.logger.warn(
|
|
1297
1351
|
`HashTreeParser#performSync --> ${this.debugId} error during sync for data set "${dataSet.name}":`,
|
|
1298
1352
|
error
|
|
1299
1353
|
);
|
|
1300
1354
|
}
|
|
1355
|
+
} finally {
|
|
1356
|
+
dataSet.syncAbortController = undefined;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Cancels any pending or in-flight syncs for the specified data sets.
|
|
1362
|
+
* This removes matching entries from the sync queue and aborts any in-flight sync HTTP requests.
|
|
1363
|
+
*
|
|
1364
|
+
* @param {string[]} dataSetNames - The names of the data sets to cancel syncs for
|
|
1365
|
+
* @returns {void}
|
|
1366
|
+
*/
|
|
1367
|
+
private cancelPendingSyncsForDataSets(dataSetNames: string[]): void {
|
|
1368
|
+
const previousLength = this.syncQueue.length;
|
|
1369
|
+
|
|
1370
|
+
this.syncQueue = this.syncQueue.filter((entry) => !dataSetNames.includes(entry.dataSetName));
|
|
1371
|
+
|
|
1372
|
+
if (previousLength !== this.syncQueue.length) {
|
|
1373
|
+
LoggerProxy.logger.info(
|
|
1374
|
+
`HashTreeParser#cancelPendingSyncsForDataSets --> ${this.debugId} removed ${
|
|
1375
|
+
previousLength - this.syncQueue.length
|
|
1376
|
+
} entries from sync queue for data sets: ${dataSetNames.join(', ')}`
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
for (const name of dataSetNames) {
|
|
1381
|
+
if (this.dataSets[name]?.syncAbortController) {
|
|
1382
|
+
LoggerProxy.logger.info(
|
|
1383
|
+
`HashTreeParser#cancelPendingSyncsForDataSets --> ${this.debugId} aborting in-flight sync for data set "${name}"`
|
|
1384
|
+
);
|
|
1385
|
+
this.dataSets[name].syncAbortController.abort();
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Enqueues a sync for the given data set. If the data set is already in the queue, the request is ignored.
|
|
1392
|
+
* This ensures that all syncs are executed sequentially and no more than 1 sync runs at a time.
|
|
1393
|
+
*
|
|
1394
|
+
* @param {string} dataSetName - The name of the data set to sync
|
|
1395
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1396
|
+
* @param {boolean} [isInitialization=false] - Whether this is an initialization sync (uses empty leaves data instead of hash comparison)
|
|
1397
|
+
* @returns {void}
|
|
1398
|
+
*/
|
|
1399
|
+
private enqueueSyncForDataset(
|
|
1400
|
+
dataSetName: string,
|
|
1401
|
+
reason: string,
|
|
1402
|
+
isInitialization = false
|
|
1403
|
+
): void {
|
|
1404
|
+
if (this.state === 'stopped') return;
|
|
1405
|
+
|
|
1406
|
+
const existingEntry = this.syncQueue.find((entry) => entry.dataSetName === dataSetName);
|
|
1407
|
+
|
|
1408
|
+
if (existingEntry) {
|
|
1409
|
+
if (isInitialization) {
|
|
1410
|
+
existingEntry.isInitialization = true;
|
|
1411
|
+
}
|
|
1412
|
+
LoggerProxy.logger.info(
|
|
1413
|
+
`HashTreeParser#enqueueSyncForDataset --> ${this.debugId} data set "${dataSetName}" already in sync queue, skipping`
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
this.syncQueue.push({dataSetName, reason, isInitialization});
|
|
1420
|
+
|
|
1421
|
+
if (!this.isSyncInProgress) {
|
|
1422
|
+
this.syncQueueProcessingPromise = this.processSyncQueue();
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Processes the sync queue sequentially. Only one instance of this method runs at a time.
|
|
1428
|
+
*
|
|
1429
|
+
* @returns {Promise<void>}
|
|
1430
|
+
*/
|
|
1431
|
+
private async processSyncQueue(): Promise<void> {
|
|
1432
|
+
if (this.isSyncInProgress) return;
|
|
1433
|
+
|
|
1434
|
+
this.isSyncInProgress = true;
|
|
1435
|
+
try {
|
|
1436
|
+
while (this.syncQueue.length > 0 && this.state !== 'stopped') {
|
|
1437
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1438
|
+
const {dataSetName, reason, isInitialization} = this.syncQueue.shift()!;
|
|
1439
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1440
|
+
|
|
1441
|
+
if (!dataSet?.hashTree) {
|
|
1442
|
+
// eslint-disable-next-line no-continue
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1447
|
+
await this.performSync(dataSet, reason, isInitialization);
|
|
1448
|
+
}
|
|
1449
|
+
} finally {
|
|
1450
|
+
this.isSyncInProgress = false;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Syncs all data sets that have hash trees, one by one in sequence, using the priority order
|
|
1456
|
+
* provided by sortByInitPriority(). Does nothing if the parser is stopped or if a syncAllDatasets
|
|
1457
|
+
* call is already in progress.
|
|
1458
|
+
*
|
|
1459
|
+
* @returns {Promise<void>}
|
|
1460
|
+
*/
|
|
1461
|
+
public async syncAllDatasets(): Promise<void> {
|
|
1462
|
+
if (this.state === 'stopped') return;
|
|
1463
|
+
if (this.isSyncAllInProgress) return;
|
|
1464
|
+
|
|
1465
|
+
this.isSyncAllInProgress = true;
|
|
1466
|
+
try {
|
|
1467
|
+
const dataSetsWithHashTrees = Object.values(this.dataSets)
|
|
1468
|
+
.filter((dataSet) => dataSet?.hashTree)
|
|
1469
|
+
.map((dataSet) => ({name: dataSet.name}));
|
|
1470
|
+
|
|
1471
|
+
const sorted = sortByInitPriority(dataSetsWithHashTrees, DATA_SET_INIT_PRIORITY);
|
|
1472
|
+
|
|
1473
|
+
LoggerProxy.logger.info(
|
|
1474
|
+
`HashTreeParser#syncAllDatasets --> ${this.debugId} syncing datasets: ${sorted
|
|
1475
|
+
.map((ds) => ds.name)
|
|
1476
|
+
.join(', ')}`
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
for (const ds of sorted) {
|
|
1480
|
+
this.enqueueSyncForDataset(ds.name, 'syncAllDatasets');
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
await this.syncQueueProcessingPromise;
|
|
1484
|
+
} finally {
|
|
1485
|
+
this.isSyncAllInProgress = false;
|
|
1301
1486
|
}
|
|
1302
1487
|
}
|
|
1303
1488
|
|
|
@@ -1319,21 +1504,14 @@ class HashTreeParser {
|
|
|
1319
1504
|
}
|
|
1320
1505
|
|
|
1321
1506
|
if (!dataSet.hashTree) {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
);
|
|
1507
|
+
// no hash tree, so no need to do any syncing
|
|
1508
|
+
// we fall into this branch often, because Locus sends dataSets in messages that are not visible to us
|
|
1325
1509
|
|
|
1326
1510
|
return;
|
|
1327
1511
|
}
|
|
1328
1512
|
|
|
1329
1513
|
dataSet.hashTree.resize(receivedDataSet.leafCount);
|
|
1330
1514
|
|
|
1331
|
-
// temporary log for the workshop // todo: remove
|
|
1332
|
-
const ourCurrentRootHash = dataSet.hashTree.getRootHash();
|
|
1333
|
-
LoggerProxy.logger.info(
|
|
1334
|
-
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} dataSet="${dataSet.name}" version=${dataSet.version} hashes before starting timer: ours=${ourCurrentRootHash} Locus=${dataSet.root}`
|
|
1335
|
-
);
|
|
1336
|
-
|
|
1337
1515
|
const delay = dataSet.idleMs + this.getWeightedBackoffTime(dataSet.backoff);
|
|
1338
1516
|
|
|
1339
1517
|
if (delay > 0) {
|
|
@@ -1341,11 +1519,7 @@ class HashTreeParser {
|
|
|
1341
1519
|
clearTimeout(dataSet.timer);
|
|
1342
1520
|
}
|
|
1343
1521
|
|
|
1344
|
-
|
|
1345
|
-
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} setting "${dataSet.name}" sync timer for ${delay}`
|
|
1346
|
-
);
|
|
1347
|
-
|
|
1348
|
-
dataSet.timer = setTimeout(async () => {
|
|
1522
|
+
dataSet.timer = setTimeout(() => {
|
|
1349
1523
|
dataSet.timer = undefined;
|
|
1350
1524
|
|
|
1351
1525
|
if (!dataSet.hashTree) {
|
|
@@ -1359,15 +1533,10 @@ class HashTreeParser {
|
|
|
1359
1533
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
1360
1534
|
|
|
1361
1535
|
if (dataSet.root !== rootHash) {
|
|
1362
|
-
|
|
1363
|
-
dataSet,
|
|
1364
|
-
rootHash,
|
|
1536
|
+
this.enqueueSyncForDataset(
|
|
1537
|
+
dataSet.name,
|
|
1365
1538
|
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
1366
1539
|
);
|
|
1367
|
-
} else {
|
|
1368
|
-
LoggerProxy.logger.info(
|
|
1369
|
-
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} "${dataSet.name}" root hash matching: ${rootHash}, version=${dataSet.version}`
|
|
1370
|
-
);
|
|
1371
1540
|
}
|
|
1372
1541
|
}, delay);
|
|
1373
1542
|
} else {
|
|
@@ -1407,18 +1576,20 @@ class HashTreeParser {
|
|
|
1407
1576
|
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1408
1577
|
const delay = this.heartbeatIntervalMs + backoffTime;
|
|
1409
1578
|
|
|
1410
|
-
dataSet.heartbeatWatchdogTimer = setTimeout(
|
|
1579
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(() => {
|
|
1411
1580
|
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1412
1581
|
|
|
1413
1582
|
LoggerProxy.logger.warn(
|
|
1414
1583
|
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1415
1584
|
);
|
|
1416
1585
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
dataSet.
|
|
1420
|
-
|
|
1421
|
-
|
|
1586
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_HEARTBEAT_WATCHDOG_EXPIRED, {
|
|
1587
|
+
debugId: this.debugId,
|
|
1588
|
+
dataSetName: dataSet.name,
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
this.enqueueSyncForDataset(dataSet.name, `heartbeat watchdog expired`);
|
|
1592
|
+
this.resetHeartbeatWatchdogs([dataSet]);
|
|
1422
1593
|
}, delay);
|
|
1423
1594
|
}
|
|
1424
1595
|
}
|
|
@@ -1451,7 +1622,10 @@ class HashTreeParser {
|
|
|
1451
1622
|
`HashTreeParser#stop --> ${this.debugId} Stopping HashTreeParser, clearing timers and hash trees`
|
|
1452
1623
|
);
|
|
1453
1624
|
this.stopAllTimers();
|
|
1625
|
+
this.syncQueue = [];
|
|
1454
1626
|
Object.values(this.dataSets).forEach((dataSet) => {
|
|
1627
|
+
dataSet.syncAbortController?.abort();
|
|
1628
|
+
dataSet.syncAbortController = undefined;
|
|
1455
1629
|
dataSet.hashTree = undefined;
|
|
1456
1630
|
});
|
|
1457
1631
|
this.visibleDataSets = [];
|
|
@@ -1459,17 +1633,27 @@ class HashTreeParser {
|
|
|
1459
1633
|
}
|
|
1460
1634
|
|
|
1461
1635
|
/**
|
|
1462
|
-
*
|
|
1636
|
+
* Cleans up the HashTreeParser, stopping all timers and clearing all internal state.
|
|
1637
|
+
* After calling this, the parser should not be used anymore.
|
|
1638
|
+
* @returns {void}
|
|
1639
|
+
*/
|
|
1640
|
+
public cleanUp() {
|
|
1641
|
+
this.stop();
|
|
1642
|
+
this.dataSets = {};
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Resumes the HashTreeParser that was previously stopped, using a hash tree message.
|
|
1463
1647
|
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
1464
1648
|
* @returns {void}
|
|
1465
1649
|
*/
|
|
1466
|
-
public
|
|
1650
|
+
public resumeFromMessage(message: HashTreeMessage) {
|
|
1467
1651
|
// check that message contains metadata with visible data sets - this is essential to be able to resume
|
|
1468
1652
|
const metadataObject = message.locusStateElements?.find((el) => isMetadata(el));
|
|
1469
1653
|
|
|
1470
1654
|
if (!metadataObject?.data?.visibleDataSets) {
|
|
1471
1655
|
LoggerProxy.logger.warn(
|
|
1472
|
-
`HashTreeParser#
|
|
1656
|
+
`HashTreeParser#resumeFromMessage --> ${this.debugId} Cannot resume HashTreeParser because the message is missing metadata with visible data sets info`
|
|
1473
1657
|
);
|
|
1474
1658
|
|
|
1475
1659
|
return;
|
|
@@ -1490,7 +1674,7 @@ class HashTreeParser {
|
|
|
1490
1674
|
};
|
|
1491
1675
|
}
|
|
1492
1676
|
LoggerProxy.logger.info(
|
|
1493
|
-
`HashTreeParser#
|
|
1677
|
+
`HashTreeParser#resumeFromMessage --> ${
|
|
1494
1678
|
this.debugId
|
|
1495
1679
|
} Resuming HashTreeParser with data sets: ${Object.keys(this.dataSets).join(
|
|
1496
1680
|
', '
|
|
@@ -1501,18 +1685,47 @@ class HashTreeParser {
|
|
|
1501
1685
|
this.handleMessage(message, 'on resume');
|
|
1502
1686
|
}
|
|
1503
1687
|
|
|
1688
|
+
/**
|
|
1689
|
+
* Resumes the HashTreeParser that was previously stopped, using a Locus API response.
|
|
1690
|
+
* Unlike resumeFromMessage(), this does not require metadata/dataSets in the input,
|
|
1691
|
+
* as it fetches all necessary information from Locus via initializeFromGetLociResponse.
|
|
1692
|
+
* @param {LocusDTO} locus - locus object from an API response
|
|
1693
|
+
* @returns {Promise}
|
|
1694
|
+
*/
|
|
1695
|
+
public async resumeFromApiResponse(locus: LocusDTO) {
|
|
1696
|
+
this.state = 'active';
|
|
1697
|
+
this.dataSets = {};
|
|
1698
|
+
|
|
1699
|
+
LoggerProxy.logger.info(
|
|
1700
|
+
`HashTreeParser#resumeFromApiResponse --> ${this.debugId} Resuming HashTreeParser from API response`
|
|
1701
|
+
);
|
|
1702
|
+
|
|
1703
|
+
await this.initializeFromGetLociResponse(locus);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1504
1706
|
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1707
|
+
// 404 for any dataset means the locus is no longer available at this URL - could be replaced or ended
|
|
1708
|
+
// if a dataset is just not visible, we would get a 400
|
|
1709
|
+
if (error.statusCode === 404) {
|
|
1710
|
+
LoggerProxy.logger.info(
|
|
1711
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received 404 for data set "${dataSetName}", locus not found`
|
|
1712
|
+
);
|
|
1713
|
+
this.stopAllTimers();
|
|
1714
|
+
|
|
1715
|
+
throw new LocusNotFoundError();
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1505
1718
|
const isValidDataSetForSentinel =
|
|
1506
1719
|
dataSetName === undefined ||
|
|
1507
1720
|
PossibleSentinelMessageDataSetNames.includes(dataSetName.toLowerCase());
|
|
1508
1721
|
|
|
1509
1722
|
if (
|
|
1510
|
-
|
|
1511
|
-
|
|
1723
|
+
error.statusCode === 409 &&
|
|
1724
|
+
error.body?.errorCode === LocusErrorCodes.LOCUS_INACTIVE &&
|
|
1512
1725
|
isValidDataSetForSentinel
|
|
1513
1726
|
) {
|
|
1514
1727
|
LoggerProxy.logger.info(
|
|
1515
|
-
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1728
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode}/${error.body?.errorCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1516
1729
|
);
|
|
1517
1730
|
this.stopAllTimers();
|
|
1518
1731
|
|
|
@@ -1524,7 +1737,7 @@ class HashTreeParser {
|
|
|
1524
1737
|
* Gets the current hashes from the locus for a specific data set.
|
|
1525
1738
|
* @param {string} dataSetName
|
|
1526
1739
|
* @param {string} currentRootHash
|
|
1527
|
-
* @returns {
|
|
1740
|
+
* @returns {Object|null} An object containing the hashes and leaf count, or null if the hashes match and no sync is needed
|
|
1528
1741
|
*/
|
|
1529
1742
|
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1530
1743
|
LoggerProxy.logger.info(
|
|
@@ -1543,6 +1756,15 @@ class HashTreeParser {
|
|
|
1543
1756
|
},
|
|
1544
1757
|
})
|
|
1545
1758
|
.then((response) => {
|
|
1759
|
+
if (!response.body || isEmpty(response.body)) {
|
|
1760
|
+
// 204 with empty body means our hashes match Locus, no sync needed
|
|
1761
|
+
LoggerProxy.logger.info(
|
|
1762
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got ${response.statusCode} with empty body for data set "${dataSetName}", hashes match - no sync needed`
|
|
1763
|
+
);
|
|
1764
|
+
|
|
1765
|
+
return null;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1546
1768
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
1547
1769
|
const dataSetFromResponse = response.body?.dataSet;
|
|
1548
1770
|
|
|
@@ -1571,6 +1793,13 @@ class HashTreeParser {
|
|
|
1571
1793
|
error
|
|
1572
1794
|
);
|
|
1573
1795
|
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1796
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
1797
|
+
debugId: this.debugId,
|
|
1798
|
+
dataSetName,
|
|
1799
|
+
request: 'GET /hashtree',
|
|
1800
|
+
statusCode: error.statusCode,
|
|
1801
|
+
reason: error.message,
|
|
1802
|
+
});
|
|
1574
1803
|
|
|
1575
1804
|
throw error;
|
|
1576
1805
|
});
|
|
@@ -1580,29 +1809,43 @@ class HashTreeParser {
|
|
|
1580
1809
|
* Sends a sync request to Locus for the specified data set.
|
|
1581
1810
|
*
|
|
1582
1811
|
* @param {InternalDataSet} dataSet The data set to sync.
|
|
1583
|
-
* @param {
|
|
1812
|
+
* @param {Object} options Either `{ isInitialization: true }` for init syncs (uses leafCount=1 with empty leaf data) or `{ mismatchedLeavesData }` for normal syncs.
|
|
1584
1813
|
* @returns {Promise<HashTreeMessage|null>}
|
|
1585
1814
|
*/
|
|
1586
1815
|
private sendSyncRequestToLocus(
|
|
1587
1816
|
dataSet: InternalDataSet,
|
|
1588
|
-
mismatchedLeavesData: Record<number, LeafDataItem[]>
|
|
1817
|
+
options: {isInitialization: true} | {mismatchedLeavesData: Record<number, LeafDataItem[]>}
|
|
1589
1818
|
): Promise<HashTreeMessage | null> {
|
|
1590
1819
|
LoggerProxy.logger.info(
|
|
1591
1820
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sending sync request for data set "${dataSet.name}"`
|
|
1592
1821
|
);
|
|
1593
1822
|
|
|
1823
|
+
const isInitialization = 'isInitialization' in options;
|
|
1824
|
+
|
|
1594
1825
|
const url = `${dataSet.url}/sync`;
|
|
1595
|
-
const body
|
|
1596
|
-
leafCount:
|
|
1826
|
+
const body: {
|
|
1827
|
+
leafCount: number;
|
|
1828
|
+
leafDataEntries: {leafIndex: number; elementIds: LeafDataItem[]}[];
|
|
1829
|
+
} = {
|
|
1830
|
+
leafCount: isInitialization ? 1 : dataSet.leafCount,
|
|
1597
1831
|
leafDataEntries: [],
|
|
1598
1832
|
};
|
|
1599
1833
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1834
|
+
if (isInitialization) {
|
|
1835
|
+
// initialization sync: Locus requires leafCount=1 with a single empty leaf
|
|
1836
|
+
body.leafDataEntries.push({leafIndex: 0, elementIds: []});
|
|
1837
|
+
} else {
|
|
1838
|
+
const {mismatchedLeavesData} = options;
|
|
1839
|
+
|
|
1840
|
+
Object.keys(mismatchedLeavesData).forEach((index) => {
|
|
1841
|
+
const leafIndex = parseInt(index, 10);
|
|
1842
|
+
|
|
1843
|
+
body.leafDataEntries.push({
|
|
1844
|
+
leafIndex,
|
|
1845
|
+
elementIds: mismatchedLeavesData[leafIndex],
|
|
1846
|
+
});
|
|
1604
1847
|
});
|
|
1605
|
-
}
|
|
1848
|
+
}
|
|
1606
1849
|
|
|
1607
1850
|
const ourCurrentRootHash = dataSet.hashTree ? dataSet.hashTree.getRootHash() : EMPTY_HASH;
|
|
1608
1851
|
|
|
@@ -1615,10 +1858,6 @@ class HashTreeParser {
|
|
|
1615
1858
|
body,
|
|
1616
1859
|
})
|
|
1617
1860
|
.then((resp) => {
|
|
1618
|
-
LoggerProxy.logger.info(
|
|
1619
|
-
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sync request succeeded for "${dataSet.name}"`
|
|
1620
|
-
);
|
|
1621
|
-
|
|
1622
1861
|
if (!resp.body || isEmpty(resp.body)) {
|
|
1623
1862
|
LoggerProxy.logger.info(
|
|
1624
1863
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Got ${resp.statusCode} with empty body for sync request for data set "${dataSet.name}", data should arrive via messages`
|
|
@@ -1635,6 +1874,13 @@ class HashTreeParser {
|
|
|
1635
1874
|
error
|
|
1636
1875
|
);
|
|
1637
1876
|
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1877
|
+
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.HASH_TREE_SYNC_FAILURE, {
|
|
1878
|
+
debugId: this.debugId,
|
|
1879
|
+
dataSetName: dataSet.name,
|
|
1880
|
+
request: 'POST /sync',
|
|
1881
|
+
statusCode: error.statusCode,
|
|
1882
|
+
reason: error.message,
|
|
1883
|
+
});
|
|
1638
1884
|
|
|
1639
1885
|
throw error;
|
|
1640
1886
|
});
|