@webex/plugin-meetings 3.11.0-next.3 → 3.11.0-next.31
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 +181 -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 +21 -4
- package/dist/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 +603 -266
- 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/index.js +88 -44
- 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 +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +149 -42
- 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 +121 -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 +16 -0
- package/dist/types/hashTree/hashTree.d.ts +7 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +83 -12
- 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 +2 -0
- package/dist/types/meeting/index.d.ts +28 -5
- 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 +29 -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 +164 -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 +20 -0
- package/src/hashTree/hashTree.ts +17 -0
- package/src/hashTree/hashTreeParser.ts +525 -188
- 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/index.ts +123 -35
- 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 +4 -0
- package/src/meeting/index.ts +116 -22
- package/src/meeting/request.ts +42 -0
- package/src/meeting/request.type.ts +6 -0
- package/src/meeting/util.ts +148 -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 +953 -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 +1594 -162
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
- package/test/unit/spec/locus-info/index.js +173 -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 +4 -2
- package/test/unit/spec/meeting/index.js +497 -81
- package/test/unit/spec/meeting/request.js +64 -0
- package/test/unit/spec/meeting/utils.js +369 -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,12 +62,24 @@ 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
76
|
class MeetingEndedError extends Error {}
|
|
57
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
|
+
|
|
58
83
|
/**
|
|
59
84
|
* Parses hash tree eventing locus data
|
|
60
85
|
*/
|
|
@@ -63,8 +88,10 @@ class HashTreeParser {
|
|
|
63
88
|
visibleDataSetsUrl: string; // url from which we can get info about all data sets
|
|
64
89
|
webexRequest: WebexRequestMethod;
|
|
65
90
|
locusInfoUpdateCallback: LocusInfoUpdateCallback;
|
|
66
|
-
visibleDataSets:
|
|
91
|
+
visibleDataSets: VisibleDataSetInfo[];
|
|
67
92
|
debugId: string;
|
|
93
|
+
heartbeatIntervalMs?: number;
|
|
94
|
+
private excludedDataSets: string[];
|
|
68
95
|
|
|
69
96
|
/**
|
|
70
97
|
* Constructor for HashTreeParser
|
|
@@ -76,25 +103,36 @@ class HashTreeParser {
|
|
|
76
103
|
dataSets: Array<DataSet>;
|
|
77
104
|
locus: any;
|
|
78
105
|
};
|
|
106
|
+
metadata: Metadata | null;
|
|
79
107
|
webexRequest: WebexRequestMethod;
|
|
80
108
|
locusInfoUpdateCallback: LocusInfoUpdateCallback;
|
|
81
109
|
debugId: string;
|
|
110
|
+
excludedDataSets?: string[];
|
|
82
111
|
}) {
|
|
83
112
|
const {dataSets, locus} = options.initialLocus; // extract dataSets from initialLocus
|
|
84
113
|
|
|
85
114
|
this.debugId = options.debugId;
|
|
86
115
|
this.webexRequest = options.webexRequest;
|
|
87
116
|
this.locusInfoUpdateCallback = options.locusInfoUpdateCallback;
|
|
88
|
-
this.
|
|
117
|
+
this.excludedDataSets = options.excludedDataSets || [];
|
|
118
|
+
this.visibleDataSetsUrl = locus?.links?.resources?.visibleDataSets?.url;
|
|
119
|
+
this.visibleDataSets = (
|
|
120
|
+
cloneDeep(options.metadata?.visibleDataSets || []) as VisibleDataSetInfo[]
|
|
121
|
+
).filter((vds) => !this.isExcludedDataSet(vds.name));
|
|
89
122
|
|
|
90
|
-
if (
|
|
123
|
+
if (options.metadata?.visibleDataSets?.length === 0) {
|
|
91
124
|
LoggerProxy.logger.warn(
|
|
92
|
-
`HashTreeParser#constructor --> ${this.debugId} No visibleDataSets found in
|
|
125
|
+
`HashTreeParser#constructor --> ${this.debugId} No visibleDataSets found in Metadata`
|
|
93
126
|
);
|
|
94
127
|
}
|
|
95
128
|
// object mapping dataset names to arrays of leaf data
|
|
96
129
|
const leafData = this.analyzeLocusHtMeta(locus);
|
|
97
130
|
|
|
131
|
+
if (options.metadata) {
|
|
132
|
+
// add also the metadata that's outside of locus object itself
|
|
133
|
+
this.analyzeMetadata(leafData, options.metadata);
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
LoggerProxy.logger.info(
|
|
99
137
|
`HashTreeParser#constructor --> creating HashTreeParser for datasets: ${JSON.stringify(
|
|
100
138
|
dataSets.map((ds) => ds.name)
|
|
@@ -106,46 +144,87 @@ class HashTreeParser {
|
|
|
106
144
|
|
|
107
145
|
this.dataSets[name] = {
|
|
108
146
|
...dataSet,
|
|
109
|
-
hashTree: this.
|
|
147
|
+
hashTree: this.isVisibleDataSet(name)
|
|
110
148
|
? new HashTree(leafData[name] || [], leafCount)
|
|
111
149
|
: undefined,
|
|
112
150
|
};
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Checks if the given data set name is in the list of visible data sets
|
|
156
|
+
* @param {string} dataSetName data set name to check
|
|
157
|
+
* @returns {Boolean} True if the data set is visible, false otherwise
|
|
158
|
+
*/
|
|
159
|
+
private isVisibleDataSet(dataSetName: string): boolean {
|
|
160
|
+
return this.visibleDataSets.some((vds) => vds.name === dataSetName);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Checks if the given data set name is in the excluded list
|
|
165
|
+
* @param {string} dataSetName data set name to check
|
|
166
|
+
* @returns {boolean} True if the data set is excluded, false otherwise
|
|
167
|
+
*/
|
|
168
|
+
private isExcludedDataSet(dataSetName: string): boolean {
|
|
169
|
+
return this.excludedDataSets.some((name) => name === dataSetName);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Adds a data set to the visible data sets list, unless it is in the excluded list.
|
|
174
|
+
* @param {VisibleDataSetInfo} dataSetInfo data set info to add
|
|
175
|
+
* @returns {boolean} True if the data set was added, false if it was excluded
|
|
176
|
+
*/
|
|
177
|
+
private addToVisibleDataSetsList(dataSetInfo: VisibleDataSetInfo): boolean {
|
|
178
|
+
if (this.isExcludedDataSet(dataSetInfo.name)) {
|
|
179
|
+
LoggerProxy.logger.info(
|
|
180
|
+
`HashTreeParser#addToVisibleDataSetsList --> ${this.debugId} Data set "${dataSetInfo.name}" is in the excluded list, ignoring`
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.visibleDataSets.push(dataSetInfo);
|
|
187
|
+
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
116
191
|
/**
|
|
117
192
|
* Initializes a new visible data set by creating a hash tree for it, adding it to all the internal structures,
|
|
118
193
|
* and sending an initial sync request to Locus with empty leaf data - that will trigger Locus to gives us all the data
|
|
119
194
|
* from that dataset (in the response or via messages).
|
|
120
195
|
*
|
|
121
|
-
* @param {
|
|
196
|
+
* @param {VisibleDataSetInfo} visibleDataSetInfo Information about the new visible data set
|
|
197
|
+
* @param {DataSet} dataSetInfo The new data set to be added
|
|
122
198
|
* @returns {Promise}
|
|
123
199
|
*/
|
|
124
200
|
private initializeNewVisibleDataSet(
|
|
125
|
-
|
|
201
|
+
visibleDataSetInfo: VisibleDataSetInfo,
|
|
202
|
+
dataSetInfo: DataSet
|
|
126
203
|
): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
|
|
127
|
-
if (this.
|
|
204
|
+
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
128
205
|
LoggerProxy.logger.info(
|
|
129
|
-
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${
|
|
206
|
+
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
130
207
|
);
|
|
131
208
|
|
|
132
209
|
return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
|
|
133
210
|
}
|
|
134
211
|
|
|
135
212
|
LoggerProxy.logger.info(
|
|
136
|
-
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Adding visible data set "${
|
|
213
|
+
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Adding visible data set "${dataSetInfo.name}"`
|
|
137
214
|
);
|
|
138
215
|
|
|
139
|
-
this.
|
|
216
|
+
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
217
|
+
return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
|
|
218
|
+
}
|
|
140
219
|
|
|
141
|
-
const hashTree = new HashTree([],
|
|
220
|
+
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
142
221
|
|
|
143
|
-
this.dataSets[
|
|
144
|
-
...
|
|
222
|
+
this.dataSets[dataSetInfo.name] = {
|
|
223
|
+
...dataSetInfo,
|
|
145
224
|
hashTree,
|
|
146
225
|
};
|
|
147
226
|
|
|
148
|
-
return this.sendInitializationSyncRequestToLocus(
|
|
227
|
+
return this.sendInitializationSyncRequestToLocus(dataSetInfo.name, 'new visible data set');
|
|
149
228
|
}
|
|
150
229
|
|
|
151
230
|
/**
|
|
@@ -190,15 +269,22 @@ class HashTreeParser {
|
|
|
190
269
|
}
|
|
191
270
|
|
|
192
271
|
/**
|
|
193
|
-
* Queries Locus for information about all
|
|
272
|
+
* Queries Locus for all up-to-date information about all visible data sets
|
|
194
273
|
*
|
|
195
|
-
* @param {string} url - url from which we can get info about all data sets
|
|
196
274
|
* @returns {Promise}
|
|
197
275
|
*/
|
|
198
|
-
private
|
|
276
|
+
private getAllVisibleDataSetsFromLocus() {
|
|
277
|
+
if (!this.visibleDataSetsUrl) {
|
|
278
|
+
LoggerProxy.logger.warn(
|
|
279
|
+
`HashTreeParser#getAllVisibleDataSetsFromLocus --> ${this.debugId} No visibleDataSetsUrl, cannot get data sets information`
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return Promise.resolve([]);
|
|
283
|
+
}
|
|
284
|
+
|
|
199
285
|
return this.webexRequest({
|
|
200
286
|
method: HTTP_VERBS.GET,
|
|
201
|
-
uri:
|
|
287
|
+
uri: this.visibleDataSetsUrl,
|
|
202
288
|
}).then((response) => {
|
|
203
289
|
return response.body.dataSets as Array<DataSet>;
|
|
204
290
|
});
|
|
@@ -211,12 +297,14 @@ class HashTreeParser {
|
|
|
211
297
|
* @returns {Promise}
|
|
212
298
|
*/
|
|
213
299
|
async initializeFromMessage(message: HashTreeMessage) {
|
|
300
|
+
this.visibleDataSetsUrl = message.visibleDataSetsUrl;
|
|
301
|
+
|
|
214
302
|
LoggerProxy.logger.info(
|
|
215
|
-
`HashTreeParser#initializeFromMessage --> ${this.debugId} visibleDataSetsUrl=${
|
|
303
|
+
`HashTreeParser#initializeFromMessage --> ${this.debugId} visibleDataSetsUrl=${this.visibleDataSetsUrl}`
|
|
216
304
|
);
|
|
217
|
-
const
|
|
305
|
+
const visibleDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
218
306
|
|
|
219
|
-
await this.initializeDataSets(
|
|
307
|
+
await this.initializeDataSets(visibleDataSets, 'initialization from message');
|
|
220
308
|
}
|
|
221
309
|
|
|
222
310
|
/**
|
|
@@ -236,28 +324,29 @@ class HashTreeParser {
|
|
|
236
324
|
|
|
237
325
|
return;
|
|
238
326
|
}
|
|
327
|
+
this.visibleDataSetsUrl = locus.links.resources.visibleDataSets.url;
|
|
239
328
|
|
|
240
329
|
LoggerProxy.logger.info(
|
|
241
|
-
`HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} visibleDataSets url: ${
|
|
330
|
+
`HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} visibleDataSets url: ${this.visibleDataSetsUrl}`
|
|
242
331
|
);
|
|
243
332
|
|
|
244
|
-
const
|
|
333
|
+
const visibleDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
245
334
|
|
|
246
|
-
await this.initializeDataSets(
|
|
335
|
+
await this.initializeDataSets(visibleDataSets, 'initialization from GET /loci response');
|
|
247
336
|
}
|
|
248
337
|
|
|
249
338
|
/**
|
|
250
339
|
* Initializes data sets by doing an initialization sync on each visible data set that doesn't have a hash tree yet.
|
|
251
340
|
*
|
|
252
|
-
* @param {DataSet[]}
|
|
341
|
+
* @param {DataSet[]} visibleDataSets Array of visible DataSet objects to initialize
|
|
253
342
|
* @param {string} debugText Text to include in logs for debugging purposes
|
|
254
343
|
* @returns {Promise}
|
|
255
344
|
*/
|
|
256
|
-
private async initializeDataSets(
|
|
345
|
+
private async initializeDataSets(visibleDataSets: Array<DataSet>, debugText: string) {
|
|
257
346
|
const updatedObjects: HashTreeObject[] = [];
|
|
258
347
|
|
|
259
|
-
for (const dataSet of
|
|
260
|
-
const {name, leafCount} = dataSet;
|
|
348
|
+
for (const dataSet of visibleDataSets) {
|
|
349
|
+
const {name, leafCount, url} = dataSet;
|
|
261
350
|
|
|
262
351
|
if (!this.dataSets[name]) {
|
|
263
352
|
LoggerProxy.logger.info(
|
|
@@ -273,7 +362,20 @@ class HashTreeParser {
|
|
|
273
362
|
);
|
|
274
363
|
}
|
|
275
364
|
|
|
276
|
-
if (this.
|
|
365
|
+
if (!this.isVisibleDataSet(name)) {
|
|
366
|
+
if (
|
|
367
|
+
!this.addToVisibleDataSetsList({
|
|
368
|
+
name,
|
|
369
|
+
url,
|
|
370
|
+
})
|
|
371
|
+
) {
|
|
372
|
+
// dataset is excluded, skip it
|
|
373
|
+
// eslint-disable-next-line no-continue
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!this.dataSets[name].hashTree) {
|
|
277
379
|
LoggerProxy.logger.info(
|
|
278
380
|
`HashTreeParser#initializeDataSets --> ${this.debugId} creating hash tree for visible dataset "${name}" (${debugText})`
|
|
279
381
|
);
|
|
@@ -316,10 +418,7 @@ class HashTreeParser {
|
|
|
316
418
|
private analyzeLocusHtMeta(locus: any, options?: {copyData?: boolean}) {
|
|
317
419
|
const {copyData = false} = options || {};
|
|
318
420
|
// 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
|
-
> = {};
|
|
421
|
+
const leafInfo: Record<string, Array<LeafInfo>> = {};
|
|
323
422
|
|
|
324
423
|
const findAndStoreMetaData = (currentLocusPart: any) => {
|
|
325
424
|
if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
|
|
@@ -329,7 +428,7 @@ class HashTreeParser {
|
|
|
329
428
|
if (currentLocusPart.htMeta && currentLocusPart.htMeta.dataSetNames) {
|
|
330
429
|
const {type, id, version} = currentLocusPart.htMeta.elementId;
|
|
331
430
|
const {dataSetNames} = currentLocusPart.htMeta;
|
|
332
|
-
const newLeafInfo:
|
|
431
|
+
const newLeafInfo: LeafInfo = {
|
|
333
432
|
type,
|
|
334
433
|
id,
|
|
335
434
|
version,
|
|
@@ -368,6 +467,43 @@ class HashTreeParser {
|
|
|
368
467
|
return leafInfo;
|
|
369
468
|
}
|
|
370
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Analyzes the Metadata object that is sent outside of Locus object, and appends its data to passed in leafInfo
|
|
472
|
+
* structure.
|
|
473
|
+
*
|
|
474
|
+
* @param {Record<string, LeafInfo[]>} leafInfo the structure to which the Metadata info will be appended
|
|
475
|
+
* @param {Metadata} metadata Metadata object
|
|
476
|
+
* @returns {void}
|
|
477
|
+
*/
|
|
478
|
+
private analyzeMetadata(leafInfo: Record<string, LeafInfo[]>, metadata: Metadata) {
|
|
479
|
+
const {htMeta} = metadata;
|
|
480
|
+
|
|
481
|
+
if (
|
|
482
|
+
htMeta?.dataSetNames?.length === 1 &&
|
|
483
|
+
htMeta.dataSetNames[0].toLowerCase() === MetadataDataSetName
|
|
484
|
+
) {
|
|
485
|
+
const {type, id, version} = metadata.htMeta.elementId;
|
|
486
|
+
|
|
487
|
+
const dataSetName = htMeta.dataSetNames[0];
|
|
488
|
+
|
|
489
|
+
if (!leafInfo[dataSetName]) {
|
|
490
|
+
leafInfo[dataSetName] = [];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
leafInfo[dataSetName].push({
|
|
494
|
+
type,
|
|
495
|
+
id,
|
|
496
|
+
version,
|
|
497
|
+
});
|
|
498
|
+
} else {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`${this.debugId} Metadata htMeta has unexpected dataSetNames: ${
|
|
501
|
+
htMeta && htMeta.dataSetNames.join(',')
|
|
502
|
+
}`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
371
507
|
/**
|
|
372
508
|
* Checks if the provided hash tree message indicates the end of the meeting and that there won't be any more updates.
|
|
373
509
|
*
|
|
@@ -420,6 +556,58 @@ class HashTreeParser {
|
|
|
420
556
|
});
|
|
421
557
|
}
|
|
422
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Handles updates to Metadata object that we receive from Locus via other means than messages. Right now
|
|
561
|
+
* that means only in the API response alongside locus object.
|
|
562
|
+
*
|
|
563
|
+
* @param {Metadata} metadata received in Locus update other than a message (for example in an API response)
|
|
564
|
+
* @param {HashTreeObject[]} updatedObjects a list of updated hash tree objects to which any updates resulting from new Metadata will be added
|
|
565
|
+
* @returns {void}
|
|
566
|
+
*/
|
|
567
|
+
handleMetadataUpdate(metadata: Metadata, updatedObjects: HashTreeObject[]): void {
|
|
568
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
569
|
+
|
|
570
|
+
// current assumption based on Locus docs is that Metadata object lives always in "self" data set
|
|
571
|
+
const hashTree = this.dataSets[MetadataDataSetName]?.hashTree;
|
|
572
|
+
|
|
573
|
+
if (!hashTree) {
|
|
574
|
+
LoggerProxy.logger.warn(
|
|
575
|
+
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received Metadata object but no hash tree for "${MetadataDataSetName}" data set exists`
|
|
576
|
+
);
|
|
577
|
+
} else {
|
|
578
|
+
const metadataUpdated = hashTree.putItem(metadata.htMeta.elementId);
|
|
579
|
+
|
|
580
|
+
if (metadataUpdated) {
|
|
581
|
+
// metadata in Locus API response is in a slightly different format than the objects in messages, so need to adapt it
|
|
582
|
+
const metadataObject: HashTreeObject = {
|
|
583
|
+
htMeta: metadata.htMeta,
|
|
584
|
+
data: metadata,
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
updatedObjects.push(metadataObject);
|
|
588
|
+
|
|
589
|
+
const {changeDetected, removedDataSets, addedDataSets} = this.checkForVisibleDataSetChanges(
|
|
590
|
+
[metadataObject]
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (changeDetected) {
|
|
594
|
+
dataSetsRequiringInitialization = this.processVisibleDataSetChanges(
|
|
595
|
+
removedDataSets,
|
|
596
|
+
addedDataSets,
|
|
597
|
+
updatedObjects
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (dataSetsRequiringInitialization.length > 0) {
|
|
602
|
+
// there are some data sets that we need to initialize asynchronously
|
|
603
|
+
queueMicrotask(() => {
|
|
604
|
+
this.initializeNewVisibleDataSets(dataSetsRequiringInitialization);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
423
611
|
/**
|
|
424
612
|
* This method should be called when we receive a partial locus DTO that contains dataSets and htMeta information
|
|
425
613
|
* It updates the hash trees with the new leaf data based on the received Locus
|
|
@@ -427,22 +615,28 @@ class HashTreeParser {
|
|
|
427
615
|
* @param {Object} update - The locus update containing data sets and locus information
|
|
428
616
|
* @returns {void}
|
|
429
617
|
*/
|
|
430
|
-
handleLocusUpdate(update: {dataSets?: Array<DataSet>; locus: any}): void {
|
|
431
|
-
const {dataSets, locus} = update;
|
|
618
|
+
handleLocusUpdate(update: {dataSets?: Array<DataSet>; locus: any; metadata?: Metadata}): void {
|
|
619
|
+
const {dataSets, locus, metadata} = update;
|
|
432
620
|
|
|
433
621
|
if (!dataSets) {
|
|
434
|
-
LoggerProxy.logger.
|
|
622
|
+
LoggerProxy.logger.info(
|
|
435
623
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
436
624
|
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
625
|
+
} else {
|
|
626
|
+
for (const dataSet of dataSets) {
|
|
627
|
+
this.updateDataSetInfo(dataSet);
|
|
628
|
+
}
|
|
440
629
|
}
|
|
441
630
|
const updatedObjects: HashTreeObject[] = [];
|
|
442
631
|
|
|
443
632
|
// first, analyze the locus object to extract the hash tree objects' htMeta and data from it
|
|
444
633
|
const leafInfo = this.analyzeLocusHtMeta(locus, {copyData: true});
|
|
445
634
|
|
|
635
|
+
// if we got metadata, process it (currently that means only potential visible data set list changes)
|
|
636
|
+
if (metadata) {
|
|
637
|
+
this.handleMetadataUpdate(metadata, updatedObjects);
|
|
638
|
+
}
|
|
639
|
+
|
|
446
640
|
// then process the data in hash trees, if it is a new version, then add it to updatedObjects
|
|
447
641
|
Object.keys(leafInfo).forEach((dataSetName) => {
|
|
448
642
|
if (this.dataSets[dataSetName]) {
|
|
@@ -493,9 +687,6 @@ class HashTreeParser {
|
|
|
493
687
|
updatedObjects,
|
|
494
688
|
});
|
|
495
689
|
}
|
|
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
690
|
}
|
|
500
691
|
|
|
501
692
|
/**
|
|
@@ -511,7 +702,7 @@ class HashTreeParser {
|
|
|
511
702
|
};
|
|
512
703
|
|
|
513
704
|
LoggerProxy.logger.info(
|
|
514
|
-
`HashTreeParser#
|
|
705
|
+
`HashTreeParser#updateDataSetInfo --> ${this.debugId} created entry for "${receivedDataSet.name}" dataset: version=${receivedDataSet.version}, root=${receivedDataSet.root}`
|
|
515
706
|
);
|
|
516
707
|
|
|
517
708
|
return;
|
|
@@ -526,7 +717,7 @@ class HashTreeParser {
|
|
|
526
717
|
exponent: receivedDataSet.backoff.exponent,
|
|
527
718
|
};
|
|
528
719
|
LoggerProxy.logger.info(
|
|
529
|
-
`HashTreeParser#
|
|
720
|
+
`HashTreeParser#updateDataSetInfo --> ${this.debugId} updated "${receivedDataSet.name}" dataset to version=${receivedDataSet.version}, root=${receivedDataSet.root}`
|
|
530
721
|
);
|
|
531
722
|
}
|
|
532
723
|
}
|
|
@@ -537,25 +728,30 @@ class HashTreeParser {
|
|
|
537
728
|
* @returns {Object} An object containing the removed and added visible data sets.
|
|
538
729
|
*/
|
|
539
730
|
private checkForVisibleDataSetChanges(updatedObjects: HashTreeObject[]) {
|
|
540
|
-
let removedDataSets:
|
|
541
|
-
let addedDataSets:
|
|
731
|
+
let removedDataSets: VisibleDataSetInfo[] = [];
|
|
732
|
+
let addedDataSets: VisibleDataSetInfo[] = [];
|
|
542
733
|
|
|
543
|
-
// visibleDataSets can only be changed by
|
|
734
|
+
// visibleDataSets can only be changed by Metadata object updates
|
|
544
735
|
updatedObjects.forEach((object) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
736
|
+
if (isMetadata(object) && object.data?.visibleDataSets) {
|
|
737
|
+
const newVisibleDataSets = object.data.visibleDataSets.filter(
|
|
738
|
+
(vds) => !this.isExcludedDataSet(vds.name)
|
|
739
|
+
);
|
|
548
740
|
|
|
549
|
-
removedDataSets = this.visibleDataSets.filter(
|
|
550
|
-
|
|
741
|
+
removedDataSets = this.visibleDataSets.filter(
|
|
742
|
+
(ds) => !newVisibleDataSets.some((nvs) => nvs.name === ds.name)
|
|
743
|
+
);
|
|
744
|
+
addedDataSets = newVisibleDataSets.filter((nvs) =>
|
|
745
|
+
this.visibleDataSets.every((ds) => ds.name !== nvs.name)
|
|
746
|
+
);
|
|
551
747
|
|
|
552
748
|
if (removedDataSets.length > 0 || addedDataSets.length > 0) {
|
|
553
749
|
LoggerProxy.logger.info(
|
|
554
750
|
`HashTreeParser#checkForVisibleDataSetChanges --> ${
|
|
555
751
|
this.debugId
|
|
556
|
-
} visible data sets change: removed: ${removedDataSets
|
|
557
|
-
|
|
558
|
-
|
|
752
|
+
} visible data sets change: removed: ${removedDataSets
|
|
753
|
+
.map((ds) => ds.name)
|
|
754
|
+
.join(', ')}, added: ${addedDataSets.map((ds) => ds.name).join(', ')}`
|
|
559
755
|
);
|
|
560
756
|
}
|
|
561
757
|
}
|
|
@@ -577,11 +773,15 @@ class HashTreeParser {
|
|
|
577
773
|
private deleteHashTree(dataSetName: string) {
|
|
578
774
|
this.dataSets[dataSetName].hashTree = undefined;
|
|
579
775
|
|
|
580
|
-
// we also need to stop the
|
|
776
|
+
// we also need to stop the timers as there is no hash tree anymore to sync
|
|
581
777
|
if (this.dataSets[dataSetName].timer) {
|
|
582
778
|
clearTimeout(this.dataSets[dataSetName].timer);
|
|
583
779
|
this.dataSets[dataSetName].timer = undefined;
|
|
584
780
|
}
|
|
781
|
+
if (this.dataSets[dataSetName].heartbeatWatchdogTimer) {
|
|
782
|
+
clearTimeout(this.dataSets[dataSetName].heartbeatWatchdogTimer);
|
|
783
|
+
this.dataSets[dataSetName].heartbeatWatchdogTimer = undefined;
|
|
784
|
+
}
|
|
585
785
|
}
|
|
586
786
|
|
|
587
787
|
/**
|
|
@@ -593,49 +793,51 @@ class HashTreeParser {
|
|
|
593
793
|
* visible data sets and they require async initialization, the names of these data sets
|
|
594
794
|
* are returned in an array.
|
|
595
795
|
*
|
|
596
|
-
* @param {
|
|
597
|
-
* @param {
|
|
796
|
+
* @param {VisibleDataSetInfo[]} removedDataSets - The list of removed data sets.
|
|
797
|
+
* @param {VisibleDataSetInfo[]} addedDataSets - The list of added data sets.
|
|
598
798
|
* @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects to which changes will be added.
|
|
599
|
-
* @returns {
|
|
799
|
+
* @returns {VisibleDataSetInfo[]} list of data sets that couldn't be initialized synchronously
|
|
600
800
|
*/
|
|
601
801
|
private processVisibleDataSetChanges(
|
|
602
|
-
removedDataSets:
|
|
603
|
-
addedDataSets:
|
|
802
|
+
removedDataSets: VisibleDataSetInfo[],
|
|
803
|
+
addedDataSets: VisibleDataSetInfo[],
|
|
604
804
|
updatedObjects: HashTreeObject[]
|
|
605
|
-
):
|
|
606
|
-
const dataSetsRequiringInitialization = [];
|
|
805
|
+
): VisibleDataSetInfo[] {
|
|
806
|
+
const dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
607
807
|
|
|
608
808
|
// if a visible data set was removed, we need to tell our client that all objects from it are removed
|
|
609
809
|
const removedObjects: HashTreeObject[] = [];
|
|
610
810
|
|
|
611
811
|
removedDataSets.forEach((ds) => {
|
|
612
|
-
if (this.dataSets[ds]?.hashTree) {
|
|
613
|
-
for (let i = 0; i < this.dataSets[ds].hashTree.numLeaves; i += 1) {
|
|
812
|
+
if (this.dataSets[ds.name]?.hashTree) {
|
|
813
|
+
for (let i = 0; i < this.dataSets[ds.name].hashTree.numLeaves; i += 1) {
|
|
614
814
|
removedObjects.push(
|
|
615
|
-
...this.dataSets[ds].hashTree.getLeafData(i).map((elementId) => ({
|
|
815
|
+
...this.dataSets[ds.name].hashTree.getLeafData(i).map((elementId) => ({
|
|
616
816
|
htMeta: {
|
|
617
817
|
elementId,
|
|
618
|
-
dataSetNames: [ds],
|
|
818
|
+
dataSetNames: [ds.name],
|
|
619
819
|
},
|
|
620
820
|
data: null,
|
|
621
821
|
}))
|
|
622
822
|
);
|
|
623
823
|
}
|
|
624
824
|
|
|
625
|
-
this.deleteHashTree(ds);
|
|
825
|
+
this.deleteHashTree(ds.name);
|
|
626
826
|
}
|
|
627
827
|
});
|
|
628
|
-
this.visibleDataSets = this.visibleDataSets.filter(
|
|
828
|
+
this.visibleDataSets = this.visibleDataSets.filter(
|
|
829
|
+
(vds) => !removedDataSets.some((rds) => rds.name === vds.name)
|
|
830
|
+
);
|
|
629
831
|
updatedObjects.push(...removedObjects);
|
|
630
832
|
|
|
631
833
|
// now setup the new visible data sets
|
|
632
834
|
for (const ds of addedDataSets) {
|
|
633
|
-
const dataSetInfo = this.dataSets[ds];
|
|
835
|
+
const dataSetInfo = this.dataSets[ds.name];
|
|
634
836
|
|
|
635
837
|
if (dataSetInfo) {
|
|
636
|
-
if (this.
|
|
838
|
+
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
637
839
|
LoggerProxy.logger.info(
|
|
638
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Data set "${ds}" is already visible, skipping`
|
|
840
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Data set "${ds.name}" is already visible, skipping`
|
|
639
841
|
);
|
|
640
842
|
|
|
641
843
|
// eslint-disable-next-line no-continue
|
|
@@ -643,10 +845,13 @@ class HashTreeParser {
|
|
|
643
845
|
}
|
|
644
846
|
|
|
645
847
|
LoggerProxy.logger.info(
|
|
646
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Adding visible data set "${ds}"`
|
|
848
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Adding visible data set "${ds.name}"`
|
|
647
849
|
);
|
|
648
850
|
|
|
649
|
-
this.
|
|
851
|
+
if (!this.addToVisibleDataSetsList(ds)) {
|
|
852
|
+
// eslint-disable-next-line no-continue
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
650
855
|
|
|
651
856
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
652
857
|
|
|
@@ -656,7 +861,7 @@ class HashTreeParser {
|
|
|
656
861
|
};
|
|
657
862
|
} else {
|
|
658
863
|
LoggerProxy.logger.info(
|
|
659
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} visible data set "${ds}" added but no info about it in our dataSets structures`
|
|
864
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} visible data set "${ds.name}" added but no info about it in our dataSets structures`
|
|
660
865
|
);
|
|
661
866
|
// todo: add a metric here
|
|
662
867
|
dataSetsRequiringInitialization.push(ds);
|
|
@@ -670,32 +875,28 @@ class HashTreeParser {
|
|
|
670
875
|
* Adds entries to the passed in updateObjects array
|
|
671
876
|
* for the changes that result from adding and removing visible data sets.
|
|
672
877
|
*
|
|
673
|
-
* @param {
|
|
674
|
-
* @param {string[]} addedDataSets - The list of added data sets.
|
|
878
|
+
* @param {VisibleDataSetInfo[]} addedDataSets - The list of added data sets.
|
|
675
879
|
* @returns {Promise<void>}
|
|
676
880
|
*/
|
|
677
|
-
private async initializeNewVisibleDataSets(
|
|
678
|
-
|
|
679
|
-
addedDataSets: string[]
|
|
680
|
-
): Promise<void> {
|
|
681
|
-
const allDataSets = await this.getAllDataSetsMetadata(message.visibleDataSetsUrl);
|
|
881
|
+
private async initializeNewVisibleDataSets(addedDataSets: VisibleDataSetInfo[]): Promise<void> {
|
|
882
|
+
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
682
883
|
|
|
683
884
|
for (const ds of addedDataSets) {
|
|
684
|
-
const dataSetInfo = allDataSets.find((d) => d.name === ds);
|
|
885
|
+
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
685
886
|
|
|
686
887
|
LoggerProxy.logger.info(
|
|
687
|
-
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} initializing data set "${ds}"`
|
|
888
|
+
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} initializing data set "${ds.name}"`
|
|
688
889
|
);
|
|
689
890
|
|
|
690
891
|
if (!dataSetInfo) {
|
|
691
892
|
LoggerProxy.logger.warn(
|
|
692
|
-
`HashTreeParser#handleHashTreeMessage --> ${this.debugId} missing info about data set "${ds}" in Locus response from visibleDataSetsUrl`
|
|
893
|
+
`HashTreeParser#handleHashTreeMessage --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
693
894
|
);
|
|
694
895
|
} else {
|
|
695
896
|
// we're awaiting in a loop, because in practice there will be only one new data set at a time,
|
|
696
897
|
// so no point in trying to parallelize this
|
|
697
898
|
// eslint-disable-next-line no-await-in-loop
|
|
698
|
-
const updates = await this.initializeNewVisibleDataSet(dataSetInfo);
|
|
899
|
+
const updates = await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
699
900
|
|
|
700
901
|
this.callLocusInfoUpdateCallback(updates);
|
|
701
902
|
}
|
|
@@ -746,32 +947,31 @@ class HashTreeParser {
|
|
|
746
947
|
// available in the message, they will require separate async initialization
|
|
747
948
|
let dataSetsRequiringInitialization = [];
|
|
748
949
|
|
|
749
|
-
// first find out if there are any visible data set changes - they're signalled in
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
isSelf(object)
|
|
950
|
+
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
951
|
+
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
952
|
+
isMetadata(object)
|
|
753
953
|
);
|
|
754
954
|
|
|
755
|
-
if (
|
|
756
|
-
const
|
|
955
|
+
if (metadataUpdates.length > 0) {
|
|
956
|
+
const updatedMetadataObjects = [];
|
|
757
957
|
|
|
758
|
-
|
|
958
|
+
metadataUpdates.forEach((object) => {
|
|
759
959
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
760
960
|
for (const dataSetName of object.htMeta.dataSetNames) {
|
|
761
961
|
const hashTree = this.dataSets[dataSetName]?.hashTree;
|
|
762
962
|
|
|
763
963
|
if (hashTree && object.data) {
|
|
764
964
|
if (hashTree.putItem(object.htMeta.elementId)) {
|
|
765
|
-
|
|
965
|
+
updatedMetadataObjects.push(object);
|
|
766
966
|
}
|
|
767
967
|
}
|
|
768
968
|
}
|
|
769
969
|
});
|
|
770
970
|
|
|
771
|
-
updatedObjects.push(...
|
|
971
|
+
updatedObjects.push(...updatedMetadataObjects);
|
|
772
972
|
|
|
773
973
|
const {changeDetected, removedDataSets, addedDataSets} =
|
|
774
|
-
this.checkForVisibleDataSetChanges(
|
|
974
|
+
this.checkForVisibleDataSetChanges(updatedMetadataObjects);
|
|
775
975
|
|
|
776
976
|
if (changeDetected) {
|
|
777
977
|
dataSetsRequiringInitialization = this.processVisibleDataSetChanges(
|
|
@@ -782,48 +982,50 @@ class HashTreeParser {
|
|
|
782
982
|
}
|
|
783
983
|
}
|
|
784
984
|
|
|
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
|
-
|
|
985
|
+
if (message.locusStateElements?.length > 0) {
|
|
986
|
+
// by this point we now have this.dataSets setup for data sets from this message
|
|
987
|
+
// and hash trees created for the new visible data sets,
|
|
988
|
+
// so we can now process all the updates from the message
|
|
989
|
+
dataSets.forEach((dataSet) => {
|
|
990
|
+
if (this.dataSets[dataSet.name]) {
|
|
991
|
+
const {hashTree} = this.dataSets[dataSet.name];
|
|
992
|
+
|
|
993
|
+
if (hashTree) {
|
|
994
|
+
const locusStateElementsForThisSet = message.locusStateElements.filter((object) =>
|
|
995
|
+
object.htMeta.dataSetNames.includes(dataSet.name)
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
const appliedChangesList = hashTree.updateItems(
|
|
999
|
+
locusStateElementsForThisSet.map((object) =>
|
|
1000
|
+
object.data
|
|
1001
|
+
? {operation: 'update', item: object.htMeta.elementId}
|
|
1002
|
+
: {operation: 'remove', item: object.htMeta.elementId}
|
|
1003
|
+
)
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
zip(appliedChangesList, locusStateElementsForThisSet).forEach(
|
|
1007
|
+
([changeApplied, object]) => {
|
|
1008
|
+
if (changeApplied) {
|
|
1009
|
+
if (isSelf(object) && !object.data) {
|
|
1010
|
+
isRosterDropped = true;
|
|
1011
|
+
}
|
|
1012
|
+
// add to updatedObjects so that our locus DTO will get updated with the new object
|
|
1013
|
+
updatedObjects.push(object);
|
|
810
1014
|
}
|
|
811
|
-
// add to updatedObjects so that our locus DTO will get updated with the new object
|
|
812
|
-
updatedObjects.push(object);
|
|
813
1015
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1016
|
+
);
|
|
1017
|
+
} else {
|
|
1018
|
+
LoggerProxy.logger.info(
|
|
1019
|
+
`Locus-info:index#parseMessage --> ${this.debugId} unexpected (not visible) dataSet ${dataSet.name} received in hash tree message`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
820
1022
|
}
|
|
821
|
-
}
|
|
822
1023
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1024
|
+
if (!isRosterDropped) {
|
|
1025
|
+
this.runSyncAlgorithm(dataSet);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
827
1029
|
|
|
828
1030
|
if (isRosterDropped) {
|
|
829
1031
|
LoggerProxy.logger.info(
|
|
@@ -838,7 +1040,7 @@ class HashTreeParser {
|
|
|
838
1040
|
if (dataSetsRequiringInitialization.length > 0) {
|
|
839
1041
|
// there are some data sets that we need to initialize asynchronously
|
|
840
1042
|
queueMicrotask(() => {
|
|
841
|
-
this.initializeNewVisibleDataSets(
|
|
1043
|
+
this.initializeNewVisibleDataSets(dataSetsRequiringInitialization);
|
|
842
1044
|
});
|
|
843
1045
|
}
|
|
844
1046
|
|
|
@@ -859,11 +1061,20 @@ class HashTreeParser {
|
|
|
859
1061
|
* @returns {void}
|
|
860
1062
|
*/
|
|
861
1063
|
async handleMessage(message: HashTreeMessage, debugText?: string): Promise<void> {
|
|
1064
|
+
if (message.heartbeatIntervalMs) {
|
|
1065
|
+
this.heartbeatIntervalMs = message.heartbeatIntervalMs;
|
|
1066
|
+
}
|
|
862
1067
|
if (message.locusStateElements === undefined) {
|
|
863
1068
|
this.handleRootHashHeartBeatMessage(message);
|
|
1069
|
+
this.resetHeartbeatWatchdogs(message.dataSets);
|
|
864
1070
|
} else {
|
|
865
1071
|
const updates = await this.parseMessage(message, debugText);
|
|
866
1072
|
|
|
1073
|
+
// Only reset watchdogs if the meeting hasn't ended
|
|
1074
|
+
if (updates.updateType !== LocusInfoUpdateType.MEETING_ENDED) {
|
|
1075
|
+
this.resetHeartbeatWatchdogs(message.dataSets);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
867
1078
|
this.callLocusInfoUpdateCallback(updates);
|
|
868
1079
|
}
|
|
869
1080
|
}
|
|
@@ -880,7 +1091,49 @@ class HashTreeParser {
|
|
|
880
1091
|
}) {
|
|
881
1092
|
const {updateType, updatedObjects} = updates;
|
|
882
1093
|
|
|
883
|
-
if (updateType
|
|
1094
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1095
|
+
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1096
|
+
// or removals for objects that still exist in any of their datasets
|
|
1097
|
+
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1098
|
+
const {elementId} = object.htMeta;
|
|
1099
|
+
const {type, id, version} = elementId;
|
|
1100
|
+
|
|
1101
|
+
// Check all datasets
|
|
1102
|
+
for (const dataSetName of Object.keys(this.dataSets)) {
|
|
1103
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1104
|
+
|
|
1105
|
+
// only visible datasets have hash trees set
|
|
1106
|
+
if (dataSet?.hashTree) {
|
|
1107
|
+
const existingVersion = dataSet.hashTree.getItemVersion(id, type);
|
|
1108
|
+
if (existingVersion !== undefined) {
|
|
1109
|
+
if (object.data) {
|
|
1110
|
+
// For updates: filter out if any dataset has a higher version
|
|
1111
|
+
if (existingVersion > version) {
|
|
1112
|
+
LoggerProxy.logger.info(
|
|
1113
|
+
`HashTreeParser#callLocusInfoUpdateCallback --> ${this.debugId} Filtering out update for ${type}:${id} v${version} because dataset "${dataSetName}" has v${existingVersion}`
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
} else if (existingVersion >= version) {
|
|
1119
|
+
// For removals: filter out if the object still exists in any dataset
|
|
1120
|
+
LoggerProxy.logger.info(
|
|
1121
|
+
`HashTreeParser#callLocusInfoUpdateCallback --> ${this.debugId} Filtering out removal for ${type}:${id} v${version} because dataset "${dataSetName}" still has v${existingVersion}`
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
return true;
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
if (filteredUpdates.length > 0) {
|
|
1134
|
+
this.locusInfoUpdateCallback(updateType, {updatedObjects: filteredUpdates});
|
|
1135
|
+
}
|
|
1136
|
+
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
884
1137
|
this.locusInfoUpdateCallback(updateType, {updatedObjects});
|
|
885
1138
|
}
|
|
886
1139
|
}
|
|
@@ -899,6 +1152,75 @@ class HashTreeParser {
|
|
|
899
1152
|
return Math.round(randomValue ** exponent * maxMs);
|
|
900
1153
|
}
|
|
901
1154
|
|
|
1155
|
+
/**
|
|
1156
|
+
* Performs a sync for the given data set.
|
|
1157
|
+
*
|
|
1158
|
+
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1159
|
+
* @param {string} rootHash - Our current root hash for this data set
|
|
1160
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1161
|
+
* @returns {Promise<void>}
|
|
1162
|
+
*/
|
|
1163
|
+
private async performSync(
|
|
1164
|
+
dataSet: InternalDataSet,
|
|
1165
|
+
rootHash: string,
|
|
1166
|
+
reason: string
|
|
1167
|
+
): Promise<void> {
|
|
1168
|
+
if (!dataSet.hashTree) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
LoggerProxy.logger.info(
|
|
1173
|
+
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
|
|
1177
|
+
|
|
1178
|
+
if (dataSet.leafCount !== 1) {
|
|
1179
|
+
let receivedHashes;
|
|
1180
|
+
|
|
1181
|
+
try {
|
|
1182
|
+
// request hashes from sender
|
|
1183
|
+
const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
|
|
1184
|
+
dataSet.name,
|
|
1185
|
+
rootHash
|
|
1186
|
+
);
|
|
1187
|
+
|
|
1188
|
+
receivedHashes = hashes;
|
|
1189
|
+
|
|
1190
|
+
dataSet.hashTree.resize(latestDataSetInfo.leafCount);
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
if (error.statusCode === 409) {
|
|
1193
|
+
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1194
|
+
LoggerProxy.logger.info(
|
|
1195
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
throw error;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// identify mismatched leaves
|
|
1204
|
+
const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
|
|
1205
|
+
|
|
1206
|
+
mismatchedLeaveIndexes.forEach((index) => {
|
|
1207
|
+
mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
|
|
1208
|
+
});
|
|
1209
|
+
} else {
|
|
1210
|
+
mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
|
|
1211
|
+
}
|
|
1212
|
+
// request sync for mismatched leaves
|
|
1213
|
+
if (Object.keys(mismatchedLeavesData).length > 0) {
|
|
1214
|
+
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1215
|
+
|
|
1216
|
+
// sync API may return nothing (in that case data will arrive via messages)
|
|
1217
|
+
// or it may return a response in the same format as messages
|
|
1218
|
+
if (syncResponse) {
|
|
1219
|
+
this.handleMessage(syncResponse, 'via sync API');
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
902
1224
|
/**
|
|
903
1225
|
* Runs the sync algorithm for the given data set.
|
|
904
1226
|
*
|
|
@@ -957,55 +1279,11 @@ class HashTreeParser {
|
|
|
957
1279
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
958
1280
|
|
|
959
1281
|
if (dataSet.root !== rootHash) {
|
|
960
|
-
|
|
961
|
-
|
|
1282
|
+
await this.performSync(
|
|
1283
|
+
dataSet,
|
|
1284
|
+
rootHash,
|
|
1285
|
+
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
962
1286
|
);
|
|
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
1287
|
} else {
|
|
1010
1288
|
LoggerProxy.logger.info(
|
|
1011
1289
|
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} "${dataSet.name}" root hash matching: ${rootHash}, version=${dataSet.version}`
|
|
@@ -1019,6 +1297,52 @@ class HashTreeParser {
|
|
|
1019
1297
|
}
|
|
1020
1298
|
}
|
|
1021
1299
|
|
|
1300
|
+
/**
|
|
1301
|
+
* Resets the heartbeat watchdog timers for the specified data sets. Each data set has its own
|
|
1302
|
+
* watchdog timer that monitors whether heartbeats are being received within the expected interval.
|
|
1303
|
+
* If a heartbeat is not received for a specific data set within heartbeatIntervalMs plus
|
|
1304
|
+
* a backoff-calculated time, the sync algorithm is initiated for that data set
|
|
1305
|
+
*
|
|
1306
|
+
* @param {Array<DataSet>} receivedDataSets - The data sets from the received message for which watchdog timers should be reset
|
|
1307
|
+
* @returns {void}
|
|
1308
|
+
*/
|
|
1309
|
+
private resetHeartbeatWatchdogs(receivedDataSets: Array<DataSet>): void {
|
|
1310
|
+
if (!this.heartbeatIntervalMs) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
for (const receivedDataSet of receivedDataSets) {
|
|
1315
|
+
const dataSet = this.dataSets[receivedDataSet.name];
|
|
1316
|
+
|
|
1317
|
+
if (!dataSet?.hashTree) {
|
|
1318
|
+
// eslint-disable-next-line no-continue
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
if (dataSet.heartbeatWatchdogTimer) {
|
|
1323
|
+
clearTimeout(dataSet.heartbeatWatchdogTimer);
|
|
1324
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1328
|
+
const delay = this.heartbeatIntervalMs + backoffTime;
|
|
1329
|
+
|
|
1330
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(async () => {
|
|
1331
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1332
|
+
|
|
1333
|
+
LoggerProxy.logger.warn(
|
|
1334
|
+
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
await this.performSync(
|
|
1338
|
+
dataSet,
|
|
1339
|
+
dataSet.hashTree.getRootHash(),
|
|
1340
|
+
`heartbeat watchdog expired`
|
|
1341
|
+
);
|
|
1342
|
+
}, delay);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1022
1346
|
/**
|
|
1023
1347
|
* Stops all timers for the data sets to prevent any further sync attempts.
|
|
1024
1348
|
* @returns {void}
|
|
@@ -1029,15 +1353,20 @@ class HashTreeParser {
|
|
|
1029
1353
|
clearTimeout(dataSet.timer);
|
|
1030
1354
|
dataSet.timer = undefined;
|
|
1031
1355
|
}
|
|
1356
|
+
if (dataSet.heartbeatWatchdogTimer) {
|
|
1357
|
+
clearTimeout(dataSet.heartbeatWatchdogTimer);
|
|
1358
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1359
|
+
}
|
|
1032
1360
|
});
|
|
1033
1361
|
}
|
|
1034
1362
|
|
|
1035
1363
|
/**
|
|
1036
1364
|
* Gets the current hashes from the locus for a specific data set.
|
|
1037
1365
|
* @param {string} dataSetName
|
|
1366
|
+
* @param {string} currentRootHash
|
|
1038
1367
|
* @returns {string[]}
|
|
1039
1368
|
*/
|
|
1040
|
-
private getHashesFromLocus(dataSetName: string) {
|
|
1369
|
+
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1041
1370
|
LoggerProxy.logger.info(
|
|
1042
1371
|
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Requesting hashes for data set "${dataSetName}"`
|
|
1043
1372
|
);
|
|
@@ -1049,6 +1378,9 @@ class HashTreeParser {
|
|
|
1049
1378
|
return this.webexRequest({
|
|
1050
1379
|
method: HTTP_VERBS.GET,
|
|
1051
1380
|
uri: url,
|
|
1381
|
+
qs: {
|
|
1382
|
+
rootHash: currentRootHash,
|
|
1383
|
+
},
|
|
1052
1384
|
})
|
|
1053
1385
|
.then((response) => {
|
|
1054
1386
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
@@ -1110,9 +1442,14 @@ class HashTreeParser {
|
|
|
1110
1442
|
});
|
|
1111
1443
|
});
|
|
1112
1444
|
|
|
1445
|
+
const ourCurrentRootHash = dataSet.hashTree ? dataSet.hashTree.getRootHash() : EMPTY_HASH;
|
|
1446
|
+
|
|
1113
1447
|
return this.webexRequest({
|
|
1114
1448
|
method: HTTP_VERBS.POST,
|
|
1115
1449
|
uri: url,
|
|
1450
|
+
qs: {
|
|
1451
|
+
rootHash: ourCurrentRootHash,
|
|
1452
|
+
},
|
|
1116
1453
|
body,
|
|
1117
1454
|
})
|
|
1118
1455
|
.then((resp) => {
|