@webex/plugin-meetings 3.11.0-next.4 → 3.11.0-next.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aiEnableRequest/index.js +184 -0
- package/dist/aiEnableRequest/index.js.map +1 -0
- package/dist/aiEnableRequest/utils.js +36 -0
- package/dist/aiEnableRequest/utils.js.map +1 -0
- package/dist/annotation/index.js +3 -3
- package/dist/annotation/index.js.map +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.js +26 -6
- package/dist/constants.js.map +1 -1
- package/dist/hashTree/constants.js +3 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTree.js +18 -0
- package/dist/hashTree/hashTree.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +709 -380
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/types.js +4 -2
- package/dist/hashTree/types.js.map +1 -1
- package/dist/hashTree/utils.js +10 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/interceptors/constant.js +12 -0
- package/dist/interceptors/constant.js.map +1 -0
- package/dist/interceptors/dataChannelAuthToken.js +233 -0
- package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
- package/dist/interceptors/index.js +7 -0
- package/dist/interceptors/index.js.map +1 -1
- package/dist/interpretation/index.js +2 -2
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +5 -3
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +125 -68
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/selfUtils.js +1 -0
- package/dist/locus-info/selfUtils.js.map +1 -1
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/MediaConnectionAwaiter.js +57 -1
- package/dist/media/MediaConnectionAwaiter.js.map +1 -1
- package/dist/media/properties.js +4 -2
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +7 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +209 -90
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +50 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/util.js +128 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +78 -36
- package/dist/meetings/index.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/util.js +10 -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/multistream/mediaRequestManager.js +1 -1
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/remoteMediaManager.js +11 -0
- package/dist/multistream/remoteMediaManager.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/types/aiEnableRequest/index.d.ts +5 -0
- package/dist/types/aiEnableRequest/utils.d.ts +2 -0
- package/dist/types/config.d.ts +3 -0
- package/dist/types/constants.d.ts +21 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTree.d.ts +7 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
- package/dist/types/hashTree/types.d.ts +3 -0
- package/dist/types/hashTree/utils.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/interceptors/constant.d.ts +5 -0
- package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
- package/dist/types/interceptors/index.d.ts +2 -1
- package/dist/types/locus-info/index.d.ts +9 -2
- package/dist/types/locus-info/types.d.ts +1 -0
- package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
- package/dist/types/media/properties.d.ts +2 -1
- package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
- package/dist/types/meeting/index.d.ts +24 -2
- package/dist/types/meeting/request.d.ts +16 -1
- package/dist/types/meeting/request.type.d.ts +5 -0
- package/dist/types/meeting/util.d.ts +31 -0
- package/dist/types/meetings/index.d.ts +4 -2
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/util.d.ts +5 -0
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/types/reactions/reactions.type.d.ts +1 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/README.md +84 -0
- package/src/aiEnableRequest/index.ts +170 -0
- package/src/aiEnableRequest/utils.ts +25 -0
- package/src/annotation/index.ts +7 -4
- package/src/config.ts +3 -0
- package/src/constants.ts +26 -1
- package/src/hashTree/constants.ts +1 -0
- package/src/hashTree/hashTree.ts +17 -0
- package/src/hashTree/hashTreeParser.ts +627 -249
- package/src/hashTree/types.ts +4 -0
- package/src/hashTree/utils.ts +9 -0
- package/src/index.ts +8 -1
- package/src/interceptors/constant.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +142 -0
- package/src/interceptors/index.ts +2 -1
- package/src/interpretation/index.ts +2 -2
- package/src/locus-info/controlsUtils.ts +11 -0
- package/src/locus-info/index.ts +146 -58
- package/src/locus-info/selfUtils.ts +1 -0
- package/src/locus-info/types.ts +1 -0
- package/src/media/MediaConnectionAwaiter.ts +41 -1
- package/src/media/properties.ts +3 -1
- package/src/meeting/in-meeting-actions.ts +12 -0
- package/src/meeting/index.ts +127 -17
- package/src/meeting/request.ts +42 -0
- package/src/meeting/request.type.ts +6 -0
- package/src/meeting/util.ts +156 -1
- package/src/meetings/index.ts +94 -9
- package/src/member/index.ts +10 -0
- package/src/member/util.ts +12 -0
- package/src/metrics/constants.ts +1 -0
- package/src/multistream/mediaRequestManager.ts +1 -1
- package/src/multistream/remoteMediaManager.ts +13 -0
- package/src/reactions/reactions.type.ts +1 -0
- package/test/unit/spec/aiEnableRequest/index.ts +981 -0
- package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
- package/test/unit/spec/hashTree/hashTree.ts +66 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
- package/test/unit/spec/locus-info/controlsUtils.js +29 -0
- package/test/unit/spec/locus-info/index.js +201 -45
- package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
- package/test/unit/spec/media/properties.ts +12 -3
- package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
- package/test/unit/spec/meeting/index.js +441 -75
- package/test/unit/spec/meeting/request.js +64 -0
- package/test/unit/spec/meeting/utils.js +433 -22
- package/test/unit/spec/meetings/index.js +550 -10
- package/test/unit/spec/member/index.js +28 -4
- package/test/unit/spec/member/util.js +65 -27
- package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
|
@@ -5,7 +5,7 @@ import {Enum, HTTP_VERBS} from '../constants';
|
|
|
5
5
|
import {DataSetNames, EMPTY_HASH} from './constants';
|
|
6
6
|
import {ObjectType, HtMeta, HashTreeObject} from './types';
|
|
7
7
|
import {LocusDTO} from '../locus-info/types';
|
|
8
|
-
import {deleteNestedObjectsWithHtMeta, isSelf} from './utils';
|
|
8
|
+
import {deleteNestedObjectsWithHtMeta, isMetadata, isSelf} from './utils';
|
|
9
9
|
|
|
10
10
|
export interface DataSet {
|
|
11
11
|
url: string;
|
|
@@ -29,11 +29,24 @@ export interface HashTreeMessage {
|
|
|
29
29
|
locusStateElements?: Array<HashTreeObject>;
|
|
30
30
|
locusSessionId?: string;
|
|
31
31
|
locusUrl: string;
|
|
32
|
+
heartbeatIntervalMs?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface VisibleDataSetInfo {
|
|
36
|
+
name: string;
|
|
37
|
+
url: string;
|
|
38
|
+
dataChannelUrl?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Metadata {
|
|
42
|
+
htMeta: HtMeta;
|
|
43
|
+
visibleDataSets: VisibleDataSetInfo[];
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
interface InternalDataSet extends DataSet {
|
|
35
47
|
hashTree?: HashTree; // set only for visible data sets
|
|
36
48
|
timer?: ReturnType<typeof setTimeout>;
|
|
49
|
+
heartbeatWatchdogTimer?: ReturnType<typeof setTimeout>;
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
|
|
@@ -49,11 +62,29 @@ export type LocusInfoUpdateCallback = (
|
|
|
49
62
|
data?: {updatedObjects: HashTreeObject[]}
|
|
50
63
|
) => void;
|
|
51
64
|
|
|
65
|
+
interface LeafInfo {
|
|
66
|
+
type: ObjectType;
|
|
67
|
+
id: number;
|
|
68
|
+
version: number;
|
|
69
|
+
data?: any;
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
/**
|
|
53
73
|
* This error is thrown if we receive information that the meeting has ended while we're processing some hash messages.
|
|
54
74
|
* It's handled internally by HashTreeParser and results in MEETING_ENDED being sent up.
|
|
55
75
|
*/
|
|
56
|
-
class MeetingEndedError extends Error {}
|
|
76
|
+
export class MeetingEndedError extends Error {}
|
|
77
|
+
|
|
78
|
+
/* Currently Locus always sends Metadata objects only in the "self" dataset.
|
|
79
|
+
* If this ever changes, update all the code that relies on this constant.
|
|
80
|
+
*/
|
|
81
|
+
const MetadataDataSetName = DataSetNames.SELF;
|
|
82
|
+
|
|
83
|
+
const PossibleSentinelMessageDataSetNames = [
|
|
84
|
+
DataSetNames.MAIN,
|
|
85
|
+
DataSetNames.SELF,
|
|
86
|
+
DataSetNames.UNJOINED,
|
|
87
|
+
];
|
|
57
88
|
|
|
58
89
|
/**
|
|
59
90
|
* Parses hash tree eventing locus data
|
|
@@ -63,8 +94,10 @@ class HashTreeParser {
|
|
|
63
94
|
visibleDataSetsUrl: string; // url from which we can get info about all data sets
|
|
64
95
|
webexRequest: WebexRequestMethod;
|
|
65
96
|
locusInfoUpdateCallback: LocusInfoUpdateCallback;
|
|
66
|
-
visibleDataSets:
|
|
97
|
+
visibleDataSets: VisibleDataSetInfo[];
|
|
67
98
|
debugId: string;
|
|
99
|
+
heartbeatIntervalMs?: number;
|
|
100
|
+
private excludedDataSets: string[];
|
|
68
101
|
|
|
69
102
|
/**
|
|
70
103
|
* Constructor for HashTreeParser
|
|
@@ -76,25 +109,36 @@ class HashTreeParser {
|
|
|
76
109
|
dataSets: Array<DataSet>;
|
|
77
110
|
locus: any;
|
|
78
111
|
};
|
|
112
|
+
metadata: Metadata | null;
|
|
79
113
|
webexRequest: WebexRequestMethod;
|
|
80
114
|
locusInfoUpdateCallback: LocusInfoUpdateCallback;
|
|
81
115
|
debugId: string;
|
|
116
|
+
excludedDataSets?: string[];
|
|
82
117
|
}) {
|
|
83
118
|
const {dataSets, locus} = options.initialLocus; // extract dataSets from initialLocus
|
|
84
119
|
|
|
85
120
|
this.debugId = options.debugId;
|
|
86
121
|
this.webexRequest = options.webexRequest;
|
|
87
122
|
this.locusInfoUpdateCallback = options.locusInfoUpdateCallback;
|
|
88
|
-
this.
|
|
123
|
+
this.excludedDataSets = options.excludedDataSets || [];
|
|
124
|
+
this.visibleDataSetsUrl = locus?.links?.resources?.visibleDataSets?.url;
|
|
125
|
+
this.visibleDataSets = (
|
|
126
|
+
cloneDeep(options.metadata?.visibleDataSets || []) as VisibleDataSetInfo[]
|
|
127
|
+
).filter((vds) => !this.isExcludedDataSet(vds.name));
|
|
89
128
|
|
|
90
|
-
if (
|
|
129
|
+
if (options.metadata?.visibleDataSets?.length === 0) {
|
|
91
130
|
LoggerProxy.logger.warn(
|
|
92
|
-
`HashTreeParser#constructor --> ${this.debugId} No visibleDataSets found in
|
|
131
|
+
`HashTreeParser#constructor --> ${this.debugId} No visibleDataSets found in Metadata`
|
|
93
132
|
);
|
|
94
133
|
}
|
|
95
134
|
// object mapping dataset names to arrays of leaf data
|
|
96
135
|
const leafData = this.analyzeLocusHtMeta(locus);
|
|
97
136
|
|
|
137
|
+
if (options.metadata) {
|
|
138
|
+
// add also the metadata that's outside of locus object itself
|
|
139
|
+
this.analyzeMetadata(leafData, options.metadata);
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
LoggerProxy.logger.info(
|
|
99
143
|
`HashTreeParser#constructor --> creating HashTreeParser for datasets: ${JSON.stringify(
|
|
100
144
|
dataSets.map((ds) => ds.name)
|
|
@@ -106,46 +150,87 @@ class HashTreeParser {
|
|
|
106
150
|
|
|
107
151
|
this.dataSets[name] = {
|
|
108
152
|
...dataSet,
|
|
109
|
-
hashTree: this.
|
|
153
|
+
hashTree: this.isVisibleDataSet(name)
|
|
110
154
|
? new HashTree(leafData[name] || [], leafCount)
|
|
111
155
|
: undefined,
|
|
112
156
|
};
|
|
113
157
|
}
|
|
114
158
|
}
|
|
115
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Checks if the given data set name is in the list of visible data sets
|
|
162
|
+
* @param {string} dataSetName data set name to check
|
|
163
|
+
* @returns {Boolean} True if the data set is visible, false otherwise
|
|
164
|
+
*/
|
|
165
|
+
private isVisibleDataSet(dataSetName: string): boolean {
|
|
166
|
+
return this.visibleDataSets.some((vds) => vds.name === dataSetName);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Checks if the given data set name is in the excluded list
|
|
171
|
+
* @param {string} dataSetName data set name to check
|
|
172
|
+
* @returns {boolean} True if the data set is excluded, false otherwise
|
|
173
|
+
*/
|
|
174
|
+
private isExcludedDataSet(dataSetName: string): boolean {
|
|
175
|
+
return this.excludedDataSets.some((name) => name === dataSetName);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Adds a data set to the visible data sets list, unless it is in the excluded list.
|
|
180
|
+
* @param {VisibleDataSetInfo} dataSetInfo data set info to add
|
|
181
|
+
* @returns {boolean} True if the data set was added, false if it was excluded
|
|
182
|
+
*/
|
|
183
|
+
private addToVisibleDataSetsList(dataSetInfo: VisibleDataSetInfo): boolean {
|
|
184
|
+
if (this.isExcludedDataSet(dataSetInfo.name)) {
|
|
185
|
+
LoggerProxy.logger.info(
|
|
186
|
+
`HashTreeParser#addToVisibleDataSetsList --> ${this.debugId} Data set "${dataSetInfo.name}" is in the excluded list, ignoring`
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.visibleDataSets.push(dataSetInfo);
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
116
197
|
/**
|
|
117
198
|
* Initializes a new visible data set by creating a hash tree for it, adding it to all the internal structures,
|
|
118
199
|
* and sending an initial sync request to Locus with empty leaf data - that will trigger Locus to gives us all the data
|
|
119
200
|
* from that dataset (in the response or via messages).
|
|
120
201
|
*
|
|
121
|
-
* @param {
|
|
202
|
+
* @param {VisibleDataSetInfo} visibleDataSetInfo Information about the new visible data set
|
|
203
|
+
* @param {DataSet} dataSetInfo The new data set to be added
|
|
122
204
|
* @returns {Promise}
|
|
123
205
|
*/
|
|
124
206
|
private initializeNewVisibleDataSet(
|
|
125
|
-
|
|
207
|
+
visibleDataSetInfo: VisibleDataSetInfo,
|
|
208
|
+
dataSetInfo: DataSet
|
|
126
209
|
): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
|
|
127
|
-
if (this.
|
|
210
|
+
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
128
211
|
LoggerProxy.logger.info(
|
|
129
|
-
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${
|
|
212
|
+
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
130
213
|
);
|
|
131
214
|
|
|
132
215
|
return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
|
|
133
216
|
}
|
|
134
217
|
|
|
135
218
|
LoggerProxy.logger.info(
|
|
136
|
-
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Adding visible data set "${
|
|
219
|
+
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Adding visible data set "${dataSetInfo.name}"`
|
|
137
220
|
);
|
|
138
221
|
|
|
139
|
-
this.
|
|
222
|
+
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
223
|
+
return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
|
|
224
|
+
}
|
|
140
225
|
|
|
141
|
-
const hashTree = new HashTree([],
|
|
226
|
+
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
142
227
|
|
|
143
|
-
this.dataSets[
|
|
144
|
-
...
|
|
228
|
+
this.dataSets[dataSetInfo.name] = {
|
|
229
|
+
...dataSetInfo,
|
|
145
230
|
hashTree,
|
|
146
231
|
};
|
|
147
232
|
|
|
148
|
-
return this.sendInitializationSyncRequestToLocus(
|
|
233
|
+
return this.sendInitializationSyncRequestToLocus(dataSetInfo.name, 'new visible data set');
|
|
149
234
|
}
|
|
150
235
|
|
|
151
236
|
/**
|
|
@@ -178,10 +263,13 @@ class HashTreeParser {
|
|
|
178
263
|
return this.sendSyncRequestToLocus(this.dataSets[datasetName], emptyLeavesData).then(
|
|
179
264
|
(syncResponse) => {
|
|
180
265
|
if (syncResponse) {
|
|
181
|
-
return
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
266
|
+
return {
|
|
267
|
+
updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
268
|
+
updatedObjects: this.parseMessage(
|
|
269
|
+
syncResponse,
|
|
270
|
+
`via empty leaves /sync API call for ${debugText}`
|
|
271
|
+
),
|
|
272
|
+
};
|
|
185
273
|
}
|
|
186
274
|
|
|
187
275
|
return {updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []};
|
|
@@ -190,18 +278,31 @@ class HashTreeParser {
|
|
|
190
278
|
}
|
|
191
279
|
|
|
192
280
|
/**
|
|
193
|
-
* Queries Locus for information about all
|
|
281
|
+
* Queries Locus for all up-to-date information about all visible data sets
|
|
194
282
|
*
|
|
195
|
-
* @param {string} url - url from which we can get info about all data sets
|
|
196
283
|
* @returns {Promise}
|
|
197
284
|
*/
|
|
198
|
-
private
|
|
285
|
+
private getAllVisibleDataSetsFromLocus() {
|
|
286
|
+
if (!this.visibleDataSetsUrl) {
|
|
287
|
+
LoggerProxy.logger.warn(
|
|
288
|
+
`HashTreeParser#getAllVisibleDataSetsFromLocus --> ${this.debugId} No visibleDataSetsUrl, cannot get data sets information`
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return Promise.resolve([]);
|
|
292
|
+
}
|
|
293
|
+
|
|
199
294
|
return this.webexRequest({
|
|
200
295
|
method: HTTP_VERBS.GET,
|
|
201
|
-
uri:
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
|
|
296
|
+
uri: this.visibleDataSetsUrl,
|
|
297
|
+
})
|
|
298
|
+
.then((response) => {
|
|
299
|
+
return response.body.dataSets as Array<DataSet>;
|
|
300
|
+
})
|
|
301
|
+
.catch((error) => {
|
|
302
|
+
this.checkForSentinelHttpResponse(error);
|
|
303
|
+
|
|
304
|
+
throw error;
|
|
305
|
+
});
|
|
205
306
|
}
|
|
206
307
|
|
|
207
308
|
/**
|
|
@@ -211,12 +312,14 @@ class HashTreeParser {
|
|
|
211
312
|
* @returns {Promise}
|
|
212
313
|
*/
|
|
213
314
|
async initializeFromMessage(message: HashTreeMessage) {
|
|
315
|
+
this.visibleDataSetsUrl = message.visibleDataSetsUrl;
|
|
316
|
+
|
|
214
317
|
LoggerProxy.logger.info(
|
|
215
|
-
`HashTreeParser#initializeFromMessage --> ${this.debugId} visibleDataSetsUrl=${
|
|
318
|
+
`HashTreeParser#initializeFromMessage --> ${this.debugId} visibleDataSetsUrl=${this.visibleDataSetsUrl}`
|
|
216
319
|
);
|
|
217
|
-
const
|
|
320
|
+
const visibleDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
218
321
|
|
|
219
|
-
await this.initializeDataSets(
|
|
322
|
+
await this.initializeDataSets(visibleDataSets, 'initialization from message');
|
|
220
323
|
}
|
|
221
324
|
|
|
222
325
|
/**
|
|
@@ -236,28 +339,29 @@ class HashTreeParser {
|
|
|
236
339
|
|
|
237
340
|
return;
|
|
238
341
|
}
|
|
342
|
+
this.visibleDataSetsUrl = locus.links.resources.visibleDataSets.url;
|
|
239
343
|
|
|
240
344
|
LoggerProxy.logger.info(
|
|
241
|
-
`HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} visibleDataSets url: ${
|
|
345
|
+
`HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} visibleDataSets url: ${this.visibleDataSetsUrl}`
|
|
242
346
|
);
|
|
243
347
|
|
|
244
|
-
const
|
|
348
|
+
const visibleDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
245
349
|
|
|
246
|
-
await this.initializeDataSets(
|
|
350
|
+
await this.initializeDataSets(visibleDataSets, 'initialization from GET /loci response');
|
|
247
351
|
}
|
|
248
352
|
|
|
249
353
|
/**
|
|
250
354
|
* Initializes data sets by doing an initialization sync on each visible data set that doesn't have a hash tree yet.
|
|
251
355
|
*
|
|
252
|
-
* @param {DataSet[]}
|
|
356
|
+
* @param {DataSet[]} visibleDataSets Array of visible DataSet objects to initialize
|
|
253
357
|
* @param {string} debugText Text to include in logs for debugging purposes
|
|
254
358
|
* @returns {Promise}
|
|
255
359
|
*/
|
|
256
|
-
private async initializeDataSets(
|
|
360
|
+
private async initializeDataSets(visibleDataSets: Array<DataSet>, debugText: string) {
|
|
257
361
|
const updatedObjects: HashTreeObject[] = [];
|
|
258
362
|
|
|
259
|
-
for (const dataSet of
|
|
260
|
-
const {name, leafCount} = dataSet;
|
|
363
|
+
for (const dataSet of visibleDataSets) {
|
|
364
|
+
const {name, leafCount, url} = dataSet;
|
|
261
365
|
|
|
262
366
|
if (!this.dataSets[name]) {
|
|
263
367
|
LoggerProxy.logger.info(
|
|
@@ -273,7 +377,20 @@ class HashTreeParser {
|
|
|
273
377
|
);
|
|
274
378
|
}
|
|
275
379
|
|
|
276
|
-
if (this.
|
|
380
|
+
if (!this.isVisibleDataSet(name)) {
|
|
381
|
+
if (
|
|
382
|
+
!this.addToVisibleDataSetsList({
|
|
383
|
+
name,
|
|
384
|
+
url,
|
|
385
|
+
})
|
|
386
|
+
) {
|
|
387
|
+
// dataset is excluded, skip it
|
|
388
|
+
// eslint-disable-next-line no-continue
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!this.dataSets[name].hashTree) {
|
|
277
394
|
LoggerProxy.logger.info(
|
|
278
395
|
`HashTreeParser#initializeDataSets --> ${this.debugId} creating hash tree for visible dataset "${name}" (${debugText})`
|
|
279
396
|
);
|
|
@@ -282,15 +399,6 @@ class HashTreeParser {
|
|
|
282
399
|
// eslint-disable-next-line no-await-in-loop
|
|
283
400
|
const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
|
|
284
401
|
|
|
285
|
-
if (data.updateType === LocusInfoUpdateType.MEETING_ENDED) {
|
|
286
|
-
LoggerProxy.logger.warn(
|
|
287
|
-
`HashTreeParser#initializeDataSets --> ${this.debugId} meeting ended while initializing new visible data set "${name}"`
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
// throw an error, it will be caught higher up and the meeting will be destroyed
|
|
291
|
-
throw new MeetingEndedError();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
402
|
if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
295
403
|
updatedObjects.push(...(data.updatedObjects || []));
|
|
296
404
|
}
|
|
@@ -316,10 +424,7 @@ class HashTreeParser {
|
|
|
316
424
|
private analyzeLocusHtMeta(locus: any, options?: {copyData?: boolean}) {
|
|
317
425
|
const {copyData = false} = options || {};
|
|
318
426
|
// object mapping dataset names to arrays of leaf data
|
|
319
|
-
const leafInfo: Record<
|
|
320
|
-
string,
|
|
321
|
-
Array<{type: ObjectType; id: number; version: number; data?: any}>
|
|
322
|
-
> = {};
|
|
427
|
+
const leafInfo: Record<string, Array<LeafInfo>> = {};
|
|
323
428
|
|
|
324
429
|
const findAndStoreMetaData = (currentLocusPart: any) => {
|
|
325
430
|
if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
|
|
@@ -329,7 +434,7 @@ class HashTreeParser {
|
|
|
329
434
|
if (currentLocusPart.htMeta && currentLocusPart.htMeta.dataSetNames) {
|
|
330
435
|
const {type, id, version} = currentLocusPart.htMeta.elementId;
|
|
331
436
|
const {dataSetNames} = currentLocusPart.htMeta;
|
|
332
|
-
const newLeafInfo:
|
|
437
|
+
const newLeafInfo: LeafInfo = {
|
|
333
438
|
type,
|
|
334
439
|
id,
|
|
335
440
|
version,
|
|
@@ -369,27 +474,62 @@ class HashTreeParser {
|
|
|
369
474
|
}
|
|
370
475
|
|
|
371
476
|
/**
|
|
372
|
-
*
|
|
477
|
+
* Analyzes the Metadata object that is sent outside of Locus object, and appends its data to passed in leafInfo
|
|
478
|
+
* structure.
|
|
373
479
|
*
|
|
374
|
-
* @param {
|
|
375
|
-
* @
|
|
480
|
+
* @param {Record<string, LeafInfo[]>} leafInfo the structure to which the Metadata info will be appended
|
|
481
|
+
* @param {Metadata} metadata Metadata object
|
|
482
|
+
* @returns {void}
|
|
376
483
|
*/
|
|
377
|
-
private
|
|
378
|
-
const
|
|
379
|
-
(dataSet) => dataSet.name.toLowerCase() === DataSetNames.MAIN
|
|
380
|
-
);
|
|
484
|
+
private analyzeMetadata(leafInfo: Record<string, LeafInfo[]>, metadata: Metadata) {
|
|
485
|
+
const {htMeta} = metadata;
|
|
381
486
|
|
|
382
487
|
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
mainDataSet.root === EMPTY_HASH &&
|
|
386
|
-
this.dataSets[DataSetNames.MAIN].version < mainDataSet.version
|
|
488
|
+
htMeta?.dataSetNames?.length === 1 &&
|
|
489
|
+
htMeta.dataSetNames[0].toLowerCase() === MetadataDataSetName
|
|
387
490
|
) {
|
|
388
|
-
|
|
389
|
-
|
|
491
|
+
const {type, id, version} = metadata.htMeta.elementId;
|
|
492
|
+
|
|
493
|
+
const dataSetName = htMeta.dataSetNames[0];
|
|
494
|
+
|
|
495
|
+
if (!leafInfo[dataSetName]) {
|
|
496
|
+
leafInfo[dataSetName] = [];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
leafInfo[dataSetName].push({
|
|
500
|
+
type,
|
|
501
|
+
id,
|
|
502
|
+
version,
|
|
503
|
+
});
|
|
504
|
+
} else {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`${this.debugId} Metadata htMeta has unexpected dataSetNames: ${
|
|
507
|
+
htMeta && htMeta.dataSetNames.join(',')
|
|
508
|
+
}`
|
|
509
|
+
);
|
|
390
510
|
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Checks if the provided hash tree message indicates the end of the meeting and that there won't be any more updates.
|
|
515
|
+
*
|
|
516
|
+
* @param {HashTreeMessage} message - The hash tree message to check
|
|
517
|
+
* @returns {boolean} - Returns true if the message indicates the end of the meeting, false otherwise
|
|
518
|
+
*/
|
|
519
|
+
private isEndMessage(message: HashTreeMessage) {
|
|
520
|
+
return message.dataSets.some((dataSet) => {
|
|
521
|
+
if (
|
|
522
|
+
dataSet.leafCount === 1 &&
|
|
523
|
+
dataSet.root === EMPTY_HASH &&
|
|
524
|
+
(!this.dataSets[dataSet.name] || this.dataSets[dataSet.name].version < dataSet.version) &&
|
|
525
|
+
PossibleSentinelMessageDataSetNames.includes(dataSet.name.toLowerCase())
|
|
526
|
+
) {
|
|
527
|
+
// this is a special way for Locus to indicate that this meeting has ended
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
391
530
|
|
|
392
|
-
|
|
531
|
+
return false;
|
|
532
|
+
});
|
|
393
533
|
}
|
|
394
534
|
|
|
395
535
|
/**
|
|
@@ -420,6 +560,83 @@ class HashTreeParser {
|
|
|
420
560
|
});
|
|
421
561
|
}
|
|
422
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Asynchronously initializes new visible data sets
|
|
565
|
+
*
|
|
566
|
+
* @param {VisibleDataSetInfo[]} dataSetsRequiringInitialization list of datasets to initialize
|
|
567
|
+
* @returns {void}
|
|
568
|
+
*/
|
|
569
|
+
private queueInitForNewVisibleDataSets(dataSetsRequiringInitialization: VisibleDataSetInfo[]) {
|
|
570
|
+
queueMicrotask(() => {
|
|
571
|
+
this.initializeNewVisibleDataSets(dataSetsRequiringInitialization).catch((error) => {
|
|
572
|
+
if (error instanceof MeetingEndedError) {
|
|
573
|
+
this.callLocusInfoUpdateCallback({
|
|
574
|
+
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
575
|
+
});
|
|
576
|
+
} else {
|
|
577
|
+
LoggerProxy.logger.warn(
|
|
578
|
+
`HashTreeParser#queueInitForNewVisibleDataSets --> ${
|
|
579
|
+
this.debugId
|
|
580
|
+
} error while initializing new visible datasets: ${dataSetsRequiringInitialization
|
|
581
|
+
.map((ds) => ds.name)
|
|
582
|
+
.join(', ')}: `,
|
|
583
|
+
error
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Handles updates to Metadata object that we receive from Locus via other means than messages. Right now
|
|
592
|
+
* that means only in the API response alongside locus object.
|
|
593
|
+
*
|
|
594
|
+
* @param {Metadata} metadata received in Locus update other than a message (for example in an API response)
|
|
595
|
+
* @param {HashTreeObject[]} updatedObjects a list of updated hash tree objects to which any updates resulting from new Metadata will be added
|
|
596
|
+
* @returns {void}
|
|
597
|
+
*/
|
|
598
|
+
handleMetadataUpdate(metadata: Metadata, updatedObjects: HashTreeObject[]): void {
|
|
599
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
600
|
+
|
|
601
|
+
// current assumption based on Locus docs is that Metadata object lives always in "self" data set
|
|
602
|
+
const hashTree = this.dataSets[MetadataDataSetName]?.hashTree;
|
|
603
|
+
|
|
604
|
+
if (!hashTree) {
|
|
605
|
+
LoggerProxy.logger.warn(
|
|
606
|
+
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received Metadata object but no hash tree for "${MetadataDataSetName}" data set exists`
|
|
607
|
+
);
|
|
608
|
+
} else {
|
|
609
|
+
const metadataUpdated = hashTree.putItem(metadata.htMeta.elementId);
|
|
610
|
+
|
|
611
|
+
if (metadataUpdated) {
|
|
612
|
+
// metadata in Locus API response is in a slightly different format than the objects in messages, so need to adapt it
|
|
613
|
+
const metadataObject: HashTreeObject = {
|
|
614
|
+
htMeta: metadata.htMeta,
|
|
615
|
+
data: metadata,
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
updatedObjects.push(metadataObject);
|
|
619
|
+
|
|
620
|
+
const {changeDetected, removedDataSets, addedDataSets} = this.checkForVisibleDataSetChanges(
|
|
621
|
+
[metadataObject]
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
if (changeDetected) {
|
|
625
|
+
dataSetsRequiringInitialization = this.processVisibleDataSetChanges(
|
|
626
|
+
removedDataSets,
|
|
627
|
+
addedDataSets,
|
|
628
|
+
updatedObjects
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (dataSetsRequiringInitialization.length > 0) {
|
|
633
|
+
// there are some data sets that we need to initialize asynchronously
|
|
634
|
+
this.queueInitForNewVisibleDataSets(dataSetsRequiringInitialization);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
423
640
|
/**
|
|
424
641
|
* This method should be called when we receive a partial locus DTO that contains dataSets and htMeta information
|
|
425
642
|
* It updates the hash trees with the new leaf data based on the received Locus
|
|
@@ -427,22 +644,28 @@ class HashTreeParser {
|
|
|
427
644
|
* @param {Object} update - The locus update containing data sets and locus information
|
|
428
645
|
* @returns {void}
|
|
429
646
|
*/
|
|
430
|
-
handleLocusUpdate(update: {dataSets?: Array<DataSet>; locus: any}): void {
|
|
431
|
-
const {dataSets, locus} = update;
|
|
647
|
+
handleLocusUpdate(update: {dataSets?: Array<DataSet>; locus: any; metadata?: Metadata}): void {
|
|
648
|
+
const {dataSets, locus, metadata} = update;
|
|
432
649
|
|
|
433
650
|
if (!dataSets) {
|
|
434
|
-
LoggerProxy.logger.
|
|
651
|
+
LoggerProxy.logger.info(
|
|
435
652
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
436
653
|
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
654
|
+
} else {
|
|
655
|
+
for (const dataSet of dataSets) {
|
|
656
|
+
this.updateDataSetInfo(dataSet);
|
|
657
|
+
}
|
|
440
658
|
}
|
|
441
659
|
const updatedObjects: HashTreeObject[] = [];
|
|
442
660
|
|
|
443
661
|
// first, analyze the locus object to extract the hash tree objects' htMeta and data from it
|
|
444
662
|
const leafInfo = this.analyzeLocusHtMeta(locus, {copyData: true});
|
|
445
663
|
|
|
664
|
+
// if we got metadata, process it (currently that means only potential visible data set list changes)
|
|
665
|
+
if (metadata) {
|
|
666
|
+
this.handleMetadataUpdate(metadata, updatedObjects);
|
|
667
|
+
}
|
|
668
|
+
|
|
446
669
|
// then process the data in hash trees, if it is a new version, then add it to updatedObjects
|
|
447
670
|
Object.keys(leafInfo).forEach((dataSetName) => {
|
|
448
671
|
if (this.dataSets[dataSetName]) {
|
|
@@ -493,9 +716,6 @@ class HashTreeParser {
|
|
|
493
716
|
updatedObjects,
|
|
494
717
|
});
|
|
495
718
|
}
|
|
496
|
-
|
|
497
|
-
// todo: once Locus design on how visible data sets will be communicated in subsequent API responses is confirmed,
|
|
498
|
-
// we'll need to check here if visible data sets have changed and update this.visibleDataSets, remove/create hash trees etc
|
|
499
719
|
}
|
|
500
720
|
|
|
501
721
|
/**
|
|
@@ -511,7 +731,7 @@ class HashTreeParser {
|
|
|
511
731
|
};
|
|
512
732
|
|
|
513
733
|
LoggerProxy.logger.info(
|
|
514
|
-
`HashTreeParser#
|
|
734
|
+
`HashTreeParser#updateDataSetInfo --> ${this.debugId} created entry for "${receivedDataSet.name}" dataset: version=${receivedDataSet.version}, root=${receivedDataSet.root}`
|
|
515
735
|
);
|
|
516
736
|
|
|
517
737
|
return;
|
|
@@ -526,7 +746,7 @@ class HashTreeParser {
|
|
|
526
746
|
exponent: receivedDataSet.backoff.exponent,
|
|
527
747
|
};
|
|
528
748
|
LoggerProxy.logger.info(
|
|
529
|
-
`HashTreeParser#
|
|
749
|
+
`HashTreeParser#updateDataSetInfo --> ${this.debugId} updated "${receivedDataSet.name}" dataset to version=${receivedDataSet.version}, root=${receivedDataSet.root}`
|
|
530
750
|
);
|
|
531
751
|
}
|
|
532
752
|
}
|
|
@@ -537,25 +757,30 @@ class HashTreeParser {
|
|
|
537
757
|
* @returns {Object} An object containing the removed and added visible data sets.
|
|
538
758
|
*/
|
|
539
759
|
private checkForVisibleDataSetChanges(updatedObjects: HashTreeObject[]) {
|
|
540
|
-
let removedDataSets:
|
|
541
|
-
let addedDataSets:
|
|
760
|
+
let removedDataSets: VisibleDataSetInfo[] = [];
|
|
761
|
+
let addedDataSets: VisibleDataSetInfo[] = [];
|
|
542
762
|
|
|
543
|
-
// visibleDataSets can only be changed by
|
|
763
|
+
// visibleDataSets can only be changed by Metadata object updates
|
|
544
764
|
updatedObjects.forEach((object) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
765
|
+
if (isMetadata(object) && object.data?.visibleDataSets) {
|
|
766
|
+
const newVisibleDataSets = object.data.visibleDataSets.filter(
|
|
767
|
+
(vds) => !this.isExcludedDataSet(vds.name)
|
|
768
|
+
);
|
|
548
769
|
|
|
549
|
-
removedDataSets = this.visibleDataSets.filter(
|
|
550
|
-
|
|
770
|
+
removedDataSets = this.visibleDataSets.filter(
|
|
771
|
+
(ds) => !newVisibleDataSets.some((nvs) => nvs.name === ds.name)
|
|
772
|
+
);
|
|
773
|
+
addedDataSets = newVisibleDataSets.filter((nvs) =>
|
|
774
|
+
this.visibleDataSets.every((ds) => ds.name !== nvs.name)
|
|
775
|
+
);
|
|
551
776
|
|
|
552
777
|
if (removedDataSets.length > 0 || addedDataSets.length > 0) {
|
|
553
778
|
LoggerProxy.logger.info(
|
|
554
779
|
`HashTreeParser#checkForVisibleDataSetChanges --> ${
|
|
555
780
|
this.debugId
|
|
556
|
-
} visible data sets change: removed: ${removedDataSets
|
|
557
|
-
|
|
558
|
-
|
|
781
|
+
} visible data sets change: removed: ${removedDataSets
|
|
782
|
+
.map((ds) => ds.name)
|
|
783
|
+
.join(', ')}, added: ${addedDataSets.map((ds) => ds.name).join(', ')}`
|
|
559
784
|
);
|
|
560
785
|
}
|
|
561
786
|
}
|
|
@@ -577,11 +802,15 @@ class HashTreeParser {
|
|
|
577
802
|
private deleteHashTree(dataSetName: string) {
|
|
578
803
|
this.dataSets[dataSetName].hashTree = undefined;
|
|
579
804
|
|
|
580
|
-
// we also need to stop the
|
|
805
|
+
// we also need to stop the timers as there is no hash tree anymore to sync
|
|
581
806
|
if (this.dataSets[dataSetName].timer) {
|
|
582
807
|
clearTimeout(this.dataSets[dataSetName].timer);
|
|
583
808
|
this.dataSets[dataSetName].timer = undefined;
|
|
584
809
|
}
|
|
810
|
+
if (this.dataSets[dataSetName].heartbeatWatchdogTimer) {
|
|
811
|
+
clearTimeout(this.dataSets[dataSetName].heartbeatWatchdogTimer);
|
|
812
|
+
this.dataSets[dataSetName].heartbeatWatchdogTimer = undefined;
|
|
813
|
+
}
|
|
585
814
|
}
|
|
586
815
|
|
|
587
816
|
/**
|
|
@@ -593,49 +822,51 @@ class HashTreeParser {
|
|
|
593
822
|
* visible data sets and they require async initialization, the names of these data sets
|
|
594
823
|
* are returned in an array.
|
|
595
824
|
*
|
|
596
|
-
* @param {
|
|
597
|
-
* @param {
|
|
825
|
+
* @param {VisibleDataSetInfo[]} removedDataSets - The list of removed data sets.
|
|
826
|
+
* @param {VisibleDataSetInfo[]} addedDataSets - The list of added data sets.
|
|
598
827
|
* @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects to which changes will be added.
|
|
599
|
-
* @returns {
|
|
828
|
+
* @returns {VisibleDataSetInfo[]} list of data sets that couldn't be initialized synchronously
|
|
600
829
|
*/
|
|
601
830
|
private processVisibleDataSetChanges(
|
|
602
|
-
removedDataSets:
|
|
603
|
-
addedDataSets:
|
|
831
|
+
removedDataSets: VisibleDataSetInfo[],
|
|
832
|
+
addedDataSets: VisibleDataSetInfo[],
|
|
604
833
|
updatedObjects: HashTreeObject[]
|
|
605
|
-
):
|
|
606
|
-
const dataSetsRequiringInitialization = [];
|
|
834
|
+
): VisibleDataSetInfo[] {
|
|
835
|
+
const dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
607
836
|
|
|
608
837
|
// if a visible data set was removed, we need to tell our client that all objects from it are removed
|
|
609
838
|
const removedObjects: HashTreeObject[] = [];
|
|
610
839
|
|
|
611
840
|
removedDataSets.forEach((ds) => {
|
|
612
|
-
if (this.dataSets[ds]?.hashTree) {
|
|
613
|
-
for (let i = 0; i < this.dataSets[ds].hashTree.numLeaves; i += 1) {
|
|
841
|
+
if (this.dataSets[ds.name]?.hashTree) {
|
|
842
|
+
for (let i = 0; i < this.dataSets[ds.name].hashTree.numLeaves; i += 1) {
|
|
614
843
|
removedObjects.push(
|
|
615
|
-
...this.dataSets[ds].hashTree.getLeafData(i).map((elementId) => ({
|
|
844
|
+
...this.dataSets[ds.name].hashTree.getLeafData(i).map((elementId) => ({
|
|
616
845
|
htMeta: {
|
|
617
846
|
elementId,
|
|
618
|
-
dataSetNames: [ds],
|
|
847
|
+
dataSetNames: [ds.name],
|
|
619
848
|
},
|
|
620
849
|
data: null,
|
|
621
850
|
}))
|
|
622
851
|
);
|
|
623
852
|
}
|
|
624
853
|
|
|
625
|
-
this.deleteHashTree(ds);
|
|
854
|
+
this.deleteHashTree(ds.name);
|
|
626
855
|
}
|
|
627
856
|
});
|
|
628
|
-
this.visibleDataSets = this.visibleDataSets.filter(
|
|
857
|
+
this.visibleDataSets = this.visibleDataSets.filter(
|
|
858
|
+
(vds) => !removedDataSets.some((rds) => rds.name === vds.name)
|
|
859
|
+
);
|
|
629
860
|
updatedObjects.push(...removedObjects);
|
|
630
861
|
|
|
631
862
|
// now setup the new visible data sets
|
|
632
863
|
for (const ds of addedDataSets) {
|
|
633
|
-
const dataSetInfo = this.dataSets[ds];
|
|
864
|
+
const dataSetInfo = this.dataSets[ds.name];
|
|
634
865
|
|
|
635
866
|
if (dataSetInfo) {
|
|
636
|
-
if (this.
|
|
867
|
+
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
637
868
|
LoggerProxy.logger.info(
|
|
638
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Data set "${ds}" is already visible, skipping`
|
|
869
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Data set "${ds.name}" is already visible, skipping`
|
|
639
870
|
);
|
|
640
871
|
|
|
641
872
|
// eslint-disable-next-line no-continue
|
|
@@ -643,10 +874,13 @@ class HashTreeParser {
|
|
|
643
874
|
}
|
|
644
875
|
|
|
645
876
|
LoggerProxy.logger.info(
|
|
646
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Adding visible data set "${ds}"`
|
|
877
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Adding visible data set "${ds.name}"`
|
|
647
878
|
);
|
|
648
879
|
|
|
649
|
-
this.
|
|
880
|
+
if (!this.addToVisibleDataSetsList(ds)) {
|
|
881
|
+
// eslint-disable-next-line no-continue
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
650
884
|
|
|
651
885
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
652
886
|
|
|
@@ -656,7 +890,7 @@ class HashTreeParser {
|
|
|
656
890
|
};
|
|
657
891
|
} else {
|
|
658
892
|
LoggerProxy.logger.info(
|
|
659
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} visible data set "${ds}" added but no info about it in our dataSets structures`
|
|
893
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} visible data set "${ds.name}" added but no info about it in our dataSets structures`
|
|
660
894
|
);
|
|
661
895
|
// todo: add a metric here
|
|
662
896
|
dataSetsRequiringInitialization.push(ds);
|
|
@@ -670,32 +904,28 @@ class HashTreeParser {
|
|
|
670
904
|
* Adds entries to the passed in updateObjects array
|
|
671
905
|
* for the changes that result from adding and removing visible data sets.
|
|
672
906
|
*
|
|
673
|
-
* @param {
|
|
674
|
-
* @param {string[]} addedDataSets - The list of added data sets.
|
|
907
|
+
* @param {VisibleDataSetInfo[]} addedDataSets - The list of added data sets.
|
|
675
908
|
* @returns {Promise<void>}
|
|
676
909
|
*/
|
|
677
|
-
private async initializeNewVisibleDataSets(
|
|
678
|
-
|
|
679
|
-
addedDataSets: string[]
|
|
680
|
-
): Promise<void> {
|
|
681
|
-
const allDataSets = await this.getAllDataSetsMetadata(message.visibleDataSetsUrl);
|
|
910
|
+
private async initializeNewVisibleDataSets(addedDataSets: VisibleDataSetInfo[]): Promise<void> {
|
|
911
|
+
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
682
912
|
|
|
683
913
|
for (const ds of addedDataSets) {
|
|
684
|
-
const dataSetInfo = allDataSets.find((d) => d.name === ds);
|
|
914
|
+
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
685
915
|
|
|
686
916
|
LoggerProxy.logger.info(
|
|
687
|
-
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} initializing data set "${ds}"`
|
|
917
|
+
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} initializing data set "${ds.name}"`
|
|
688
918
|
);
|
|
689
919
|
|
|
690
920
|
if (!dataSetInfo) {
|
|
691
921
|
LoggerProxy.logger.warn(
|
|
692
|
-
`HashTreeParser#handleHashTreeMessage --> ${this.debugId} missing info about data set "${ds}" in Locus response from visibleDataSetsUrl`
|
|
922
|
+
`HashTreeParser#handleHashTreeMessage --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
693
923
|
);
|
|
694
924
|
} else {
|
|
695
925
|
// we're awaiting in a loop, because in practice there will be only one new data set at a time,
|
|
696
926
|
// so no point in trying to parallelize this
|
|
697
927
|
// eslint-disable-next-line no-await-in-loop
|
|
698
|
-
const updates = await this.initializeNewVisibleDataSet(dataSetInfo);
|
|
928
|
+
const updates = await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
699
929
|
|
|
700
930
|
this.callLocusInfoUpdateCallback(updates);
|
|
701
931
|
}
|
|
@@ -707,12 +937,9 @@ class HashTreeParser {
|
|
|
707
937
|
*
|
|
708
938
|
* @param {HashTreeMessage} message - The hash tree message containing data sets and objects to be processed
|
|
709
939
|
* @param {string} [debugText] - Optional debug text to include in logs
|
|
710
|
-
* @returns {
|
|
940
|
+
* @returns {HashTreeObject[]} list of hash tree objects that were updated as a result of processing the message
|
|
711
941
|
*/
|
|
712
|
-
private
|
|
713
|
-
message: HashTreeMessage,
|
|
714
|
-
debugText?: string
|
|
715
|
-
): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
|
|
942
|
+
private parseMessage(message: HashTreeMessage, debugText?: string): HashTreeObject[] {
|
|
716
943
|
const {dataSets, visibleDataSetsUrl} = message;
|
|
717
944
|
|
|
718
945
|
LoggerProxy.logger.info(
|
|
@@ -730,48 +957,37 @@ class HashTreeParser {
|
|
|
730
957
|
this.visibleDataSetsUrl = visibleDataSetsUrl;
|
|
731
958
|
dataSets.forEach((dataSet) => this.updateDataSetInfo(dataSet));
|
|
732
959
|
|
|
733
|
-
if (this.isEndMessage(message)) {
|
|
734
|
-
LoggerProxy.logger.info(
|
|
735
|
-
`HashTreeParser#parseMessage --> ${this.debugId} received END message`
|
|
736
|
-
);
|
|
737
|
-
this.stopAllTimers();
|
|
738
|
-
|
|
739
|
-
return {updateType: LocusInfoUpdateType.MEETING_ENDED};
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
let isRosterDropped = false;
|
|
743
960
|
const updatedObjects: HashTreeObject[] = [];
|
|
744
961
|
|
|
745
962
|
// when we detect new visible datasets, it may be that the metadata about them is not
|
|
746
963
|
// available in the message, they will require separate async initialization
|
|
747
964
|
let dataSetsRequiringInitialization = [];
|
|
748
965
|
|
|
749
|
-
// first find out if there are any visible data set changes - they're signalled in
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
isSelf(object)
|
|
966
|
+
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
967
|
+
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
968
|
+
isMetadata(object)
|
|
753
969
|
);
|
|
754
970
|
|
|
755
|
-
if (
|
|
756
|
-
const
|
|
971
|
+
if (metadataUpdates.length > 0) {
|
|
972
|
+
const updatedMetadataObjects = [];
|
|
757
973
|
|
|
758
|
-
|
|
974
|
+
metadataUpdates.forEach((object) => {
|
|
759
975
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
760
976
|
for (const dataSetName of object.htMeta.dataSetNames) {
|
|
761
977
|
const hashTree = this.dataSets[dataSetName]?.hashTree;
|
|
762
978
|
|
|
763
979
|
if (hashTree && object.data) {
|
|
764
980
|
if (hashTree.putItem(object.htMeta.elementId)) {
|
|
765
|
-
|
|
981
|
+
updatedMetadataObjects.push(object);
|
|
766
982
|
}
|
|
767
983
|
}
|
|
768
984
|
}
|
|
769
985
|
});
|
|
770
986
|
|
|
771
|
-
updatedObjects.push(...
|
|
987
|
+
updatedObjects.push(...updatedMetadataObjects);
|
|
772
988
|
|
|
773
989
|
const {changeDetected, removedDataSets, addedDataSets} =
|
|
774
|
-
this.checkForVisibleDataSetChanges(
|
|
990
|
+
this.checkForVisibleDataSetChanges(updatedMetadataObjects);
|
|
775
991
|
|
|
776
992
|
if (changeDetected) {
|
|
777
993
|
dataSetsRequiringInitialization = this.processVisibleDataSetChanges(
|
|
@@ -782,64 +998,49 @@ class HashTreeParser {
|
|
|
782
998
|
}
|
|
783
999
|
}
|
|
784
1000
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
object
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
(
|
|
807
|
-
|
|
808
|
-
if (
|
|
809
|
-
|
|
1001
|
+
if (message.locusStateElements?.length > 0) {
|
|
1002
|
+
// by this point we now have this.dataSets setup for data sets from this message
|
|
1003
|
+
// and hash trees created for the new visible data sets,
|
|
1004
|
+
// so we can now process all the updates from the message
|
|
1005
|
+
dataSets.forEach((dataSet) => {
|
|
1006
|
+
if (this.dataSets[dataSet.name]) {
|
|
1007
|
+
const {hashTree} = this.dataSets[dataSet.name];
|
|
1008
|
+
|
|
1009
|
+
if (hashTree) {
|
|
1010
|
+
const locusStateElementsForThisSet = message.locusStateElements.filter((object) =>
|
|
1011
|
+
object.htMeta.dataSetNames.includes(dataSet.name)
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
const appliedChangesList = hashTree.updateItems(
|
|
1015
|
+
locusStateElementsForThisSet.map((object) =>
|
|
1016
|
+
object.data
|
|
1017
|
+
? {operation: 'update', item: object.htMeta.elementId}
|
|
1018
|
+
: {operation: 'remove', item: object.htMeta.elementId}
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
|
|
1022
|
+
zip(appliedChangesList, locusStateElementsForThisSet).forEach(
|
|
1023
|
+
([changeApplied, object]) => {
|
|
1024
|
+
if (changeApplied) {
|
|
1025
|
+
// add to updatedObjects so that our locus DTO will get updated with the new object
|
|
1026
|
+
updatedObjects.push(object);
|
|
810
1027
|
}
|
|
811
|
-
// add to updatedObjects so that our locus DTO will get updated with the new object
|
|
812
|
-
updatedObjects.push(object);
|
|
813
1028
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1029
|
+
);
|
|
1030
|
+
} else {
|
|
1031
|
+
LoggerProxy.logger.info(
|
|
1032
|
+
`Locus-info:index#parseMessage --> ${this.debugId} unexpected (not visible) dataSet ${dataSet.name} received in hash tree message`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
820
1035
|
}
|
|
821
|
-
}
|
|
822
1036
|
|
|
823
|
-
if (!isRosterDropped) {
|
|
824
1037
|
this.runSyncAlgorithm(dataSet);
|
|
825
|
-
}
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
if (isRosterDropped) {
|
|
829
|
-
LoggerProxy.logger.info(
|
|
830
|
-
`HashTreeParser#parseMessage --> ${this.debugId} detected roster drop`
|
|
831
|
-
);
|
|
832
|
-
this.stopAllTimers();
|
|
833
|
-
|
|
834
|
-
// in case of roster drop we don't care about other updates
|
|
835
|
-
return {updateType: LocusInfoUpdateType.MEETING_ENDED};
|
|
1038
|
+
});
|
|
836
1039
|
}
|
|
837
1040
|
|
|
838
1041
|
if (dataSetsRequiringInitialization.length > 0) {
|
|
839
1042
|
// there are some data sets that we need to initialize asynchronously
|
|
840
|
-
|
|
841
|
-
this.initializeNewVisibleDataSets(message, dataSetsRequiringInitialization);
|
|
842
|
-
});
|
|
1043
|
+
this.queueInitForNewVisibleDataSets(dataSetsRequiringInitialization);
|
|
843
1044
|
}
|
|
844
1045
|
|
|
845
1046
|
if (updatedObjects.length === 0) {
|
|
@@ -848,7 +1049,7 @@ class HashTreeParser {
|
|
|
848
1049
|
);
|
|
849
1050
|
}
|
|
850
1051
|
|
|
851
|
-
return
|
|
1052
|
+
return updatedObjects;
|
|
852
1053
|
}
|
|
853
1054
|
|
|
854
1055
|
/**
|
|
@@ -858,13 +1059,28 @@ class HashTreeParser {
|
|
|
858
1059
|
* @param {string} [debugText] - Optional debug text to include in logs
|
|
859
1060
|
* @returns {void}
|
|
860
1061
|
*/
|
|
861
|
-
|
|
862
|
-
if (message.
|
|
1062
|
+
handleMessage(message: HashTreeMessage, debugText?: string) {
|
|
1063
|
+
if (message.heartbeatIntervalMs) {
|
|
1064
|
+
this.heartbeatIntervalMs = message.heartbeatIntervalMs;
|
|
1065
|
+
}
|
|
1066
|
+
if (this.isEndMessage(message)) {
|
|
1067
|
+
LoggerProxy.logger.info(
|
|
1068
|
+
`HashTreeParser#parseMessage --> ${this.debugId} received sentinel END MEETING message`
|
|
1069
|
+
);
|
|
1070
|
+
this.stopAllTimers();
|
|
1071
|
+
|
|
1072
|
+
this.callLocusInfoUpdateCallback({updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1073
|
+
} else if (message.locusStateElements === undefined) {
|
|
863
1074
|
this.handleRootHashHeartBeatMessage(message);
|
|
1075
|
+
this.resetHeartbeatWatchdogs(message.dataSets);
|
|
864
1076
|
} else {
|
|
865
|
-
const
|
|
1077
|
+
const updatedObjects = this.parseMessage(message, debugText);
|
|
866
1078
|
|
|
867
|
-
this.
|
|
1079
|
+
this.resetHeartbeatWatchdogs(message.dataSets);
|
|
1080
|
+
this.callLocusInfoUpdateCallback({
|
|
1081
|
+
updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1082
|
+
updatedObjects,
|
|
1083
|
+
});
|
|
868
1084
|
}
|
|
869
1085
|
}
|
|
870
1086
|
|
|
@@ -880,7 +1096,49 @@ class HashTreeParser {
|
|
|
880
1096
|
}) {
|
|
881
1097
|
const {updateType, updatedObjects} = updates;
|
|
882
1098
|
|
|
883
|
-
if (updateType
|
|
1099
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1100
|
+
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1101
|
+
// or removals for objects that still exist in any of their datasets
|
|
1102
|
+
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1103
|
+
const {elementId} = object.htMeta;
|
|
1104
|
+
const {type, id, version} = elementId;
|
|
1105
|
+
|
|
1106
|
+
// Check all datasets
|
|
1107
|
+
for (const dataSetName of Object.keys(this.dataSets)) {
|
|
1108
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1109
|
+
|
|
1110
|
+
// only visible datasets have hash trees set
|
|
1111
|
+
if (dataSet?.hashTree) {
|
|
1112
|
+
const existingVersion = dataSet.hashTree.getItemVersion(id, type);
|
|
1113
|
+
if (existingVersion !== undefined) {
|
|
1114
|
+
if (object.data) {
|
|
1115
|
+
// For updates: filter out if any dataset has a higher version
|
|
1116
|
+
if (existingVersion > version) {
|
|
1117
|
+
LoggerProxy.logger.info(
|
|
1118
|
+
`HashTreeParser#callLocusInfoUpdateCallback --> ${this.debugId} Filtering out update for ${type}:${id} v${version} because dataset "${dataSetName}" has v${existingVersion}`
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
} else if (existingVersion >= version) {
|
|
1124
|
+
// For removals: filter out if the object still exists in any dataset
|
|
1125
|
+
LoggerProxy.logger.info(
|
|
1126
|
+
`HashTreeParser#callLocusInfoUpdateCallback --> ${this.debugId} Filtering out removal for ${type}:${id} v${version} because dataset "${dataSetName}" still has v${existingVersion}`
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return true;
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
if (filteredUpdates.length > 0) {
|
|
1139
|
+
this.locusInfoUpdateCallback(updateType, {updatedObjects: filteredUpdates});
|
|
1140
|
+
}
|
|
1141
|
+
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
884
1142
|
this.locusInfoUpdateCallback(updateType, {updatedObjects});
|
|
885
1143
|
}
|
|
886
1144
|
}
|
|
@@ -899,6 +1157,88 @@ class HashTreeParser {
|
|
|
899
1157
|
return Math.round(randomValue ** exponent * maxMs);
|
|
900
1158
|
}
|
|
901
1159
|
|
|
1160
|
+
/**
|
|
1161
|
+
* Performs a sync for the given data set.
|
|
1162
|
+
*
|
|
1163
|
+
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1164
|
+
* @param {string} rootHash - Our current root hash for this data set
|
|
1165
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1166
|
+
* @returns {Promise<void>}
|
|
1167
|
+
*/
|
|
1168
|
+
private async performSync(
|
|
1169
|
+
dataSet: InternalDataSet,
|
|
1170
|
+
rootHash: string,
|
|
1171
|
+
reason: string
|
|
1172
|
+
): Promise<void> {
|
|
1173
|
+
if (!dataSet.hashTree) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
try {
|
|
1178
|
+
LoggerProxy.logger.info(
|
|
1179
|
+
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
|
|
1183
|
+
|
|
1184
|
+
if (dataSet.leafCount !== 1) {
|
|
1185
|
+
let receivedHashes;
|
|
1186
|
+
|
|
1187
|
+
try {
|
|
1188
|
+
// request hashes from sender
|
|
1189
|
+
const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
|
|
1190
|
+
dataSet.name,
|
|
1191
|
+
rootHash
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
receivedHashes = hashes;
|
|
1195
|
+
|
|
1196
|
+
dataSet.hashTree.resize(latestDataSetInfo.leafCount);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
if (error.statusCode === 409) {
|
|
1199
|
+
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1200
|
+
LoggerProxy.logger.info(
|
|
1201
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// identify mismatched leaves
|
|
1210
|
+
const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
|
|
1211
|
+
|
|
1212
|
+
mismatchedLeaveIndexes.forEach((index) => {
|
|
1213
|
+
mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
|
|
1214
|
+
});
|
|
1215
|
+
} else {
|
|
1216
|
+
mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
|
|
1217
|
+
}
|
|
1218
|
+
// request sync for mismatched leaves
|
|
1219
|
+
if (Object.keys(mismatchedLeavesData).length > 0) {
|
|
1220
|
+
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1221
|
+
|
|
1222
|
+
// sync API may return nothing (in that case data will arrive via messages)
|
|
1223
|
+
// or it may return a response in the same format as messages
|
|
1224
|
+
if (syncResponse) {
|
|
1225
|
+
this.handleMessage(syncResponse, 'via sync API');
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
if (error instanceof MeetingEndedError) {
|
|
1230
|
+
this.callLocusInfoUpdateCallback({
|
|
1231
|
+
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
1232
|
+
});
|
|
1233
|
+
} else {
|
|
1234
|
+
LoggerProxy.logger.warn(
|
|
1235
|
+
`HashTreeParser#performSync --> ${this.debugId} error during sync for data set "${dataSet.name}":`,
|
|
1236
|
+
error
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
902
1242
|
/**
|
|
903
1243
|
* Runs the sync algorithm for the given data set.
|
|
904
1244
|
*
|
|
@@ -957,55 +1297,11 @@ class HashTreeParser {
|
|
|
957
1297
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
958
1298
|
|
|
959
1299
|
if (dataSet.root !== rootHash) {
|
|
960
|
-
|
|
961
|
-
|
|
1300
|
+
await this.performSync(
|
|
1301
|
+
dataSet,
|
|
1302
|
+
rootHash,
|
|
1303
|
+
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
962
1304
|
);
|
|
963
|
-
|
|
964
|
-
const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
|
|
965
|
-
|
|
966
|
-
if (dataSet.leafCount !== 1) {
|
|
967
|
-
let receivedHashes;
|
|
968
|
-
|
|
969
|
-
try {
|
|
970
|
-
// request hashes from sender
|
|
971
|
-
const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
|
|
972
|
-
dataSet.name
|
|
973
|
-
);
|
|
974
|
-
|
|
975
|
-
receivedHashes = hashes;
|
|
976
|
-
|
|
977
|
-
dataSet.hashTree.resize(latestDataSetInfo.leafCount);
|
|
978
|
-
} catch (error) {
|
|
979
|
-
if (error.statusCode === 409) {
|
|
980
|
-
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
981
|
-
LoggerProxy.logger.info(
|
|
982
|
-
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
983
|
-
);
|
|
984
|
-
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
throw error;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
// identify mismatched leaves
|
|
991
|
-
const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
|
|
992
|
-
|
|
993
|
-
mismatchedLeaveIndexes.forEach((index) => {
|
|
994
|
-
mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
|
|
995
|
-
});
|
|
996
|
-
} else {
|
|
997
|
-
mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
|
|
998
|
-
}
|
|
999
|
-
// request sync for mismatched leaves
|
|
1000
|
-
if (Object.keys(mismatchedLeavesData).length > 0) {
|
|
1001
|
-
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1002
|
-
|
|
1003
|
-
// sync API may return nothing (in that case data will arrive via messages)
|
|
1004
|
-
// or it may return a response in the same format as messages
|
|
1005
|
-
if (syncResponse) {
|
|
1006
|
-
this.handleMessage(syncResponse, 'via sync API');
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
1305
|
} else {
|
|
1010
1306
|
LoggerProxy.logger.info(
|
|
1011
1307
|
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} "${dataSet.name}" root hash matching: ${rootHash}, version=${dataSet.version}`
|
|
@@ -1019,6 +1315,52 @@ class HashTreeParser {
|
|
|
1019
1315
|
}
|
|
1020
1316
|
}
|
|
1021
1317
|
|
|
1318
|
+
/**
|
|
1319
|
+
* Resets the heartbeat watchdog timers for the specified data sets. Each data set has its own
|
|
1320
|
+
* watchdog timer that monitors whether heartbeats are being received within the expected interval.
|
|
1321
|
+
* If a heartbeat is not received for a specific data set within heartbeatIntervalMs plus
|
|
1322
|
+
* a backoff-calculated time, the sync algorithm is initiated for that data set
|
|
1323
|
+
*
|
|
1324
|
+
* @param {Array<DataSet>} receivedDataSets - The data sets from the received message for which watchdog timers should be reset
|
|
1325
|
+
* @returns {void}
|
|
1326
|
+
*/
|
|
1327
|
+
private resetHeartbeatWatchdogs(receivedDataSets: Array<DataSet>): void {
|
|
1328
|
+
if (!this.heartbeatIntervalMs) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
for (const receivedDataSet of receivedDataSets) {
|
|
1333
|
+
const dataSet = this.dataSets[receivedDataSet.name];
|
|
1334
|
+
|
|
1335
|
+
if (!dataSet?.hashTree) {
|
|
1336
|
+
// eslint-disable-next-line no-continue
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (dataSet.heartbeatWatchdogTimer) {
|
|
1341
|
+
clearTimeout(dataSet.heartbeatWatchdogTimer);
|
|
1342
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1346
|
+
const delay = this.heartbeatIntervalMs + backoffTime;
|
|
1347
|
+
|
|
1348
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(async () => {
|
|
1349
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1350
|
+
|
|
1351
|
+
LoggerProxy.logger.warn(
|
|
1352
|
+
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
await this.performSync(
|
|
1356
|
+
dataSet,
|
|
1357
|
+
dataSet.hashTree.getRootHash(),
|
|
1358
|
+
`heartbeat watchdog expired`
|
|
1359
|
+
);
|
|
1360
|
+
}, delay);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1022
1364
|
/**
|
|
1023
1365
|
* Stops all timers for the data sets to prevent any further sync attempts.
|
|
1024
1366
|
* @returns {void}
|
|
@@ -1029,15 +1371,39 @@ class HashTreeParser {
|
|
|
1029
1371
|
clearTimeout(dataSet.timer);
|
|
1030
1372
|
dataSet.timer = undefined;
|
|
1031
1373
|
}
|
|
1374
|
+
if (dataSet.heartbeatWatchdogTimer) {
|
|
1375
|
+
clearTimeout(dataSet.heartbeatWatchdogTimer);
|
|
1376
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1377
|
+
}
|
|
1032
1378
|
});
|
|
1033
1379
|
}
|
|
1034
1380
|
|
|
1381
|
+
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1382
|
+
const isValidDataSetForSentinel =
|
|
1383
|
+
dataSetName === undefined ||
|
|
1384
|
+
PossibleSentinelMessageDataSetNames.includes(dataSetName.toLowerCase());
|
|
1385
|
+
|
|
1386
|
+
if (
|
|
1387
|
+
((error.statusCode === 409 && error.body?.errorCode === 2403004) ||
|
|
1388
|
+
error.statusCode === 404) &&
|
|
1389
|
+
isValidDataSetForSentinel
|
|
1390
|
+
) {
|
|
1391
|
+
LoggerProxy.logger.info(
|
|
1392
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1393
|
+
);
|
|
1394
|
+
this.stopAllTimers();
|
|
1395
|
+
|
|
1396
|
+
throw new MeetingEndedError();
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1035
1400
|
/**
|
|
1036
1401
|
* Gets the current hashes from the locus for a specific data set.
|
|
1037
1402
|
* @param {string} dataSetName
|
|
1403
|
+
* @param {string} currentRootHash
|
|
1038
1404
|
* @returns {string[]}
|
|
1039
1405
|
*/
|
|
1040
|
-
private getHashesFromLocus(dataSetName: string) {
|
|
1406
|
+
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1041
1407
|
LoggerProxy.logger.info(
|
|
1042
1408
|
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Requesting hashes for data set "${dataSetName}"`
|
|
1043
1409
|
);
|
|
@@ -1049,6 +1415,9 @@ class HashTreeParser {
|
|
|
1049
1415
|
return this.webexRequest({
|
|
1050
1416
|
method: HTTP_VERBS.GET,
|
|
1051
1417
|
uri: url,
|
|
1418
|
+
qs: {
|
|
1419
|
+
rootHash: currentRootHash,
|
|
1420
|
+
},
|
|
1052
1421
|
})
|
|
1053
1422
|
.then((response) => {
|
|
1054
1423
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
@@ -1078,6 +1447,8 @@ class HashTreeParser {
|
|
|
1078
1447
|
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Error ${error.statusCode} fetching hashes for data set "${dataSetName}":`,
|
|
1079
1448
|
error
|
|
1080
1449
|
);
|
|
1450
|
+
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1451
|
+
|
|
1081
1452
|
throw error;
|
|
1082
1453
|
});
|
|
1083
1454
|
}
|
|
@@ -1110,9 +1481,14 @@ class HashTreeParser {
|
|
|
1110
1481
|
});
|
|
1111
1482
|
});
|
|
1112
1483
|
|
|
1484
|
+
const ourCurrentRootHash = dataSet.hashTree ? dataSet.hashTree.getRootHash() : EMPTY_HASH;
|
|
1485
|
+
|
|
1113
1486
|
return this.webexRequest({
|
|
1114
1487
|
method: HTTP_VERBS.POST,
|
|
1115
1488
|
uri: url,
|
|
1489
|
+
qs: {
|
|
1490
|
+
rootHash: ourCurrentRootHash,
|
|
1491
|
+
},
|
|
1116
1492
|
body,
|
|
1117
1493
|
})
|
|
1118
1494
|
.then((resp) => {
|
|
@@ -1135,6 +1511,8 @@ class HashTreeParser {
|
|
|
1135
1511
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Error ${error.statusCode} sending sync request for data set "${dataSet.name}":`,
|
|
1136
1512
|
error
|
|
1137
1513
|
);
|
|
1514
|
+
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1515
|
+
|
|
1138
1516
|
throw error;
|
|
1139
1517
|
});
|
|
1140
1518
|
}
|