@webex/plugin-meetings 3.8.1-web-workers-keepalive.1 → 3.9.0-webinar5k.1

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 (87) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +8 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/hashTree/constants.js +23 -0
  6. package/dist/hashTree/constants.js.map +1 -0
  7. package/dist/hashTree/hashTree.js +516 -0
  8. package/dist/hashTree/hashTree.js.map +1 -0
  9. package/dist/hashTree/hashTreeParser.js +521 -0
  10. package/dist/hashTree/hashTreeParser.js.map +1 -0
  11. package/dist/interpretation/index.js +1 -1
  12. package/dist/interpretation/siLanguage.js +1 -1
  13. package/dist/locus-info/index.js +301 -59
  14. package/dist/locus-info/index.js.map +1 -1
  15. package/dist/meeting/brbState.js +14 -12
  16. package/dist/meeting/brbState.js.map +1 -1
  17. package/dist/meeting/index.js +110 -12
  18. package/dist/meeting/index.js.map +1 -1
  19. package/dist/meeting/muteState.js +2 -5
  20. package/dist/meeting/muteState.js.map +1 -1
  21. package/dist/meeting/request.js +19 -0
  22. package/dist/meeting/request.js.map +1 -1
  23. package/dist/meeting/request.type.js.map +1 -1
  24. package/dist/meeting/util.js +8 -11
  25. package/dist/meeting/util.js.map +1 -1
  26. package/dist/meetings/index.js +6 -2
  27. package/dist/meetings/index.js.map +1 -1
  28. package/dist/member/index.js.map +1 -1
  29. package/dist/member/types.js.map +1 -1
  30. package/dist/members/collection.js +13 -0
  31. package/dist/members/collection.js.map +1 -1
  32. package/dist/members/index.js +44 -23
  33. package/dist/members/index.js.map +1 -1
  34. package/dist/members/request.js +3 -3
  35. package/dist/members/request.js.map +1 -1
  36. package/dist/members/util.js +18 -6
  37. package/dist/members/util.js.map +1 -1
  38. package/dist/multistream/sendSlotManager.js +32 -2
  39. package/dist/multistream/sendSlotManager.js.map +1 -1
  40. package/dist/types/constants.d.ts +6 -0
  41. package/dist/types/hashTree/constants.d.ts +8 -0
  42. package/dist/types/hashTree/hashTree.d.ts +128 -0
  43. package/dist/types/hashTree/hashTreeParser.d.ts +152 -0
  44. package/dist/types/locus-info/index.d.ts +93 -3
  45. package/dist/types/meeting/brbState.d.ts +0 -1
  46. package/dist/types/meeting/index.d.ts +29 -3
  47. package/dist/types/meeting/request.d.ts +9 -1
  48. package/dist/types/meeting/request.type.d.ts +74 -0
  49. package/dist/types/meeting/util.d.ts +3 -3
  50. package/dist/types/member/types.d.ts +1 -0
  51. package/dist/types/members/collection.d.ts +6 -0
  52. package/dist/types/members/index.d.ts +15 -3
  53. package/dist/types/members/request.d.ts +1 -1
  54. package/dist/types/members/util.d.ts +5 -2
  55. package/dist/types/multistream/sendSlotManager.d.ts +16 -0
  56. package/dist/webinar/index.js +1 -1
  57. package/package.json +24 -23
  58. package/src/constants.ts +7 -0
  59. package/src/hashTree/constants.ts +12 -0
  60. package/src/hashTree/hashTree.ts +460 -0
  61. package/src/hashTree/hashTreeParser.ts +556 -0
  62. package/src/locus-info/index.ts +393 -58
  63. package/src/meeting/brbState.ts +9 -7
  64. package/src/meeting/index.ts +104 -6
  65. package/src/meeting/muteState.ts +2 -6
  66. package/src/meeting/request.ts +16 -0
  67. package/src/meeting/request.type.ts +64 -0
  68. package/src/meeting/util.ts +17 -20
  69. package/src/meetings/index.ts +17 -3
  70. package/src/member/index.ts +1 -0
  71. package/src/member/types.ts +1 -0
  72. package/src/members/collection.ts +11 -0
  73. package/src/members/index.ts +33 -7
  74. package/src/members/request.ts +2 -2
  75. package/src/members/util.ts +14 -3
  76. package/src/multistream/sendSlotManager.ts +34 -2
  77. package/test/unit/spec/hashTree/hashTree.ts +394 -0
  78. package/test/unit/spec/hashTree/hashTreeParser.ts +156 -0
  79. package/test/unit/spec/locus-info/index.js +506 -55
  80. package/test/unit/spec/meeting/brbState.ts +9 -9
  81. package/test/unit/spec/meeting/index.js +475 -42
  82. package/test/unit/spec/meeting/request.js +71 -0
  83. package/test/unit/spec/members/index.js +33 -10
  84. package/test/unit/spec/members/request.js +2 -2
  85. package/test/unit/spec/members/utils.js +27 -7
  86. package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
  87. package/test/unit/spec/reachability/index.ts +3 -1
