@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.
Files changed (138) hide show
  1. package/dist/aiEnableRequest/index.js +181 -0
  2. package/dist/aiEnableRequest/index.js.map +1 -0
  3. package/dist/aiEnableRequest/utils.js +36 -0
  4. package/dist/aiEnableRequest/utils.js.map +1 -0
  5. package/dist/annotation/index.js +3 -3
  6. package/dist/annotation/index.js.map +1 -1
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/index.js +1 -1
  9. package/dist/config.js +5 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +21 -4
  12. package/dist/constants.js.map +1 -1
  13. package/dist/hashTree/hashTree.js +18 -0
  14. package/dist/hashTree/hashTree.js.map +1 -1
  15. package/dist/hashTree/hashTreeParser.js +603 -266
  16. package/dist/hashTree/hashTreeParser.js.map +1 -1
  17. package/dist/hashTree/types.js +4 -2
  18. package/dist/hashTree/types.js.map +1 -1
  19. package/dist/hashTree/utils.js +10 -0
  20. package/dist/hashTree/utils.js.map +1 -1
  21. package/dist/index.js +11 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/interceptors/constant.js +12 -0
  24. package/dist/interceptors/constant.js.map +1 -0
  25. package/dist/interceptors/dataChannelAuthToken.js +233 -0
  26. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  27. package/dist/interceptors/index.js +7 -0
  28. package/dist/interceptors/index.js.map +1 -1
  29. package/dist/interpretation/index.js +2 -2
  30. package/dist/interpretation/index.js.map +1 -1
  31. package/dist/interpretation/siLanguage.js +1 -1
  32. package/dist/locus-info/index.js +88 -44
  33. package/dist/locus-info/index.js.map +1 -1
  34. package/dist/locus-info/selfUtils.js +1 -0
  35. package/dist/locus-info/selfUtils.js.map +1 -1
  36. package/dist/locus-info/types.js.map +1 -1
  37. package/dist/media/MediaConnectionAwaiter.js +57 -1
  38. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  39. package/dist/media/properties.js +4 -2
  40. package/dist/media/properties.js.map +1 -1
  41. package/dist/meeting/in-meeting-actions.js +3 -1
  42. package/dist/meeting/in-meeting-actions.js.map +1 -1
  43. package/dist/meeting/index.js +149 -42
  44. package/dist/meeting/index.js.map +1 -1
  45. package/dist/meeting/request.js +50 -0
  46. package/dist/meeting/request.js.map +1 -1
  47. package/dist/meeting/request.type.js.map +1 -1
  48. package/dist/meeting/util.js +121 -2
  49. package/dist/meeting/util.js.map +1 -1
  50. package/dist/meetings/index.js +78 -36
  51. package/dist/meetings/index.js.map +1 -1
  52. package/dist/member/index.js +10 -0
  53. package/dist/member/index.js.map +1 -1
  54. package/dist/member/util.js +10 -0
  55. package/dist/member/util.js.map +1 -1
  56. package/dist/metrics/constants.js +2 -1
  57. package/dist/metrics/constants.js.map +1 -1
  58. package/dist/multistream/mediaRequestManager.js +1 -1
  59. package/dist/multistream/mediaRequestManager.js.map +1 -1
  60. package/dist/multistream/remoteMediaManager.js +11 -0
  61. package/dist/multistream/remoteMediaManager.js.map +1 -1
  62. package/dist/reactions/reactions.type.js.map +1 -1
  63. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  64. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  65. package/dist/types/config.d.ts +3 -0
  66. package/dist/types/constants.d.ts +16 -0
  67. package/dist/types/hashTree/hashTree.d.ts +7 -0
  68. package/dist/types/hashTree/hashTreeParser.d.ts +83 -12
  69. package/dist/types/hashTree/types.d.ts +3 -0
  70. package/dist/types/hashTree/utils.d.ts +6 -0
  71. package/dist/types/index.d.ts +1 -0
  72. package/dist/types/interceptors/constant.d.ts +5 -0
  73. package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
  74. package/dist/types/interceptors/index.d.ts +2 -1
  75. package/dist/types/locus-info/index.d.ts +9 -2
  76. package/dist/types/locus-info/types.d.ts +1 -0
  77. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  78. package/dist/types/media/properties.d.ts +2 -1
  79. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  80. package/dist/types/meeting/index.d.ts +28 -5
  81. package/dist/types/meeting/request.d.ts +16 -1
  82. package/dist/types/meeting/request.type.d.ts +5 -0
  83. package/dist/types/meeting/util.d.ts +29 -0
  84. package/dist/types/meetings/index.d.ts +4 -2
  85. package/dist/types/member/index.d.ts +1 -0
  86. package/dist/types/member/util.d.ts +5 -0
  87. package/dist/types/metrics/constants.d.ts +1 -0
  88. package/dist/types/reactions/reactions.type.d.ts +1 -0
  89. package/dist/webinar/index.js +1 -1
  90. package/package.json +22 -22
  91. package/src/aiEnableRequest/README.md +84 -0
  92. package/src/aiEnableRequest/index.ts +164 -0
  93. package/src/aiEnableRequest/utils.ts +25 -0
  94. package/src/annotation/index.ts +7 -4
  95. package/src/config.ts +3 -0
  96. package/src/constants.ts +20 -0
  97. package/src/hashTree/hashTree.ts +17 -0
  98. package/src/hashTree/hashTreeParser.ts +525 -188
  99. package/src/hashTree/types.ts +4 -0
  100. package/src/hashTree/utils.ts +9 -0
  101. package/src/index.ts +8 -1
  102. package/src/interceptors/constant.ts +6 -0
  103. package/src/interceptors/dataChannelAuthToken.ts +142 -0
  104. package/src/interceptors/index.ts +2 -1
  105. package/src/interpretation/index.ts +2 -2
  106. package/src/locus-info/index.ts +123 -35
  107. package/src/locus-info/selfUtils.ts +1 -0
  108. package/src/locus-info/types.ts +1 -0
  109. package/src/media/MediaConnectionAwaiter.ts +41 -1
  110. package/src/media/properties.ts +3 -1
  111. package/src/meeting/in-meeting-actions.ts +4 -0
  112. package/src/meeting/index.ts +116 -22
  113. package/src/meeting/request.ts +42 -0
  114. package/src/meeting/request.type.ts +6 -0
  115. package/src/meeting/util.ts +148 -1
  116. package/src/meetings/index.ts +94 -9
  117. package/src/member/index.ts +10 -0
  118. package/src/member/util.ts +12 -0
  119. package/src/metrics/constants.ts +1 -0
  120. package/src/multistream/mediaRequestManager.ts +1 -1
  121. package/src/multistream/remoteMediaManager.ts +13 -0
  122. package/src/reactions/reactions.type.ts +1 -0
  123. package/test/unit/spec/aiEnableRequest/index.ts +953 -0
  124. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  125. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  126. package/test/unit/spec/hashTree/hashTreeParser.ts +1594 -162
  127. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
  128. package/test/unit/spec/locus-info/index.js +173 -45
  129. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  130. package/test/unit/spec/media/properties.ts +12 -3
  131. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -2
  132. package/test/unit/spec/meeting/index.js +497 -81
  133. package/test/unit/spec/meeting/request.js +64 -0
  134. package/test/unit/spec/meeting/utils.js +369 -22
  135. package/test/unit/spec/meetings/index.js +550 -10
  136. package/test/unit/spec/member/index.js +28 -4
  137. package/test/unit/spec/member/util.js +65 -27
  138. 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: string[];
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.visibleDataSets = locus?.self?.visibleDataSets || [];
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 (this.visibleDataSets.length === 0) {
123
+ if (options.metadata?.visibleDataSets?.length === 0) {
91
124
  LoggerProxy.logger.warn(
92
- `HashTreeParser#constructor --> ${this.debugId} No visibleDataSets found in locus.self`
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.visibleDataSets.includes(name)
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 {DataSet} dataSet The new data set to be added
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
- dataSet: DataSet
201
+ visibleDataSetInfo: VisibleDataSetInfo,
202
+ dataSetInfo: DataSet
126
203
  ): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
127
- if (this.visibleDataSets.includes(dataSet.name)) {
204
+ if (this.isVisibleDataSet(dataSetInfo.name)) {
128
205
  LoggerProxy.logger.info(
129
- `HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSet.name}" already exists, skipping init`
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 "${dataSet.name}"`
213
+ `HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Adding visible data set "${dataSetInfo.name}"`
137
214
  );
138
215
 
139
- this.visibleDataSets.push(dataSet.name);
216
+ if (!this.addToVisibleDataSetsList(visibleDataSetInfo)) {
217
+ return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
218
+ }
140
219
 
141
- const hashTree = new HashTree([], dataSet.leafCount);
220
+ const hashTree = new HashTree([], dataSetInfo.leafCount);
142
221
 
143
- this.dataSets[dataSet.name] = {
144
- ...dataSet,
222
+ this.dataSets[dataSetInfo.name] = {
223
+ ...dataSetInfo,
145
224
  hashTree,
146
225
  };
147
226
 
148
- return this.sendInitializationSyncRequestToLocus(dataSet.name, 'new visible data set');
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 the data sets
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 getAllDataSetsMetadata(url) {
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: url,
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=${message.visibleDataSetsUrl}`
303
+ `HashTreeParser#initializeFromMessage --> ${this.debugId} visibleDataSetsUrl=${this.visibleDataSetsUrl}`
216
304
  );
217
- const dataSets = await this.getAllDataSetsMetadata(message.visibleDataSetsUrl);
305
+ const visibleDataSets = await this.getAllVisibleDataSetsFromLocus();
218
306
 
219
- await this.initializeDataSets(dataSets, 'initialization from message');
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: ${locus.links.resources.visibleDataSets.url}`
330
+ `HashTreeParser#initializeFromGetLociResponse --> ${this.debugId} visibleDataSets url: ${this.visibleDataSetsUrl}`
242
331
  );
243
332
 
244
- const dataSets = await this.getAllDataSetsMetadata(locus.links.resources.visibleDataSets.url);
333
+ const visibleDataSets = await this.getAllVisibleDataSetsFromLocus();
245
334
 
246
- await this.initializeDataSets(dataSets, 'initialization from GET /loci response');
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[]} dataSets Array of DataSet objects to initialize
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(dataSets: Array<DataSet>, debugText: string) {
345
+ private async initializeDataSets(visibleDataSets: Array<DataSet>, debugText: string) {
257
346
  const updatedObjects: HashTreeObject[] = [];
258
347
 
259
- for (const dataSet of dataSets) {
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.visibleDataSets.includes(name) && !this.dataSets[name].hashTree) {
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: {type: ObjectType; id: number; version: number; data?: any} = {
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.warn(
622
+ LoggerProxy.logger.info(
435
623
  `HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets`
436
624
  );
437
- }
438
- for (const dataSet of dataSets) {
439
- this.updateDataSetInfo(dataSet);
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#handleMessage --> ${this.debugId} created entry for "${receivedDataSet.name}" dataset: version=${receivedDataSet.version}, root=${receivedDataSet.root}`
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#handleMessage --> ${this.debugId} updated "${receivedDataSet.name}" to version=${receivedDataSet.version}, root=${receivedDataSet.root}`
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: string[] = [];
541
- let addedDataSets: string[] = [];
731
+ let removedDataSets: VisibleDataSetInfo[] = [];
732
+ let addedDataSets: VisibleDataSetInfo[] = [];
542
733
 
543
- // visibleDataSets can only be changed by self object updates
734
+ // visibleDataSets can only be changed by Metadata object updates
544
735
  updatedObjects.forEach((object) => {
545
- // todo: in the future visibleDataSets will be in "Metadata" object, not in "self"
546
- if (isSelf(object) && object.data?.visibleDataSets) {
547
- const newVisibleDataSets = object.data.visibleDataSets;
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((ds) => !newVisibleDataSets.includes(ds));
550
- addedDataSets = newVisibleDataSets.filter((ds) => !this.visibleDataSets.includes(ds));
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.join(
557
- ', '
558
- )}, added: ${addedDataSets.join(', ')}`
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 timer as there is no hash tree anymore to sync
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 {string[]} removedDataSets - The list of removed data sets.
597
- * @param {string[]} addedDataSets - The list of added data sets.
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 {string[]} names of data sets that couldn't be initialized synchronously
799
+ * @returns {VisibleDataSetInfo[]} list of data sets that couldn't be initialized synchronously
600
800
  */
601
801
  private processVisibleDataSetChanges(
602
- removedDataSets: string[],
603
- addedDataSets: string[],
802
+ removedDataSets: VisibleDataSetInfo[],
803
+ addedDataSets: VisibleDataSetInfo[],
604
804
  updatedObjects: HashTreeObject[]
605
- ): string[] {
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((vds) => !removedDataSets.includes(vds));
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.visibleDataSets.includes(dataSetInfo.name)) {
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.visibleDataSets.push(ds);
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 {HashTreeMessage} message - The hash tree message that triggered the visible data set changes.
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
- message: HashTreeMessage,
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 SELF object updates
750
- const selfUpdates = (message.locusStateElements || []).filter((object) =>
751
- // todo: SPARK-744859 once Locus supports it, we will filter for "Metadata" type here instead of "self"
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 (selfUpdates.length > 0) {
756
- const updatedSelfObjects = [];
955
+ if (metadataUpdates.length > 0) {
956
+ const updatedMetadataObjects = [];
757
957
 
758
- selfUpdates.forEach((object) => {
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
- updatedSelfObjects.push(object);
965
+ updatedMetadataObjects.push(object);
766
966
  }
767
967
  }
768
968
  }
769
969
  });
770
970
 
771
- updatedObjects.push(...updatedSelfObjects);
971
+ updatedObjects.push(...updatedMetadataObjects);
772
972
 
773
973
  const {changeDetected, removedDataSets, addedDataSets} =
774
- this.checkForVisibleDataSetChanges(updatedSelfObjects);
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
- // by this point we now have this.dataSets setup for data sets from this message
786
- // and hash trees created for the new visible data sets,
787
- // so we can now process all the updates from the message
788
- dataSets.forEach((dataSet) => {
789
- if (this.dataSets[dataSet.name]) {
790
- const {hashTree} = this.dataSets[dataSet.name];
791
-
792
- if (hashTree) {
793
- const locusStateElementsForThisSet = message.locusStateElements.filter((object) =>
794
- object.htMeta.dataSetNames.includes(dataSet.name)
795
- );
796
-
797
- const appliedChangesList = hashTree.updateItems(
798
- locusStateElementsForThisSet.map((object) =>
799
- object.data
800
- ? {operation: 'update', item: object.htMeta.elementId}
801
- : {operation: 'remove', item: object.htMeta.elementId}
802
- )
803
- );
804
-
805
- zip(appliedChangesList, locusStateElementsForThisSet).forEach(
806
- ([changeApplied, object]) => {
807
- if (changeApplied) {
808
- if (isSelf(object) && !object.data) {
809
- isRosterDropped = true;
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
- } else {
817
- LoggerProxy.logger.info(
818
- `Locus-info:index#parseMessage --> ${this.debugId} unexpected (not visible) dataSet ${dataSet.name} received in hash tree message`
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
- if (!isRosterDropped) {
824
- this.runSyncAlgorithm(dataSet);
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(message, dataSetsRequiringInitialization);
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 !== LocusInfoUpdateType.OBJECTS_UPDATED || updatedObjects?.length > 0) {
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
- LoggerProxy.logger.info(
961
- `HashTreeParser#runSyncAlgorithm --> ${this.debugId} Root hash mismatch: received=${dataSet.root}, ours=${rootHash}, syncing data set "${dataSet.name}"`
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) => {