@webex/plugin-meetings 3.11.0 → 3.12.0-next.10
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 +14 -5
- 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 +7 -2
- package/dist/config.js.map +1 -1
- package/dist/constants.js +28 -6
- package/dist/constants.js.map +1 -1
- package/dist/hashTree/constants.js +13 -2
- 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 +869 -420
- 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 +32 -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 +290 -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/interceptors/utils.js +27 -0
- package/dist/interceptors/utils.js.map +1 -0
- 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 +522 -131
- 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 +1304 -928
- 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 +133 -3
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +117 -48
- 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 +6 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/mediaRequestManager.js +9 -60
- 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/multistream/sendSlotManager.js +116 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/reachability/index.js +18 -10
- package/dist/reachability/index.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/reconnection-manager/index.js +0 -1
- package/dist/reconnection-manager/index.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 +4 -0
- package/dist/types/constants.d.ts +23 -1
- package/dist/types/hashTree/constants.d.ts +2 -0
- package/dist/types/hashTree/hashTree.d.ts +7 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +122 -14
- package/dist/types/hashTree/types.d.ts +3 -0
- package/dist/types/hashTree/utils.d.ts +17 -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 +43 -0
- package/dist/types/interceptors/index.d.ts +2 -1
- package/dist/types/interceptors/utils.d.ts +1 -0
- package/dist/types/locus-info/index.d.ts +60 -8
- package/dist/types/locus-info/types.d.ts +7 -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 +72 -7
- 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 +5 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
- package/dist/types/multistream/sendSlotManager.d.ts +23 -1
- package/dist/types/reactions/reactions.type.d.ts +1 -0
- package/dist/types/webinar/utils.d.ts +6 -0
- package/dist/webinar/index.js +438 -163
- package/dist/webinar/index.js.map +1 -1
- package/dist/webinar/utils.js +25 -0
- package/dist/webinar/utils.js.map +1 -0
- package/package.json +24 -23
- 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 +27 -7
- package/src/config.ts +4 -0
- package/src/constants.ts +29 -1
- package/src/hashTree/constants.ts +10 -0
- package/src/hashTree/hashTree.ts +17 -0
- package/src/hashTree/hashTreeParser.ts +764 -264
- package/src/hashTree/types.ts +4 -0
- package/src/hashTree/utils.ts +26 -0
- package/src/index.ts +8 -1
- package/src/interceptors/constant.ts +6 -0
- package/src/interceptors/dataChannelAuthToken.ts +170 -0
- package/src/interceptors/index.ts +2 -1
- package/src/interceptors/utils.ts +16 -0
- package/src/interpretation/index.ts +2 -2
- package/src/locus-info/controlsUtils.ts +11 -0
- package/src/locus-info/index.ts +579 -113
- package/src/locus-info/selfUtils.ts +1 -0
- package/src/locus-info/types.ts +8 -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 +389 -87
- package/src/meeting/request.ts +42 -0
- package/src/meeting/request.type.ts +6 -0
- package/src/meeting/util.ts +160 -2
- package/src/meetings/index.ts +157 -44
- package/src/member/index.ts +10 -0
- package/src/member/util.ts +12 -0
- package/src/metrics/constants.ts +6 -0
- package/src/multistream/mediaRequestManager.ts +4 -54
- package/src/multistream/remoteMediaManager.ts +13 -0
- package/src/multistream/sendSlotManager.ts +97 -3
- package/src/reachability/index.ts +9 -0
- package/src/reactions/reactions.type.ts +1 -0
- package/src/reconnection-manager/index.ts +0 -1
- package/src/webinar/index.ts +265 -6
- package/src/webinar/utils.ts +16 -0
- package/test/unit/spec/aiEnableRequest/index.ts +981 -0
- package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
- package/test/unit/spec/annotation/index.ts +69 -7
- package/test/unit/spec/hashTree/hashTree.ts +66 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +2469 -195
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
- package/test/unit/spec/interceptors/utils.ts +75 -0
- package/test/unit/spec/locus-info/controlsUtils.js +29 -0
- package/test/unit/spec/locus-info/index.js +1134 -55
- 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 +884 -152
- package/test/unit/spec/meeting/request.js +70 -0
- package/test/unit/spec/meeting/utils.js +438 -26
- package/test/unit/spec/meetings/index.js +653 -32
- package/test/unit/spec/member/index.js +28 -4
- package/test/unit/spec/member/util.js +65 -27
- package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
- package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
- package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
- package/test/unit/spec/reachability/index.ts +23 -0
- package/test/unit/spec/reconnection-manager/index.js +4 -8
- package/test/unit/spec/webinar/index.ts +534 -37
- package/test/unit/spec/webinar/utils.ts +39 -0
|
@@ -2,10 +2,10 @@ import {cloneDeep, isEmpty, zip} from 'lodash';
|
|
|
2
2
|
import HashTree, {LeafDataItem} from './hashTree';
|
|
3
3
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
4
4
|
import {Enum, HTTP_VERBS} from '../constants';
|
|
5
|
-
import {DataSetNames, EMPTY_HASH} from './constants';
|
|
5
|
+
import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH} from './constants';
|
|
6
6
|
import {ObjectType, HtMeta, HashTreeObject} from './types';
|
|
7
7
|
import {LocusDTO} from '../locus-info/types';
|
|
8
|
-
import {deleteNestedObjectsWithHtMeta,
|
|
8
|
+
import {deleteNestedObjectsWithHtMeta, isMetadata, sortByInitPriority} 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,11 @@ 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[];
|
|
101
|
+
state: 'active' | 'stopped';
|
|
68
102
|
|
|
69
103
|
/**
|
|
70
104
|
* Constructor for HashTreeParser
|
|
@@ -76,29 +110,42 @@ class HashTreeParser {
|
|
|
76
110
|
dataSets: Array<DataSet>;
|
|
77
111
|
locus: any;
|
|
78
112
|
};
|
|
113
|
+
metadata: Metadata | null;
|
|
79
114
|
webexRequest: WebexRequestMethod;
|
|
80
115
|
locusInfoUpdateCallback: LocusInfoUpdateCallback;
|
|
81
116
|
debugId: string;
|
|
117
|
+
excludedDataSets?: string[];
|
|
82
118
|
}) {
|
|
83
119
|
const {dataSets, locus} = options.initialLocus; // extract dataSets from initialLocus
|
|
84
120
|
|
|
85
121
|
this.debugId = options.debugId;
|
|
86
122
|
this.webexRequest = options.webexRequest;
|
|
87
123
|
this.locusInfoUpdateCallback = options.locusInfoUpdateCallback;
|
|
88
|
-
this.
|
|
124
|
+
this.excludedDataSets = options.excludedDataSets || [];
|
|
125
|
+
this.visibleDataSetsUrl = locus?.links?.resources?.visibleDataSets?.url;
|
|
126
|
+
this.setVisibleDataSets(options.metadata?.visibleDataSets || [], dataSets);
|
|
89
127
|
|
|
90
|
-
|
|
128
|
+
this.state = 'active';
|
|
129
|
+
|
|
130
|
+
if (options.metadata?.visibleDataSets?.length === 0) {
|
|
91
131
|
LoggerProxy.logger.warn(
|
|
92
|
-
`HashTreeParser#constructor --> ${this.debugId} No visibleDataSets found in
|
|
132
|
+
`HashTreeParser#constructor --> ${this.debugId} No visibleDataSets found in Metadata`
|
|
93
133
|
);
|
|
94
134
|
}
|
|
95
135
|
// object mapping dataset names to arrays of leaf data
|
|
96
136
|
const leafData = this.analyzeLocusHtMeta(locus);
|
|
97
137
|
|
|
138
|
+
if (options.metadata) {
|
|
139
|
+
// add also the metadata that's outside of locus object itself
|
|
140
|
+
this.analyzeMetadata(leafData, options.metadata);
|
|
141
|
+
}
|
|
142
|
+
|
|
98
143
|
LoggerProxy.logger.info(
|
|
99
|
-
`HashTreeParser#constructor -->
|
|
144
|
+
`HashTreeParser#constructor --> ${
|
|
145
|
+
this.debugId
|
|
146
|
+
} creating HashTreeParser for datasets: ${JSON.stringify(
|
|
100
147
|
dataSets.map((ds) => ds.name)
|
|
101
|
-
)}`
|
|
148
|
+
)} with visible datasets: ${JSON.stringify(this.visibleDataSets.map((vds) => vds.name))}`
|
|
102
149
|
);
|
|
103
150
|
|
|
104
151
|
for (const dataSet of dataSets) {
|
|
@@ -106,46 +153,105 @@ class HashTreeParser {
|
|
|
106
153
|
|
|
107
154
|
this.dataSets[name] = {
|
|
108
155
|
...dataSet,
|
|
109
|
-
hashTree: this.
|
|
156
|
+
hashTree: this.isVisibleDataSet(name)
|
|
110
157
|
? new HashTree(leafData[name] || [], leafCount)
|
|
111
158
|
: undefined,
|
|
112
159
|
};
|
|
113
160
|
}
|
|
114
161
|
}
|
|
115
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Sets the visible data sets list for the HashTreeParser. This method should be called only at the start, to initialize
|
|
165
|
+
* the visible data sets, before any message processsing, so for example from the constructor or when resuming the parser.
|
|
166
|
+
*
|
|
167
|
+
* @param {Array<VisibleDataSetInfo>} visibleDataSets - The visible data sets to set
|
|
168
|
+
* @param {Array<DataSet>} dataSets - The "dataSets" list from Locus (yes, Locus sends visibleDataSets and dataSets as separate lists and they can differ)
|
|
169
|
+
* @returns {void}
|
|
170
|
+
*/
|
|
171
|
+
private setVisibleDataSets(visibleDataSets: VisibleDataSetInfo[], dataSets: Array<DataSet>) {
|
|
172
|
+
this.visibleDataSets = cloneDeep(visibleDataSets).filter(
|
|
173
|
+
(vds) =>
|
|
174
|
+
// exclude data sets we will never care about
|
|
175
|
+
!this.isExcludedDataSet(vds.name) &&
|
|
176
|
+
// and make sure that visibleDataSets list is consistent with dataSets list
|
|
177
|
+
dataSets.some((ds) => ds.name === vds.name)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Checks if the given data set name is in the list of visible data sets
|
|
183
|
+
* @param {string} dataSetName data set name to check
|
|
184
|
+
* @returns {Boolean} True if the data set is visible, false otherwise
|
|
185
|
+
*/
|
|
186
|
+
private isVisibleDataSet(dataSetName: string): boolean {
|
|
187
|
+
return this.visibleDataSets.some((vds) => vds.name === dataSetName);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Checks if the given data set name is in the excluded list
|
|
192
|
+
* @param {string} dataSetName data set name to check
|
|
193
|
+
* @returns {boolean} True if the data set is excluded, false otherwise
|
|
194
|
+
*/
|
|
195
|
+
private isExcludedDataSet(dataSetName: string): boolean {
|
|
196
|
+
return this.excludedDataSets.some((name) => name === dataSetName);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Adds a data set to the visible data sets list, unless it is in the excluded list.
|
|
201
|
+
* @param {VisibleDataSetInfo} dataSetInfo data set info to add
|
|
202
|
+
* @returns {boolean} True if the data set was added, false if it was excluded
|
|
203
|
+
*/
|
|
204
|
+
private addToVisibleDataSetsList(dataSetInfo: VisibleDataSetInfo): boolean {
|
|
205
|
+
if (this.isExcludedDataSet(dataSetInfo.name)) {
|
|
206
|
+
LoggerProxy.logger.info(
|
|
207
|
+
`HashTreeParser#addToVisibleDataSetsList --> ${this.debugId} Data set "${dataSetInfo.name}" is in the excluded list, ignoring`
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.visibleDataSets.push(dataSetInfo);
|
|
214
|
+
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
116
218
|
/**
|
|
117
219
|
* Initializes a new visible data set by creating a hash tree for it, adding it to all the internal structures,
|
|
118
220
|
* and sending an initial sync request to Locus with empty leaf data - that will trigger Locus to gives us all the data
|
|
119
221
|
* from that dataset (in the response or via messages).
|
|
120
222
|
*
|
|
121
|
-
* @param {
|
|
223
|
+
* @param {VisibleDataSetInfo} visibleDataSetInfo Information about the new visible data set
|
|
224
|
+
* @param {DataSet} dataSetInfo The new data set to be added
|
|
122
225
|
* @returns {Promise}
|
|
123
226
|
*/
|
|
124
227
|
private initializeNewVisibleDataSet(
|
|
125
|
-
|
|
228
|
+
visibleDataSetInfo: VisibleDataSetInfo,
|
|
229
|
+
dataSetInfo: DataSet
|
|
126
230
|
): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
|
|
127
|
-
if (this.
|
|
231
|
+
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
128
232
|
LoggerProxy.logger.info(
|
|
129
|
-
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${
|
|
233
|
+
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
|
|
130
234
|
);
|
|
131
235
|
|
|
132
236
|
return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
|
|
133
237
|
}
|
|
134
238
|
|
|
135
239
|
LoggerProxy.logger.info(
|
|
136
|
-
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Adding visible data set "${
|
|
240
|
+
`HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Adding visible data set "${dataSetInfo.name}"`
|
|
137
241
|
);
|
|
138
242
|
|
|
139
|
-
this.
|
|
243
|
+
if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
|
|
244
|
+
return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
|
|
245
|
+
}
|
|
140
246
|
|
|
141
|
-
const hashTree = new HashTree([],
|
|
247
|
+
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
142
248
|
|
|
143
|
-
this.dataSets[
|
|
144
|
-
...
|
|
249
|
+
this.dataSets[dataSetInfo.name] = {
|
|
250
|
+
...dataSetInfo,
|
|
145
251
|
hashTree,
|
|
146
252
|
};
|
|
147
253
|
|
|
148
|
-
return this.sendInitializationSyncRequestToLocus(
|
|
254
|
+
return this.sendInitializationSyncRequestToLocus(dataSetInfo.name, 'new visible data set');
|
|
149
255
|
}
|
|
150
256
|
|
|
151
257
|
/**
|
|
@@ -178,10 +284,13 @@ class HashTreeParser {
|
|
|
178
284
|
return this.sendSyncRequestToLocus(this.dataSets[datasetName], emptyLeavesData).then(
|
|
179
285
|
(syncResponse) => {
|
|
180
286
|
if (syncResponse) {
|
|
181
|
-
return
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
287
|
+
return {
|
|
288
|
+
updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
289
|
+
updatedObjects: this.parseMessage(
|
|
290
|
+
syncResponse,
|
|
291
|
+
`via empty leaves /sync API call for ${debugText}`
|
|
292
|
+
),
|
|
293
|
+
};
|
|
185
294
|
}
|
|
186
295
|
|
|
187
296
|
return {updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []};
|
|
@@ -190,18 +299,31 @@ class HashTreeParser {
|
|
|
190
299
|
}
|
|
191
300
|
|
|
192
301
|
/**
|
|
193
|
-
* Queries Locus for information about all
|
|
302
|
+
* Queries Locus for all up-to-date information about all visible data sets
|
|
194
303
|
*
|
|
195
|
-
* @param {string} url - url from which we can get info about all data sets
|
|
196
304
|
* @returns {Promise}
|
|
197
305
|
*/
|
|
198
|
-
private
|
|
306
|
+
private getAllVisibleDataSetsFromLocus() {
|
|
307
|
+
if (!this.visibleDataSetsUrl) {
|
|
308
|
+
LoggerProxy.logger.warn(
|
|
309
|
+
`HashTreeParser#getAllVisibleDataSetsFromLocus --> ${this.debugId} No visibleDataSetsUrl, cannot get data sets information`
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return Promise.resolve([]);
|
|
313
|
+
}
|
|
314
|
+
|
|
199
315
|
return this.webexRequest({
|
|
200
316
|
method: HTTP_VERBS.GET,
|
|
201
|
-
uri:
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
|
|
317
|
+
uri: this.visibleDataSetsUrl,
|
|
318
|
+
})
|
|
319
|
+
.then((response) => {
|
|
320
|
+
return response.body.dataSets as Array<DataSet>;
|
|
321
|
+
})
|
|
322
|
+
.catch((error) => {
|
|
323
|
+
this.checkForSentinelHttpResponse(error);
|
|
324
|
+
|
|
325
|
+
throw error;
|
|
326
|
+
});
|
|
205
327
|
}
|
|
206
328
|
|
|
207
329
|
/**
|
|
@@ -211,12 +333,14 @@ class HashTreeParser {
|
|
|
211
333
|
* @returns {Promise}
|
|
212
334
|
*/
|
|
213
335
|
async initializeFromMessage(message: HashTreeMessage) {
|
|
336
|
+
this.visibleDataSetsUrl = message.visibleDataSetsUrl;
|
|
337
|
+
|
|
214
338
|
LoggerProxy.logger.info(
|
|
215
|
-
`HashTreeParser#initializeFromMessage --> ${this.debugId} visibleDataSetsUrl=${
|
|
339
|
+
`HashTreeParser#initializeFromMessage --> ${this.debugId} visibleDataSetsUrl=${this.visibleDataSetsUrl}`
|
|
216
340
|
);
|
|
217
|
-
const
|
|
341
|
+
const visibleDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
218
342
|
|
|
219
|
-
await this.initializeDataSets(
|
|
343
|
+
await this.initializeDataSets(visibleDataSets, 'initialization from message');
|
|
220
344
|
}
|
|
221
345
|
|
|
222
346
|
/**
|
|
@@ -236,28 +360,33 @@ class HashTreeParser {
|
|
|
236
360
|
|
|
237
361
|
return;
|
|
238
362
|
}
|
|
363
|
+
this.visibleDataSetsUrl = locus.links.resources.visibleDataSets.url;
|
|
239
364
|
|
|
240
365
|
LoggerProxy.logger.info(
|
|
241
|
-
`HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} visibleDataSets url: ${
|
|
366
|
+
`HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} visibleDataSets url: ${this.visibleDataSetsUrl}`
|
|
242
367
|
);
|
|
243
368
|
|
|
244
|
-
const
|
|
369
|
+
const visibleDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
245
370
|
|
|
246
|
-
await this.initializeDataSets(
|
|
371
|
+
await this.initializeDataSets(visibleDataSets, 'initialization from GET /loci response');
|
|
247
372
|
}
|
|
248
373
|
|
|
249
374
|
/**
|
|
250
375
|
* Initializes data sets by doing an initialization sync on each visible data set that doesn't have a hash tree yet.
|
|
251
376
|
*
|
|
252
|
-
* @param {DataSet[]}
|
|
377
|
+
* @param {DataSet[]} visibleDataSets Array of visible DataSet objects to initialize
|
|
253
378
|
* @param {string} debugText Text to include in logs for debugging purposes
|
|
254
379
|
* @returns {Promise}
|
|
255
380
|
*/
|
|
256
|
-
private async initializeDataSets(
|
|
381
|
+
private async initializeDataSets(visibleDataSets: Array<DataSet>, debugText: string) {
|
|
382
|
+
if (this.state === 'stopped') {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
257
386
|
const updatedObjects: HashTreeObject[] = [];
|
|
258
387
|
|
|
259
|
-
for (const dataSet of
|
|
260
|
-
const {name, leafCount} = dataSet;
|
|
388
|
+
for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
389
|
+
const {name, leafCount, url} = dataSet;
|
|
261
390
|
|
|
262
391
|
if (!this.dataSets[name]) {
|
|
263
392
|
LoggerProxy.logger.info(
|
|
@@ -273,7 +402,20 @@ class HashTreeParser {
|
|
|
273
402
|
);
|
|
274
403
|
}
|
|
275
404
|
|
|
276
|
-
if (this.
|
|
405
|
+
if (!this.isVisibleDataSet(name)) {
|
|
406
|
+
if (
|
|
407
|
+
!this.addToVisibleDataSetsList({
|
|
408
|
+
name,
|
|
409
|
+
url,
|
|
410
|
+
})
|
|
411
|
+
) {
|
|
412
|
+
// dataset is excluded, skip it
|
|
413
|
+
// eslint-disable-next-line no-continue
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!this.dataSets[name].hashTree) {
|
|
277
419
|
LoggerProxy.logger.info(
|
|
278
420
|
`HashTreeParser#initializeDataSets --> ${this.debugId} creating hash tree for visible dataset "${name}" (${debugText})`
|
|
279
421
|
);
|
|
@@ -282,15 +424,6 @@ class HashTreeParser {
|
|
|
282
424
|
// eslint-disable-next-line no-await-in-loop
|
|
283
425
|
const data = await this.sendInitializationSyncRequestToLocus(name, debugText);
|
|
284
426
|
|
|
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
427
|
if (data.updateType === LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
295
428
|
updatedObjects.push(...(data.updatedObjects || []));
|
|
296
429
|
}
|
|
@@ -316,12 +449,9 @@ class HashTreeParser {
|
|
|
316
449
|
private analyzeLocusHtMeta(locus: any, options?: {copyData?: boolean}) {
|
|
317
450
|
const {copyData = false} = options || {};
|
|
318
451
|
// 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
|
-
> = {};
|
|
452
|
+
const leafInfo: Record<string, Array<LeafInfo>> = {};
|
|
323
453
|
|
|
324
|
-
const findAndStoreMetaData = (currentLocusPart: any) => {
|
|
454
|
+
const findAndStoreMetaData = (currentLocusPart: any, currentLocusPartName: string) => {
|
|
325
455
|
if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
|
|
326
456
|
return;
|
|
327
457
|
}
|
|
@@ -329,17 +459,25 @@ class HashTreeParser {
|
|
|
329
459
|
if (currentLocusPart.htMeta && currentLocusPart.htMeta.dataSetNames) {
|
|
330
460
|
const {type, id, version} = currentLocusPart.htMeta.elementId;
|
|
331
461
|
const {dataSetNames} = currentLocusPart.htMeta;
|
|
332
|
-
const newLeafInfo:
|
|
462
|
+
const newLeafInfo: LeafInfo = {
|
|
333
463
|
type,
|
|
334
464
|
id,
|
|
335
465
|
version,
|
|
336
466
|
};
|
|
337
467
|
|
|
338
468
|
if (copyData) {
|
|
339
|
-
|
|
469
|
+
if ((type as string).toLowerCase() === ObjectType.control) {
|
|
470
|
+
// control entries require special handling, because they are signalled by Locus
|
|
471
|
+
// differently when coming in messages vs API responses
|
|
472
|
+
newLeafInfo.data = {
|
|
473
|
+
[currentLocusPartName]: cloneDeep(currentLocusPart),
|
|
474
|
+
};
|
|
475
|
+
} else {
|
|
476
|
+
newLeafInfo.data = cloneDeep(currentLocusPart);
|
|
340
477
|
|
|
341
|
-
|
|
342
|
-
|
|
478
|
+
// remove any nested other objects that have their own htMeta
|
|
479
|
+
deleteNestedObjectsWithHtMeta(newLeafInfo.data);
|
|
480
|
+
}
|
|
343
481
|
}
|
|
344
482
|
|
|
345
483
|
for (const dataSetName of dataSetNames) {
|
|
@@ -351,45 +489,80 @@ class HashTreeParser {
|
|
|
351
489
|
}
|
|
352
490
|
|
|
353
491
|
if (Array.isArray(currentLocusPart)) {
|
|
354
|
-
for (const item of currentLocusPart) {
|
|
355
|
-
findAndStoreMetaData(item);
|
|
492
|
+
for (const [index, item] of currentLocusPart.entries()) {
|
|
493
|
+
findAndStoreMetaData(item, index.toString());
|
|
356
494
|
}
|
|
357
495
|
} else {
|
|
358
496
|
for (const key of Object.keys(currentLocusPart)) {
|
|
359
497
|
if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
|
|
360
|
-
findAndStoreMetaData(currentLocusPart[key]);
|
|
498
|
+
findAndStoreMetaData(currentLocusPart[key], key);
|
|
361
499
|
}
|
|
362
500
|
}
|
|
363
501
|
}
|
|
364
502
|
};
|
|
365
503
|
|
|
366
|
-
findAndStoreMetaData(locus);
|
|
504
|
+
findAndStoreMetaData(locus, 'locus');
|
|
367
505
|
|
|
368
506
|
return leafInfo;
|
|
369
507
|
}
|
|
370
508
|
|
|
371
509
|
/**
|
|
372
|
-
*
|
|
510
|
+
* Analyzes the Metadata object that is sent outside of Locus object, and appends its data to passed in leafInfo
|
|
511
|
+
* structure.
|
|
373
512
|
*
|
|
374
|
-
* @param {
|
|
375
|
-
* @
|
|
513
|
+
* @param {Record<string, LeafInfo[]>} leafInfo the structure to which the Metadata info will be appended
|
|
514
|
+
* @param {Metadata} metadata Metadata object
|
|
515
|
+
* @returns {void}
|
|
376
516
|
*/
|
|
377
|
-
private
|
|
378
|
-
const
|
|
379
|
-
(dataSet) => dataSet.name.toLowerCase() === DataSetNames.MAIN
|
|
380
|
-
);
|
|
517
|
+
private analyzeMetadata(leafInfo: Record<string, LeafInfo[]>, metadata: Metadata) {
|
|
518
|
+
const {htMeta} = metadata;
|
|
381
519
|
|
|
382
520
|
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
mainDataSet.root === EMPTY_HASH &&
|
|
386
|
-
this.dataSets[DataSetNames.MAIN].version < mainDataSet.version
|
|
521
|
+
htMeta?.dataSetNames?.length === 1 &&
|
|
522
|
+
htMeta.dataSetNames[0].toLowerCase() === MetadataDataSetName
|
|
387
523
|
) {
|
|
388
|
-
|
|
389
|
-
|
|
524
|
+
const {type, id, version} = metadata.htMeta.elementId;
|
|
525
|
+
|
|
526
|
+
const dataSetName = htMeta.dataSetNames[0];
|
|
527
|
+
|
|
528
|
+
if (!leafInfo[dataSetName]) {
|
|
529
|
+
leafInfo[dataSetName] = [];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
leafInfo[dataSetName].push({
|
|
533
|
+
type,
|
|
534
|
+
id,
|
|
535
|
+
version,
|
|
536
|
+
});
|
|
537
|
+
} else {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`${this.debugId} Metadata htMeta has unexpected dataSetNames: ${
|
|
540
|
+
htMeta && htMeta.dataSetNames.join(',')
|
|
541
|
+
}`
|
|
542
|
+
);
|
|
390
543
|
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Checks if the provided hash tree message indicates the end of the meeting and that there won't be any more updates.
|
|
548
|
+
*
|
|
549
|
+
* @param {HashTreeMessage} message - The hash tree message to check
|
|
550
|
+
* @returns {boolean} - Returns true if the message indicates the end of the meeting, false otherwise
|
|
551
|
+
*/
|
|
552
|
+
private isEndMessage(message: HashTreeMessage) {
|
|
553
|
+
return message.dataSets.some((dataSet) => {
|
|
554
|
+
if (
|
|
555
|
+
dataSet.leafCount === 1 &&
|
|
556
|
+
dataSet.root === EMPTY_HASH &&
|
|
557
|
+
(!this.dataSets[dataSet.name] || this.dataSets[dataSet.name].version < dataSet.version) &&
|
|
558
|
+
PossibleSentinelMessageDataSetNames.includes(dataSet.name.toLowerCase())
|
|
559
|
+
) {
|
|
560
|
+
// this is a special way for Locus to indicate that this meeting has ended
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
391
563
|
|
|
392
|
-
|
|
564
|
+
return false;
|
|
565
|
+
});
|
|
393
566
|
}
|
|
394
567
|
|
|
395
568
|
/**
|
|
@@ -420,6 +593,90 @@ class HashTreeParser {
|
|
|
420
593
|
});
|
|
421
594
|
}
|
|
422
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Asynchronously initializes new visible data sets
|
|
598
|
+
*
|
|
599
|
+
* @param {VisibleDataSetInfo[]} dataSetsRequiringInitialization list of datasets to initialize
|
|
600
|
+
* @returns {void}
|
|
601
|
+
*/
|
|
602
|
+
private queueInitForNewVisibleDataSets(dataSetsRequiringInitialization: VisibleDataSetInfo[]) {
|
|
603
|
+
LoggerProxy.logger.info(
|
|
604
|
+
`HashTreeParser#queueInitForNewVisibleDataSets --> ${
|
|
605
|
+
this.debugId
|
|
606
|
+
} queuing initialization of new visible datasets: ${dataSetsRequiringInitialization
|
|
607
|
+
.map((ds) => ds.name)
|
|
608
|
+
.join(', ')}`
|
|
609
|
+
);
|
|
610
|
+
queueMicrotask(() => {
|
|
611
|
+
this.initializeNewVisibleDataSets(dataSetsRequiringInitialization).catch((error) => {
|
|
612
|
+
if (error instanceof MeetingEndedError) {
|
|
613
|
+
this.callLocusInfoUpdateCallback({
|
|
614
|
+
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
615
|
+
});
|
|
616
|
+
} else {
|
|
617
|
+
LoggerProxy.logger.warn(
|
|
618
|
+
`HashTreeParser#queueInitForNewVisibleDataSets --> ${
|
|
619
|
+
this.debugId
|
|
620
|
+
} error while initializing new visible datasets: ${dataSetsRequiringInitialization
|
|
621
|
+
.map((ds) => ds.name)
|
|
622
|
+
.join(', ')}: `,
|
|
623
|
+
error
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Handles updates to Metadata object that we receive from Locus via other means than messages. Right now
|
|
632
|
+
* that means only in the API response alongside locus object.
|
|
633
|
+
*
|
|
634
|
+
* @param {Metadata} metadata received in Locus update other than a message (for example in an API response)
|
|
635
|
+
* @param {HashTreeObject[]} updatedObjects a list of updated hash tree objects to which any updates resulting from new Metadata will be added
|
|
636
|
+
* @returns {void}
|
|
637
|
+
*/
|
|
638
|
+
handleMetadataUpdate(metadata: Metadata, updatedObjects: HashTreeObject[]): void {
|
|
639
|
+
let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
640
|
+
|
|
641
|
+
// current assumption based on Locus docs is that Metadata object lives always in "self" data set
|
|
642
|
+
const hashTree = this.dataSets[MetadataDataSetName]?.hashTree;
|
|
643
|
+
|
|
644
|
+
if (!hashTree) {
|
|
645
|
+
LoggerProxy.logger.warn(
|
|
646
|
+
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received Metadata object but no hash tree for "${MetadataDataSetName}" data set exists`
|
|
647
|
+
);
|
|
648
|
+
} else {
|
|
649
|
+
const metadataUpdated = hashTree.putItem(metadata.htMeta.elementId);
|
|
650
|
+
|
|
651
|
+
if (metadataUpdated) {
|
|
652
|
+
// metadata in Locus API response is in a slightly different format than the objects in messages, so need to adapt it
|
|
653
|
+
const metadataObject: HashTreeObject = {
|
|
654
|
+
htMeta: metadata.htMeta,
|
|
655
|
+
data: metadata,
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
updatedObjects.push(metadataObject);
|
|
659
|
+
|
|
660
|
+
const {changeDetected, removedDataSets, addedDataSets} = this.checkForVisibleDataSetChanges(
|
|
661
|
+
[metadataObject]
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
if (changeDetected) {
|
|
665
|
+
dataSetsRequiringInitialization = this.processVisibleDataSetChanges(
|
|
666
|
+
removedDataSets,
|
|
667
|
+
addedDataSets,
|
|
668
|
+
updatedObjects
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (dataSetsRequiringInitialization.length > 0) {
|
|
673
|
+
// there are some data sets that we need to initialize asynchronously
|
|
674
|
+
this.queueInitForNewVisibleDataSets(dataSetsRequiringInitialization);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
423
680
|
/**
|
|
424
681
|
* This method should be called when we receive a partial locus DTO that contains dataSets and htMeta information
|
|
425
682
|
* It updates the hash trees with the new leaf data based on the received Locus
|
|
@@ -427,22 +684,32 @@ class HashTreeParser {
|
|
|
427
684
|
* @param {Object} update - The locus update containing data sets and locus information
|
|
428
685
|
* @returns {void}
|
|
429
686
|
*/
|
|
430
|
-
handleLocusUpdate(update: {dataSets?: Array<DataSet>; locus: any}): void {
|
|
431
|
-
|
|
687
|
+
handleLocusUpdate(update: {dataSets?: Array<DataSet>; locus: any; metadata?: Metadata}): void {
|
|
688
|
+
if (this.state === 'stopped') {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const {dataSets, locus, metadata} = update;
|
|
432
693
|
|
|
433
694
|
if (!dataSets) {
|
|
434
|
-
LoggerProxy.logger.
|
|
695
|
+
LoggerProxy.logger.info(
|
|
435
696
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
|
|
436
697
|
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
698
|
+
} else {
|
|
699
|
+
for (const dataSet of dataSets) {
|
|
700
|
+
this.updateDataSetInfo(dataSet);
|
|
701
|
+
}
|
|
440
702
|
}
|
|
441
703
|
const updatedObjects: HashTreeObject[] = [];
|
|
442
704
|
|
|
443
705
|
// first, analyze the locus object to extract the hash tree objects' htMeta and data from it
|
|
444
706
|
const leafInfo = this.analyzeLocusHtMeta(locus, {copyData: true});
|
|
445
707
|
|
|
708
|
+
// if we got metadata, process it (currently that means only potential visible data set list changes)
|
|
709
|
+
if (metadata) {
|
|
710
|
+
this.handleMetadataUpdate(metadata, updatedObjects);
|
|
711
|
+
}
|
|
712
|
+
|
|
446
713
|
// then process the data in hash trees, if it is a new version, then add it to updatedObjects
|
|
447
714
|
Object.keys(leafInfo).forEach((dataSetName) => {
|
|
448
715
|
if (this.dataSets[dataSetName]) {
|
|
@@ -477,7 +744,7 @@ class HashTreeParser {
|
|
|
477
744
|
);
|
|
478
745
|
}
|
|
479
746
|
} else {
|
|
480
|
-
LoggerProxy.logger.
|
|
747
|
+
LoggerProxy.logger.info(
|
|
481
748
|
`HashTreeParser#handleLocusUpdate --> ${this.debugId} received leaf data for unknown data set "${dataSetName}", ignoring`
|
|
482
749
|
);
|
|
483
750
|
}
|
|
@@ -493,9 +760,6 @@ class HashTreeParser {
|
|
|
493
760
|
updatedObjects,
|
|
494
761
|
});
|
|
495
762
|
}
|
|
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
763
|
}
|
|
500
764
|
|
|
501
765
|
/**
|
|
@@ -511,7 +775,7 @@ class HashTreeParser {
|
|
|
511
775
|
};
|
|
512
776
|
|
|
513
777
|
LoggerProxy.logger.info(
|
|
514
|
-
`HashTreeParser#
|
|
778
|
+
`HashTreeParser#updateDataSetInfo --> ${this.debugId} created entry for "${receivedDataSet.name}" dataset: version=${receivedDataSet.version}, root=${receivedDataSet.root}`
|
|
515
779
|
);
|
|
516
780
|
|
|
517
781
|
return;
|
|
@@ -526,7 +790,7 @@ class HashTreeParser {
|
|
|
526
790
|
exponent: receivedDataSet.backoff.exponent,
|
|
527
791
|
};
|
|
528
792
|
LoggerProxy.logger.info(
|
|
529
|
-
`HashTreeParser#
|
|
793
|
+
`HashTreeParser#updateDataSetInfo --> ${this.debugId} updated "${receivedDataSet.name}" dataset to version=${receivedDataSet.version}, root=${receivedDataSet.root}`
|
|
530
794
|
);
|
|
531
795
|
}
|
|
532
796
|
}
|
|
@@ -537,25 +801,30 @@ class HashTreeParser {
|
|
|
537
801
|
* @returns {Object} An object containing the removed and added visible data sets.
|
|
538
802
|
*/
|
|
539
803
|
private checkForVisibleDataSetChanges(updatedObjects: HashTreeObject[]) {
|
|
540
|
-
let removedDataSets:
|
|
541
|
-
let addedDataSets:
|
|
804
|
+
let removedDataSets: VisibleDataSetInfo[] = [];
|
|
805
|
+
let addedDataSets: VisibleDataSetInfo[] = [];
|
|
542
806
|
|
|
543
|
-
// visibleDataSets can only be changed by
|
|
807
|
+
// visibleDataSets can only be changed by Metadata object updates
|
|
544
808
|
updatedObjects.forEach((object) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
809
|
+
if (isMetadata(object) && object.data?.visibleDataSets) {
|
|
810
|
+
const newVisibleDataSets = object.data.visibleDataSets.filter(
|
|
811
|
+
(vds) => !this.isExcludedDataSet(vds.name)
|
|
812
|
+
);
|
|
548
813
|
|
|
549
|
-
removedDataSets = this.visibleDataSets.filter(
|
|
550
|
-
|
|
814
|
+
removedDataSets = this.visibleDataSets.filter(
|
|
815
|
+
(ds) => !newVisibleDataSets.some((nvs) => nvs.name === ds.name)
|
|
816
|
+
);
|
|
817
|
+
addedDataSets = newVisibleDataSets.filter((nvs) =>
|
|
818
|
+
this.visibleDataSets.every((ds) => ds.name !== nvs.name)
|
|
819
|
+
);
|
|
551
820
|
|
|
552
821
|
if (removedDataSets.length > 0 || addedDataSets.length > 0) {
|
|
553
822
|
LoggerProxy.logger.info(
|
|
554
823
|
`HashTreeParser#checkForVisibleDataSetChanges --> ${
|
|
555
824
|
this.debugId
|
|
556
|
-
} visible data sets change: removed: ${removedDataSets
|
|
557
|
-
|
|
558
|
-
|
|
825
|
+
} visible data sets change: removed: ${removedDataSets
|
|
826
|
+
.map((ds) => ds.name)
|
|
827
|
+
.join(', ')}, added: ${addedDataSets.map((ds) => ds.name).join(', ')}`
|
|
559
828
|
);
|
|
560
829
|
}
|
|
561
830
|
}
|
|
@@ -577,11 +846,15 @@ class HashTreeParser {
|
|
|
577
846
|
private deleteHashTree(dataSetName: string) {
|
|
578
847
|
this.dataSets[dataSetName].hashTree = undefined;
|
|
579
848
|
|
|
580
|
-
// we also need to stop the
|
|
849
|
+
// we also need to stop the timers as there is no hash tree anymore to sync
|
|
581
850
|
if (this.dataSets[dataSetName].timer) {
|
|
582
851
|
clearTimeout(this.dataSets[dataSetName].timer);
|
|
583
852
|
this.dataSets[dataSetName].timer = undefined;
|
|
584
853
|
}
|
|
854
|
+
if (this.dataSets[dataSetName].heartbeatWatchdogTimer) {
|
|
855
|
+
clearTimeout(this.dataSets[dataSetName].heartbeatWatchdogTimer);
|
|
856
|
+
this.dataSets[dataSetName].heartbeatWatchdogTimer = undefined;
|
|
857
|
+
}
|
|
585
858
|
}
|
|
586
859
|
|
|
587
860
|
/**
|
|
@@ -593,49 +866,51 @@ class HashTreeParser {
|
|
|
593
866
|
* visible data sets and they require async initialization, the names of these data sets
|
|
594
867
|
* are returned in an array.
|
|
595
868
|
*
|
|
596
|
-
* @param {
|
|
597
|
-
* @param {
|
|
869
|
+
* @param {VisibleDataSetInfo[]} removedDataSets - The list of removed data sets.
|
|
870
|
+
* @param {VisibleDataSetInfo[]} addedDataSets - The list of added data sets.
|
|
598
871
|
* @param {HashTreeObject[]} updatedObjects - The list of updated hash tree objects to which changes will be added.
|
|
599
|
-
* @returns {
|
|
872
|
+
* @returns {VisibleDataSetInfo[]} list of data sets that couldn't be initialized synchronously
|
|
600
873
|
*/
|
|
601
874
|
private processVisibleDataSetChanges(
|
|
602
|
-
removedDataSets:
|
|
603
|
-
addedDataSets:
|
|
875
|
+
removedDataSets: VisibleDataSetInfo[],
|
|
876
|
+
addedDataSets: VisibleDataSetInfo[],
|
|
604
877
|
updatedObjects: HashTreeObject[]
|
|
605
|
-
):
|
|
606
|
-
const dataSetsRequiringInitialization = [];
|
|
878
|
+
): VisibleDataSetInfo[] {
|
|
879
|
+
const dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
|
|
607
880
|
|
|
608
881
|
// if a visible data set was removed, we need to tell our client that all objects from it are removed
|
|
609
882
|
const removedObjects: HashTreeObject[] = [];
|
|
610
883
|
|
|
611
884
|
removedDataSets.forEach((ds) => {
|
|
612
|
-
if (this.dataSets[ds]?.hashTree) {
|
|
613
|
-
for (let i = 0; i < this.dataSets[ds].hashTree.numLeaves; i += 1) {
|
|
885
|
+
if (this.dataSets[ds.name]?.hashTree) {
|
|
886
|
+
for (let i = 0; i < this.dataSets[ds.name].hashTree.numLeaves; i += 1) {
|
|
614
887
|
removedObjects.push(
|
|
615
|
-
...this.dataSets[ds].hashTree.getLeafData(i).map((elementId) => ({
|
|
888
|
+
...this.dataSets[ds.name].hashTree.getLeafData(i).map((elementId) => ({
|
|
616
889
|
htMeta: {
|
|
617
890
|
elementId,
|
|
618
|
-
dataSetNames: [ds],
|
|
891
|
+
dataSetNames: [ds.name],
|
|
619
892
|
},
|
|
620
893
|
data: null,
|
|
621
894
|
}))
|
|
622
895
|
);
|
|
623
896
|
}
|
|
624
897
|
|
|
625
|
-
this.deleteHashTree(ds);
|
|
898
|
+
this.deleteHashTree(ds.name);
|
|
626
899
|
}
|
|
627
900
|
});
|
|
628
|
-
this.visibleDataSets = this.visibleDataSets.filter(
|
|
901
|
+
this.visibleDataSets = this.visibleDataSets.filter(
|
|
902
|
+
(vds) => !removedDataSets.some((rds) => rds.name === vds.name)
|
|
903
|
+
);
|
|
629
904
|
updatedObjects.push(...removedObjects);
|
|
630
905
|
|
|
631
906
|
// now setup the new visible data sets
|
|
632
907
|
for (const ds of addedDataSets) {
|
|
633
|
-
const dataSetInfo = this.dataSets[ds];
|
|
908
|
+
const dataSetInfo = this.dataSets[ds.name];
|
|
634
909
|
|
|
635
910
|
if (dataSetInfo) {
|
|
636
|
-
if (this.
|
|
911
|
+
if (this.isVisibleDataSet(dataSetInfo.name)) {
|
|
637
912
|
LoggerProxy.logger.info(
|
|
638
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Data set "${ds}" is already visible, skipping`
|
|
913
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Data set "${ds.name}" is already visible, skipping`
|
|
639
914
|
);
|
|
640
915
|
|
|
641
916
|
// eslint-disable-next-line no-continue
|
|
@@ -643,10 +918,13 @@ class HashTreeParser {
|
|
|
643
918
|
}
|
|
644
919
|
|
|
645
920
|
LoggerProxy.logger.info(
|
|
646
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Adding visible data set "${ds}"`
|
|
921
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} Adding visible data set "${ds.name}"`
|
|
647
922
|
);
|
|
648
923
|
|
|
649
|
-
this.
|
|
924
|
+
if (!this.addToVisibleDataSetsList(ds)) {
|
|
925
|
+
// eslint-disable-next-line no-continue
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
650
928
|
|
|
651
929
|
const hashTree = new HashTree([], dataSetInfo.leafCount);
|
|
652
930
|
|
|
@@ -654,9 +932,13 @@ class HashTreeParser {
|
|
|
654
932
|
...dataSetInfo,
|
|
655
933
|
hashTree,
|
|
656
934
|
};
|
|
935
|
+
|
|
936
|
+
// this call is needed here for the edge case where we receive a message with new visible data sets
|
|
937
|
+
// and there are no objects belonging to these data sets in the message but we already have the info about them in this.dataSets
|
|
938
|
+
this.runSyncAlgorithm(this.dataSets[dataSetInfo.name]);
|
|
657
939
|
} else {
|
|
658
940
|
LoggerProxy.logger.info(
|
|
659
|
-
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} visible data set "${ds}" added but no info about it in our dataSets structures`
|
|
941
|
+
`HashTreeParser#processVisibleDataSetChanges --> ${this.debugId} visible data set "${ds.name}" added but no info about it in our dataSets structures`
|
|
660
942
|
);
|
|
661
943
|
// todo: add a metric here
|
|
662
944
|
dataSetsRequiringInitialization.push(ds);
|
|
@@ -670,32 +952,29 @@ class HashTreeParser {
|
|
|
670
952
|
* Adds entries to the passed in updateObjects array
|
|
671
953
|
* for the changes that result from adding and removing visible data sets.
|
|
672
954
|
*
|
|
673
|
-
* @param {
|
|
674
|
-
* @param {string[]} addedDataSets - The list of added data sets.
|
|
955
|
+
* @param {VisibleDataSetInfo[]} addedDataSets - The list of added data sets.
|
|
675
956
|
* @returns {Promise<void>}
|
|
676
957
|
*/
|
|
677
|
-
private async initializeNewVisibleDataSets(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const allDataSets = await this.
|
|
958
|
+
private async initializeNewVisibleDataSets(addedDataSets: VisibleDataSetInfo[]): Promise<void> {
|
|
959
|
+
if (this.state === 'stopped') {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const allDataSets = await this.getAllVisibleDataSetsFromLocus();
|
|
682
963
|
|
|
683
|
-
for (const ds of addedDataSets) {
|
|
684
|
-
const dataSetInfo = allDataSets.find((d) => d.name === ds);
|
|
964
|
+
for (const ds of sortByInitPriority(addedDataSets, DATA_SET_INIT_PRIORITY)) {
|
|
965
|
+
const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
|
|
685
966
|
|
|
686
967
|
LoggerProxy.logger.info(
|
|
687
|
-
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} initializing data set "${ds}"`
|
|
968
|
+
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} initializing data set "${ds.name}"`
|
|
688
969
|
);
|
|
689
970
|
|
|
690
971
|
if (!dataSetInfo) {
|
|
691
972
|
LoggerProxy.logger.warn(
|
|
692
|
-
`HashTreeParser#
|
|
973
|
+
`HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
|
|
693
974
|
);
|
|
694
975
|
} else {
|
|
695
|
-
// we're awaiting in a loop, because in practice there will be only one new data set at a time,
|
|
696
|
-
// so no point in trying to parallelize this
|
|
697
976
|
// eslint-disable-next-line no-await-in-loop
|
|
698
|
-
const updates = await this.initializeNewVisibleDataSet(dataSetInfo);
|
|
977
|
+
const updates = await this.initializeNewVisibleDataSet(ds, dataSetInfo);
|
|
699
978
|
|
|
700
979
|
this.callLocusInfoUpdateCallback(updates);
|
|
701
980
|
}
|
|
@@ -707,12 +986,13 @@ class HashTreeParser {
|
|
|
707
986
|
*
|
|
708
987
|
* @param {HashTreeMessage} message - The hash tree message containing data sets and objects to be processed
|
|
709
988
|
* @param {string} [debugText] - Optional debug text to include in logs
|
|
710
|
-
* @returns {
|
|
989
|
+
* @returns {HashTreeObject[]} list of hash tree objects that were updated as a result of processing the message
|
|
711
990
|
*/
|
|
712
|
-
private
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
991
|
+
private parseMessage(message: HashTreeMessage, debugText?: string): HashTreeObject[] {
|
|
992
|
+
if (this.state === 'stopped') {
|
|
993
|
+
return [];
|
|
994
|
+
}
|
|
995
|
+
|
|
716
996
|
const {dataSets, visibleDataSetsUrl} = message;
|
|
717
997
|
|
|
718
998
|
LoggerProxy.logger.info(
|
|
@@ -730,48 +1010,37 @@ class HashTreeParser {
|
|
|
730
1010
|
this.visibleDataSetsUrl = visibleDataSetsUrl;
|
|
731
1011
|
dataSets.forEach((dataSet) => this.updateDataSetInfo(dataSet));
|
|
732
1012
|
|
|
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
1013
|
const updatedObjects: HashTreeObject[] = [];
|
|
744
1014
|
|
|
745
1015
|
// when we detect new visible datasets, it may be that the metadata about them is not
|
|
746
1016
|
// available in the message, they will require separate async initialization
|
|
747
1017
|
let dataSetsRequiringInitialization = [];
|
|
748
1018
|
|
|
749
|
-
// first find out if there are any visible data set changes - they're signalled in
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
isSelf(object)
|
|
1019
|
+
// first find out if there are any visible data set changes - they're signalled in Metadata object updates
|
|
1020
|
+
const metadataUpdates = (message.locusStateElements || []).filter((object) =>
|
|
1021
|
+
isMetadata(object)
|
|
753
1022
|
);
|
|
754
1023
|
|
|
755
|
-
if (
|
|
756
|
-
const
|
|
1024
|
+
if (metadataUpdates.length > 0) {
|
|
1025
|
+
const updatedMetadataObjects = [];
|
|
757
1026
|
|
|
758
|
-
|
|
1027
|
+
metadataUpdates.forEach((object) => {
|
|
759
1028
|
// todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
|
|
760
1029
|
for (const dataSetName of object.htMeta.dataSetNames) {
|
|
761
1030
|
const hashTree = this.dataSets[dataSetName]?.hashTree;
|
|
762
1031
|
|
|
763
1032
|
if (hashTree && object.data) {
|
|
764
1033
|
if (hashTree.putItem(object.htMeta.elementId)) {
|
|
765
|
-
|
|
1034
|
+
updatedMetadataObjects.push(object);
|
|
766
1035
|
}
|
|
767
1036
|
}
|
|
768
1037
|
}
|
|
769
1038
|
});
|
|
770
1039
|
|
|
771
|
-
updatedObjects.push(...
|
|
1040
|
+
updatedObjects.push(...updatedMetadataObjects);
|
|
772
1041
|
|
|
773
1042
|
const {changeDetected, removedDataSets, addedDataSets} =
|
|
774
|
-
this.checkForVisibleDataSetChanges(
|
|
1043
|
+
this.checkForVisibleDataSetChanges(updatedMetadataObjects);
|
|
775
1044
|
|
|
776
1045
|
if (changeDetected) {
|
|
777
1046
|
dataSetsRequiringInitialization = this.processVisibleDataSetChanges(
|
|
@@ -782,64 +1051,49 @@ class HashTreeParser {
|
|
|
782
1051
|
}
|
|
783
1052
|
}
|
|
784
1053
|
|
|
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
|
-
|
|
1054
|
+
if (message.locusStateElements?.length > 0) {
|
|
1055
|
+
// by this point we now have this.dataSets setup for data sets from this message
|
|
1056
|
+
// and hash trees created for the new visible data sets,
|
|
1057
|
+
// so we can now process all the updates from the message
|
|
1058
|
+
dataSets.forEach((dataSet) => {
|
|
1059
|
+
if (this.dataSets[dataSet.name]) {
|
|
1060
|
+
const {hashTree} = this.dataSets[dataSet.name];
|
|
1061
|
+
|
|
1062
|
+
if (hashTree) {
|
|
1063
|
+
const locusStateElementsForThisSet = message.locusStateElements.filter((object) =>
|
|
1064
|
+
object.htMeta.dataSetNames.includes(dataSet.name)
|
|
1065
|
+
);
|
|
1066
|
+
|
|
1067
|
+
const appliedChangesList = hashTree.updateItems(
|
|
1068
|
+
locusStateElementsForThisSet.map((object) =>
|
|
1069
|
+
object.data
|
|
1070
|
+
? {operation: 'update', item: object.htMeta.elementId}
|
|
1071
|
+
: {operation: 'remove', item: object.htMeta.elementId}
|
|
1072
|
+
)
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
zip(appliedChangesList, locusStateElementsForThisSet).forEach(
|
|
1076
|
+
([changeApplied, object]) => {
|
|
1077
|
+
if (changeApplied) {
|
|
1078
|
+
// add to updatedObjects so that our locus DTO will get updated with the new object
|
|
1079
|
+
updatedObjects.push(object);
|
|
810
1080
|
}
|
|
811
|
-
// add to updatedObjects so that our locus DTO will get updated with the new object
|
|
812
|
-
updatedObjects.push(object);
|
|
813
1081
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1082
|
+
);
|
|
1083
|
+
} else {
|
|
1084
|
+
LoggerProxy.logger.info(
|
|
1085
|
+
`Locus-info:index#parseMessage --> ${this.debugId} unexpected (not visible) dataSet ${dataSet.name} received in hash tree message`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
820
1088
|
}
|
|
821
|
-
}
|
|
822
1089
|
|
|
823
|
-
if (!isRosterDropped) {
|
|
824
1090
|
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};
|
|
1091
|
+
});
|
|
836
1092
|
}
|
|
837
1093
|
|
|
838
1094
|
if (dataSetsRequiringInitialization.length > 0) {
|
|
839
1095
|
// there are some data sets that we need to initialize asynchronously
|
|
840
|
-
|
|
841
|
-
this.initializeNewVisibleDataSets(message, dataSetsRequiringInitialization);
|
|
842
|
-
});
|
|
1096
|
+
this.queueInitForNewVisibleDataSets(dataSetsRequiringInitialization);
|
|
843
1097
|
}
|
|
844
1098
|
|
|
845
1099
|
if (updatedObjects.length === 0) {
|
|
@@ -848,7 +1102,7 @@ class HashTreeParser {
|
|
|
848
1102
|
);
|
|
849
1103
|
}
|
|
850
1104
|
|
|
851
|
-
return
|
|
1105
|
+
return updatedObjects;
|
|
852
1106
|
}
|
|
853
1107
|
|
|
854
1108
|
/**
|
|
@@ -858,13 +1112,32 @@ class HashTreeParser {
|
|
|
858
1112
|
* @param {string} [debugText] - Optional debug text to include in logs
|
|
859
1113
|
* @returns {void}
|
|
860
1114
|
*/
|
|
861
|
-
|
|
862
|
-
if (
|
|
1115
|
+
handleMessage(message: HashTreeMessage, debugText?: string) {
|
|
1116
|
+
if (this.state === 'stopped') {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (message.heartbeatIntervalMs) {
|
|
1121
|
+
this.heartbeatIntervalMs = message.heartbeatIntervalMs;
|
|
1122
|
+
}
|
|
1123
|
+
if (this.isEndMessage(message)) {
|
|
1124
|
+
LoggerProxy.logger.info(
|
|
1125
|
+
`HashTreeParser#handleMessage --> ${this.debugId} received sentinel END MEETING message`
|
|
1126
|
+
);
|
|
1127
|
+
this.stopAllTimers();
|
|
1128
|
+
|
|
1129
|
+
this.callLocusInfoUpdateCallback({updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1130
|
+
} else if (message.locusStateElements === undefined) {
|
|
863
1131
|
this.handleRootHashHeartBeatMessage(message);
|
|
1132
|
+
this.resetHeartbeatWatchdogs(message.dataSets);
|
|
864
1133
|
} else {
|
|
865
|
-
const
|
|
1134
|
+
const updatedObjects = this.parseMessage(message, debugText);
|
|
866
1135
|
|
|
867
|
-
this.
|
|
1136
|
+
this.resetHeartbeatWatchdogs(message.dataSets);
|
|
1137
|
+
this.callLocusInfoUpdateCallback({
|
|
1138
|
+
updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1139
|
+
updatedObjects,
|
|
1140
|
+
});
|
|
868
1141
|
}
|
|
869
1142
|
}
|
|
870
1143
|
|
|
@@ -878,9 +1151,55 @@ class HashTreeParser {
|
|
|
878
1151
|
updateType: LocusInfoUpdateType;
|
|
879
1152
|
updatedObjects?: HashTreeObject[];
|
|
880
1153
|
}) {
|
|
1154
|
+
if (this.state === 'stopped') {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
881
1158
|
const {updateType, updatedObjects} = updates;
|
|
882
1159
|
|
|
883
|
-
if (updateType
|
|
1160
|
+
if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
|
|
1161
|
+
// Filter out updates for objects that already have a higher version in their datasets,
|
|
1162
|
+
// or removals for objects that still exist in any of their datasets
|
|
1163
|
+
const filteredUpdates = updatedObjects.filter((object) => {
|
|
1164
|
+
const {elementId} = object.htMeta;
|
|
1165
|
+
const {type, id, version} = elementId;
|
|
1166
|
+
|
|
1167
|
+
// Check all datasets
|
|
1168
|
+
for (const dataSetName of Object.keys(this.dataSets)) {
|
|
1169
|
+
const dataSet = this.dataSets[dataSetName];
|
|
1170
|
+
|
|
1171
|
+
// only visible datasets have hash trees set
|
|
1172
|
+
if (dataSet?.hashTree) {
|
|
1173
|
+
const existingVersion = dataSet.hashTree.getItemVersion(id, type);
|
|
1174
|
+
if (existingVersion !== undefined) {
|
|
1175
|
+
if (object.data) {
|
|
1176
|
+
// For updates: filter out if any dataset has a higher version
|
|
1177
|
+
if (existingVersion > version) {
|
|
1178
|
+
LoggerProxy.logger.info(
|
|
1179
|
+
`HashTreeParser#callLocusInfoUpdateCallback --> ${this.debugId} Filtering out update for ${type}:${id} v${version} because dataset "${dataSetName}" has v${existingVersion}`
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
} else if (existingVersion >= version) {
|
|
1185
|
+
// For removals: filter out if the object still exists in any dataset
|
|
1186
|
+
LoggerProxy.logger.info(
|
|
1187
|
+
`HashTreeParser#callLocusInfoUpdateCallback --> ${this.debugId} Filtering out removal for ${type}:${id} v${version} because dataset "${dataSetName}" still has v${existingVersion}`
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return true;
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
if (filteredUpdates.length > 0) {
|
|
1200
|
+
this.locusInfoUpdateCallback(updateType, {updatedObjects: filteredUpdates});
|
|
1201
|
+
}
|
|
1202
|
+
} else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
|
|
884
1203
|
this.locusInfoUpdateCallback(updateType, {updatedObjects});
|
|
885
1204
|
}
|
|
886
1205
|
}
|
|
@@ -899,6 +1218,88 @@ class HashTreeParser {
|
|
|
899
1218
|
return Math.round(randomValue ** exponent * maxMs);
|
|
900
1219
|
}
|
|
901
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* Performs a sync for the given data set.
|
|
1223
|
+
*
|
|
1224
|
+
* @param {InternalDataSet} dataSet - The data set to sync
|
|
1225
|
+
* @param {string} rootHash - Our current root hash for this data set
|
|
1226
|
+
* @param {string} reason - The reason for the sync (used for logging)
|
|
1227
|
+
* @returns {Promise<void>}
|
|
1228
|
+
*/
|
|
1229
|
+
private async performSync(
|
|
1230
|
+
dataSet: InternalDataSet,
|
|
1231
|
+
rootHash: string,
|
|
1232
|
+
reason: string
|
|
1233
|
+
): Promise<void> {
|
|
1234
|
+
if (!dataSet.hashTree) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
try {
|
|
1239
|
+
LoggerProxy.logger.info(
|
|
1240
|
+
`HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
|
|
1244
|
+
|
|
1245
|
+
if (dataSet.leafCount !== 1) {
|
|
1246
|
+
let receivedHashes;
|
|
1247
|
+
|
|
1248
|
+
try {
|
|
1249
|
+
// request hashes from sender
|
|
1250
|
+
const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
|
|
1251
|
+
dataSet.name,
|
|
1252
|
+
rootHash
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
receivedHashes = hashes;
|
|
1256
|
+
|
|
1257
|
+
dataSet.hashTree.resize(latestDataSetInfo.leafCount);
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
if (error.statusCode === 409) {
|
|
1260
|
+
// this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
|
|
1261
|
+
LoggerProxy.logger.info(
|
|
1262
|
+
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// identify mismatched leaves
|
|
1271
|
+
const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
|
|
1272
|
+
|
|
1273
|
+
mismatchedLeaveIndexes.forEach((index) => {
|
|
1274
|
+
mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
|
|
1275
|
+
});
|
|
1276
|
+
} else {
|
|
1277
|
+
mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
|
|
1278
|
+
}
|
|
1279
|
+
// request sync for mismatched leaves
|
|
1280
|
+
if (Object.keys(mismatchedLeavesData).length > 0) {
|
|
1281
|
+
const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
|
|
1282
|
+
|
|
1283
|
+
// sync API may return nothing (in that case data will arrive via messages)
|
|
1284
|
+
// or it may return a response in the same format as messages
|
|
1285
|
+
if (syncResponse) {
|
|
1286
|
+
this.handleMessage(syncResponse, 'via sync API');
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
if (error instanceof MeetingEndedError) {
|
|
1291
|
+
this.callLocusInfoUpdateCallback({
|
|
1292
|
+
updateType: LocusInfoUpdateType.MEETING_ENDED,
|
|
1293
|
+
});
|
|
1294
|
+
} else {
|
|
1295
|
+
LoggerProxy.logger.warn(
|
|
1296
|
+
`HashTreeParser#performSync --> ${this.debugId} error during sync for data set "${dataSet.name}":`,
|
|
1297
|
+
error
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
902
1303
|
/**
|
|
903
1304
|
* Runs the sync algorithm for the given data set.
|
|
904
1305
|
*
|
|
@@ -957,55 +1358,11 @@ class HashTreeParser {
|
|
|
957
1358
|
const rootHash = dataSet.hashTree.getRootHash();
|
|
958
1359
|
|
|
959
1360
|
if (dataSet.root !== rootHash) {
|
|
960
|
-
|
|
961
|
-
|
|
1361
|
+
await this.performSync(
|
|
1362
|
+
dataSet,
|
|
1363
|
+
rootHash,
|
|
1364
|
+
`Root hash mismatch: received=${dataSet.root}, ours=${rootHash}`
|
|
962
1365
|
);
|
|
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
1366
|
} else {
|
|
1010
1367
|
LoggerProxy.logger.info(
|
|
1011
1368
|
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} "${dataSet.name}" root hash matching: ${rootHash}, version=${dataSet.version}`
|
|
@@ -1019,6 +1376,52 @@ class HashTreeParser {
|
|
|
1019
1376
|
}
|
|
1020
1377
|
}
|
|
1021
1378
|
|
|
1379
|
+
/**
|
|
1380
|
+
* Resets the heartbeat watchdog timers for the specified data sets. Each data set has its own
|
|
1381
|
+
* watchdog timer that monitors whether heartbeats are being received within the expected interval.
|
|
1382
|
+
* If a heartbeat is not received for a specific data set within heartbeatIntervalMs plus
|
|
1383
|
+
* a backoff-calculated time, the sync algorithm is initiated for that data set
|
|
1384
|
+
*
|
|
1385
|
+
* @param {Array<DataSet>} receivedDataSets - The data sets from the received message for which watchdog timers should be reset
|
|
1386
|
+
* @returns {void}
|
|
1387
|
+
*/
|
|
1388
|
+
private resetHeartbeatWatchdogs(receivedDataSets: Array<DataSet>): void {
|
|
1389
|
+
if (!this.heartbeatIntervalMs) {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
for (const receivedDataSet of receivedDataSets) {
|
|
1394
|
+
const dataSet = this.dataSets[receivedDataSet.name];
|
|
1395
|
+
|
|
1396
|
+
if (!dataSet?.hashTree) {
|
|
1397
|
+
// eslint-disable-next-line no-continue
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (dataSet.heartbeatWatchdogTimer) {
|
|
1402
|
+
clearTimeout(dataSet.heartbeatWatchdogTimer);
|
|
1403
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const backoffTime = this.getWeightedBackoffTime(dataSet.backoff);
|
|
1407
|
+
const delay = this.heartbeatIntervalMs + backoffTime;
|
|
1408
|
+
|
|
1409
|
+
dataSet.heartbeatWatchdogTimer = setTimeout(async () => {
|
|
1410
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1411
|
+
|
|
1412
|
+
LoggerProxy.logger.warn(
|
|
1413
|
+
`HashTreeParser#resetHeartbeatWatchdogs --> ${this.debugId} Heartbeat watchdog fired for data set "${dataSet.name}" - no heartbeat received within expected interval, initiating sync`
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
await this.performSync(
|
|
1417
|
+
dataSet,
|
|
1418
|
+
dataSet.hashTree.getRootHash(),
|
|
1419
|
+
`heartbeat watchdog expired`
|
|
1420
|
+
);
|
|
1421
|
+
}, delay);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1022
1425
|
/**
|
|
1023
1426
|
* Stops all timers for the data sets to prevent any further sync attempts.
|
|
1024
1427
|
* @returns {void}
|
|
@@ -1029,15 +1432,100 @@ class HashTreeParser {
|
|
|
1029
1432
|
clearTimeout(dataSet.timer);
|
|
1030
1433
|
dataSet.timer = undefined;
|
|
1031
1434
|
}
|
|
1435
|
+
if (dataSet.heartbeatWatchdogTimer) {
|
|
1436
|
+
clearTimeout(dataSet.heartbeatWatchdogTimer);
|
|
1437
|
+
dataSet.heartbeatWatchdogTimer = undefined;
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Stops the HashTreeParser, preventing it from processing any further messages and clearing all timers.
|
|
1444
|
+
* It also clears all the hash trees, so if the parser is resumed later, it will need to do a sync
|
|
1445
|
+
* to be up-to-date.
|
|
1446
|
+
* @returns {void}
|
|
1447
|
+
*/
|
|
1448
|
+
public stop() {
|
|
1449
|
+
LoggerProxy.logger.info(
|
|
1450
|
+
`HashTreeParser#stop --> ${this.debugId} Stopping HashTreeParser, clearing timers and hash trees`
|
|
1451
|
+
);
|
|
1452
|
+
this.stopAllTimers();
|
|
1453
|
+
Object.values(this.dataSets).forEach((dataSet) => {
|
|
1454
|
+
dataSet.hashTree = undefined;
|
|
1032
1455
|
});
|
|
1456
|
+
this.visibleDataSets = [];
|
|
1457
|
+
this.state = 'stopped';
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Resumes the HashTreeParser that was previously stopped.
|
|
1462
|
+
* @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
|
|
1463
|
+
* @returns {void}
|
|
1464
|
+
*/
|
|
1465
|
+
public resume(message: HashTreeMessage) {
|
|
1466
|
+
// check that message contains metadata with visible data sets - this is essential to be able to resume
|
|
1467
|
+
const metadataObject = message.locusStateElements?.find((el) => isMetadata(el));
|
|
1468
|
+
|
|
1469
|
+
if (!metadataObject?.data?.visibleDataSets) {
|
|
1470
|
+
LoggerProxy.logger.warn(
|
|
1471
|
+
`HashTreeParser#resume --> ${this.debugId} Cannot resume HashTreeParser because the message is missing metadata with visible data sets info`
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
this.setVisibleDataSets(
|
|
1477
|
+
metadataObject.data.visibleDataSets as VisibleDataSetInfo[],
|
|
1478
|
+
message.dataSets
|
|
1479
|
+
);
|
|
1480
|
+
|
|
1481
|
+
this.dataSets = {};
|
|
1482
|
+
|
|
1483
|
+
for (const dataSet of message.dataSets) {
|
|
1484
|
+
const {name, leafCount} = dataSet;
|
|
1485
|
+
|
|
1486
|
+
this.dataSets[name] = {
|
|
1487
|
+
...dataSet,
|
|
1488
|
+
hashTree: this.isVisibleDataSet(name) ? new HashTree([], leafCount) : undefined,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
LoggerProxy.logger.info(
|
|
1492
|
+
`HashTreeParser#resume --> ${
|
|
1493
|
+
this.debugId
|
|
1494
|
+
} Resuming HashTreeParser with data sets: ${Object.keys(this.dataSets).join(
|
|
1495
|
+
', '
|
|
1496
|
+
)}, visible data sets: ${this.visibleDataSets.map((ds) => ds.name).join(', ')}`
|
|
1497
|
+
);
|
|
1498
|
+
this.state = 'active';
|
|
1499
|
+
|
|
1500
|
+
this.handleMessage(message, 'on resume');
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
|
|
1504
|
+
const isValidDataSetForSentinel =
|
|
1505
|
+
dataSetName === undefined ||
|
|
1506
|
+
PossibleSentinelMessageDataSetNames.includes(dataSetName.toLowerCase());
|
|
1507
|
+
|
|
1508
|
+
if (
|
|
1509
|
+
((error.statusCode === 409 && error.body?.errorCode === 2403004) ||
|
|
1510
|
+
error.statusCode === 404) &&
|
|
1511
|
+
isValidDataSetForSentinel
|
|
1512
|
+
) {
|
|
1513
|
+
LoggerProxy.logger.info(
|
|
1514
|
+
`HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode} for data set "${dataSetName}", indicating that the meeting has ended`
|
|
1515
|
+
);
|
|
1516
|
+
this.stopAllTimers();
|
|
1517
|
+
|
|
1518
|
+
throw new MeetingEndedError();
|
|
1519
|
+
}
|
|
1033
1520
|
}
|
|
1034
1521
|
|
|
1035
1522
|
/**
|
|
1036
1523
|
* Gets the current hashes from the locus for a specific data set.
|
|
1037
1524
|
* @param {string} dataSetName
|
|
1525
|
+
* @param {string} currentRootHash
|
|
1038
1526
|
* @returns {string[]}
|
|
1039
1527
|
*/
|
|
1040
|
-
private getHashesFromLocus(dataSetName: string) {
|
|
1528
|
+
private getHashesFromLocus(dataSetName: string, currentRootHash: string) {
|
|
1041
1529
|
LoggerProxy.logger.info(
|
|
1042
1530
|
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Requesting hashes for data set "${dataSetName}"`
|
|
1043
1531
|
);
|
|
@@ -1049,6 +1537,9 @@ class HashTreeParser {
|
|
|
1049
1537
|
return this.webexRequest({
|
|
1050
1538
|
method: HTTP_VERBS.GET,
|
|
1051
1539
|
uri: url,
|
|
1540
|
+
qs: {
|
|
1541
|
+
rootHash: currentRootHash,
|
|
1542
|
+
},
|
|
1052
1543
|
})
|
|
1053
1544
|
.then((response) => {
|
|
1054
1545
|
const hashes = response.body?.hashes as string[] | undefined;
|
|
@@ -1078,6 +1569,8 @@ class HashTreeParser {
|
|
|
1078
1569
|
`HashTreeParser#getHashesFromLocus --> ${this.debugId} Error ${error.statusCode} fetching hashes for data set "${dataSetName}":`,
|
|
1079
1570
|
error
|
|
1080
1571
|
);
|
|
1572
|
+
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1573
|
+
|
|
1081
1574
|
throw error;
|
|
1082
1575
|
});
|
|
1083
1576
|
}
|
|
@@ -1110,9 +1603,14 @@ class HashTreeParser {
|
|
|
1110
1603
|
});
|
|
1111
1604
|
});
|
|
1112
1605
|
|
|
1606
|
+
const ourCurrentRootHash = dataSet.hashTree ? dataSet.hashTree.getRootHash() : EMPTY_HASH;
|
|
1607
|
+
|
|
1113
1608
|
return this.webexRequest({
|
|
1114
1609
|
method: HTTP_VERBS.POST,
|
|
1115
1610
|
uri: url,
|
|
1611
|
+
qs: {
|
|
1612
|
+
rootHash: ourCurrentRootHash,
|
|
1613
|
+
},
|
|
1116
1614
|
body,
|
|
1117
1615
|
})
|
|
1118
1616
|
.then((resp) => {
|
|
@@ -1135,6 +1633,8 @@ class HashTreeParser {
|
|
|
1135
1633
|
`HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Error ${error.statusCode} sending sync request for data set "${dataSet.name}":`,
|
|
1136
1634
|
error
|
|
1137
1635
|
);
|
|
1636
|
+
this.checkForSentinelHttpResponse(error, dataSet.name);
|
|
1637
|
+
|
|
1138
1638
|
throw error;
|
|
1139
1639
|
});
|
|
1140
1640
|
}
|