@webex/plugin-meetings 3.11.0 → 3.12.0-next.2

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 (170) hide show
  1. package/dist/aiEnableRequest/index.js +184 -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 +14 -5
  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 +7 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +28 -6
  12. package/dist/constants.js.map +1 -1
  13. package/dist/hashTree/constants.js +3 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTree.js +18 -0
  16. package/dist/hashTree/hashTree.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +850 -410
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/types.js +4 -2
  20. package/dist/hashTree/types.js.map +1 -1
  21. package/dist/hashTree/utils.js +10 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +11 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/constant.js +12 -0
  26. package/dist/interceptors/constant.js.map +1 -0
  27. package/dist/interceptors/dataChannelAuthToken.js +290 -0
  28. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  29. package/dist/interceptors/index.js +7 -0
  30. package/dist/interceptors/index.js.map +1 -1
  31. package/dist/interceptors/utils.js +27 -0
  32. package/dist/interceptors/utils.js.map +1 -0
  33. package/dist/interpretation/index.js +2 -2
  34. package/dist/interpretation/index.js.map +1 -1
  35. package/dist/interpretation/siLanguage.js +1 -1
  36. package/dist/locus-info/controlsUtils.js +5 -3
  37. package/dist/locus-info/controlsUtils.js.map +1 -1
  38. package/dist/locus-info/index.js +522 -131
  39. package/dist/locus-info/index.js.map +1 -1
  40. package/dist/locus-info/selfUtils.js +1 -0
  41. package/dist/locus-info/selfUtils.js.map +1 -1
  42. package/dist/locus-info/types.js.map +1 -1
  43. package/dist/media/MediaConnectionAwaiter.js +57 -1
  44. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  45. package/dist/media/properties.js +4 -2
  46. package/dist/media/properties.js.map +1 -1
  47. package/dist/meeting/in-meeting-actions.js +7 -1
  48. package/dist/meeting/in-meeting-actions.js.map +1 -1
  49. package/dist/meeting/index.js +1173 -877
  50. package/dist/meeting/index.js.map +1 -1
  51. package/dist/meeting/request.js +50 -0
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/request.type.js.map +1 -1
  54. package/dist/meeting/util.js +133 -3
  55. package/dist/meeting/util.js.map +1 -1
  56. package/dist/meetings/index.js +117 -48
  57. package/dist/meetings/index.js.map +1 -1
  58. package/dist/member/index.js +10 -0
  59. package/dist/member/index.js.map +1 -1
  60. package/dist/member/util.js +10 -0
  61. package/dist/member/util.js.map +1 -1
  62. package/dist/metrics/constants.js +2 -1
  63. package/dist/metrics/constants.js.map +1 -1
  64. package/dist/multistream/mediaRequestManager.js +9 -60
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/remoteMediaManager.js +11 -0
  67. package/dist/multistream/remoteMediaManager.js.map +1 -1
  68. package/dist/reachability/index.js +18 -10
  69. package/dist/reachability/index.js.map +1 -1
  70. package/dist/reactions/reactions.type.js.map +1 -1
  71. package/dist/reconnection-manager/index.js +0 -1
  72. package/dist/reconnection-manager/index.js.map +1 -1
  73. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  74. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  75. package/dist/types/config.d.ts +4 -0
  76. package/dist/types/constants.d.ts +23 -1
  77. package/dist/types/hashTree/constants.d.ts +1 -0
  78. package/dist/types/hashTree/hashTree.d.ts +7 -0
  79. package/dist/types/hashTree/hashTreeParser.d.ts +122 -14
  80. package/dist/types/hashTree/types.d.ts +3 -0
  81. package/dist/types/hashTree/utils.d.ts +6 -0
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/interceptors/constant.d.ts +5 -0
  84. package/dist/types/interceptors/dataChannelAuthToken.d.ts +43 -0
  85. package/dist/types/interceptors/index.d.ts +2 -1
  86. package/dist/types/interceptors/utils.d.ts +1 -0
  87. package/dist/types/locus-info/index.d.ts +60 -8
  88. package/dist/types/locus-info/types.d.ts +7 -0
  89. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  90. package/dist/types/media/properties.d.ts +2 -1
  91. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  92. package/dist/types/meeting/index.d.ts +61 -7
  93. package/dist/types/meeting/request.d.ts +16 -1
  94. package/dist/types/meeting/request.type.d.ts +5 -0
  95. package/dist/types/meeting/util.d.ts +31 -0
  96. package/dist/types/meetings/index.d.ts +4 -2
  97. package/dist/types/member/index.d.ts +1 -0
  98. package/dist/types/member/util.d.ts +5 -0
  99. package/dist/types/metrics/constants.d.ts +1 -0
  100. package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
  101. package/dist/types/reactions/reactions.type.d.ts +1 -0
  102. package/dist/types/webinar/utils.d.ts +6 -0
  103. package/dist/webinar/index.js +291 -91
  104. package/dist/webinar/index.js.map +1 -1
  105. package/dist/webinar/utils.js +25 -0
  106. package/dist/webinar/utils.js.map +1 -0
  107. package/package.json +24 -23
  108. package/src/aiEnableRequest/README.md +84 -0
  109. package/src/aiEnableRequest/index.ts +170 -0
  110. package/src/aiEnableRequest/utils.ts +25 -0
  111. package/src/annotation/index.ts +27 -7
  112. package/src/config.ts +4 -0
  113. package/src/constants.ts +29 -1
  114. package/src/hashTree/constants.ts +1 -0
  115. package/src/hashTree/hashTree.ts +17 -0
  116. package/src/hashTree/hashTreeParser.ts +745 -252
  117. package/src/hashTree/types.ts +4 -0
  118. package/src/hashTree/utils.ts +9 -0
  119. package/src/index.ts +8 -1
  120. package/src/interceptors/constant.ts +6 -0
  121. package/src/interceptors/dataChannelAuthToken.ts +170 -0
  122. package/src/interceptors/index.ts +2 -1
  123. package/src/interceptors/utils.ts +16 -0
  124. package/src/interpretation/index.ts +2 -2
  125. package/src/locus-info/controlsUtils.ts +11 -0
  126. package/src/locus-info/index.ts +579 -113
  127. package/src/locus-info/selfUtils.ts +1 -0
  128. package/src/locus-info/types.ts +8 -0
  129. package/src/media/MediaConnectionAwaiter.ts +41 -1
  130. package/src/media/properties.ts +3 -1
  131. package/src/meeting/in-meeting-actions.ts +12 -0
  132. package/src/meeting/index.ts +291 -76
  133. package/src/meeting/request.ts +42 -0
  134. package/src/meeting/request.type.ts +6 -0
  135. package/src/meeting/util.ts +160 -2
  136. package/src/meetings/index.ts +157 -44
  137. package/src/member/index.ts +10 -0
  138. package/src/member/util.ts +12 -0
  139. package/src/metrics/constants.ts +1 -0
  140. package/src/multistream/mediaRequestManager.ts +4 -54
  141. package/src/multistream/remoteMediaManager.ts +13 -0
  142. package/src/reachability/index.ts +9 -0
  143. package/src/reactions/reactions.type.ts +1 -0
  144. package/src/reconnection-manager/index.ts +0 -1
  145. package/src/webinar/index.ts +191 -6
  146. package/src/webinar/utils.ts +16 -0
  147. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  148. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  149. package/test/unit/spec/annotation/index.ts +69 -7
  150. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  151. package/test/unit/spec/hashTree/hashTreeParser.ts +2225 -189
  152. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
  153. package/test/unit/spec/interceptors/utils.ts +75 -0
  154. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  155. package/test/unit/spec/locus-info/index.js +1134 -55
  156. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  157. package/test/unit/spec/media/properties.ts +12 -3
  158. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  159. package/test/unit/spec/meeting/index.js +829 -115
  160. package/test/unit/spec/meeting/request.js +70 -0
  161. package/test/unit/spec/meeting/utils.js +438 -26
  162. package/test/unit/spec/meetings/index.js +653 -32
  163. package/test/unit/spec/member/index.js +28 -4
  164. package/test/unit/spec/member/util.js +65 -27
  165. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
  166. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
  167. package/test/unit/spec/reachability/index.ts +23 -0
  168. package/test/unit/spec/reconnection-manager/index.js +4 -8
  169. package/test/unit/spec/webinar/index.ts +474 -37
  170. package/test/unit/spec/webinar/utils.ts +39 -0
