@webex/plugin-meetings 3.10.0-next.1 → 3.10.0-next.11

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 (246) hide show
  1. package/dist/annotation/annotation.types.js.map +1 -1
  2. package/dist/annotation/constants.js.map +1 -1
  3. package/dist/annotation/index.js +19 -22
  4. package/dist/annotation/index.js.map +1 -1
  5. package/dist/breakouts/breakout.js +6 -6
  6. package/dist/breakouts/breakout.js.map +1 -1
  7. package/dist/breakouts/collection.js.map +1 -1
  8. package/dist/breakouts/edit-lock-error.js +9 -11
  9. package/dist/breakouts/edit-lock-error.js.map +1 -1
  10. package/dist/breakouts/events.js.map +1 -1
  11. package/dist/breakouts/index.js +126 -127
  12. package/dist/breakouts/index.js.map +1 -1
  13. package/dist/breakouts/request.js +6 -8
  14. package/dist/breakouts/request.js.map +1 -1
  15. package/dist/breakouts/utils.js.map +1 -1
  16. package/dist/common/browser-detection.js.map +1 -1
  17. package/dist/common/collection.js +1 -2
  18. package/dist/common/collection.js.map +1 -1
  19. package/dist/common/config.js.map +1 -1
  20. package/dist/common/errors/captcha-error.js +9 -11
  21. package/dist/common/errors/captcha-error.js.map +1 -1
  22. package/dist/common/errors/intent-to-join.js +10 -12
  23. package/dist/common/errors/intent-to-join.js.map +1 -1
  24. package/dist/common/errors/join-forbidden-error.js +10 -12
  25. package/dist/common/errors/join-forbidden-error.js.map +1 -1
  26. package/dist/common/errors/join-meeting.js +10 -12
  27. package/dist/common/errors/join-meeting.js.map +1 -1
  28. package/dist/common/errors/join-webinar-error.js +9 -11
  29. package/dist/common/errors/join-webinar-error.js.map +1 -1
  30. package/dist/common/errors/media.js +9 -11
  31. package/dist/common/errors/media.js.map +1 -1
  32. package/dist/common/errors/multistream-not-supported-error.js +9 -11
  33. package/dist/common/errors/multistream-not-supported-error.js.map +1 -1
  34. package/dist/common/errors/no-meeting-info.js +9 -11
  35. package/dist/common/errors/no-meeting-info.js.map +1 -1
  36. package/dist/common/errors/parameter.js +11 -14
  37. package/dist/common/errors/parameter.js.map +1 -1
  38. package/dist/common/errors/password-error.js +9 -11
  39. package/dist/common/errors/password-error.js.map +1 -1
  40. package/dist/common/errors/permission.js +9 -11
  41. package/dist/common/errors/permission.js.map +1 -1
  42. package/dist/common/errors/reclaim-host-role-errors.js +32 -38
  43. package/dist/common/errors/reclaim-host-role-errors.js.map +1 -1
  44. package/dist/common/errors/reconnection-not-started.js +5 -6
  45. package/dist/common/errors/reconnection-not-started.js.map +1 -1
  46. package/dist/common/errors/reconnection.js +9 -11
  47. package/dist/common/errors/reconnection.js.map +1 -1
  48. package/dist/common/errors/stats.js +9 -11
  49. package/dist/common/errors/stats.js.map +1 -1
  50. package/dist/common/errors/webex-errors.js +38 -27
  51. package/dist/common/errors/webex-errors.js.map +1 -1
  52. package/dist/common/errors/webex-meetings-error.js +9 -12
  53. package/dist/common/errors/webex-meetings-error.js.map +1 -1
  54. package/dist/common/events/events-scope.js +9 -10
  55. package/dist/common/events/events-scope.js.map +1 -1
  56. package/dist/common/events/events.js +9 -10
  57. package/dist/common/events/events.js.map +1 -1
  58. package/dist/common/events/trigger-proxy.js.map +1 -1
  59. package/dist/common/events/util.js.map +1 -1
  60. package/dist/common/logs/logger-config.js.map +1 -1
  61. package/dist/common/logs/logger-proxy.js.map +1 -1
  62. package/dist/common/logs/request.js +17 -17
  63. package/dist/common/logs/request.js.map +1 -1
  64. package/dist/common/queue.js +1 -2
  65. package/dist/common/queue.js.map +1 -1
  66. package/dist/config.js +0 -1
  67. package/dist/config.js.map +1 -1
  68. package/dist/constants.js +12 -8
  69. package/dist/constants.js.map +1 -1
  70. package/dist/controls-options-manager/constants.js.map +1 -1
  71. package/dist/controls-options-manager/enums.js.map +1 -1
  72. package/dist/controls-options-manager/index.js +1 -2
  73. package/dist/controls-options-manager/index.js.map +1 -1
  74. package/dist/controls-options-manager/types.js.map +1 -1
  75. package/dist/controls-options-manager/util.js +1 -2
  76. package/dist/controls-options-manager/util.js.map +1 -1
  77. package/dist/hashTree/hashTreeParser.js +165 -0
  78. package/dist/hashTree/hashTreeParser.js.map +1 -0
  79. package/dist/hashTree/types.js +15 -0
  80. package/dist/hashTree/types.js.map +1 -0
  81. package/dist/index.js +8 -2
  82. package/dist/index.js.map +1 -1
  83. package/dist/interceptors/index.js.map +1 -1
  84. package/dist/interceptors/locusRetry.js +6 -8
  85. package/dist/interceptors/locusRetry.js.map +1 -1
  86. package/dist/interceptors/locusRouteToken.js +6 -8
  87. package/dist/interceptors/locusRouteToken.js.map +1 -1
  88. package/dist/interpretation/collection.js.map +1 -1
  89. package/dist/interpretation/index.js +1 -2
  90. package/dist/interpretation/index.js.map +1 -1
  91. package/dist/interpretation/siLanguage.js +1 -1
  92. package/dist/interpretation/siLanguage.js.map +1 -1
  93. package/dist/locus-info/controlsUtils.js.map +1 -1
  94. package/dist/locus-info/embeddedAppsUtils.js.map +1 -1
  95. package/dist/locus-info/fullState.js.map +1 -1
  96. package/dist/locus-info/hostUtils.js.map +1 -1
  97. package/dist/locus-info/index.js +532 -94
  98. package/dist/locus-info/index.js.map +1 -1
  99. package/dist/locus-info/infoUtils.js.map +1 -1
  100. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  101. package/dist/locus-info/parser.js +3 -4
  102. package/dist/locus-info/parser.js.map +1 -1
  103. package/dist/locus-info/selfUtils.js.map +1 -1
  104. package/dist/locus-info/types.js +7 -0
  105. package/dist/locus-info/types.js.map +1 -0
  106. package/dist/media/MediaConnectionAwaiter.js +1 -2
  107. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  108. package/dist/media/index.js +5 -2
  109. package/dist/media/index.js.map +1 -1
  110. package/dist/media/properties.js +15 -17
  111. package/dist/media/properties.js.map +1 -1
  112. package/dist/media/util.js.map +1 -1
  113. package/dist/meeting/brbState.js +8 -9
  114. package/dist/meeting/brbState.js.map +1 -1
  115. package/dist/meeting/connectionStateHandler.js +10 -13
  116. package/dist/meeting/connectionStateHandler.js.map +1 -1
  117. package/dist/meeting/in-meeting-actions.js.map +1 -1
  118. package/dist/meeting/index.js +1556 -1528
  119. package/dist/meeting/index.js.map +1 -1
  120. package/dist/meeting/locusMediaRequest.js +13 -17
  121. package/dist/meeting/locusMediaRequest.js.map +1 -1
  122. package/dist/meeting/muteState.js +11 -12
  123. package/dist/meeting/muteState.js.map +1 -1
  124. package/dist/meeting/request.js +101 -104
  125. package/dist/meeting/request.js.map +1 -1
  126. package/dist/meeting/request.type.js.map +1 -1
  127. package/dist/meeting/state.js.map +1 -1
  128. package/dist/meeting/type.js.map +1 -1
  129. package/dist/meeting/util.js +24 -23
  130. package/dist/meeting/util.js.map +1 -1
  131. package/dist/meeting/voicea-meeting.js +3 -3
  132. package/dist/meeting/voicea-meeting.js.map +1 -1
  133. package/dist/meeting-info/collection.js +7 -10
  134. package/dist/meeting-info/collection.js.map +1 -1
  135. package/dist/meeting-info/index.js +1 -2
  136. package/dist/meeting-info/index.js.map +1 -1
  137. package/dist/meeting-info/meeting-info-v2.js +135 -146
  138. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  139. package/dist/meeting-info/request.js +1 -2
  140. package/dist/meeting-info/request.js.map +1 -1
  141. package/dist/meeting-info/util.js +36 -37
  142. package/dist/meeting-info/util.js.map +1 -1
  143. package/dist/meeting-info/utilv2.js +30 -31
  144. package/dist/meeting-info/utilv2.js.map +1 -1
  145. package/dist/meetings/collection.js +6 -8
  146. package/dist/meetings/collection.js.map +1 -1
  147. package/dist/meetings/index.js +179 -141
  148. package/dist/meetings/index.js.map +1 -1
  149. package/dist/meetings/meetings.types.js.map +1 -1
  150. package/dist/meetings/request.js +6 -8
  151. package/dist/meetings/request.js.map +1 -1
  152. package/dist/meetings/util.js +25 -23
  153. package/dist/meetings/util.js.map +1 -1
  154. package/dist/member/index.js +1 -2
  155. package/dist/member/index.js.map +1 -1
  156. package/dist/member/types.js +6 -3
  157. package/dist/member/types.js.map +1 -1
  158. package/dist/member/util.js.map +1 -1
  159. package/dist/members/collection.js +1 -2
  160. package/dist/members/collection.js.map +1 -1
  161. package/dist/members/index.js +18 -21
  162. package/dist/members/index.js.map +1 -1
  163. package/dist/members/request.js +8 -11
  164. package/dist/members/request.js.map +1 -1
  165. package/dist/members/types.js.map +1 -1
  166. package/dist/members/util.js.map +1 -1
  167. package/dist/metrics/constants.js +3 -1
  168. package/dist/metrics/constants.js.map +1 -1
  169. package/dist/metrics/index.js +3 -4
  170. package/dist/metrics/index.js.map +1 -1
  171. package/dist/multistream/mediaRequestManager.js +1 -2
  172. package/dist/multistream/mediaRequestManager.js.map +1 -1
  173. package/dist/multistream/receiveSlot.js +34 -45
  174. package/dist/multistream/receiveSlot.js.map +1 -1
  175. package/dist/multistream/receiveSlotManager.js +8 -9
  176. package/dist/multistream/receiveSlotManager.js.map +1 -1
  177. package/dist/multistream/remoteMedia.js +12 -15
  178. package/dist/multistream/remoteMedia.js.map +1 -1
  179. package/dist/multistream/remoteMediaGroup.js +1 -2
  180. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  181. package/dist/multistream/remoteMediaManager.js +122 -123
  182. package/dist/multistream/remoteMediaManager.js.map +1 -1
  183. package/dist/multistream/sendSlotManager.js +29 -30
  184. package/dist/multistream/sendSlotManager.js.map +1 -1
  185. package/dist/personal-meeting-room/index.js +16 -19
  186. package/dist/personal-meeting-room/index.js.map +1 -1
  187. package/dist/personal-meeting-room/request.js +7 -10
  188. package/dist/personal-meeting-room/request.js.map +1 -1
  189. package/dist/personal-meeting-room/util.js.map +1 -1
  190. package/dist/reachability/clusterReachability.js +36 -39
  191. package/dist/reachability/clusterReachability.js.map +1 -1
  192. package/dist/reachability/index.js +203 -205
  193. package/dist/reachability/index.js.map +1 -1
  194. package/dist/reachability/reachability.types.js.map +1 -1
  195. package/dist/reachability/request.js.map +1 -1
  196. package/dist/reachability/util.js.map +1 -1
  197. package/dist/reactions/constants.js.map +1 -1
  198. package/dist/reactions/reactions.js.map +1 -1
  199. package/dist/reactions/reactions.type.js.map +1 -1
  200. package/dist/reconnection-manager/index.js +178 -176
  201. package/dist/reconnection-manager/index.js.map +1 -1
  202. package/dist/recording-controller/enums.js.map +1 -1
  203. package/dist/recording-controller/index.js +1 -2
  204. package/dist/recording-controller/index.js.map +1 -1
  205. package/dist/recording-controller/util.js.map +1 -1
  206. package/dist/roap/index.js +12 -15
  207. package/dist/roap/index.js.map +1 -1
  208. package/dist/roap/request.js +24 -26
  209. package/dist/roap/request.js.map +1 -1
  210. package/dist/roap/turnDiscovery.js +75 -76
  211. package/dist/roap/turnDiscovery.js.map +1 -1
  212. package/dist/roap/types.js.map +1 -1
  213. package/dist/transcription/index.js +4 -5
  214. package/dist/transcription/index.js.map +1 -1
  215. package/dist/types/common/errors/webex-errors.d.ts +12 -0
  216. package/dist/types/constants.d.ts +26 -21
  217. package/dist/types/hashTree/hashTreeParser.d.ts +109 -0
  218. package/dist/types/hashTree/types.d.ts +16 -0
  219. package/dist/types/index.d.ts +2 -1
  220. package/dist/types/locus-info/index.d.ts +91 -42
  221. package/dist/types/locus-info/types.d.ts +45 -0
  222. package/dist/types/meeting/index.d.ts +22 -9
  223. package/dist/types/meetings/index.d.ts +9 -2
  224. package/dist/types/metrics/constants.d.ts +2 -0
  225. package/dist/webinar/collection.js +1 -2
  226. package/dist/webinar/collection.js.map +1 -1
  227. package/dist/webinar/index.js +148 -158
  228. package/dist/webinar/index.js.map +1 -1
  229. package/package.json +16 -16
  230. package/src/common/errors/webex-errors.ts +19 -0
  231. package/src/constants.ts +14 -2
  232. package/src/hashTree/hashTreeParser.ts +146 -0
  233. package/src/hashTree/types.ts +20 -0
  234. package/src/index.ts +2 -0
  235. package/src/locus-info/index.ts +534 -85
  236. package/src/locus-info/types.ts +46 -0
  237. package/src/media/index.ts +6 -0
  238. package/src/meeting/index.ts +61 -27
  239. package/src/meeting/util.ts +1 -0
  240. package/src/meetings/index.ts +104 -51
  241. package/src/metrics/constants.ts +2 -0
  242. package/test/unit/spec/locus-info/index.js +576 -1
  243. package/test/unit/spec/media/index.ts +140 -9
  244. package/test/unit/spec/meeting/index.js +178 -94
  245. package/test/unit/spec/meeting/utils.js +77 -0
  246. package/test/unit/spec/meetings/index.js +71 -28
