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

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 (67) hide show
  1. package/dist/aiEnableRequest/index.js +1 -1
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/controls-options-manager/constants.js +11 -1
  5. package/dist/controls-options-manager/constants.js.map +1 -1
  6. package/dist/controls-options-manager/index.js +23 -21
  7. package/dist/controls-options-manager/index.js.map +1 -1
  8. package/dist/controls-options-manager/util.js +91 -0
  9. package/dist/controls-options-manager/util.js.map +1 -1
  10. package/dist/hashTree/constants.js +10 -1
  11. package/dist/hashTree/constants.js.map +1 -1
  12. package/dist/hashTree/hashTreeParser.js +56 -31
  13. package/dist/hashTree/hashTreeParser.js.map +1 -1
  14. package/dist/hashTree/utils.js +22 -0
  15. package/dist/hashTree/utils.js.map +1 -1
  16. package/dist/interpretation/index.js +1 -1
  17. package/dist/interpretation/siLanguage.js +1 -1
  18. package/dist/locus-info/index.js +51 -23
  19. package/dist/locus-info/index.js.map +1 -1
  20. package/dist/meeting/index.js +372 -292
  21. package/dist/meeting/index.js.map +1 -1
  22. package/dist/meeting/util.js +1 -0
  23. package/dist/meeting/util.js.map +1 -1
  24. package/dist/meetings/index.js +8 -9
  25. package/dist/meetings/index.js.map +1 -1
  26. package/dist/meetings/util.js +21 -2
  27. package/dist/meetings/util.js.map +1 -1
  28. package/dist/metrics/constants.js +5 -1
  29. package/dist/metrics/constants.js.map +1 -1
  30. package/dist/multistream/sendSlotManager.js +116 -2
  31. package/dist/multistream/sendSlotManager.js.map +1 -1
  32. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  33. package/dist/types/hashTree/constants.d.ts +1 -0
  34. package/dist/types/hashTree/hashTreeParser.d.ts +12 -2
  35. package/dist/types/hashTree/utils.d.ts +11 -0
  36. package/dist/types/locus-info/index.d.ts +9 -5
  37. package/dist/types/meeting/index.d.ts +11 -0
  38. package/dist/types/metrics/constants.d.ts +4 -0
  39. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  40. package/dist/webinar/index.js +301 -226
  41. package/dist/webinar/index.js.map +1 -1
  42. package/package.json +15 -15
  43. package/src/controls-options-manager/constants.ts +14 -1
  44. package/src/controls-options-manager/index.ts +26 -19
  45. package/src/controls-options-manager/util.ts +81 -1
  46. package/src/hashTree/constants.ts +9 -0
  47. package/src/hashTree/hashTreeParser.ts +60 -36
  48. package/src/hashTree/utils.ts +17 -0
  49. package/src/locus-info/index.ts +56 -30
  50. package/src/meeting/index.ts +98 -11
  51. package/src/meeting/util.ts +1 -0
  52. package/src/meetings/index.ts +15 -16
  53. package/src/meetings/util.ts +26 -1
  54. package/src/metrics/constants.ts +5 -0
  55. package/src/multistream/sendSlotManager.ts +97 -3
  56. package/src/webinar/index.ts +75 -1
  57. package/test/unit/spec/controls-options-manager/index.js +114 -6
  58. package/test/unit/spec/controls-options-manager/util.js +165 -0
  59. package/test/unit/spec/hashTree/hashTreeParser.ts +441 -30
  60. package/test/unit/spec/hashTree/utils.ts +88 -1
  61. package/test/unit/spec/locus-info/index.js +75 -27
  62. package/test/unit/spec/meeting/index.js +54 -36
  63. package/test/unit/spec/meeting/utils.js +4 -0
  64. package/test/unit/spec/meetings/index.js +36 -3
  65. package/test/unit/spec/meetings/utils.js +108 -0
  66. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  67. package/test/unit/spec/webinar/index.ts +60 -0
@@ -2,10 +2,10 @@ import {cloneDeep, isEmpty, zip} from 'lodash';
2
2
  import HashTree, {LeafDataItem} from './hashTree';
3
3
  import LoggerProxy from '../common/logs/logger-proxy';
4
4
  import {Enum, HTTP_VERBS} from '../constants';
5
- import {DataSetNames, EMPTY_HASH} from './constants';
5
+ import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH} from './constants';
6
6
  import {ObjectType, HtMeta, HashTreeObject} from './types';
7
7
  import {LocusDTO} from '../locus-info/types';
8
- import {deleteNestedObjectsWithHtMeta, isMetadata} from './utils';
8
+ import {deleteNestedObjectsWithHtMeta, isMetadata, sortByInitPriority} from './utils';
9
9
 