@@ -35,10 +35,14 @@ import HashTreeParser, {
35
35
  DataSet,
36
36
  HashTreeMessage,
37
37
  LocusInfoUpdateType,
38
+ Metadata,
38
39
  } from '../hashTree/hashTreeParser';
39
40
  import {HashTreeObject, ObjectType, ObjectTypeToLocusKeyMap} from '../hashTree/types';
40
- import {isSelf} from '../hashTree/utils';
41
- import {Links, LocusDTO, LocusFullState} from './types';
41
+ import {isMetadata, isSelf} from '../hashTree/utils';
42
+ import {Links, LocusDTO, ReplacesInfo} from './types';
43
+ import MeetingsUtil from '../meetings/util';
44
+ import {MEETING_KEY} from '../meetings/meetings.types';
45
+ import MeetingCollection from '../meetings/collection';
42
46
 
43
47
  export type LocusLLMEvent = {
44
48
  data: {
@@ -52,6 +56,7 @@ export type LocusLLMEvent = {
52
56
  const LocusDtoTopLevelKeys = [
53
57
  'controls',
54
58
  'fullState',
59
+ 'embeddedApps',
55
60
  'host',
56
61
  'info',
57
62
  'links',
@@ -66,10 +71,13 @@ const LocusDtoTopLevelKeys = [
66
71
  'htMeta', // only exists when hash trees are used
67
72
  ];
68
73
 
69
- export type LocusApiResponseBody = {
70
- dataSets?: DataSet[];
71
- 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)
72
- };
74
+ export type LocusApiResponseBody =
75
+ | {
76
+ dataSets?: DataSet[];
77
+ 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)
78
+ metadata?: Metadata;
79
+ }
80
+ | LocusDTO; // when we invoke APIs on the whole Locus like "mute all" backend returns the whole Locus in the response like this
73
81
 
74
82
  const LocusObjectStateAfterUpdates = {
75
83
  unchanged: 'unchanged',
@@ -79,6 +87,167 @@ const LocusObjectStateAfterUpdates = {
79
87
 
80
88
  type LocusObjectStateAfterUpdates = Enum<typeof LocusObjectStateAfterUpdates>;
81
89
 
90
+ export type HashTreeParserEntry = {
91
+ parser: HashTreeParser;
92
+ replacedAt?: string;
93
+ initializedFromHashTree: boolean;
94
+ };
95
+
96
+ /**
97
+ * Gets the replacement information
98
+ *
99
+ * @param {any} self - "self" object from Locus DTO
100
+ * @param {string} deviceUrl - The URL of the user's device
101
+ * @returns {any} The replace information if available, otherwise undefined
102
+ */
103
+ function getReplaceInfoFromSelf(self: any, deviceUrl: string): ReplacesInfo | undefined {
104
+ if (self) {
105
+ const device = MeetingsUtil.getThisDevice({self}, deviceUrl);
106
+
107
+ if (device?.replaces?.length > 0) {
108
+ return device.replaces[0];
109
+ }
110
+ }
111
+
112
+ return undefined;
113
+ }
114
+
115
+ /**
116
+ * Finds a meeting by its locus URL in meeting collection. It checks all HashTreeParsers of all meetings in the collection.
117
+ *
118
+ * @param {MeetingCollection} meetingCollection - The collection of meetings to search
119
+ * @param {string} locusUrl - The locus URL to search for
120
+ * @returns {any} The meeting if found, otherwise undefined
121
+ */
122
+ function findLocusUrlInAnyHashTreeParser(
123
+ meetingCollection: MeetingCollection,
124
+ locusUrl: string
125
+ ): any {
126
+ for (const meeting of Object.values(meetingCollection.getAll()) as any[]) {
127
+ if (meeting?.locusInfo?.hashTreeParsers?.has(locusUrl)) {
128
+ return meeting;
129
+ }
130
+ }
131
+
132
+ return undefined;
133
+ }
134
+
135
+ /**
136
+ * Finds a meeting for a given hash tree message.
137
+ *
138
+ * @param {HashTreeMessage} message - The hash tree message to find the meeting for
139
+ * @param {MeetingCollection} meetingCollection - The collection of meetings to search
140
+ * @param {string} deviceUrl - The URL of the user's device
141
+ * @returns {any} The meeting if found, otherwise undefined
142
+ */
143
+ export function findMeetingForHashTreeMessage(
144
+ message: HashTreeMessage,
145
+ meetingCollection: MeetingCollection,
146
+ deviceUrl: string
147
+ ): any {
148
+ let foundMeeting = findLocusUrlInAnyHashTreeParser(meetingCollection, message.locusUrl);
149
+
150
+ if (foundMeeting) {
151
+ return foundMeeting;
152
+ }
153
+
154
+ // if we haven't found anything, it may mean that message has a new locusUrl
155
+ // check if it indicates that it replaces some existing current locusUrl (this is indicated in "self")
156
+ const self = message.locusStateElements?.find((el) => isSelf(el))?.data;
157
+ const replaces = getReplaceInfoFromSelf(self, deviceUrl);
158
+
159
+ if (replaces?.locusUrl) {
160
+ foundMeeting = findLocusUrlInAnyHashTreeParser(meetingCollection, replaces.locusUrl);
161
+
162
+ return foundMeeting;
163
+ }
164
+
165
+ return undefined;
166
+ }
167
+
168
+ /**
169
+ * Creates a locus object from the objects received in a hash tree message. It usually will be
170
+ * incomplete, because hash tree messages only contain the parts of locus that have changed,
171
+ * and some updates come separately over Mercury or LLM in separate messages.
172
+ *
173
+ * @param {HashTreeMessage} message hash tree message to created the locus from
174
+ * @returns {Object} the created locus object and metadata if present
175
+ */
176
+ export function createLocusFromHashTreeMessage(message: HashTreeMessage): {
177
+ locus: LocusDTO;
178
+ metadata?: Metadata;
179
+ } {
180
+ const locus: LocusDTO = {
181
+ participants: [],
182
+ url: message.locusUrl,
183
+ };
184
+ let metadata: Metadata | undefined;
185
+
186
+ if (!message.locusStateElements) {
187
+ return {locus, metadata};
188
+ }
189
+
190
+ for (const element of message.locusStateElements) {
191
+ if (!element.data) {
192
+ // eslint-disable-next-line no-continue
193
+ continue;
194
+ }
195
+
196
+ const type = element.htMeta.elementId.type.toLowerCase();
197
+
198
+ switch (type) {
199
+ case ObjectType.locus: {
200
+ // spread locus object data onto the top level, but remove keys managed by other ObjectTypes
201
+ const locusObjectData = {...element.data};
202
+
203
+ Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
204
+ delete locusObjectData[locusDtoKey];
205
+ });
206
+
207
+ Object.assign(locus, locusObjectData);
208
+ break;
209
+ }
210
+ case ObjectType.participant:
211
+ locus.participants.push(element.data);
212
+ break;
213
+ case ObjectType.mediaShare:
214
+ if (!locus.mediaShares) {
215
+ locus.mediaShares = [];
216
+ }
217
+ locus.mediaShares.push(element.data);
218
+ break;
219
+ case ObjectType.embeddedApp:
220
+ if (!locus.embeddedApps) {
221
+ locus.embeddedApps = [];
222
+ }
223
+ locus.embeddedApps.push(element.data);
224
+ break;
225
+ case ObjectType.control:
226
+ if (!locus.controls) {
227
+ locus.controls = {};
228
+ }
229
+ Object.assign(locus.controls, element.data);
230
+ break;
231
+ case ObjectType.links:
232
+ case ObjectType.info:
233
+ case ObjectType.fullState:
234
+ case ObjectType.self: {
235
+ const locusDtoKey = ObjectTypeToLocusKeyMap[type];
236
+ locus[locusDtoKey] = element.data;
237
+ break;
238
+ }
239
+ case ObjectType.metadata:
240
+ // metadata is not part of Locus DTO
241
+ metadata = {...element.data, htMeta: element.htMeta} as Metadata;
242
+ break;
243
+ default:
244
+ break;
245
+ }
246
+ }
247
+
248
+ return {locus, metadata};
249
+ }
250
+
82
251
  /**
83
252
  * @description LocusInfo extends ChildEmitter to convert locusInfo info a private emitter to parent object
84
253
  * @export
@@ -112,7 +281,7 @@ export default class LocusInfo extends EventsScope {
112
281
  links?: Links;
113
282
  mainSessionLocusCache: any;
114
283
  self: any;
115
- hashTreeParser?: HashTreeParser;
284
+ hashTreeParsers: Map<string, HashTreeParserEntry>;
116
285
  hashTreeObjectId2ParticipantId: Map<number, string>; // mapping of hash tree object ids to participant ids
117
286
  classicVsHashTreeMismatchMetricCounter = 0;
118
287
 
@@ -134,6 +303,7 @@ export default class LocusInfo extends EventsScope {
134
303
  this.meetingId = meetingId;
135
304
  this.updateMeeting = updateMeeting;
136
305
  this.locusParser = new LocusDeltaParser();
306
+ this.hashTreeParsers = new Map();
137
307
  this.hashTreeObjectId2ParticipantId = new Map();
138
308
  }
139
309
 
@@ -239,7 +409,7 @@ export default class LocusInfo extends EventsScope {
239
409
  'Locus-info:index#doLocusSync --> got full DTO when we asked for delta'
240
410
  );
241
411
  }
242
- meeting.locusInfo.onFullLocus(res.body);
412
+ meeting.locusInfo.onFullLocus('classic Locus sync', res.body);
243
413
  })
244
414
  .catch((e) => {
245
415
  LoggerProxy.logger.info(
@@ -356,24 +526,56 @@ export default class LocusInfo extends EventsScope {
356
526
  }
357
527
 
358
528
  /**
359
- * Creates the HashTreeParser instance.
360
- * @param {Object} initial locus data
361
- * @returns {void}
529
+ * Creates a HashTreeParser instance for a given locusUrl and stores it in the map.
530
+ * @param {Object} params
531
+ * @param {string} params.locusUrl - the locus URL used as the map key
532
+ * @param {Object} params.initialLocus - initial locus data
533
+ * @param {Object} params.metadata - hash tree metadata
534
+ * @param {string} params.replacedAt - timestamp from Locus indicating when the replacement happened
535
+ * @returns {HashTreeParser} the newly created parser
362
536
  */
363
537
  private createHashTreeParser({
538
+ locusUrl,
364
539
  initialLocus,
540
+ metadata,
541
+ replacedAt,
365
542
  }: {
543
+ locusUrl: string;
366
544
  initialLocus: {
367
545
  dataSets: Array<DataSet>;
368
546
  locus: any;
369
547
  };
370
- }) {
371
- return new HashTreeParser({
548
+ metadata: Metadata;
549
+ replacedAt?: string;
550
+ }): HashTreeParser {
551
+ const parser = new HashTreeParser({
372
552
  initialLocus,
553
+ metadata,
373
554
  webexRequest: this.webex.request.bind(this.webex),
374
- locusInfoUpdateCallback: this.updateFromHashTree.bind(this),
375
- debugId: `HT-${this.meetingId.substring(0, 4)}`,
555
+ locusInfoUpdateCallback: this.updateFromHashTree.bind(this, locusUrl),
556
+ debugId: `HT-${locusUrl.split('/').pop().substring(0, 4)}`,
557
+ excludedDataSets: this.webex.config.meetings.locus?.excludedDataSets,
376
558
  });
559
+
560
+ // When a new HashTreeParser is created, previous one should be stopped.
561
+ // Locus will only be sending us updates for the current one.
562
+ for (const [existingLocusUrl, existingEntry] of this.hashTreeParsers) {
563
+ if (existingEntry.parser.state !== 'stopped') {
564
+ existingEntry.parser.stop();
565
+ if (replacedAt) {
566
+ existingEntry.replacedAt = replacedAt;
567
+ } else {
568
+ LoggerProxy.logger.warn(
569
+ `Locus-info:index#createHashTreeParser --> no replacedAt timestamp provided for new HashTreeParser with locusUrl ${locusUrl}, replacing ${existingLocusUrl}`
570
+ );
571
+ }
572
+ }
573
+ }
574
+
575
+ this.hashTreeParsers.set(locusUrl, {parser, initializedFromHashTree: false});
576
+ this.hashTreeObjectId2ParticipantId.clear();
577
+
578
+ return parser;
377
579
  }
378
580
 
379
581
  /**
@@ -387,6 +589,7 @@ export default class LocusInfo extends EventsScope {
387
589
  trigger: 'join-response';
388
590
  locus: LocusDTO;
389
591
  dataSets?: DataSet[];
592
+ metadata?: Metadata;
390
593
  }
391
594
  | {
392
595
  trigger: 'locus-message';
@@ -401,41 +604,50 @@ export default class LocusInfo extends EventsScope {
401
604
  switch (data.trigger) {
402
605
  case 'locus-message':
403
606
  if (data.hashTreeMessage) {
404
- // we need the SELF object to be in the received message, because it contains visibleDataSets
607
+ // we need the Metadata object to be in the received message, because it contains visibleDataSets
405
608
  // and these are needed to initialize all the hash trees
406
- const selfObject = data.hashTreeMessage.locusStateElements?.find((el) => isSelf(el));
609
+ const metadataObject = data.hashTreeMessage.locusStateElements?.find((el) =>
610
+ isMetadata(el)
611
+ );
407
612
 
408
- if (!selfObject?.data?.visibleDataSets) {
409
- LoggerProxy.logger.warn(
410
- `Locus-info:index#initialSetup --> cannot initialize HashTreeParser, SELF object with visibleDataSets is missing in the message`
613
+ if (!metadataObject?.data?.visibleDataSets) {
614
+ // this is a common case (not an error)
615
+ // it happens for example after we leave the meeting and still get some heartbeats or delayed messages
616
+ LoggerProxy.logger.info(
617
+ `Locus-info:index#initialSetup --> cannot initialize HashTreeParser, Metadata object with visibleDataSets is missing in the message`
411
618
  );
412
619
 
413
- throw new Error('SELF object with visibleDataSets is missing in the message');
620
+ // throw so that handleLocusEvent() catches it and destroys the partially created meeting object
621
+ throw new Error('Metadata object with visibleDataSets is missing in the message');
414
622
  }
415
623
 
416
624
  LoggerProxy.logger.info(
417
625
  'Locus-info:index#initialSetup --> creating HashTreeParser from message'
418
626
  );
419
627
  // first create the HashTreeParser, but don't initialize it with any data yet
420
- // pass just a fake locus that contains only the visibleDataSets
421
- this.hashTreeParser = this.createHashTreeParser({
628
+ const hashTreeParser = this.createHashTreeParser({
629
+ locusUrl: data.hashTreeMessage.locusUrl,
422
630
  initialLocus: {
423
- locus: {self: {visibleDataSets: selfObject.data.visibleDataSets}},
424
- dataSets: [], // empty, because they will be populated in initializeFromMessage() call // dataSets: data.hashTreeMessage.dataSets,
631
+ locus: null,
632
+ dataSets: data.hashTreeMessage.dataSets,
633
+ },
634
+ metadata: {
635
+ htMeta: metadataObject.htMeta,
636
+ visibleDataSets: metadataObject.data.visibleDataSets,
425
637
  },
426
638
  });
427
639
 
428
640
  // now handle the message - that should populate all the visible datasets
429
- await this.hashTreeParser.initializeFromMessage(data.hashTreeMessage);
641
+ await hashTreeParser.initializeFromMessage(data.hashTreeMessage);
430
642
  } else {
431
643
  // "classic" Locus case, no hash trees involved
432
644
  this.updateLocusCache(data.locus);
433
- this.onFullLocus(data.locus, undefined);
645
+ this.onFullLocus('classic locus message', data.locus, undefined);
434
646
  }
435
647
  break;
436
648
  case 'join-response':
437
649
  this.updateLocusCache(data.locus);
438
- this.onFullLocus(data.locus, undefined, data.dataSets);
650
+ this.onFullLocus('join response', data.locus, undefined, data.dataSets, data.metadata);
439
651
  break;
440
652
  case 'get-loci-response':
441
653
  if (data.locus?.links?.resources?.visibleDataSets?.url) {
@@ -443,20 +655,21 @@ export default class LocusInfo extends EventsScope {
443
655
  'Locus-info:index#initialSetup --> creating HashTreeParser from get-loci-response'
444
656
  );
445
657
  // first create the HashTreeParser, but don't initialize it with any data yet
446
- // pass just a fake locus that contains only the visibleDataSets
447
- this.hashTreeParser = this.createHashTreeParser({
658
+ const hashTreeParser = this.createHashTreeParser({
659
+ locusUrl: data.locus.url,
448
660
  initialLocus: {
449
- locus: {self: {visibleDataSets: data.locus?.self?.visibleDataSets}},
661
+ locus: null,
450
662
  dataSets: [], // empty, because we don't have them yet
451
663
  },
664
+ metadata: null, // get-loci-response doesn't contain Metadata object
452
665
  });
453
666
 
454
667
  // now initialize all the data
455
- await this.hashTreeParser.initializeFromGetLociResponse(data.locus);
668
+ await hashTreeParser.initializeFromGetLociResponse(data.locus);
456
669
  } else {
457
670
  // "classic" Locus case, no hash trees involved
458
671
  this.updateLocusCache(data.locus);
459
- this.onFullLocus(data.locus, undefined);
672
+ this.onFullLocus('classic get-loci-response', data.locus, undefined);
460
673
  }
461
674
  }
462
675
  // Change it to true after it receives it first locus object
@@ -470,34 +683,42 @@ export default class LocusInfo extends EventsScope {
470
683
  * @returns {void}
471
684
  */
472
685
  handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
473
- if (this.hashTreeParser) {
474
- if (!responseBody.dataSets) {
475
- this.sendClassicVsHashTreeMismatchMetric(
476
- meeting,
477
- `expected hash tree dataSets in API response but they are missing`
686
+ const isWrapped = 'locus' in responseBody;
687
+ const locusUrl = isWrapped ? responseBody.locus?.url : responseBody.url;
688
+ const hashTreeParserEntry = locusUrl && this.hashTreeParsers.get(locusUrl);
689
+ if (hashTreeParserEntry) {
690
+ if (isWrapped) {
691
+ if (!responseBody.dataSets) {
692
+ this.sendClassicVsHashTreeMismatchMetric(
693
+ meeting,
694
+ `expected hash tree dataSets in API response but they are missing`
695
+ );
696
+ // continuing as we can still manage without responseBody.dataSets, but this is very suspicious
697
+ }
698
+ LoggerProxy.logger.info(
699
+ 'Locus-info:index#handleLocusAPIResponse --> passing Locus API response to HashTreeParser: ',
700
+ responseBody
478
701
  );
479
- // continuing as we can still manage without responseBody.dataSets, but this is very suspicious
702
+ // update the data in our hash trees
703
+ hashTreeParserEntry.parser.handleLocusUpdate(responseBody);
704
+ } else {
705
+ // LocusDTO without wrapper - pass it through as if it had no dataSets
706
+ hashTreeParserEntry.parser.handleLocusUpdate({locus: responseBody});
480
707
  }
481
- LoggerProxy.logger.info(
482
- 'Locus-info:index#handleLocusAPIResponse --> passing Locus API response to HashTreeParser: ',
483
- responseBody
484
- );
485
- // update the data in our hash trees
486
- this.hashTreeParser.handleLocusUpdate(responseBody);
487
708
  } else {
488
- if (responseBody.dataSets) {
709
+ if (isWrapped && responseBody.dataSets) {
489
710
  this.sendClassicVsHashTreeMismatchMetric(
490
711
  meeting,
491
712
  `unexpected hash tree dataSets in API response`
492
713
  );
493
714
  }
494
715
  // classic Locus delta
495
- this.handleLocusDelta(responseBody.locus, meeting);
716
+ const locus = isWrapped ? responseBody.locus : responseBody;
717
+ this.handleLocusDelta(locus, meeting);
496
718
  }
497
719
  }
498
720
 
499
721
  /**
500
- *
501
722
  * @param {HashTreeObject} object data set object
502
723
  * @param {any} locus
503
724
  * @returns {void}
@@ -571,6 +792,31 @@ export default class LocusInfo extends EventsScope {
571
792
  );
572
793
  }
573
794
  break;
795
+ case ObjectType.embeddedApp:
796
+ if (object.data) {
797
+ LoggerProxy.logger.info(
798
+ `Locus-info:index#updateLocusFromHashTreeObject --> embeddedApp id=${object.htMeta.elementId.id} url='${object.data.url}' updated version=${object.htMeta.elementId.version}:`,
799
+ object.data
800
+ );
801
+ const existingEmbeddedApp = locus.embeddedApps?.find(
802
+ (ms) => ms.htMeta.elementId.id === object.htMeta.elementId.id
803
+ );
804
+
805
+ if (existingEmbeddedApp) {
806
+ Object.assign(existingEmbeddedApp, object.data);
807
+ } else {
808
+ locus.embeddedApps = locus.embeddedApps || [];
809
+ locus.embeddedApps.push(object.data);
810
+ }
811
+ } else {
812
+ LoggerProxy.logger.info(
813
+ `Locus-info:index#updateLocusFromHashTreeObject --> embeddedApp id=${object.htMeta.elementId.id} removed, version=${object.htMeta.elementId.version}`
814
+ );
815
+ locus.embeddedApps = locus.embeddedApps?.filter(
816
+ (ms) => ms.htMeta.elementId.id !== object.htMeta.elementId.id
817
+ );
818
+ }
819
+ break;
574
820
  case ObjectType.participant:
575
821
  LoggerProxy.logger.info(
576
822
  `Locus-info:index#updateLocusFromHashTreeObject --> participant id=${
@@ -588,6 +834,23 @@ export default class LocusInfo extends EventsScope {
588
834
  locus.jsSdkMeta.removedParticipantIds.push(participantId);
589
835
  this.hashTreeObjectId2ParticipantId.delete(object.htMeta.elementId.id);
590
836
  }
837
+ // Create self from the participant if it matches self identity and is being moved.
838
+ // We need this, because participant update comes in LLM message often before the self update from Mercury.
839
+ // Other parts of the code detect move only by looking at self, while some other parts of the SDK/webapp code
840
+ // look at participant for roles etc, so if participant is updated but not self, then it looks like we our lost roles temporarily
841
+ // (until self is updated)
842
+ // This will be fixed properly in SPARK-790239
843
+ if (
844
+ object.data &&
845
+ object.data.identity === locus.self?.identity &&
846
+ object.data.state === 'LEFT' &&
847
+ object.data.reason === 'MOVED'
848
+ ) {
849
+ LoggerProxy.logger.info(
850
+ `Locus-info:index#updateLocusFromHashTreeObject --> FOUND a match for MOVED self in participant object ${object.htMeta.elementId.id}`
851
+ );
852
+ Object.assign(locus[ObjectTypeToLocusKeyMap[ObjectType.self]], object.data);
853
+ }
591
854
  break;
592
855
  case ObjectType.control:
593
856
  if (object.data) {
@@ -643,6 +906,12 @@ export default class LocusInfo extends EventsScope {
643
906
  }
644
907
  }
645
908
  break;
909
+ case ObjectType.metadata:
910
+ LoggerProxy.logger.info(
911
+ `Locus-info:index#updateLocusFromHashTreeObject --> metadata object updated to version ${object.htMeta.elementId.version}`
912
+ );
913
+ // we don't use hash tree metadata right now for anything, it's mainly used internally by HashTreeParser
914
+ break;
646
915
  default:
647
916
  LoggerProxy.logger.warn(
648
917
  `Locus-info:index#updateLocusFromHashTreeObject --> received unsupported object type ${type}`
@@ -675,6 +944,92 @@ export default class LocusInfo extends EventsScope {
675
944
  }
676
945
  }
677
946
 
947
+ /**
948
+ * Checks if the hash tree message should trigger a switch to a different HashTreeParser
949
+ *
950
+ * @param {HashTreeMessage} message incoming hash tree message
951
+ * @returns {boolean} true if the message was handled as a parser switch, false otherwise
952
+ */
953
+ private handleHashTreeParserSwitch(message: HashTreeMessage): boolean {
954
+ const entry = this.hashTreeParsers.get(message.locusUrl);
955
+
956
+ const self = message.locusStateElements?.find((el) => isSelf(el))?.data;
957
+ const replaces = getReplaceInfoFromSelf(
958
+ self,
959
+ // @ts-ignore
960
+ this.webex.internal.device.url
961
+ );
962
+
963
+ if (!entry) {
964
+ // Metadata object that contains information about visible datasets is needed to initialize the HashTreeParser,
965
+ // but it's buried inside the message, we need to find it and pass it to HashTreeParser constructor
966
+ const metadata = message.locusStateElements?.find((el) => isMetadata(el));
967
+
968
+ if (metadata?.data?.visibleDataSets?.length > 0) {
969
+ LoggerProxy.logger.info(
970
+ `Locus-info:index#handleHashTreeParserSwitch --> no hash tree parser found for locusUrl ${message.locusUrl}, creating a new one`
971
+ );
972
+
973
+ const parser = this.createHashTreeParser({
974
+ locusUrl: message.locusUrl,
975
+ initialLocus: {
976
+ locus: null,
977
+ dataSets: message.dataSets,
978
+ },
979
+ metadata: {
980
+ htMeta: metadata.htMeta,
981
+ visibleDataSets: metadata.data.visibleDataSets,
982
+ },
983
+ replacedAt: replaces?.replacedAt,
984
+ });
985
+
986
+ // handle the message with the new parser
987
+ parser.handleMessage(message);
988
+ }
989
+
990
+ return true;
991
+ }
992
+ if (entry.parser.state === 'stopped') {
993
+ // the message matches a stopped parser, we need to check if maybe this is a new "replacement" and we need to re-activate the parser
994
+ // this happens when you move from breakout A -> breakout B -> back to breakout A
995
+ if (replaces) {
996
+ if (replaces.replacedAt > (entry.replacedAt || '')) {
997
+ LoggerProxy.logger.info(
998
+ `Locus-info:index#handleHashTreeParserSwitch --> resuming a HashTreeParser for locusUrl=${message.locusUrl}, which replaces ${replaces.locusUrl}`
999
+ );
1000
+ const replacedEntry = this.hashTreeParsers.get(replaces.locusUrl);
1001
+
1002
+ if (replacedEntry) {
1003
+ replacedEntry.replacedAt = replaces.replacedAt;
1004
+ entry.initializedFromHashTree = false;
1005
+ this.hashTreeObjectId2ParticipantId.clear();
1006
+
1007
+ replacedEntry.parser.stop();
1008
+ entry.parser.resume(message);
1009
+ } else {
1010
+ LoggerProxy.logger.warn(
1011
+ `Locus-info:index#handleHashTreeParserSwitch --> the parser that is supposed to be replaced with the currently resumed parser is not found, locusUrl=${replaces.locusUrl}`
1012
+ );
1013
+ }
1014
+ } else {
1015
+ LoggerProxy.logger.info(
1016
+ `Locus-info:index#handleHashTreeParserSwitch --> received message for stopped HashTreeParser with locusUrl ${message.locusUrl}, but replaces info provided is not newer, so not re-activating the parser`
1017
+ );
1018
+ }
1019
+
1020
+ return true;
1021
+ }
1022
+
1023
+ LoggerProxy.logger.info(
1024
+ `Locus-info:index#handleHashTreeParserSwitch --> received message for stopped HashTreeParser with locusUrl ${message.locusUrl}, but no replaces info provided, so not re-activating the parser`
1025
+ );
1026
+
1027
+ return true;
1028
+ }
1029
+
1030
+ return false;
1031
+ }
1032
+
678
1033
  /**
679
1034
  * Handles a hash tree message received from Locus.
680
1035
  *
@@ -693,18 +1048,28 @@ export default class LocusInfo extends EventsScope {
693
1048
  return;
694
1049
  }
695
1050
 
696
- this.hashTreeParser.handleMessage(message);
1051
+ const parserSwitched = this.handleHashTreeParserSwitch(message);
1052
+
1053
+ if (parserSwitched) {
1054
+ return;
1055
+ }
1056
+
1057
+ const entry = this.hashTreeParsers.get(message.locusUrl);
1058
+
1059
+ entry.parser.handleMessage(message);
697
1060
  }
698
1061
 
699
1062
  /**
700
1063
  * Callback registered with HashTreeParser to receive locus info updates.
701
1064
  * Updates our locus info based on the data parsed by the hash tree parser.
702
1065
  *
1066
+ * @param {string} locusUrl - the locus URL for which the update is received
703
1067
  * @param {LocusInfoUpdateType} updateType - The type of update received.
704
1068
  * @param {Object} [data] - Additional data for the update, if applicable.
705
1069
  * @returns {void}
706
1070
  */
707
1071
  private updateFromHashTree(
1072
+ locusUrl: string,
708
1073
  updateType: LocusInfoUpdateType,
709
1074
  data?: {updatedObjects: HashTreeObject[]}
710
1075
  ) {
@@ -713,7 +1078,10 @@ export default class LocusInfo extends EventsScope {
713
1078
  // initialize our new locus
714
1079
  let locus: LocusDTO = {
715
1080
  participants: [],
716
- jsSdkMeta: {removedParticipantIds: []},
1081
+ jsSdkMeta: {
1082
+ removedParticipantIds: [],
1083
+ forceReplaceMembers: false,
1084
+ },
717
1085
  };
718
1086
 
719
1087
  // first go over all the updates and check what happens with the main locus object
@@ -746,12 +1114,23 @@ export default class LocusInfo extends EventsScope {
746
1114
  }
747
1115
  });
748
1116
 
749
- // if Locus object is unchanged or removed, we need to keep using the existing locus
750
- // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
751
- // if it gets updated, we only need to have the fields that are not part of "locus" object (like "info" or "mediaShares")
752
- // so that when Locus object gets updated, if the new one is missing some field, that field will
753
- // be removed from our locusInfo
754
- if (
1117
+ const hashTreeParserEntry = this.hashTreeParsers.get(locusUrl);
1118
+
1119
+ if (!hashTreeParserEntry.initializedFromHashTree) {
1120
+ // this is the first time we're getting an update for this locusUrl,
1121
+ // so it's probably a move to/from breakout. We need to start from a clean state,
1122
+ // so empty locus and we rely on Locus giving us sufficient data in the updates to populate it.
1123
+ LoggerProxy.logger.info(
1124
+ `Locus-info:index#updateFromHashTree --> first INITIAL update for locusUrl ${locusUrl}, starting from empty state`
1125
+ );
1126
+ hashTreeParserEntry.initializedFromHashTree = true;
1127
+ locus.jsSdkMeta.forceReplaceMembers = true;
1128
+ } else if (
1129
+ // if Locus object is unchanged or removed, we need to keep using the existing locus
1130
+ // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
1131
+ // if it gets updated, we only need to have the fields that are not part of "locus" object (like "info" or "mediaShares")
1132
+ // so that when Locus object gets updated, if the new one is missing some field, that field will
1133
+ // be removed from our locusInfo
755
1134
  locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.unchanged ||
756
1135
  locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.removed
757
1136
  ) {
@@ -792,11 +1171,14 @@ export default class LocusInfo extends EventsScope {
792
1171
  }
793
1172
 
794
1173
  case LocusInfoUpdateType.MEETING_ENDED: {
795
- LoggerProxy.logger.info(
796
- `Locus-info:index#updateFromHashTree --> received signal that meeting ended, destroying meeting ${this.meetingId}`
797
- );
798
1174
  const meeting = this.webex.meetings.meetingCollection.get(this.meetingId);
799
- this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
1175
+
1176
+ if (meeting) {
1177
+ LoggerProxy.logger.info(
1178
+ `Locus-info:index#updateFromHashTree --> received signal that meeting ended, destroying meeting ${this.meetingId}`
1179
+ );
1180
+ this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
1181
+ }
800
1182
  }
801
1183
  }
802
1184
  }
@@ -808,15 +1190,25 @@ export default class LocusInfo extends EventsScope {
808
1190
  * @memberof LocusInfo
809
1191
  */
810
1192
  parse(meeting: any, data: any) {
811
- if (this.hashTreeParser) {
1193
+ if (this.hashTreeParsers.size > 0) {
812
1194
  this.handleHashTreeMessage(
813
1195
  meeting,
814
1196
  data.eventType,
815
1197
  data.stateElementsMessage as HashTreeMessage
816
1198
  );
817
1199
  } else {
818
- // eslint-disable-next-line @typescript-eslint/no-shadow
819
1200
  const {eventType} = data;
1201
+
1202
+ if (eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
1203
+ // this can happen when we get an event before join http response
1204
+ // it's OK to just ignore it
1205
+ LoggerProxy.logger.info(
1206
+ `Locus-info:index#parse --> received locus hash tree event before hashTreeParser is created`
1207
+ );
1208
+
1209
+ return;
1210
+ }
1211
+
820
1212
  const locus = this.getTheLocusToUpdate(data.locus);
821
1213
  LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
822
1214
 
@@ -837,17 +1229,11 @@ export default class LocusInfo extends EventsScope {
837
1229
  case LOCUSEVENT.PARTICIPANT_DECLINED:
838
1230
  case LOCUSEVENT.FLOOR_GRANTED:
839
1231
  case LOCUSEVENT.FLOOR_RELEASED:
840
- this.onFullLocus(locus, eventType);
1232
+ this.onFullLocus(`classic locus event ${eventType}`, locus, eventType);
841
1233
  break;
842
1234
  case LOCUSEVENT.DIFFERENCE:
843
1235
  this.handleLocusDelta(locus, meeting);
844
1236
  break;
845
- case LOCUSEVENT.HASH_TREE_DATA_UPDATED:
846
- this.sendClassicVsHashTreeMismatchMetric(
847
- meeting,
848
- `got ${eventType}, expected classic events`
849
- );
850
- break;
851
1237
 
852
1238
  default:
853
1239
  // Why will there be a event with no eventType ????
@@ -871,46 +1257,63 @@ export default class LocusInfo extends EventsScope {
871
1257
  /**
872
1258
  * Function for handling full locus when it's using hash trees (so not the "classic" one).
873
1259
  *
1260
+ * @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
874
1261
  * @param {object} locus locus object
1262
+ * @param {object} metadata locus hash trees metadata
875
1263
  * @param {string} eventType locus event
876
1264
  * @param {DataSet[]} dataSets
877
1265
  * @returns {void}
878
1266
  */
879
- private onFullLocusWithHashTrees(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
880
- if (!this.hashTreeParser) {
881
- LoggerProxy.logger.info(`Locus-info:index#onFullLocus --> creating hash tree parser`);
1267
+ private onFullLocusWithHashTrees(
1268
+ debugText: string,
1269
+ locus: any,
1270
+ metadata: Metadata,
1271
+ eventType?: string,
1272
+ dataSets?: Array<DataSet>
1273
+ ) {
1274
+ if (!this.hashTreeParsers.has(locus.url)) {
882
1275
  LoggerProxy.logger.info(
883
- 'Locus-info:index#onFullLocus --> dataSets:',
1276
+ `Locus-info:index#onFullLocus (${debugText}) --> creating hash tree parser for locusUrl=${locus.url}`
1277
+ );
1278
+ LoggerProxy.logger.info(
1279
+ `Locus-info:index#onFullLocus (${debugText}) --> dataSets:`,
884
1280
  dataSets,
885
1281
  ' and locus:',
886
- locus
1282
+ locus,
1283
+ ' and metadata:',
1284
+ metadata
887
1285
  );
888
- this.hashTreeParser = this.createHashTreeParser({
1286
+ this.createHashTreeParser({
1287
+ locusUrl: locus.url,
889
1288
  initialLocus: {locus, dataSets},
1289
+ metadata,
890
1290
  });
1291
+ // we have a full locus to start with, so we consider Locus info to be "initialized"
1292
+ this.hashTreeParsers.get(locus.url).initializedFromHashTree = true;
891
1293
  this.onFullLocusCommon(locus, eventType);
892
1294
  } else {
893
1295
  // in this case the Locus we're getting is not necessarily the full one
894
1296
  // so treat it like if we just got it in any api response
895
1297
 
896
1298
  LoggerProxy.logger.info(
897
- 'Locus-info:index#onFullLocus --> hash tree parser already exists, handling it like a normal API response'
1299
+ `Locus-info:index#onFullLocus (${debugText}) --> hash tree parser already exists, handling it like a normal API response`
898
1300
  );
899
- this.handleLocusAPIResponse(undefined, {dataSets, locus});
1301
+ this.handleLocusAPIResponse(undefined, {dataSets, locus, metadata});
900
1302
  }
901
1303
  }
902
1304
 
903
1305
  /**
904
1306
  * Function for handling full locus when it's the "classic" one (not hash trees)
905
1307
  *
1308
+ * @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
906
1309
  * @param {object} locus locus object
907
1310
  * @param {string} eventType locus event
908
1311
  * @returns {void}
909
1312
  */
910
- private onFullLocusClassic(locus: any, eventType?: string) {
1313
+ private onFullLocusClassic(debugText: string, locus: any, eventType?: string) {
911
1314
  if (!this.locusParser.isNewFullLocus(locus)) {
912
1315
  LoggerProxy.logger.info(
913
- `Locus-info:index#onFullLocus --> ignoring old full locus DTO, eventType=${eventType}`
1316
+ `Locus-info:index#onFullLocus (${debugText}) --> ignoring old full locus DTO, eventType=${eventType}`
914
1317
  );
915
1318
 
916
1319
  return;
@@ -920,24 +1323,37 @@ export default class LocusInfo extends EventsScope {
920
1323
 
921
1324
  /**
922
1325
  * updates the locus with full locus object
1326
+ * @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
923
1327
  * @param {object} locus locus object
924
1328
  * @param {string} eventType locus event
925
1329
  * @param {DataSet[]} dataSets
1330
+ * @param {object} metadata locus hash trees metadata
926
1331
  * @returns {object} null
927
1332
  * @memberof LocusInfo
928
1333
  */
929
- onFullLocus(locus: any, eventType?: string, dataSets?: Array<DataSet>) {
1334
+ onFullLocus(
1335
+ debugText: string,
1336
+ locus: any,
1337
+ eventType?: string,
1338
+ dataSets?: Array<DataSet>,
1339
+ metadata?: Metadata
1340
+ ) {
930
1341
  if (!locus) {
931
1342
  LoggerProxy.logger.error(
932
- 'Locus-info:index#onFullLocus --> object passed as argument was invalid, continuing.'
1343
+ `Locus-info:index#onFullLocus (${debugText}) --> object passed as argument was invalid, continuing.`
933
1344
  );
934
1345
  }
935
1346
 
936
1347
  if (dataSets) {
1348
+ if (!metadata) {
1349
+ throw new Error(
1350
+ `Locus-info:index#onFullLocus (${debugText}) --> hash tree metadata is missing with full Locus`
1351
+ );
1352
+ }
937
1353
  // this is the new hashmap Locus DTO format (only applicable to webinars for now)
938
- this.onFullLocusWithHashTrees(locus, eventType, dataSets);
1354
+ this.onFullLocusWithHashTrees(debugText, locus, metadata, eventType, dataSets);
939
1355
  } else {
940
- this.onFullLocusClassic(locus, eventType);
1356
+ this.onFullLocusClassic(debugText, locus, eventType);
941
1357
  }
942
1358
  }
943
1359
 
@@ -952,7 +1368,7 @@ export default class LocusInfo extends EventsScope {
952
1368
  this.participants = locus.participants;
953
1369
  this.participants?.forEach((participant) => {
954
1370
  // participant.htMeta is set only for hash tree based locus
955
- if (participant.htMeta?.elementId.id) {
1371
+ if (typeof participant.htMeta?.elementId.id === 'number') {
956
1372
  this.hashTreeObjectId2ParticipantId.set(participant.htMeta.elementId.id, participant.id);
957
1373
  }
958
1374
  });
@@ -1015,20 +1431,61 @@ export default class LocusInfo extends EventsScope {
1015
1431
  }
1016
1432
  }
1017
1433
 
1434
+ /**
1435
+ * Makes sure that passed in locus object has a participant object for self.
1436
+ *
1437
+ * @param {LocusDTO} locus The locus object to check and modify if needed
1438
+ * @returns {void}
1439
+ */
1440
+ ensureSelfParticipantExists(locus: any) {
1441
+ const {self} = locus;
1442
+
1443
+ // sanity check, this should never fail
1444
+ if (!self?.identity || !Array.isArray(locus.participants)) {
1445
+ LoggerProxy.logger.warn(
1446
+ `Locus-info:index#ensureSelfParticipantExists --> locus object is missing required fields, cannot ensure self participant exists. self?.identity="${self?.identity}"`
1447
+ );
1448
+
1449
+ return;
1450
+ }
1451
+
1452
+ const selfExists = locus.participants.some(
1453
+ (participant) => participant.identity === self.identity
1454
+ );
1455
+
1456
+ if (!selfExists) {
1457
+ locus.participants.push({...self});
1458
+ }
1459
+ }
1460
+
1018
1461
  /**
1019
1462
  * @param {Object} locus
1020
1463
  * @returns {undefined}
1021
1464
  * @memberof LocusInfo
1022
1465
  */
1023
1466
  onDeltaLocus(locus: any) {
1024
- const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
1467
+ const isReplaceMembers =
1468
+ locus.jsSdkMeta?.forceReplaceMembers !== undefined
1469
+ ? locus.jsSdkMeta.forceReplaceMembers
1470
+ : ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
1471
+
1472
+ if (isReplaceMembers) {
1473
+ // when we're moving between breakouts, Locus sometimes doesn't send us
1474
+ // any participants at all for a few seconds
1475
+ // Web app relies on having at least the self participant always there
1476
+ // so we copy self into participants if it's not there.
1477
+ this.ensureSelfParticipantExists(locus);
1478
+ }
1025
1479
  this.mergeParticipants(this.participants, locus.participants);
1026
- this.updateLocusInfo(locus);
1027
- this.updateParticipants(
1028
- locus.participants,
1029
- locus.jsSdkMeta?.removedParticipantIds,
1030
- isReplaceMembers
1031
- );
1480
+ const updatesApplied = this.updateLocusInfo(locus);
1481
+
1482
+ if (updatesApplied) {
1483
+ this.updateParticipants(
1484
+ locus.participants,
1485
+ locus.jsSdkMeta?.removedParticipantIds,
1486
+ isReplaceMembers
1487
+ );
1488
+ }
1032
1489
  this.isMeetingActive();
1033
1490
  }
1034
1491
 
@@ -1042,7 +1499,7 @@ export default class LocusInfo extends EventsScope {
1042
1499
  // When moved to a breakout session locus sends a message for the previous locus
1043
1500
  // indicating that we have been moved. It isn't helpful to continue parsing this
1044
1501
  // as it gets interpreted as if we have left the call
1045
- return;
1502
+ return false;
1046
1503
  }
1047
1504
 
1048
1505
  this.updateControls(locus.controls, locus.self);
@@ -1062,6 +1519,8 @@ export default class LocusInfo extends EventsScope {
1062
1519
  this.updateLinks(locus.links);
1063
1520
  this.compareAndUpdate();
1064
1521
  // update which required to compare different objects from locus
1522
+
1523
+ return true;
1065
1524
  }
1066
1525
 
1067
1526
  /**
@@ -1217,27 +1676,6 @@ export default class LocusInfo extends EventsScope {
1217
1676
  shouldLeave: false,
1218
1677
  }
1219
1678
  );
1220
- } else if (this.fullState && this.fullState.removed) {
1221
- // user has been dropped from a meeting
1222
-
1223
- // @ts-ignore
1224
- this.webex.internal.newMetrics.submitClientEvent({
1225
- name: 'client.call.remote-ended',
1226
- options: {
1227
- meetingId: this.meetingId,
1228
- },
1229
- });
1230
- this.emitScoped(
1231
- {
1232
- file: 'locus-info',
1233
- function: 'isMeetingActive',
1234
- },
1235
- EVENTS.DESTROY_MEETING,
1236
- {
1237
- reason: MEETING_REMOVED_REASON.FULLSTATE_REMOVED,
1238
- shouldLeave: false,
1239
- }
1240
- );
1241
1679
  }
1242
1680
  // If you are guest and you are removed from the meeting
1243
1681
  // You wont get any further events
@@ -1373,6 +1811,7 @@ export default class LocusInfo extends EventsScope {
1373
1811
  hasMeetingContainerChanged,
1374
1812
  hasTranscribeChanged,
1375
1813
  hasHesiodLLMIdChanged,
1814
+ hasAiSummaryNotificationChanged,
1376
1815
  hasTranscribeSpokenLanguageChanged,
1377
1816
  hasManualCaptionChanged,
1378
1817
  hasEntryExitToneChanged,
@@ -1529,6 +1968,19 @@ export default class LocusInfo extends EventsScope {
1529
1968
  );
1530
1969
  }
1531
1970
 
1971
+ if (hasAiSummaryNotificationChanged) {
1972
+ this.emitScoped(
1973
+ {
1974
+ file: 'locus-info',
1975
+ function: 'updateControls',
1976
+ },
1977
+ LOCUSINFO.EVENTS.CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
1978
+ {
1979
+ aiSummaryNotification: current.transcribe.aiSummaryNotification,
1980
+ }
1981
+ );
1982
+ }
1983
+
1532
1984
  if (hasTranscribeSpokenLanguageChanged) {
1533
1985
  const {spokenLanguage} = current.transcribe;
1534
1986
 
@@ -1561,6 +2013,7 @@ export default class LocusInfo extends EventsScope {
1561
2013
 
1562
2014
  if (hasBreakoutChanged) {
1563
2015
  const {breakout} = current;
2016
+
1564
2017
  breakout.breakoutMoveId = SelfUtils.getReplacedBreakoutMoveId(
1565
2018
  self,
1566
2019
  this.webex.internal.device.url
@@ -2038,6 +2491,19 @@ export default class LocusInfo extends EventsScope {
2038
2491
  );
2039
2492
  }
2040
2493
 
2494
+ if (parsedSelves.updates.selfIdChanged) {
2495
+ this.emitScoped(
2496
+ {
2497
+ file: 'locus-info',
2498
+ function: 'updateSelf',
2499
+ },
2500
+ LOCUSINFO.EVENTS.SELF_ID_CHANGED,
2501
+ {
2502
+ selfId: parsedSelves.current.selfId,
2503
+ }
2504
+ );
2505
+ }
2506
+
2041
2507
  if (parsedSelves.updates.interpretationChanged) {
2042
2508
  this.emitScoped(
2043
2509
  {