@@ -1,4 +1,4 @@
1
- import {isEqual, assignWith, cloneDeep, isEmpty, forEach} from 'lodash';
1
+ import {isEqual, assignWith, cloneDeep, isEmpty} from 'lodash';
2
2
 
3
3
  import LoggerProxy from '../common/logs/logger-proxy';
4
4
  import EventsScope from '../common/events/events-scope';
@@ -17,7 +17,7 @@ import {
17
17
  MEETING_REMOVED_REASON,
18
18
  CALL_REMOVED_REASON,
19
19
  RECORDING_STATE,
20
- BREAKOUTS,
20
+ Enum,
21
21
  } from '../constants';
22
22
 
23
23
  import InfoUtils from './infoUtils';
@@ -30,52 +30,53 @@ import MediaSharesUtils from './mediaSharesUtils';
30
30
  import LocusDeltaParser from './parser';
31
31
  import Metrics from '../metrics';
32
32
  import BEHAVIORAL_METRICS from '../metrics/constants';
33
-
34
- export type LocusDTO = {
35
- controls?: any;
36
- fullState?: {
37
- active: boolean;
38
- count: number;
39
- lastActive: string;
40
- locked: boolean;
41
- sessionId: string;
42
- seessionIds: string[];
43
- startTime: number;
44
- state: string;
45
- type: string;
46
- };
47
- host?: {
48
- id: string;
49
- incomingCallProtocols: any[];
50
- isExternal: boolean;
51
- name: string;
52
- orgId: string;
33
+ import HashTreeParser, {
34
+ DataSet,
35
+ HashTreeMessage,
36
+ HashTreeObject,
37
+ isSelf,
38
+ LocusInfoUpdateType,
39
+ } from '../hashTree/hashTreeParser';
40
+ import {ObjectType} from '../hashTree/types';
41
+ import {LocusDTO} from './types';
42
+
43
+ export type LocusLLMEvent = {
44
+ data: {
45
+ eventType: typeof LOCUSEVENT.HASH_TREE_DATA_UPDATED;
46
+ stateElementsMessage: HashTreeMessage;
53
47
  };
54
- info?: any;
55
- links?: any;
56
- mediaShares?: any[];
57
- meetings?: any[];
58
- participants: any[];
59
- replaces?: any[];
60
- self?: any;
61
- sequence?: {
62
- dirtyParticipants: number;
63
- entries: number[];
64
- rangeEnd: number;
65
- rangeStart: number;
66
- sequenceHash: number;
67
- sessionToken: string;
68
- since: string;
69
- totalParticipants: number;
70
- };
71
- syncUrl?: string;
72
- url?: string;
73
48
  };
74
49
 
50
+ const LocusDtoTopLevelKeys = [
51
+ 'controls',
52
+ 'fullState',
53
+ 'host',
54
+ 'info',
55
+ 'links',
56
+ 'mediaShares',
57
+ 'meetings',
58
+ 'participants',
59
+ 'replaces',
60
+ 'self',
61
+ 'sequence',
62
+ 'syncUrl',
63
+ 'url',
64
+ 'htMeta', // only exists when hash trees are used
65
+ ];
66
+
75
67
  export type LocusApiResponseBody = {
68
+ dataSets?: DataSet[];
76
69
  locus: LocusDTO; // this LocusDTO here might not be the full one (for example it won't have all the participants, but it should have self)
77
70
  };
78
71
 
72
+ const LocusObjectStateAfterUpdates = {
73
+ unchanged: 'unchanged',
74
+ removed: 'removed',
75
+ updated: 'updated',
76
+ } as const;
77
+
78
+ type LocusObjectStateAfterUpdates = Enum<typeof LocusObjectStateAfterUpdates>;
79
+
79
80
  /**
80
81
  * @description LocusInfo extends ChildEmitter to convert locusInfo info a private emitter to parent object
81
82
  * @export
@@ -114,6 +115,10 @@ export default class LocusInfo extends EventsScope {
114
115
  resources: any;
115
116
  mainSessionLocusCache: any;
116
117
  self: any;
118
+ hashTreeParser?: HashTreeParser;
119
+ hashTreeObjectId2ParticipantId: Map<number, string>; // mapping of hash tree object ids to participant ids
120
+ classicVsHashTreeMismatchMetricCounter = 0;
121
+
117
122
  /**
118
123
  * Constructor
119
124
  * @param {function} updateMeeting callback to update the meeting object from an object
@@ -132,10 +137,12 @@ export default class LocusInfo extends EventsScope {
132
137
  this.meetingId = meetingId;
133
138
  this.updateMeeting = updateMeeting;
134
139
  this.locusParser = new LocusDeltaParser();
140
+ this.hashTreeObjectId2ParticipantId = new Map();
135
141
  }
136
142
 
137
143
  /**
138
144
  * Does a Locus sync. It tries to get the latest delta DTO or if it can't, it falls back to getting the full Locus DTO.
145
+ * WARNING: This function must not be used for hash tree based Locus meetings.
139
146
  *
140
147
  * @param {Meeting} meeting
141
148
  * @param {boolean} isLocusUrlChanged
@@ -356,14 +363,109 @@ export default class LocusInfo extends EventsScope {
356
363
  }
357
364
 
358
365
  /**
359
- * @param {Object} locus
366
+ * Creates the HashTreeParser instance.
367
+ * @param {Object} initial locus data
368
+ * @returns {void}
369
+ */
370
+ private createHashTreeParser({
371
+ initialLocus,
372
+ }: {
373
+ initialLocus: {
374
+ dataSets: Array<DataSet>;
375
+ locus: any;
376
+ };
377
+ }) {
378
+ return new HashTreeParser({
379
+ initialLocus,
380
+ webexRequest: this.webex.request.bind(this.webex),
381
+ locusInfoUpdateCallback: this.updateFromHashTree.bind(this),
382
+ debugId: `HT-${this.meetingId.substring(0, 4)}`,
383
+ });
384
+ }
385
+
386
+ /**
387
+ * @param {Object} data - data to initialize locus info with. It may be from a join or GET /loci response or from a Mercury event that triggers a creation of meeting object
360
388
  * @returns {undefined}
361
389
  * @memberof LocusInfo
362
390
  */
363
- initialSetup(locus: object) {
364
- this.updateLocusCache(locus);
365
- this.onFullLocus(locus);
391
+ async initialSetup(
392
+ data:
393
+ | {
394
+ trigger: 'join-response';
395
+ locus: LocusDTO;
396
+ dataSets?: DataSet[];
397
+ }
398
+ | {
399
+ trigger: 'locus-message';
400
+ locus?: LocusDTO;
401
+ hashTreeMessage?: HashTreeMessage;
402
+ }
403
+ | {
404
+ trigger: 'get-loci-response';
405
+ locus?: LocusDTO;
406
+ }
407
+ ) {
408
+ switch (data.trigger) {
409
+ case 'locus-message':
410
+ if (data.hashTreeMessage) {
411
+ // we need the SELF object to be in the received message, because it contains visibleDataSets
412
+ // and these are needed to initialize all the hash trees
413
+ const selfObject = data.hashTreeMessage.locusStateElements?.find((el) => isSelf(el));
414
+
415
+ if (!selfObject?.data?.visibleDataSets) {
416
+ LoggerProxy.logger.warn(
417
+ `Locus-info:index#initialSetup --> cannot initialize HashTreeParser, SELF object with visibleDataSets is missing in the message`
418
+ );
419
+
420
+ throw new Error('SELF object with visibleDataSets is missing in the message');
421
+ }
366
422
 
423
+ LoggerProxy.logger.info(
424
+ 'Locus-info:index#initialSetup --> creating HashTreeParser from message'
425
+ );
426
+ // first create the HashTreeParser, but don't initialize it with any data yet
427
+ // pass just a fake locus that contains only the visibleDataSets
428
+ this.hashTreeParser = this.createHashTreeParser({
429
+ initialLocus: {
430
+ locus: {self: {visibleDataSets: selfObject.data.visibleDataSets}},
431
+ dataSets: [], // empty, because they will be populated in initializeFromMessage() call // dataSets: data.hashTreeMessage.dataSets,
432
+ },
433
+ });
434
+
435
+ // now handle the message - that should populate all the visible datasets
436
+ await this.hashTreeParser.initializeFromMessage(data.hashTreeMessage);
437
+ } else {
438
+ // "classic" Locus case, no hash trees involved
439
+ this.updateLocusCache(data.locus);
440
+ this.onFullLocus(data.locus, undefined);
441
+ }
442
+ break;
443
+ case 'join-response':
444
+ this.updateLocusCache(data.locus);
445
+ this.onFullLocus(data.locus, undefined, data.dataSets);
446
+ break;
447
+ case 'get-loci-response':
448
+ if (data.locus?.links?.resources?.visibleDataSets?.url) {
449
+ LoggerProxy.logger.info(
450
+ 'Locus-info:index#initialSetup --> creating HashTreeParser from get-loci-response'
451
+ );
452
+ // first create the HashTreeParser, but don't initialize it with any data yet
453
+ // pass just a fake locus that contains only the visibleDataSets
454
+ this.hashTreeParser = this.createHashTreeParser({
455
+ initialLocus: {
456
+ locus: {self: {visibleDataSets: data.locus?.self?.visibleDataSets}},
457
+ dataSets: [], // empty, because we don't have them yet
458
+ },
459
+ });
460
+
461
+ // now initialize all the data
462
+ await this.hashTreeParser.initializeFromGetLociResponse(data.locus);
463
+ } else {
464
+ // "classic" Locus case, no hash trees involved
465
+ this.updateLocusCache(data.locus);
466
+ this.onFullLocus(data.locus, undefined);
467
+ }
468
+ }
367
469
  // Change it to true after it receives it first locus object
368
470
  this.emitChange = true;
369
471
  }
@@ -375,7 +477,276 @@ export default class LocusInfo extends EventsScope {
375
477
  * @returns {void}
376
478
  */
377
479
  handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
378
- this.handleLocusDelta(responseBody.locus, meeting);
480
+ if (this.hashTreeParser) {
481
+ // API responses with hash tree are a bit problematic and not fully confirmed how they will look like
482
+ // we don't really need them, because all updates are guaranteed to come via Mercury or LLM messages anyway
483
+ // so it's OK to skip them for now
484
+ LoggerProxy.logger.info(
485
+ 'Locus-info:index#handleLocusAPIResponse: skipping handling of API http response with hashTreeParser'
486
+ );
487
+ } else {
488
+ if (responseBody.dataSets) {
489
+ this.sendClassicVsHashTreeMismatchMetric(
490
+ meeting,
491
+ `unexpected hash tree dataSets in API response`
492
+ );
493
+ }
494
+ // classic Locus delta
495
+ this.handleLocusDelta(responseBody.locus, meeting);
496
+ }
497
+ }
498
+
499
+ /**
500
+ *
501
+ * @param {HashTreeObject} object data set object
502
+ * @param {any} locus
503
+ * @returns {void}
504
+ */
505
+ updateLocusFromHashTreeObject(object: HashTreeObject, locus: LocusDTO): LocusDTO {
506
+ const type = object.htMeta.elementId.type.toLowerCase();
507
+
508
+ switch (type) {
509
+ case ObjectType.locus: {
510
+ if (!object.data) {
511
+ // not doing anything here, as we need Locus to always be there (at least some fields)
512
+ // and that's already taken care of in updateFromHashTree()
513
+ LoggerProxy.logger.info(
514
+ `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object removed`
515
+ );
516
+
517
+ return locus;
518
+ }
519
+ // replace the main locus
520
+
521
+ // The Locus object we receive from backend has empty participants, so removing them to avoid it overriding the ones in our current locus object
522
+ // Also, other fields like mediaShares are managed by other ObjectType updates, so removing them too
523
+ // BTW, it also doesn't have "self". That's OK as it won't override existing locus.self and also existing SDK code can handle that missing self in Locus updates
524
+ const locusObjectFromData = object.data;
525
+ delete locusObjectFromData.participants;
526
+ delete locusObjectFromData.mediaShares;
527
+
528
+ locus = {...locus, ...locusObjectFromData};
529
+ LoggerProxy.logger.info(
530
+ `Locus-info:index#updateLocusFromHashTreeObject --> LOCUS object updated`
531
+ );
532
+ break;
533
+ }
534
+ case ObjectType.mediaShare:
535
+ if (object.data) {
536
+ LoggerProxy.logger.info(
537
+ `Locus-info:index#updateLocusFromHashTreeObject --> mediaShare id=${
538
+ object.htMeta.elementId.id
539
+ } name='${object.data.name}' updated ${
540
+ object.data.name === 'content'
541
+ ? `floor=${object.data.floor?.disposition}, ${object.data.floor?.beneficiary?.id}`
542
+ : ''
543
+ }`
544
+ );
545
+ const existingMediaShare = locus.mediaShares?.find(
546
+ (ms) => ms.htMeta.elementId.id === object.htMeta.elementId.id
547
+ );
548
+
549
+ if (existingMediaShare) {
550
+ Object.assign(existingMediaShare, object.data);
551
+ } else {
552
+ locus.mediaShares = locus.mediaShares || [];
553
+ locus.mediaShares.push(object.data);
554
+ }
555
+ } else {
556
+ LoggerProxy.logger.info(
557
+ `Locus-info:index#updateLocusFromHashTreeObject --> mediaShare id=${object.htMeta.elementId.id} removed`
558
+ );
559
+ locus.mediaShares = locus.mediaShares?.filter(
560
+ (ms) => ms.htMeta.elementId.id !== object.htMeta.elementId.id
561
+ );
562
+ }
563
+ break;
564
+ case ObjectType.participant:
565
+ LoggerProxy.logger.info(
566
+ `Locus-info:index#updateLocusFromHashTreeObject --> participant id=${
567
+ object.htMeta.elementId.id
568
+ } ${object.data ? 'updated' : 'removed'}`
569
+ );
570
+ if (object.data) {
571
+ if (!locus.participants) {
572
+ locus.participants = [];
573
+ }
574
+ const participantObject = object.data;
575
+ participantObject.htMeta = object.htMeta;
576
+ locus.participants.push(participantObject);
577
+ this.hashTreeObjectId2ParticipantId.set(object.htMeta.elementId.id, participantObject.id);
578
+ } else {
579
+ const participantId = this.hashTreeObjectId2ParticipantId.get(object.htMeta.elementId.id);
580
+
581
+ if (!locus.jsSdkMeta) {
582
+ locus.jsSdkMeta = {removedParticipantIds: []};
583
+ }
584
+ locus.jsSdkMeta.removedParticipantIds.push(participantId);
585
+ this.hashTreeObjectId2ParticipantId.delete(object.htMeta.elementId.id);
586
+ }
587
+ break;
588
+ case ObjectType.self:
589
+ if (!object.data) {
590
+ // self without data is handled inside HashTreeParser and results in LocusInfoUpdateType.MEETING_ENDED, so we should never get here
591
+ LoggerProxy.logger.warn(
592
+ `Locus-info:index#updateLocusFromHashTreeObject --> received SELF object without data, this is not expected!`
593
+ );
594
+
595
+ return locus;
596
+ }
597
+ LoggerProxy.logger.info(
598
+ `Locus-info:index#updateLocusFromHashTreeObject --> SELF object updated`
599
+ );
600
+ locus.self = object.data;
601
+ break;
602
+ default:
603
+ LoggerProxy.logger.warn(
604
+ `Locus-info:index#updateLocusFromHashTreeObject --> received unsupported object type ${type}`
605
+ );
606
+ break;
607
+ }
608
+
609
+ return locus;
610
+ }
611
+
612
+ /**
613
+ * Sends a metric when we receive something from Locus that uses hash trees while we
614
+ * expect classic deltas or the other way around.
615
+ * @param {Meeting} meeting
616
+ * @param {string} message
617
+ * @returns {void}
618
+ */
619
+ sendClassicVsHashTreeMismatchMetric(meeting: any, message: string) {
620
+ LoggerProxy.logger.warn(
621
+ `Locus-info:index#sendClassicVsHashTreeMismatchMetric --> classic vs hash tree mismatch! ${message}`
622
+ );
623
+
624
+ // we don't want to flood the metrics system
625
+ if (this.classicVsHashTreeMismatchMetricCounter < 5) {
626
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH, {
627
+ correlationId: meeting.correlationId,
628
+ message,
629
+ });
630
+ this.classicVsHashTreeMismatchMetricCounter += 1;
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Handles a hash tree message received from Locus.
636
+ *
637
+ * @param {Meeting} meeting - The meeting object
638
+ * @param {eventType} eventType - The event type
639
+ * @param {HashTreeMessage} message incoming hash tree message
640
+ * @returns {void}
641
+ */
642
+ private handleHashTreeMessage(meeting: any, eventType: LOCUSEVENT, message: HashTreeMessage) {
643
+ if (eventType !== LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
644
+ this.sendClassicVsHashTreeMismatchMetric(
645
+ meeting,
646
+ `got ${eventType}, expected ${LOCUSEVENT.HASH_TREE_DATA_UPDATED}`
647
+ );
648
+
649
+ return;
650
+ }
651
+
652
+ this.hashTreeParser.handleMessage(message);
653
+ }
654
+
655
+ /**
656
+ * Callback registered with HashTreeParser to receive locus info updates.
657
+ * Updates our locus info based on the data parsed by the hash tree parser.
658
+ *
659
+ * @param {LocusInfoUpdateType} updateType - The type of update received.
660
+ * @param {Object} [data] - Additional data for the update, if applicable.
661
+ * @returns {void}
662
+ */
663
+ private updateFromHashTree(
664
+ updateType: LocusInfoUpdateType,
665
+ data?: {updatedObjects: HashTreeObject[]}
666
+ ) {
667
+ switch (updateType) {
668
+ case LocusInfoUpdateType.OBJECTS_UPDATED: {
669
+ // initialize our new locus
670
+ let locus: LocusDTO = {
671
+ participants: [],
672
+ jsSdkMeta: {removedParticipantIds: []},
673
+ };
674
+
675
+ // first go over all the updates and check what happens with the main locus object
676
+ let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
677
+ LocusObjectStateAfterUpdates.unchanged;
678
+ data.updatedObjects.forEach((object) => {
679
+ if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
680
+ if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
681
+ // this code doesn't supported it right now,
682
+ // cases for "updated" followed by "removed", or multiple "updated" would need more handling
683
+ // but these should never happen
684
+ LoggerProxy.logger.warn(
685
+ `Locus-info:index#updateFromHashTree --> received multiple LOCUS objects in one update, this is unexpected!`
686
+ );
687
+ Metrics.sendBehavioralMetric(
688
+ BEHAVIORAL_METRICS.LOCUS_HASH_TREE_UNSUPPORTED_OPERATION,
689
+ {
690
+ locusUrl: object.data?.url || this.url,
691
+ message: object.data
692
+ ? 'multiple LOCUS object updates'
693
+ : 'LOCUS object update followed by removal',
694
+ }
695
+ );
696
+ }
697
+ if (object.data) {
698
+ locusObjectStateAfterUpdates = LocusObjectStateAfterUpdates.updated;
699
+ } else {
700
+ locusObjectStateAfterUpdates = LocusObjectStateAfterUpdates.removed;
701
+ }
702
+ }
703
+ });
704
+
705
+ // if Locus object is unchanged or removed, we need to keep using the existing locus
706
+ // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
707
+ // if it gets updated, we don't need to do anything and we start with an empty one
708
+ // so that when it gets updated, if the new one is missing some field, that field will
709
+ // be removed from our locusInfo
710
+ if (
711
+ locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.unchanged ||
712
+ locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.removed
713
+ ) {
714
+ // copy over existing locus
715
+ LocusDtoTopLevelKeys.forEach((key) => {
716
+ if (key !== 'participants') {
717
+ locus[key] = cloneDeep(this[key]);
718
+ }
719
+ });
720
+ }
721
+
722
+ LoggerProxy.logger.info(
723
+ `Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
724
+ data.updatedObjects.map((o) => ({
725
+ type: o.htMeta.elementId.type,
726
+ id: o.htMeta.elementId.id,
727
+ hasData: o.data !== undefined,
728
+ }))
729
+ )}`
730
+ );
731
+ // now apply all the updates from the hash tree onto the locus
732
+ data.updatedObjects.forEach((object) => {
733
+ locus = this.updateLocusFromHashTreeObject(object, locus);
734
+ });
735
+
736
+ // update our locus info with the new locus
737
+ this.onDeltaLocus(locus);
738
+
739
+ break;
740
+ }
741
+
742
+ case LocusInfoUpdateType.MEETING_ENDED: {
743
+ LoggerProxy.logger.info(
744
+ `Locus-info:index#updateFromHashTree --> received signal that meeting ended, destroying meeting ${this.meetingId}`
745
+ );
746
+ const meeting = this.webex.meetings.meetingCollection.get(this.meetingId);
747
+ this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
748
+ }
749
+ }
379
750
  }
380
751
 
381
752
  /**
@@ -385,38 +756,52 @@ export default class LocusInfo extends EventsScope {
385
756
  * @memberof LocusInfo
386
757
  */
387
758
  parse(meeting: any, data: any) {
388
- // eslint-disable-next-line @typescript-eslint/no-shadow
389
- const {eventType} = data;
390
- const locus = this.getTheLocusToUpdate(data.locus);
391
- LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
392
-
393
- locus.jsSdkMeta = {removedParticipantIds: []};
394
-
395
- switch (eventType) {
396
- case LOCUSEVENT.PARTICIPANT_JOIN:
397
- case LOCUSEVENT.PARTICIPANT_LEFT:
398
- case LOCUSEVENT.CONTROLS_UPDATED:
399
- case LOCUSEVENT.PARTICIPANT_AUDIO_MUTED:
400
- case LOCUSEVENT.PARTICIPANT_AUDIO_UNMUTED:
401
- case LOCUSEVENT.PARTICIPANT_VIDEO_MUTED:
402
- case LOCUSEVENT.PARTICIPANT_VIDEO_UNMUTED:
403
- case LOCUSEVENT.SELF_CHANGED:
404
- case LOCUSEVENT.PARTICIPANT_UPDATED:
405
- case LOCUSEVENT.PARTICIPANT_CONTROLS_UPDATED:
406
- case LOCUSEVENT.PARTICIPANT_ROLES_UPDATED:
407
- case LOCUSEVENT.PARTICIPANT_DECLINED:
408
- case LOCUSEVENT.FLOOR_GRANTED:
409
- case LOCUSEVENT.FLOOR_RELEASED:
410
- this.onFullLocus(locus, eventType);
411
- break;
412
- case LOCUSEVENT.DIFFERENCE:
413
- this.handleLocusDelta(locus, meeting);
414
- break;
759
+ if (this.hashTreeParser) {
760
+ this.handleHashTreeMessage(
761
+ meeting,
762
+ data.eventType,
763
+ data.stateElementsMessage as HashTreeMessage
764
+ );
765
+ } else {
766
+ // eslint-disable-next-line @typescript-eslint/no-shadow
767
+ const {eventType} = data;
768
+ const locus = this.getTheLocusToUpdate(data.locus);
769
+ LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
770
+
771
+ locus.jsSdkMeta = {removedParticipantIds: []};
772
+
773
+ switch (eventType) {
774
+ case LOCUSEVENT.PARTICIPANT_JOIN:
775
+ case LOCUSEVENT.PARTICIPANT_LEFT:
776
+ case LOCUSEVENT.CONTROLS_UPDATED:
777
+ case LOCUSEVENT.PARTICIPANT_AUDIO_MUTED:
778
+ case LOCUSEVENT.PARTICIPANT_AUDIO_UNMUTED:
779
+ case LOCUSEVENT.PARTICIPANT_VIDEO_MUTED:
780
+ case LOCUSEVENT.PARTICIPANT_VIDEO_UNMUTED:
781
+ case LOCUSEVENT.SELF_CHANGED:
782
+ case LOCUSEVENT.PARTICIPANT_UPDATED:
783
+ case LOCUSEVENT.PARTICIPANT_CONTROLS_UPDATED:
784
+ case LOCUSEVENT.PARTICIPANT_ROLES_UPDATED:
785
+ case LOCUSEVENT.PARTICIPANT_DECLINED:
786
+ case LOCUSEVENT.FLOOR_GRANTED:
787
+ case LOCUSEVENT.FLOOR_RELEASED:
788
+ this.onFullLocus(locus, eventType);
789
+ break;
790
+ case LOCUSEVENT.DIFFERENCE:
791
+ this.handleLocusDelta(locus, meeting);
792
+ break;
793
+ case LOCUSEVENT.HASH_TREE_DATA_UPDATED:
794
+ this.sendClassicVsHashTreeMismatchMetric(
795
+ meeting,
796
+ `got ${eventType}, expected classic events`
797
+ );
798
+ break;
415
799
 
416
- default:
417
- // Why will there be a event with no eventType ????
418
- // we may not need this, we can get full locus
419
- this.handleLocusDelta(locus, meeting);
800
+ default:
801
+ // Why will there be a event with no eventType ????
802
+ // we may not need this, we can get full locus
803
+ this.handleLocusDelta(locus, meeting);
804
+ }
420
805
  }
421
806
  }
422
807
 
@@ -432,19 +817,45 @@ export default class LocusInfo extends EventsScope {
432
817
  }
433
818
 
434
819
  /**
435
- * updates the locus with full locus object
820
+ * Function for handling full locus when it's using hash trees (so not the "classic" one).
821
+ *
436
822
  * @param {object} locus locus object
437
- * @param {string} eventType particulat locus event
438
- * @returns {object} null
439
- * @memberof LocusInfo
823
+ * @param {string} eventType locus event
824
+ * @param {DataSet[]} dataSets
825
+ * @returns {void}
440
826
  */
441
- onFullLocus(locus: any, eventType?: string) {
442
- if (!locus) {
443
- LoggerProxy.logger.error(
444
- 'Locus-info:index#onFullLocus --> object passed as argument was invalid, continuing.'
827
+ private onFullLocusWithHashTrees(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
828
+ if (!this.hashTreeParser) {
829
+ LoggerProxy.logger.info(`Locus-info:index#onFullLocus --> creating hash tree parser`);
830
+ LoggerProxy.logger.info(
831
+ 'Locus-info:index#onFullLocus --> dataSets:',
832
+ dataSets,
833
+ ' and locus:',
834
+ locus
835
+ );
836
+ this.hashTreeParser = this.createHashTreeParser({
837
+ initialLocus: {locus, dataSets},
838
+ });
839
+ this.onFullLocusCommon(locus, eventType);
840
+ } else {
841
+ // in this case the Locus we're getting is not necessarily the full one
842
+ // so treat it like if we just got it in any api response
843
+
844
+ LoggerProxy.logger.info(
845
+ 'Locus-info:index#onFullLocus --> hash tree parser already exists, handling it like a normal API response'
445
846
  );
847
+ this.handleLocusAPIResponse(undefined, {dataSets, locus});
446
848
  }
849
+ }
447
850
 
851
+ /**
852
+ * Function for handling full locus when it's the "classic" one (not hash trees)
853
+ *
854
+ * @param {object} locus locus object
855
+ * @param {string} eventType locus event
856
+ * @returns {void}
857
+ */
858
+ private onFullLocusClassic(locus: any, eventType?: string) {
448
859
  if (!this.locusParser.isNewFullLocus(locus)) {
449
860
  LoggerProxy.logger.info(
450
861
  `Locus-info:index#onFullLocus --> ignoring old full locus DTO, eventType=${eventType}`
@@ -452,9 +863,47 @@ export default class LocusInfo extends EventsScope {
452
863
 
453
864
  return;
454
865
  }
866
+ this.onFullLocusCommon(locus, eventType);
867
+ }
868
+
869
+ /**
870
+ * updates the locus with full locus object
871
+ * @param {object} locus locus object
872
+ * @param {string} eventType locus event
873
+ * @param {DataSet[]} dataSets
874
+ * @returns {object} null
875
+ * @memberof LocusInfo
876
+ */
877
+ onFullLocus(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
878
+ if (!locus) {
879
+ LoggerProxy.logger.error(
880
+ 'Locus-info:index#onFullLocus --> object passed as argument was invalid, continuing.'
881
+ );
882
+ }
883
+
884
+ if (dataSets) {
885
+ // this is the new hashmap Locus DTO format (only applicable to webinars for now)
886
+ this.onFullLocusWithHashTrees(locus, eventType, dataSets);
887
+ } else {
888
+ this.onFullLocusClassic(locus, eventType);
889
+ }
890
+ }
455
891
 
892
+ /**
893
+ * Common part of handling full locus, used by both classic and hash tree based locus handling
894
+ * @param {object} locus locus object
895
+ * @param {string} eventType locus event
896
+ * @returns {void}
897
+ */
898
+ private onFullLocusCommon(locus: any, eventType?: string) {
456
899
  this.scheduledMeeting = locus.meeting || null;
457
900
  this.participants = locus.participants;
901
+ this.participants?.forEach((participant) => {
902
+ // participant.htMeta is set only for hash tree based locus
903
+ if (participant.htMeta?.elementId.id) {
904
+ this.hashTreeObjectId2ParticipantId.set(participant.htMeta.elementId.id, participant.id);
905
+ }
906
+ });
458
907
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
459
908
  this.updateLocusInfo(locus);
460
909
  this.updateParticipants(