10
10
  export interface DataSet {
11
11
  url: string;
@@ -57,10 +57,15 @@ export const LocusInfoUpdateType = {
57
57
  } as const;
58
58
 
59
59
  export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
60
- export type LocusInfoUpdateCallback = (
61
- updateType: LocusInfoUpdateType,
62
- data?: {updatedObjects: HashTreeObject[]}
63
- ) => void;
60
+ export type LocusInfoUpdate =
61
+ | {
62
+ updateType: typeof LocusInfoUpdateType.OBJECTS_UPDATED;
63
+ updatedObjects: HashTreeObject[];
64
+ }
65
+ | {
66
+ updateType: typeof LocusInfoUpdateType.MEETING_ENDED;
67
+ };
68
+ export type LocusInfoUpdateCallback = (update: LocusInfoUpdate) => void;
64
69
 
65
70
  interface LeafInfo {
66
71
  type: ObjectType;
@@ -227,7 +232,7 @@ class HashTreeParser {
227
232
  private initializeNewVisibleDataSet(
228
233
  visibleDataSetInfo: VisibleDataSetInfo,
229
234
  dataSetInfo: DataSet
230
- ): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
235
+ ): Promise<LocusInfoUpdate> {
231
236
  if (this.isVisibleDataSet(dataSetInfo.name)) {
232
237
  LoggerProxy.logger.info(
233
238
  `HashTreeParser#initializeNewVisibleDataSet --> ${this.debugId} Data set "${dataSetInfo.name}" already exists, skipping init`
@@ -264,7 +269,7 @@ class HashTreeParser {
264
269
  private sendInitializationSyncRequestToLocus(
265
270
  datasetName: string,
266
271
  debugText: string
267
- ): Promise<{updateType: LocusInfoUpdateType; updatedObjects?: HashTreeObject[]}> {
272
+ ): Promise<LocusInfoUpdate> {
268
273
  const dataset = this.dataSets[datasetName];
269
274
 
270
275
  if (!dataset) {
@@ -272,7 +277,7 @@ class HashTreeParser {
272
277
  `HashTreeParser#sendInitializationSyncRequestToLocus --> ${this.debugId} No data set found for ${datasetName}, cannot send the request for leaf data`
273
278
  );
274
279
 
275
- return Promise.resolve(null);
280
+ return Promise.resolve({updateType: LocusInfoUpdateType.OBJECTS_UPDATED, updatedObjects: []});
276
281
  }
277
282
 
278
283
  const emptyLeavesData = new Array(dataset.leafCount).fill([]);
@@ -382,9 +387,10 @@ class HashTreeParser {
382
387
  if (this.state === 'stopped') {
383
388
  return;
384
389
  }
390
+
385
391
  const updatedObjects: HashTreeObject[] = [];
386
392
 
387
- for (const dataSet of visibleDataSets) {
393
+ for (const dataSet of sortByInitPriority(visibleDataSets, DATA_SET_INIT_PRIORITY)) {
388
394
  const {name, leafCount, url} = dataSet;
389
395
 
390
396
  if (!this.dataSets[name]) {
@@ -450,7 +456,7 @@ class HashTreeParser {
450
456
  // object mapping dataset names to arrays of leaf data
451
457
  const leafInfo: Record<string, Array<LeafInfo>> = {};
452
458
 
453
- const findAndStoreMetaData = (currentLocusPart: any) => {
459
+ const findAndStoreMetaData = (currentLocusPart: any, currentLocusPartName: string) => {
454
460
  if (typeof currentLocusPart !== 'object' || currentLocusPart === null) {
455
461
  return;
456
462
  }
@@ -465,10 +471,18 @@ class HashTreeParser {
465
471
  };
466
472
 
467
473
  if (copyData) {
468
- newLeafInfo.data = cloneDeep(currentLocusPart);
474
+ if ((type as string).toLowerCase() === ObjectType.control) {
475
+ // control entries require special handling, because they are signalled by Locus
476
+ // differently when coming in messages vs API responses
477
+ newLeafInfo.data = {
478
+ [currentLocusPartName]: cloneDeep(currentLocusPart),
479
+ };
480
+ } else {
481
+ newLeafInfo.data = cloneDeep(currentLocusPart);
469
482
 
470
- // remove any nested other objects that have their own htMeta
471
- deleteNestedObjectsWithHtMeta(newLeafInfo.data);
483
+ // remove any nested other objects that have their own htMeta
484
+ deleteNestedObjectsWithHtMeta(newLeafInfo.data);
485
+ }
472
486
  }
473
487
 
474
488
  for (const dataSetName of dataSetNames) {
@@ -480,19 +494,19 @@ class HashTreeParser {
480
494
  }
481
495
 
482
496
  if (Array.isArray(currentLocusPart)) {
483
- for (const item of currentLocusPart) {
484
- findAndStoreMetaData(item);
497
+ for (const [index, item] of currentLocusPart.entries()) {
498
+ findAndStoreMetaData(item, index.toString());
485
499
  }
486
500
  } else {
487
501
  for (const key of Object.keys(currentLocusPart)) {
488
502
  if (Object.prototype.hasOwnProperty.call(currentLocusPart, key)) {
489
- findAndStoreMetaData(currentLocusPart[key]);
503
+ findAndStoreMetaData(currentLocusPart[key], key);
490
504
  }
491
505
  }
492
506
  }
493
507
  };
494
508
 
495
- findAndStoreMetaData(locus);
509
+ findAndStoreMetaData(locus, 'locus');
496
510
 
497
511
  return leafInfo;
498
512
  }
@@ -952,7 +966,7 @@ class HashTreeParser {
952
966
  }
953
967
  const allDataSets = await this.getAllVisibleDataSetsFromLocus();
954
968
 
955
- for (const ds of addedDataSets) {
969
+ for (const ds of sortByInitPriority(addedDataSets, DATA_SET_INIT_PRIORITY)) {
956
970
  const dataSetInfo = allDataSets.find((d) => d.name === ds.name);
957
971
 
958
972
  LoggerProxy.logger.info(
@@ -964,8 +978,6 @@ class HashTreeParser {
964
978
  `HashTreeParser#initializeNewVisibleDataSets --> ${this.debugId} missing info about data set "${ds.name}" in Locus response from visibleDataSetsUrl`
965
979
  );
966
980
  } else {
967
- // we're awaiting in a loop, because in practice there will be only one new data set at a time,
968
- // so no point in trying to parallelize this
969
981
  // eslint-disable-next-line no-await-in-loop
970
982
  const updates = await this.initializeNewVisibleDataSet(ds, dataSetInfo);
971
983
 
@@ -1007,7 +1019,7 @@ class HashTreeParser {
1007
1019
 
1008
1020
  // when we detect new visible datasets, it may be that the metadata about them is not
1009
1021
  // available in the message, they will require separate async initialization
1010
- let dataSetsRequiringInitialization = [];
1022
+ let dataSetsRequiringInitialization: VisibleDataSetInfo[] = [];
1011
1023
 
1012
1024
  // first find out if there are any visible data set changes - they're signalled in Metadata object updates
1013
1025
  const metadataUpdates = (message.locusStateElements || []).filter((object) =>
@@ -1015,7 +1027,7 @@ class HashTreeParser {
1015
1027
  );
1016
1028
 
1017
1029
  if (metadataUpdates.length > 0) {
1018
- const updatedMetadataObjects = [];
1030
+ const updatedMetadataObjects: HashTreeObject[] = [];
1019
1031
 
1020
1032
  metadataUpdates.forEach((object) => {
1021
1033
  // todo: once Locus supports it, we will use the "view" field here instead of dataSetNames
@@ -1044,7 +1056,7 @@ class HashTreeParser {
1044
1056
  }
1045
1057
  }
1046
1058
 
1047
- if (message.locusStateElements?.length > 0) {
1059
+ if (message.locusStateElements && message.locusStateElements.length > 0) {
1048
1060
  // by this point we now have this.dataSets setup for data sets from this message
1049
1061
  // and hash trees created for the new visible data sets,
1050
1062
  // so we can now process all the updates from the message
@@ -1140,20 +1152,17 @@ class HashTreeParser {
1140
1152
  * @param {Object} updates parsed from a Locus message
1141
1153
  * @returns {void}
1142
1154
  */
1143
- private callLocusInfoUpdateCallback(updates: {
1144
- updateType: LocusInfoUpdateType;
1145
- updatedObjects?: HashTreeObject[];
1146
- }) {
1155
+ private callLocusInfoUpdateCallback(updates: LocusInfoUpdate) {
1147
1156
  if (this.state === 'stopped') {
1148
1157
  return;
1149
1158
  }
1150
1159
 
1151
- const {updateType, updatedObjects} = updates;
1160
+ const {updateType} = updates;
1152
1161
 
1153
- if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updatedObjects?.length > 0) {
1162
+ if (updateType === LocusInfoUpdateType.OBJECTS_UPDATED && updates.updatedObjects?.length > 0) {
1154
1163
  // Filter out updates for objects that already have a higher version in their datasets,
1155
1164
  // or removals for objects that still exist in any of their datasets
1156
- const filteredUpdates = updatedObjects.filter((object) => {
1165
+ const filteredUpdates = updates.updatedObjects.filter((object) => {
1157
1166
  const {elementId} = object.htMeta;
1158
1167
  const {type, id, version} = elementId;
1159
1168
 
@@ -1190,10 +1199,10 @@ class HashTreeParser {
1190
1199
  });
1191
1200
 
1192
1201
  if (filteredUpdates.length > 0) {
1193
- this.locusInfoUpdateCallback(updateType, {updatedObjects: filteredUpdates});
1202
+ this.locusInfoUpdateCallback({updateType, updatedObjects: filteredUpdates});
1194
1203
  }
1195
1204
  } else if (updateType !== LocusInfoUpdateType.OBJECTS_UPDATED) {
1196
- this.locusInfoUpdateCallback(updateType, {updatedObjects});
1205
+ this.locusInfoUpdateCallback({updateType});
1197
1206
  }
1198
1207
  }
1199
1208
 
@@ -1450,6 +1459,16 @@ class HashTreeParser {
1450
1459
  this.state = 'stopped';
1451
1460
  }
1452
1461
 
1462
+ /**
1463
+ * Cleans up the HashTreeParser, stopping all timers and clearing all internal state.
1464
+ * After calling this, the parser should not be used anymore.
1465
+ * @returns {void}
1466
+ */
1467
+ public cleanUp() {
1468
+ this.stop();
1469
+ this.dataSets = {};
1470
+ }
1471
+
1453
1472
  /**
1454
1473
  * Resumes the HashTreeParser that was previously stopped.
1455
1474
  * @param {HashTreeMessage} message - The message to resume with, it must contain metadata with visible data sets info
@@ -1584,15 +1603,20 @@ class HashTreeParser {
1584
1603
  );
1585
1604
 
1586
1605
  const url = `${dataSet.url}/sync`;
1587
- const body = {
1606
+ const body: {
1607
+ leafCount: number;
1608
+ leafDataEntries: {leafIndex: number; elementIds: LeafDataItem[]}[];
1609
+ } = {
1588
1610
  leafCount: dataSet.leafCount,
1589
1611
  leafDataEntries: [],
1590
1612
  };
1591
1613
 
1592
1614
  Object.keys(mismatchedLeavesData).forEach((index) => {
1615
+ const leafIndex = parseInt(index, 10);
1616
+
1593
1617
  body.leafDataEntries.push({
1594
- leafIndex: parseInt(index, 10),
1595
- elementIds: mismatchedLeavesData[index],
1618
+ leafIndex,
1619
+ elementIds: mismatchedLeavesData[leafIndex],
1596
1620
  });
1597
1621
  });
1598
1622
 
@@ -60,3 +60,20 @@ export const deleteNestedObjectsWithHtMeta = (
60
60
  }
61
61
  }
62
62
  };
63
+
64
+ /**
65
+ * Reorders items so that those matching the given priority list come first (in priority order),
66
+ * followed by everything else in their original order.
67
+ *
68
+ * @param {Array<T>} items - The items to reorder
69
+ * @param {string[]} priority - Ordered list of names that should come first
70
+ * @returns {Array<T>} A new array with prioritized items first
71
+ */
72
+ export function sortByInitPriority<T extends {name: string}>(items: T[], priority: string[]): T[] {
73
+ const prioritized = priority
74
+ .map((name) => items.find((item) => item.name === name))
75
+ .filter(Boolean) as T[];
76
+ const rest = items.filter((item) => !priority.includes(item.name));
77
+
78
+ return [...prioritized, ...rest];
79
+ }
@@ -34,6 +34,7 @@ import BEHAVIORAL_METRICS from '../metrics/constants';
34
34
  import HashTreeParser, {
35
35
  DataSet,
36
36
  HashTreeMessage,
37
+ LocusInfoUpdate,
37
38
  LocusInfoUpdateType,
38
39
  Metadata,
39
40
  } from '../hashTree/hashTreeParser';
@@ -97,7 +98,7 @@ export type HashTreeParserEntry = {
97
98
  * Gets the replacement information
98
99
  *
99
100
  * @param {any} self - "self" object from Locus DTO
100
- * @param {string} deviceUrl - The URL of the user's device
101
+ * @param {string} deviceUrl - The URL of the specified device
101
102
  * @returns {any} The replace information if available, otherwise undefined
102
103
  */
103
104
  function getReplaceInfoFromSelf(self: any, deviceUrl: string): ReplacesInfo | undefined {
@@ -137,14 +138,15 @@ function findLocusUrlInAnyHashTreeParser(
137
138
  *
138
139
  * @param {HashTreeMessage} message - The hash tree message to find the meeting for
139
140
  * @param {MeetingCollection} meetingCollection - The collection of meetings to search
140
- * @param {string} deviceUrl - The URL of the user's device
141
141
  * @returns {any} The meeting if found, otherwise undefined
142
142
  */
143
143
  export function findMeetingForHashTreeMessage(
144
- message: HashTreeMessage,
145
- meetingCollection: MeetingCollection,
146
- deviceUrl: string
144
+ message: HashTreeMessage | undefined,
145
+ meetingCollection: MeetingCollection
147
146
  ): any {
147
+ if (!message) {
148
+ return undefined;
149
+ }
148
150
  let foundMeeting = findLocusUrlInAnyHashTreeParser(meetingCollection, message.locusUrl);
149
151
 
150
152
  if (foundMeeting) {
@@ -154,7 +156,7 @@ export function findMeetingForHashTreeMessage(
154
156
  // if we haven't found anything, it may mean that message has a new locusUrl
155
157
  // check if it indicates that it replaces some existing current locusUrl (this is indicated in "self")
156
158
  const self = message.locusStateElements?.find((el) => isSelf(el))?.data;
157
- const replaces = getReplaceInfoFromSelf(self, deviceUrl);
159
+ const replaces = getReplaceInfoFromSelf(self, self?.deviceUrl);
158
160
 
159
161
  if (replaces?.locusUrl) {
160
162
  foundMeeting = findLocusUrlInAnyHashTreeParser(meetingCollection, replaces.locusUrl);
@@ -545,7 +547,7 @@ export default class LocusInfo extends EventsScope {
545
547
  dataSets: Array<DataSet>;
546
548
  locus: any;
547
549
  };
548
- metadata: Metadata;
550
+ metadata: Metadata | null;
549
551
  replacedAt?: string;
550
552
  }): HashTreeParser {
551
553
  const parser = new HashTreeParser({
@@ -553,7 +555,7 @@ export default class LocusInfo extends EventsScope {
553
555
  metadata,
554
556
  webexRequest: this.webex.request.bind(this.webex),
555
557
  locusInfoUpdateCallback: this.updateFromHashTree.bind(this, locusUrl),
556
- debugId: `HT-${locusUrl.split('/').pop().substring(0, 4)}`,
558
+ debugId: `HT-${locusUrl.split('/')?.pop()?.substring(0, 4)}`,
557
559
  excludedDataSets: this.webex.config.meetings.locus?.excludedDataSets,
558
560
  });
559
561
 
@@ -656,7 +658,7 @@ export default class LocusInfo extends EventsScope {
656
658
  );
657
659
  // first create the HashTreeParser, but don't initialize it with any data yet
658
660
  const hashTreeParser = this.createHashTreeParser({
659
- locusUrl: data.locus.url,
661
+ locusUrl: data.locus.url as string,
660
662
  initialLocus: {
661
663
  locus: null,
662
664
  dataSets: [], // empty, because we don't have them yet
@@ -965,7 +967,7 @@ export default class LocusInfo extends EventsScope {
965
967
  // but it's buried inside the message, we need to find it and pass it to HashTreeParser constructor
966
968
  const metadata = message.locusStateElements?.find((el) => isMetadata(el));
967
969
 
968
- if (metadata?.data?.visibleDataSets?.length > 0) {
970
+ if (metadata && metadata.data?.visibleDataSets?.length > 0) {
969
971
  LoggerProxy.logger.info(
970
972
  `Locus-info:index#handleHashTreeParserSwitch --> no hash tree parser found for locusUrl ${message.locusUrl}, creating a new one`
971
973
  );
@@ -1056,7 +1058,10 @@ export default class LocusInfo extends EventsScope {
1056
1058
 
1057
1059
  const entry = this.hashTreeParsers.get(message.locusUrl);
1058
1060
 
1059
- entry.parser.handleMessage(message);
1061
+ // the check is just for typescript, the case of no entry in hashTreeParsers is handled in handleHashTreeParserSwitch() above
1062
+ if (entry) {
1063
+ entry.parser.handleMessage(message);
1064
+ }
1060
1065
  }
1061
1066
 
1062
1067
  /**
@@ -1064,16 +1069,11 @@ export default class LocusInfo extends EventsScope {
1064
1069
  * Updates our locus info based on the data parsed by the hash tree parser.
1065
1070
  *
1066
1071
  * @param {string} locusUrl - the locus URL for which the update is received
1067
- * @param {LocusInfoUpdateType} updateType - The type of update received.
1068
- * @param {Object} [data] - Additional data for the update, if applicable.
1072
+ * @param {LocusInfoUpdate} update - Details about the update.
1069
1073
  * @returns {void}
1070
1074
  */
1071
- private updateFromHashTree(
1072
- locusUrl: string,
1073
- updateType: LocusInfoUpdateType,
1074
- data?: {updatedObjects: HashTreeObject[]}
1075
- ) {
1076
- switch (updateType) {
1075
+ private updateFromHashTree(locusUrl: string, update: LocusInfoUpdate) {
1076
+ switch (update.updateType) {
1077
1077
  case LocusInfoUpdateType.OBJECTS_UPDATED: {
1078
1078
  // initialize our new locus
1079
1079
  let locus: LocusDTO = {
@@ -1087,7 +1087,7 @@ export default class LocusInfo extends EventsScope {
1087
1087
  // first go over all the updates and check what happens with the main locus object
1088
1088
  let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
1089
1089
  LocusObjectStateAfterUpdates.unchanged;
1090
- data.updatedObjects.forEach((object) => {
1090
+ update.updatedObjects.forEach((object) => {
1091
1091
  if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
1092
1092
  if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
1093
1093
  // this code doesn't supported it right now,
@@ -1116,6 +1116,14 @@ export default class LocusInfo extends EventsScope {
1116
1116
 
1117
1117
  const hashTreeParserEntry = this.hashTreeParsers.get(locusUrl);
1118
1118
 
1119
+ if (!hashTreeParserEntry) {
1120
+ LoggerProxy.logger.warn(
1121
+ `Locus-info:index#updateFromHashTree --> no HashTreeParser found for locusUrl ${locusUrl} when trying to apply updates from hash tree`
1122
+ );
1123
+
1124
+ return;
1125
+ }
1126
+
1119
1127
  if (!hashTreeParserEntry.initializedFromHashTree) {
1120
1128
  // this is the first time we're getting an update for this locusUrl,
1121
1129
  // so it's probably a move to/from breakout. We need to start from a clean state,
@@ -1124,7 +1132,8 @@ export default class LocusInfo extends EventsScope {
1124
1132
  `Locus-info:index#updateFromHashTree --> first INITIAL update for locusUrl ${locusUrl}, starting from empty state`
1125
1133
  );
1126
1134
  hashTreeParserEntry.initializedFromHashTree = true;
1127
- locus.jsSdkMeta.forceReplaceMembers = true;
1135
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1136
+ locus.jsSdkMeta!.forceReplaceMembers = true;
1128
1137
  } else if (
1129
1138
  // if Locus object is unchanged or removed, we need to keep using the existing locus
1130
1139
  // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
@@ -1137,7 +1146,7 @@ export default class LocusInfo extends EventsScope {
1137
1146
  // copy over all of existing locus except participants
1138
1147
  LocusDtoTopLevelKeys.forEach((key) => {
1139
1148
  if (key !== 'participants') {
1140
- locus[key] = cloneDeep(this[key]);
1149
+ (locus as Record<string, any>)[key] = cloneDeep((this as Record<string, any>)[key]);
1141
1150
  }
1142
1151
  });
1143
1152
  } else {
@@ -1145,14 +1154,16 @@ export default class LocusInfo extends EventsScope {
1145
1154
  // (except participants, which need to stay empty - that means "no participant changes")
1146
1155
  Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
1147
1156
  if (locusDtoKey !== 'participants') {
1148
- locus[locusDtoKey] = cloneDeep(this[locusDtoKey]);
1157
+ (locus as Record<string, any>)[locusDtoKey] = cloneDeep(
1158
+ (this as Record<string, any>)[locusDtoKey]
1159
+ );
1149
1160
  }
1150
1161
  });
1151
1162
  }
1152
1163
 
1153
1164
  LoggerProxy.logger.info(
1154
1165
  `Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
1155
- data.updatedObjects.map((o) => ({
1166
+ update.updatedObjects.map((o) => ({
1156
1167
  type: o.htMeta.elementId.type,
1157
1168
  id: o.htMeta.elementId.id,
1158
1169
  hasData: !!o.data,
@@ -1160,7 +1171,7 @@ export default class LocusInfo extends EventsScope {
1160
1171
  )}`
1161
1172
  );
1162
1173
  // now apply all the updates from the hash tree onto the locus
1163
- data.updatedObjects.forEach((object) => {
1174
+ update.updatedObjects.forEach((object) => {
1164
1175
  locus = this.updateLocusFromHashTreeObject(object, locus);
1165
1176
  });
1166
1177
 
@@ -1260,16 +1271,16 @@ export default class LocusInfo extends EventsScope {
1260
1271
  * @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
1261
1272
  * @param {object} locus locus object
1262
1273
  * @param {object} metadata locus hash trees metadata
1263
- * @param {string} eventType locus event
1264
1274
  * @param {DataSet[]} dataSets
1275
+ * @param {string} eventType locus event
1265
1276
  * @returns {void}
1266
1277
  */
1267
1278
  private onFullLocusWithHashTrees(
1268
1279
  debugText: string,
1269
1280
  locus: any,
1270
1281
  metadata: Metadata,
1271
- eventType?: string,
1272
- dataSets?: Array<DataSet>
1282
+ dataSets: Array<DataSet>,
1283
+ eventType?: string
1273
1284
  ) {
1274
1285
  if (!this.hashTreeParsers.has(locus.url)) {
1275
1286
  LoggerProxy.logger.info(
@@ -1289,7 +1300,8 @@ export default class LocusInfo extends EventsScope {
1289
1300
  metadata,
1290
1301
  });
1291
1302
  // we have a full locus to start with, so we consider Locus info to be "initialized"
1292
- this.hashTreeParsers.get(locus.url).initializedFromHashTree = true;
1303
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1304
+ this.hashTreeParsers.get(locus.url)!.initializedFromHashTree = true;
1293
1305
  this.onFullLocusCommon(locus, eventType);
1294
1306
  } else {
1295
1307
  // in this case the Locus we're getting is not necessarily the full one
@@ -1351,7 +1363,7 @@ export default class LocusInfo extends EventsScope {
1351
1363
  );
1352
1364
  }
1353
1365
  // this is the new hashmap Locus DTO format (only applicable to webinars for now)
1354
- this.onFullLocusWithHashTrees(debugText, locus, metadata, eventType, dataSets);
1366
+ this.onFullLocusWithHashTrees(debugText, locus, metadata, dataSets, eventType);
1355
1367
  } else {
1356
1368
  this.onFullLocusClassic(debugText, locus, eventType);
1357
1369
  }
@@ -2587,6 +2599,7 @@ export default class LocusInfo extends EventsScope {
2587
2599
  {
2588
2600
  muted: parsedSelves.current.remoteMuted,
2589
2601
  unmuteAllowed: parsedSelves.current.unmuteAllowed,
2602
+ modifiedBy: parsedSelves.current.modifiedBy ?? null,
2590
2603
  }
2591
2604
  );
2592
2605
  }
@@ -2859,4 +2872,17 @@ export default class LocusInfo extends EventsScope {
2859
2872
  clearMainSessionLocusCache() {
2860
2873
  this.mainSessionLocusCache = null;
2861
2874
  }
2875
+
2876
+ /**
2877
+ * Cleans up all hash tree parsers and clears internal maps.
2878
+ * @returns {void}
2879
+ * @memberof LocusInfo
2880
+ */
2881
+ cleanUp() {
2882
+ this.hashTreeParsers.forEach((entry) => {
2883
+ entry.parser.cleanUp();
2884
+ });
2885
+ this.hashTreeParsers.clear();
2886
+ this.hashTreeObjectId2ParticipantId.clear();
2887
+ }
2862
2888
  }
@@ -22,6 +22,7 @@ import {
22
22
  MediaConnectionEventNames,
23
23
  MediaContent,
24
24
  MediaType,
25
+ MediaCodecMimeType,
25
26
  RemoteTrackType,
26
27
  RoapMessage,
27
28
  StatsAnalyzer,
@@ -3733,7 +3734,7 @@ export default class Meeting extends StatelessWebexPlugin {
3733
3734
  });
3734
3735
  this.updateLLMConnection();
3735
3736
  });
3736
- this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
3737
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
3737
3738
  this.stopKeepAlive();
3738
3739
 
3739
3740
  if (payload) {
@@ -3759,6 +3760,15 @@ export default class Meeting extends StatelessWebexPlugin {
3759
3760
  });
3760
3761
  }
3761
3762
  this.rtcMetrics?.sendNextMetrics();
3763
+
3764
+ this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
3765
+ LoggerProxy.logger.warn(
3766
+ `Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
3767
+ error?.message || String(error)
3768
+ }`
3769
+ );
3770
+ });
3771
+
3762
3772
  this.updateLLMConnection();
3763
3773
  });
3764
3774
 
@@ -5959,6 +5969,30 @@ export default class Meeting extends StatelessWebexPlugin {
5959
5969
  );
5960
5970
  }
5961
5971
 
5972
+ /**
5973
+ * Restores LLM subchannel subscriptions after reconnect when captions are active.
5974
+ * @returns {void}
5975
+ */
5976
+ private restoreLLMSubscriptionsIfNeeded(): void {
5977
+ try {
5978
+ // @ts-ignore
5979
+ const isCaptionBoxOn = this.webex.internal.voicea?.getIsCaptionBoxOn?.();
5980
+
5981
+ if (!isCaptionBoxOn) {
5982
+ return;
5983
+ }
5984
+
5985
+ // @ts-ignore
5986
+ this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
5987
+ } catch (error) {
5988
+ const msg = error?.message || String(error);
5989
+
5990
+ LoggerProxy.logger.warn(
5991
+ `Meeting:index#restoreLLMSubscriptionsIfNeeded --> failed to restore subscriptions after LLM online: ${msg}`
5992
+ );
5993
+ }
5994
+ }
5995
+
5962
5996
  /**
5963
5997
  * This is a callback for the LLM event that is triggered when it comes online
5964
5998
  * This method in turn will trigger an event to the developers that the LLM is connected
@@ -5967,8 +6001,8 @@ export default class Meeting extends StatelessWebexPlugin {
5967
6001
  * @returns {null}
5968
6002
  */
5969
6003
  private handleLLMOnline = (): void => {
5970
- // @ts-ignore
5971
- this.webex.internal.llm.off('online', this.handleLLMOnline);
6004
+ this.restoreLLMSubscriptionsIfNeeded();
6005
+
5972
6006
  Trigger.trigger(
5973
6007
  this,
5974
6008
  {
@@ -6199,6 +6233,8 @@ export default class Meeting extends StatelessWebexPlugin {
6199
6233
  this.saveDataChannelToken(join);
6200
6234
  // @ts-ignore - config coming from registerPlugin
6201
6235
  if (this.config.enableAutomaticLLM) {
6236
+ // @ts-ignore
6237
+ this.webex.internal.llm.off('online', this.handleLLMOnline);
6202
6238
  // @ts-ignore
6203
6239
  this.webex.internal.llm.on('online', this.handleLLMOnline);
6204
6240
  this.updateLLMConnection()
@@ -6342,6 +6378,52 @@ export default class Meeting extends StatelessWebexPlugin {
6342
6378
  }
6343
6379
  }
6344
6380
 
6381
+ /**
6382
+ * Ensures default-session data channel token exists after lobby admission.
6383
+ * Some lobby users do not receive a token until they are admitted.
6384
+ * @returns {Promise<boolean>} true when a new token is fetched and cached
6385
+ */
6386
+ private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
6387
+ try {
6388
+ // @ts-ignore
6389
+ const datachannelToken = this.webex.internal.llm.getDatachannelToken();
6390
+ // @ts-ignore
6391
+ const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
6392
+
6393
+ if (!isDataChannelTokenEnabled || datachannelToken) {
6394
+ return false;
6395
+ }
6396
+
6397
+ const response = await this.meetingRequest.fetchDatachannelToken({
6398
+ locusUrl: this.locusUrl,
6399
+ requestingParticipantId: this.members.selfId,
6400
+ isPracticeSession: false,
6401
+ });
6402
+ const fetchedDatachannelToken = response?.body?.datachannelToken;
6403
+
6404
+ if (!fetchedDatachannelToken) {
6405
+ return false;
6406
+ }
6407
+
6408
+ // @ts-ignore
6409
+ this.webex.internal.llm.setDatachannelToken(
6410
+ fetchedDatachannelToken,
6411
+ DataChannelTokenType.Default
6412
+ );
6413
+
6414
+ return true;
6415
+ } catch (error) {
6416
+ const msg = error?.message || String(error);
6417
+
6418
+ LoggerProxy.logger.warn(
6419
+ `Meeting:index#ensureDefaultDatachannelTokenAfterAdmit --> failed to proactively fetch default data channel token after admit: ${msg}`,
6420
+ {statusCode: error?.statusCode}
6421
+ );
6422
+
6423
+ return false;
6424
+ }
6425
+ }
6426
+
6345
6427
  /**
6346
6428
  * Connects to low latency mercury and reconnects if the address has changed
6347
6429
  * It will also disconnect if called when the meeting has ended
@@ -9839,15 +9921,20 @@ export default class Meeting extends StatelessWebexPlugin {
9839
9921
  }
9840
9922
 
9841
9923
  if (shouldEnableMusicMode) {
9842
- await this.sendSlotManager.setCodecParameters(MediaType.AudioMain, {
9843
- maxaveragebitrate: '64000',
9844
- maxplaybackrate: '48000',
9845
- });
9924
+ await this.sendSlotManager.setCustomCodecParameters(
9925
+ MediaType.AudioMain,
9926
+ MediaCodecMimeType.OPUS,
9927
+ {
9928
+ maxaveragebitrate: '64000',
9929
+ maxplaybackrate: '48000',
9930
+ }
9931
+ );
9846
9932
  } else {
9847
- await this.sendSlotManager.deleteCodecParameters(MediaType.AudioMain, [
9848
- 'maxaveragebitrate',
9849
- 'maxplaybackrate',
9850
- ]);
9933
+ await this.sendSlotManager.markCustomCodecParametersForDeletion(
9934
+ MediaType.AudioMain,
9935
+ MediaCodecMimeType.OPUS,
9936
+ ['maxaveragebitrate', 'maxplaybackrate']
9937
+ );
9851
9938
  }
9852
9939
  }
9853
9940
 
@@ -371,6 +371,7 @@ const MeetingUtil = {
371
371
  meeting.breakouts.cleanUp();
372
372
  meeting.webinar.cleanUp();
373
373
  meeting.simultaneousInterpretation.cleanUp();
374
+ meeting.locusInfo.cleanUp();
374
375
  meeting.locusMediaRequest = undefined;
375
376
 
376
377
  meeting.webex?.internal?.newMetrics?.callDiagnosticMetrics?.clearEventLimitsForCorrelationId(