@@ -0,0 +1,556 @@
1
+ import {zip} from 'lodash';
2
+ import HashTree, {LeafDataItem} from './hashTree';
3
+ import LoggerProxy from '../common/logs/logger-proxy';
4
+ import {Enum, HTTP_VERBS} from '../constants';
5
+ import {DataSetNames, EMPTY_HASH} from './constants';
6
+
7
+ export interface DataSet {
8
+ url: string;
9
+ root: string;
10
+ version: number;
11
+ leafCount: number;
12
+ name: string;
13
+ idleMs: number;
14
+ backoff: {
15
+ maxMs: number;
16
+ exponent: number;
17
+ };
18
+ }
19
+
20
+ // todo: Locus docs have now more types like CONTROL_ENTRY, EMBEDDED_APP, FULL_STATE, INFO, MEDIA_SHARE - need to add support for them once Locus implements them
21
+ export const ObjectType = {
22
+ participant: 'participant',
23
+ self: 'self',
24
+ locus: 'locus',
25
+ } as const;
26
+
27
+ export type ObjectType = Enum<typeof ObjectType>;
28
+
29
+ export interface HtMeta {
30
+ elementId: {
31
+ type: ObjectType;
32
+ id: number;
33
+ version: number;
34
+ };
35
+ dataSetNames: string[];
36
+ }
37
+ export interface HashTreeObject {
38
+ htMeta: HtMeta;
39
+ data: Record<string, any>;
40
+ }
41
+
42
+ export interface RootHashMessage {
43
+ dataSets: Array<DataSet>;
44
+ }
45
+ export interface HashTreeMessage {
46
+ dataSets: Array<DataSet>;
47
+ locusStateElements?: Array<HashTreeObject>;
48
+ locusSessionId?: string;
49
+ locusUrl?: string;
50
+ }
51
+
52
+ interface InternalDataSet extends DataSet {
53
+ hashTree: HashTree;
54
+ timer?: ReturnType<typeof setTimeout>;
55
+ }
56
+
57
+ type WebexRequestMethod = (options: Record<string, any>) => Promise<any>;
58
+
59
+ export const LocusInfoUpdateType = {
60
+ OBJECTS_UPDATED: 'OBJECTS_UPDATED',
61
+ MEETING_ENDED: 'MEETING_ENDED',
62
+ } as const;
63
+
64
+ export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
65
+ export type LocusInfoUpdateCallback = (
66
+ updateType: LocusInfoUpdateType,
67
+ data?: {updatedObjects: HashTreeObject[]}
68
+ ) => void;
69
+ /**
70
+ * Parses hash tree eventing locus data
71
+ */
72
+ class HashTreeParser {
73
+ dataSets: Record<string, InternalDataSet> = {};
74
+ webexRequest: WebexRequestMethod;
75
+ locusInfoUpdateCallback: LocusInfoUpdateCallback;
76
+ debugId: string;
77
+
78
+ /**
79
+ * Constructor for HashTreeParser
80
+ * @param {Object} options
81
+ * @param {Object} options.initialLocus The initial locus data containing the hash tree information
82
+ */
83
+ constructor(options: {
84
+ initialLocus: {
85
+ dataSets: Array<DataSet>;
86
+ locus: any;
87
+ };
88
+ webexRequest: WebexRequestMethod;
89
+ locusInfoUpdateCallback: LocusInfoUpdateCallback;
90
+ debugId: string;
91
+ }) {
92
+ const {dataSets, locus} = options.initialLocus; // extract dataSets from initialLocus
93
+
94
+ this.debugId = options.debugId;
95
+ this.webexRequest = options.webexRequest;
96
+ this.locusInfoUpdateCallback = options.locusInfoUpdateCallback;
97
+
98
+ // object mapping dataset names to arrays of leaf data
99
+ const leafData = this.analyzeLocusHtMeta(locus);
100
+
101
+ for (const dataSet of dataSets) {
102
+ const {name, leafCount} = dataSet;
103
+
104
+ const hashTree = new HashTree(leafData[name] || [], leafCount);
105
+
106
+ this.dataSets[name] = {
107
+ ...dataSet,
108
+ hashTree,
109
+ };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Each dataset exists at a different place in the dto
115
+ * iterate recursively over the locus and if it has a htMeta key,
116
+ * create an object with the type, id and version and add it to the appropriate leafData array
117
+ *
118
+ * @param {any} locus - The current part of the locus being processed
119
+ * @returns {any} - An object mapping dataset names to arrays of leaf data
120
+ */
121
+ private analyzeLocusHtMeta(locus: any) {
122
+ // object mapping dataset names to arrays of leaf data
123
+ const leafData: Record<string, Array<{type: string; id: number; version: number}>> = {};
124
+
125
+ const findAndStoreMetaData = (currentLocusPart: any) => {
126
+ if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
127
+ return;
128
+ }
129
+
130
+ if (currentLocusPart.htMeta && currentLocusPart.htMeta.dataSetNames) {
131
+ const {type, id, version} = currentLocusPart.htMeta.elementId;
132
+ const {dataSetNames} = currentLocusPart.htMeta;
133
+ const leafInfo = {type, id, version};
134
+
135
+ for (const dataSetName of dataSetNames) {
136
+ if (!leafData[dataSetName]) {
137
+ leafData[dataSetName] = [];
138
+ }
139
+ leafData[dataSetName].push(leafInfo);
140
+ }
141
+ }
142
+
143
+ if (Array.isArray(currentLocusPart)) {
144
+ for (const item of currentLocusPart) {
145
+ findAndStoreMetaData(item);
146
+ }
147
+ } else {
148
+ for (const key of Object.keys(currentLocusPart)) {
149
+ if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
150
+ findAndStoreMetaData(currentLocusPart[key]);
151
+ }
152
+ }
153
+ }
154
+ };
155
+
156
+ findAndStoreMetaData(locus);
157
+
158
+ return leafData;
159
+ }
160
+
161
+ /**
162
+ * Checks if the provided hash tree message indicates the end of the meeting and that there won't be any more updates.
163
+ *
164
+ * @param {HashTreeMessage} message - The hash tree message to check
165
+ * @returns {boolean} - Returns true if the message indicates the end of the meeting, false otherwise
166
+ */
167
+ private isEndMessage(message: HashTreeMessage) {
168
+ const mainDataSet = message.dataSets.find(
169
+ (dataSet) => dataSet.name.toLowerCase() === DataSetNames.MAIN
170
+ );
171
+
172
+ if (
173
+ mainDataSet &&
174
+ mainDataSet.leafCount === 1 &&
175
+ mainDataSet.root === EMPTY_HASH &&
176
+ this.dataSets[DataSetNames.MAIN].version < mainDataSet.version
177
+ ) {
178
+ // this is a special way for Locus to indicate that this meeting has ended
179
+ return true;
180
+ }
181
+
182
+ return false;
183
+ }
184
+
185
+ /**
186
+ * Handles the root hash heartbeat message
187
+ *
188
+ * @param {RootHashMessage} message - The root hash heartbeat message
189
+ * @returns {void}
190
+ */
191
+ handleRootHashHeartBeatMessage(message: RootHashMessage): void {
192
+ const {dataSets} = message;
193
+
194
+ LoggerProxy.logger.info(
195
+ `HashTreeParser#handleRootHashMessage --> ${
196
+ this.debugId
197
+ } Received heartbeat root hash message with data sets: ${JSON.stringify(
198
+ dataSets.map(({name, root, leafCount, version}) => ({
199
+ name,
200
+ root,
201
+ leafCount,
202
+ version,
203
+ }))
204
+ )}`
205
+ );
206
+ dataSets.forEach((dataSet) => {
207
+ this.runSyncAlgorithm(dataSet);
208
+ });
209
+ }
210
+
211
+ /**
212
+ * This method should be called when we receive a partial locus DTO that contains dataSets and htMeta information
213
+ * It updates the hash trees with the new leaf data based on the received Locus
214
+ *
215
+ * @param {Object} update - The locus update containing data sets and locus information
216
+ * @returns {void}
217
+ */
218
+ handleLocusUpdate(update: {dataSets?: Array<DataSet>; locus: any}): void {
219
+ const {dataSets, locus} = update;
220
+
221
+ if (!dataSets) {
222
+ LoggerProxy.logger.warn(
223
+ `HashTreeParser#handleLocusUpdate --> ${this.debugId} received hash tree update without dataSets, ignoring`
224
+ );
225
+ }
226
+
227
+ const leafData = this.analyzeLocusHtMeta(locus);
228
+
229
+ Object.keys(leafData).forEach((dataSetName) => {
230
+ this.dataSets[dataSetName].hashTree.putItems(leafData[dataSetName]);
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Handles incoming hash tree messages, updates the hash trees and calls locusInfoUpdateCallback
236
+ *
237
+ * @param {HashTreeMessage} message - The hash tree message containing data sets and objects to be processed
238
+ * @returns {void}
239
+ */
240
+ handleMessage(message: HashTreeMessage): void {
241
+ const {dataSets} = message;
242
+
243
+ LoggerProxy.logger.info(
244
+ `HashTreeParser#handleMessage --> ${this.debugId} received message:`,
245
+ message
246
+ );
247
+ if (this.isEndMessage(message)) {
248
+ LoggerProxy.logger.info(
249
+ `HashTreeParser#handleMessage --> ${this.debugId} received END message`
250
+ );
251
+ this.stopAllTimers();
252
+ this.locusInfoUpdateCallback(LocusInfoUpdateType.MEETING_ENDED);
253
+ } else {
254
+ const updatedObjects: HashTreeObject[] = [];
255
+
256
+ dataSets.forEach((dataSet) => {
257
+ if (this.dataSets[dataSet.name]) {
258
+ const {hashTree} = this.dataSets[dataSet.name];
259
+
260
+ if (hashTree) {
261
+ const locusStateElementsForThisSet = message.locusStateElements.filter((object) =>
262
+ object.htMeta.dataSetNames.includes(dataSet.name)
263
+ );
264
+
265
+ const appliedChangesList = hashTree.updateItems(
266
+ locusStateElementsForThisSet.map((object) =>
267
+ object.data
268
+ ? {operation: 'update', item: object.htMeta.elementId}
269
+ : {operation: 'remove', item: object.htMeta.elementId}
270
+ )
271
+ );
272
+
273
+ zip(appliedChangesList, locusStateElementsForThisSet).forEach(
274
+ ([changeApplied, object]) => {
275
+ if (changeApplied) {
276
+ // update the locus with the new object
277
+ updatedObjects.push(object);
278
+ }
279
+ }
280
+ );
281
+ } else {
282
+ LoggerProxy.logger.warn(
283
+ `Locus-info:index#handleHashTreeMessage --> ${this.debugId} unsupported dataSet ${dataSet.name} received in hash tree message`
284
+ );
285
+ }
286
+
287
+ // update our version of the dataSet
288
+ if (this.dataSets[dataSet.name].version < dataSet.version) {
289
+ this.dataSets[dataSet.name].version = dataSet.version;
290
+ this.dataSets[dataSet.name].root = dataSet.root;
291
+ this.dataSets[dataSet.name].idleMs = dataSet.idleMs;
292
+ this.dataSets[dataSet.name].backoff = {
293
+ maxMs: dataSet.backoff.maxMs,
294
+ exponent: dataSet.backoff.exponent,
295
+ };
296
+ LoggerProxy.logger.info(
297
+ `HashTreeParser#handleMessage --> ${this.debugId} updated "${dataSet.name}" to version=${dataSet.version}, root=${dataSet.root}`
298
+ );
299
+ }
300
+
301
+ this.runSyncAlgorithm(dataSet);
302
+ }
303
+ });
304
+
305
+ if (updatedObjects.length > 0) {
306
+ this.locusInfoUpdateCallback(LocusInfoUpdateType.OBJECTS_UPDATED, {updatedObjects});
307
+ } else {
308
+ LoggerProxy.logger.info(
309
+ `HashTreeParser#handleMessage --> ${this.debugId} No objects updated as a result of received message`
310
+ );
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Calculates a weighted backoff time that should be used for syncs
317
+ *
318
+ * @param {Object} backoff - The backoff configuration containing maxMs and exponent
319
+ * @returns {number} - A weighted backoff time based on the provided configuration, using algorithm supplied by Locus team
320
+ */
321
+ private getWeightedBackoffTime(backoff: {maxMs: number; exponent: number}): number {
322
+ const {maxMs, exponent} = backoff;
323
+
324
+ const randomValue = Math.random();
325
+
326
+ return Math.round(randomValue ** exponent * maxMs);
327
+ }
328
+
329
+ /**
330
+ * Runs the sync algorithm for the given data set.
331
+ *
332
+ * @param {DataSet} receivedDataSet - The data set to run the sync algorithm for.
333
+ * @returns {void}
334
+ */
335
+ private runSyncAlgorithm(receivedDataSet: DataSet) {
336
+ const dataSet = this.dataSets[receivedDataSet.name];
337
+
338
+ if (!dataSet) {
339
+ LoggerProxy.logger.warn(
340
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} No data set found for ${receivedDataSet.name}, skipping sync algorithm`
341
+ );
342
+
343
+ return;
344
+ }
345
+
346
+ dataSet.hashTree.resize(receivedDataSet.leafCount);
347
+
348
+ // temporary log for the workshop // todo: remove
349
+ const ourCurrentRootHash = dataSet.hashTree.getRootHash();
350
+ LoggerProxy.logger.info(
351
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} dataSet="${dataSet.name}" version=${dataSet.version} hashes before starting timer: ours=${ourCurrentRootHash} Locus=${dataSet.root}`
352
+ );
353
+
354
+ const delay = dataSet.idleMs + this.getWeightedBackoffTime(dataSet.backoff);
355
+
356
+ if (delay > 0) {
357
+ if (dataSet.timer) {
358
+ clearTimeout(dataSet.timer);
359
+ }
360
+
361
+ LoggerProxy.logger.info(
362
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} setting "${dataSet.name}" sync timer for ${delay}`
363
+ );
364
+
365
+ dataSet.timer = setTimeout(async () => {
366
+ LoggerProxy.logger.info(
367
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} Sync timer fired for "${dataSet.name}" data set`
368
+ );
369
+
370
+ dataSet.timer = undefined;
371
+
372
+ const rootHash = dataSet.hashTree.getRootHash();
373
+
374
+ if (dataSet.root !== rootHash) {
375
+ LoggerProxy.logger.info(
376
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} Root hash mismatch: received=${dataSet.root}, ours=${rootHash}, syncing data set "${dataSet.name}"`
377
+ );
378
+
379
+ const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
380
+
381
+ if (dataSet.leafCount !== 1) {
382
+ let receivedHashes;
383
+
384
+ try {
385
+ // request hashes from sender
386
+ receivedHashes = await this.getHashesFromLocus(dataSet.name);
387
+ } catch (error) {
388
+ if (error.statusCode === 409) {
389
+ // this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
390
+ LoggerProxy.logger.info(
391
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
392
+ );
393
+
394
+ return;
395
+ }
396
+ throw error;
397
+ }
398
+
399
+ // identify mismatched leaves
400
+ const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
401
+
402
+ mismatchedLeaveIndexes.forEach((index) => {
403
+ mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
404
+ });
405
+ } else {
406
+ mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
407
+ }
408
+ // request sync for mismatched leaves
409
+ if (Object.keys(mismatchedLeavesData).length > 0) {
410
+ const updatedObjects = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
411
+
412
+ if (updatedObjects.length > 0) {
413
+ this.locusInfoUpdateCallback(LocusInfoUpdateType.OBJECTS_UPDATED, {updatedObjects});
414
+ }
415
+ }
416
+ } else {
417
+ LoggerProxy.logger.info(
418
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} "${dataSet.name}" root hash matching: ${rootHash}, version=${dataSet.version}`
419
+ );
420
+ }
421
+ }, delay);
422
+ } else {
423
+ LoggerProxy.logger.info(
424
+ `HashTreeParser#runSyncAlgorithm --> ${this.debugId} No delay for "${dataSet.name}" data set, skipping sync timer reset/setup`
425
+ );
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Stops all timers for the data sets to prevent any further sync attempts.
431
+ * @returns {void}
432
+ */
433
+ stopAllTimers() {
434
+ Object.values(this.dataSets).forEach((dataSet) => {
435
+ if (dataSet.timer) {
436
+ clearTimeout(dataSet.timer);
437
+ dataSet.timer = undefined;
438
+ }
439
+ });
440
+ }
441
+
442
+ /**
443
+ * Gets the current hashes from the locus for a specific data set.
444
+ * @param {string} dataSetName
445
+ * @returns {string[]}
446
+ */
447
+ private getHashesFromLocus(dataSetName: string) {
448
+ LoggerProxy.logger.info(
449
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Requesting hashes for data set "${dataSetName}"`
450
+ );
451
+
452
+ const dataSet = this.dataSets[dataSetName];
453
+
454
+ const url = `${dataSet.url}/hashtree`;
455
+
456
+ return this.webexRequest({
457
+ method: HTTP_VERBS.GET,
458
+ uri: url,
459
+ })
460
+ .then((response) => {
461
+ const hashes = response.body?.hashes;
462
+
463
+ if (!hashes || !Array.isArray(hashes)) {
464
+ LoggerProxy.logger.warn(
465
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Locus returned invalid hashes, response body=`,
466
+ response.body
467
+ );
468
+ throw new Error(`Locus returned invalid hashes: ${hashes}`);
469
+ }
470
+
471
+ LoggerProxy.logger.info(
472
+ `HashTreeParser#getHashesFromLocus --> ${
473
+ this.debugId
474
+ } Received hashes for data set "${dataSetName}": ${JSON.stringify(hashes)}`
475
+ );
476
+
477
+ return hashes;
478
+ })
479
+ .catch((error) => {
480
+ LoggerProxy.logger.error(
481
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Error ${error.statusCode} fetching hashes for data set "${dataSetName}":`,
482
+ error
483
+ );
484
+ throw error;
485
+ });
486
+ }
487
+
488
+ /**
489
+ * Sends a sync request to Locus for the specified data set.
490
+ *
491
+ * @param {InternalDataSet} dataSet The data set to sync.
492
+ * @param {Record<number, LeafDataItem[]>} mismatchedLeavesData The mismatched leaves data to include in the sync request.
493
+ * @returns {Promise<HashTreeObject[]>}
494
+ */
495
+ private sendSyncRequestToLocus(
496
+ dataSet: InternalDataSet,
497
+ mismatchedLeavesData: Record<number, LeafDataItem[]>
498
+ ): Promise<HashTreeObject[]> {
499
+ LoggerProxy.logger.info(
500
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sending sync request for data set "${dataSet.name}"`
501
+ );
502
+
503
+ const url = `${dataSet.url}/sync`;
504
+ const body = {
505
+ dataSet: {
506
+ name: dataSet.name,
507
+ leafCount: dataSet.leafCount,
508
+ root: dataSet.hashTree.getRootHash(), // todo: avoid recalculation
509
+ },
510
+ leafDataEntries: [],
511
+ };
512
+
513
+ Object.keys(mismatchedLeavesData).forEach((index) => {
514
+ body.leafDataEntries.push({
515
+ leafIndex: parseInt(index, 10),
516
+ elementIds: mismatchedLeavesData[index],
517
+ });
518
+ });
519
+
520
+ return this.webexRequest({
521
+ method: HTTP_VERBS.POST,
522
+ uri: url,
523
+ body,
524
+ })
525
+ .then((resp) => {
526
+ LoggerProxy.logger.info(
527
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sync request succeeded for "${dataSet.name}"`
528
+ );
529
+
530
+ // todo: handle response body (it may be there or not)
531
+ if (resp.statusCode === 202) {
532
+ LoggerProxy.logger.info(
533
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Got 202 for sync request for data set "${dataSet.name}", data should arrive via messages`
534
+ );
535
+ }
536
+ const updatedObjects = resp.body?.objects || [];
537
+
538
+ if (updatedObjects.length !== body.leafDataEntries.length) {
539
+ LoggerProxy.logger.info(
540
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Sync request sent for "${dataSet.name}" with ${body.leafDataEntries.length} entries, but got ${updatedObjects.length} objects in response (statusCode=${resp.statusCode})`
541
+ );
542
+ }
543
+
544
+ return updatedObjects;
545
+ })
546
+ .catch((error) => {
547
+ LoggerProxy.logger.error(
548
+ `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Error ${error.statusCode} sending sync request for data set "${dataSet.name}":`,
549
+ error
550
+ );
551
+ throw error;
552
+ });
553
+ }
554
+ }
555
+
556
+ export default HashTreeParser;