@webex/plugin-meetings 3.12.0-next.60 → 3.12.0-next.62

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.
@@ -8,3 +8,4 @@ export declare const DataSetNames: {
8
8
  UNJOINED: string;
9
9
  };
10
10
  export declare const DATA_SET_INIT_PRIORITY: string[];
11
+ export declare const LLM_DATASET_NAMES: string[];
@@ -2,6 +2,11 @@ import HashTree from './hashTree';
2
2
  import { Enum } from '../constants';
3
3
  import { HtMeta, HashTreeObject } from './types';
4
4
  import { LocusDTO } from '../locus-info/types';
5
+ export declare enum SyncAllBackoffType {
6
+ NONE = "none",
7
+ ONLY_LLM = "onlyLLM",
8
+ ALL = "all"
9
+ }
5
10
  export interface DataSet {
6
11
  url: string;
7
12
  root: string;
@@ -88,7 +93,8 @@ declare class HashTreeParser {
88
93
  state: 'active' | 'stopped';
89
94
  private syncQueue;
90
95
  private isSyncInProgress;
91
- private isSyncAllInProgress;
96
+ private syncAllBackoffType;
97
+ private dataSetsSyncedDuringBackoff;
92
98
  private syncQueueProcessingPromise;
93
99
  /**
94
100
  * Constructor for HashTreeParser
@@ -341,6 +347,21 @@ declare class HashTreeParser {
341
347
  * @returns {void}
342
348
  */
343
349
  private cancelPendingSyncsForDataSets;
350
+ /**
351
+ * If a syncAllDatasets backoff sleep is in progress, marks the given data sets to be skipped
352
+ * after the sleep completes.
353
+ *
354
+ * @param {string[]} dataSetNames - The names of the data sets to mark
355
+ * @returns {void}
356
+ */
357
+ private markDataSetsForSyncAllBackoffSkip;
358
+ /**
359
+ * Aborts any in-flight sync HTTP requests for the specified data sets.
360
+ *
361
+ * @param {string[]} dataSetNames - The names of the data sets whose syncs should be aborted
362
+ * @returns {void}
363
+ */
364
+ private abortInFlightSyncs;
344
365
  /**
345
366
  * Enqueues a sync for the given data set. If the data set is already in the queue, the request is ignored.
346
367
  * This ensures that all syncs are executed sequentially and no more than 1 sync runs at a time.
@@ -357,14 +378,44 @@ declare class HashTreeParser {
357
378
  * @returns {Promise<void>}
358
379
  */
359
380
  private processSyncQueue;
381
+ /**
382
+ * sets the backoff type for syncAllDatasets calls, which determines the scope of datasets that will be synced after the backoff delay.
383
+ *
384
+ * @param {boolean} onlyLLM - Whether the backoff is for a syncAllDatasets call that is syncing only LLM datasets
385
+ * @returns {void}
386
+ */
387
+ private setSyncAllBackoffType;
388
+ /**
389
+ * Checks if a syncAll backoff is already in progress. If so, upgrades the scope from
390
+ * onlyLLM to all datasets when the new call has a broader scope.
391
+ *
392
+ * @param {boolean} onlyLLM - Whether the current call is for LLM datasets only
393
+ * @returns {boolean} true if a backoff is already pending (caller should return early)
394
+ */
395
+ private tryUpgradePendingBackoff;
360
396
  /**
361
397
  * Syncs all data sets that have hash trees, one by one in sequence, using the priority order
362
- * provided by sortByInitPriority(). Does nothing if the parser is stopped or if a syncAllDatasets
363
- * call is already in progress.
398
+ * provided by sortByInitPriority().
399
+ *
400
+ * If a call is already waiting in the backoff delay phase, a new call with a broader scope
401
+ * (onlyLLM=false) will upgrade the pending scope, and the dataset list will be computed after
402
+ * the backoff using the upgraded scope. After the backoff, the sync queue handles deduplication
403
+ * so no guard is needed.
364
404
  *
405
+ * @param {Object} [options={}] - Options for syncing
406
+ * @param {boolean} [options.onlyLLM=false] - Whether to sync only LLM based data sets
365
407
  * @returns {Promise<void>}
366
408
  */
367
- syncAllDatasets(): Promise<void>;
409
+ syncAllDatasets(options?: {
410
+ onlyLLM?: boolean;
411
+ }): Promise<void>;
412
+ /**
413
+ * Returns the list of data sets that have hash trees, sorted by the priority order provided by sortByInitPriority().
414
+ *
415
+ * @param {boolean} onlyLLM - Whether to include only LLM based data sets
416
+ * @returns {Array<{name: string, backoff: {maxMs: number, exponent: number}}>} The sorted list of data sets with their backoff configurations
417
+ */
418
+ private getSortedDataSetsWithHashTrees;
368
419
  /**
369
420
  * Runs the sync algorithm for the given data set.
370
421
  *
@@ -31,3 +31,10 @@ export declare const deleteNestedObjectsWithHtMeta: (currentLocusPart: any, pare
31
31
  export declare function sortByInitPriority<T extends {
32
32
  name: string;
33
33
  }>(items: T[], priority: string[]): T[];
34
+ /**
35
+ * Sleeps for the specified amount of milliseconds
36
+ *
37
+ * @param {number} ms amount of milliseconds to sleep
38
+ * @returns {Promise<void>} A promise that resolves after the specified delay
39
+ */
40
+ export declare function sleep(ms: number): Promise<void>;
@@ -215,9 +215,13 @@ export default class LocusInfo extends EventsScope {
215
215
  * Triggers a sync of all hash tree datasets for all hash tree parsers associated with this meeting.
216
216
  * The syncs are executed sequentially within each parser.
217
217
  *
218
+ * @param {Object} [options={}] - Options for syncing
219
+ * @param {boolean} [options.onlyLLM=false] - Whether to sync only LLM based data sets
218
220
  * @returns {Promise<void>}
219
221
  */
220
- syncAllHashTreeDatasets(): Promise<void>;
222
+ syncAllHashTreeDatasets(options?: {
223
+ onlyLLM?: boolean;
224
+ }): Promise<void>;
221
225
  /**
222
226
  * Callback registered with HashTreeParser to receive locus info updates.
223
227
  * Updates our locus info based on the data parsed by the hash tree parser.
@@ -774,7 +774,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
774
774
  }, _callee1);
775
775
  }))();
776
776
  },
777
- version: "3.12.0-next.60"
777
+ version: "3.12.0-next.62"
778
778
  });
779
779
  var _default = exports.default = Webinar;
780
780
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@webex/common": "3.12.0-next.1",
65
- "@webex/internal-media-core": "2.24.1",
65
+ "@webex/internal-media-core": "2.25.1",
66
66
  "@webex/internal-plugin-conversation": "3.12.0-next.17",
67
67
  "@webex/internal-plugin-device": "3.12.0-next.15",
68
68
  "@webex/internal-plugin-llm": "3.12.0-next.18",
@@ -71,7 +71,7 @@
71
71
  "@webex/internal-plugin-support": "3.12.0-next.17",
72
72
  "@webex/internal-plugin-user": "3.12.0-next.16",
73
73
  "@webex/internal-plugin-voicea": "3.12.0-next.18",
74
- "@webex/media-helpers": "3.12.0-next.3",
74
+ "@webex/media-helpers": "3.12.0-next.4",
75
75
  "@webex/plugin-people": "3.12.0-next.16",
76
76
  "@webex/plugin-rooms": "3.12.0-next.17",
77
77
  "@webex/ts-sdp": "^1.8.1",
@@ -94,5 +94,5 @@
94
94
  "//": [
95
95
  "TODO: upgrade jwt-decode when moving to node 18"
96
96
  ],
97
- "version": "3.12.0-next.60"
97
+ "version": "3.12.0-next.62"
98
98
  }
@@ -17,3 +17,10 @@ export const DataSetNames = {
17
17
  // participant object for webinar attendees. If SELF were initialized first, locus.info
18
18
  // would not yet be populated and the attendee participant would be skipped.
19
19
  export const DATA_SET_INIT_PRIORITY: string[] = [DataSetNames.MAIN, DataSetNames.SELF];
20
+
21
+ // Data sets for which we normally receive events over LLM connection
22
+ export const LLM_DATASET_NAMES = [
23
+ DataSetNames.MAIN,
24
+ DataSetNames.ATD_ACTIVE,
25
+ DataSetNames.ATD_UNMUTED,
26
+ ];
@@ -4,10 +4,16 @@ import LoggerProxy from '../common/logs/logger-proxy';
4
4
  import Metrics from '../metrics';
5
5
  import BEHAVIORAL_METRICS from '../metrics/constants';
6
6
  import {Enum, HTTP_VERBS} from '../constants';
7
- import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH} from './constants';
7
+ import {DataSetNames, DATA_SET_INIT_PRIORITY, EMPTY_HASH, LLM_DATASET_NAMES} from './constants';
8
8
  import {ObjectType, HtMeta, HashTreeObject} from './types';
9
9
  import {LocusDTO, LocusErrorCodes} from '../locus-info/types';
10
- import {deleteNestedObjectsWithHtMeta, isMetadata, sortByInitPriority} from './utils';
10
+ import {deleteNestedObjectsWithHtMeta, isMetadata, sleep, sortByInitPriority} from './utils';
11
+
12
+ export enum SyncAllBackoffType {
13
+ NONE = 'none',
14
+ ONLY_LLM = 'onlyLLM',
15
+ ALL = 'all',
16
+ }
11
17
 
12
18
  export interface DataSet {
13
19
  url: string;
@@ -122,7 +128,10 @@ class HashTreeParser {
122
128
  state: 'active' | 'stopped';
123
129
  private syncQueue: Array<{dataSetName: string; reason: string; isInitialization?: boolean}> = [];
124
130
  private isSyncInProgress = false;
125
- private isSyncAllInProgress = false;
131
+ // tracks whether syncAllDatasets is currently in its backoff delay phase and with what scope
132
+ private syncAllBackoffType: SyncAllBackoffType = SyncAllBackoffType.NONE;
133
+ // datasets that received messages during the syncAllDatasets backoff sleep and should be skipped
134
+ private dataSetsSyncedDuringBackoff: Set<string> = new Set();
126
135
  private syncQueueProcessingPromise: Promise<void> = Promise.resolve();
127
136
 
128
137
  /**
@@ -1342,8 +1351,14 @@ class HashTreeParser {
1342
1351
  // parseMessage() -> cancelPendingSyncsForDataSets() doesn't log a
1343
1352
  // misleading "aborting sync" message for this already-completed sync
1344
1353
  dataSet.syncAbortController = undefined;
1354
+
1345
1355
  // the format of sync response is the same as messages, so we can reuse the same handler
1346
- this.handleMessage(syncResponse, 'via sync API');
1356
+ this.handleMessage(
1357
+ syncResponse,
1358
+ `via sync API (${
1359
+ isInitialization ? 'init' : `${Object.keys(leavesData).length} mismatched leaves`
1360
+ })`
1361
+ );
1347
1362
  }
1348
1363
  } catch (error) {
1349
1364
  if (!this.handleSyncErrors(error)) {
@@ -1377,6 +1392,32 @@ class HashTreeParser {
1377
1392
  );
1378
1393
  }
1379
1394
 
1395
+ this.markDataSetsForSyncAllBackoffSkip(dataSetNames);
1396
+ this.abortInFlightSyncs(dataSetNames);
1397
+ }
1398
+
1399
+ /**
1400
+ * If a syncAllDatasets backoff sleep is in progress, marks the given data sets to be skipped
1401
+ * after the sleep completes.
1402
+ *
1403
+ * @param {string[]} dataSetNames - The names of the data sets to mark
1404
+ * @returns {void}
1405
+ */
1406
+ private markDataSetsForSyncAllBackoffSkip(dataSetNames: string[]): void {
1407
+ if (this.syncAllBackoffType !== SyncAllBackoffType.NONE) {
1408
+ for (const name of dataSetNames) {
1409
+ this.dataSetsSyncedDuringBackoff.add(name);
1410
+ }
1411
+ }
1412
+ }
1413
+
1414
+ /**
1415
+ * Aborts any in-flight sync HTTP requests for the specified data sets.
1416
+ *
1417
+ * @param {string[]} dataSetNames - The names of the data sets whose syncs should be aborted
1418
+ * @returns {void}
1419
+ */
1420
+ private abortInFlightSyncs(dataSetNames: string[]): void {
1380
1421
  for (const name of dataSetNames) {
1381
1422
  if (this.dataSets[name]?.syncAbortController) {
1382
1423
  LoggerProxy.logger.info(
@@ -1451,39 +1492,129 @@ class HashTreeParser {
1451
1492
  }
1452
1493
  }
1453
1494
 
1495
+ /**
1496
+ * sets the backoff type for syncAllDatasets calls, which determines the scope of datasets that will be synced after the backoff delay.
1497
+ *
1498
+ * @param {boolean} onlyLLM - Whether the backoff is for a syncAllDatasets call that is syncing only LLM datasets
1499
+ * @returns {void}
1500
+ */
1501
+ private setSyncAllBackoffType(onlyLLM: boolean): void {
1502
+ this.syncAllBackoffType = onlyLLM ? SyncAllBackoffType.ONLY_LLM : SyncAllBackoffType.ALL;
1503
+ }
1504
+
1505
+ /**
1506
+ * Checks if a syncAll backoff is already in progress. If so, upgrades the scope from
1507
+ * onlyLLM to all datasets when the new call has a broader scope.
1508
+ *
1509
+ * @param {boolean} onlyLLM - Whether the current call is for LLM datasets only
1510
+ * @returns {boolean} true if a backoff is already pending (caller should return early)
1511
+ */
1512
+ private tryUpgradePendingBackoff(onlyLLM: boolean): boolean {
1513
+ if (this.syncAllBackoffType !== SyncAllBackoffType.NONE) {
1514
+ if (!onlyLLM && this.syncAllBackoffType === SyncAllBackoffType.ONLY_LLM) {
1515
+ this.setSyncAllBackoffType(false);
1516
+ LoggerProxy.logger.info(
1517
+ `HashTreeParser#syncAllDatasets --> ${this.debugId} upgraded pending syncAll from onlyLLM to all datasets`
1518
+ );
1519
+ }
1520
+
1521
+ return true;
1522
+ }
1523
+
1524
+ return false;
1525
+ }
1526
+
1454
1527
  /**
1455
1528
  * Syncs all data sets that have hash trees, one by one in sequence, using the priority order
1456
- * provided by sortByInitPriority(). Does nothing if the parser is stopped or if a syncAllDatasets
1457
- * call is already in progress.
1529
+ * provided by sortByInitPriority().
1458
1530
  *
1531
+ * If a call is already waiting in the backoff delay phase, a new call with a broader scope
1532
+ * (onlyLLM=false) will upgrade the pending scope, and the dataset list will be computed after
1533
+ * the backoff using the upgraded scope. After the backoff, the sync queue handles deduplication
1534
+ * so no guard is needed.
1535
+ *
1536
+ * @param {Object} [options={}] - Options for syncing
1537
+ * @param {boolean} [options.onlyLLM=false] - Whether to sync only LLM based data sets
1459
1538
  * @returns {Promise<void>}
1460
1539
  */
1461
- public async syncAllDatasets(): Promise<void> {
1540
+ public async syncAllDatasets(options: {onlyLLM?: boolean} = {}): Promise<void> {
1541
+ const {onlyLLM = false} = options;
1462
1542
  if (this.state === 'stopped') return;
1463
- if (this.isSyncAllInProgress) return;
1464
1543
 
1465
- this.isSyncAllInProgress = true;
1466
- try {
1467
- const dataSetsWithHashTrees = Object.values(this.dataSets)
1468
- .filter((dataSet) => dataSet?.hashTree)
1469
- .map((dataSet) => ({name: dataSet.name}));
1544
+ // if we're already in the backoff delay phase, try to upgrade the scope instead of starting a new one
1545
+ if (this.tryUpgradePendingBackoff(onlyLLM)) {
1546
+ return;
1547
+ }
1548
+
1549
+ const dataSetsToSync = this.getSortedDataSetsWithHashTrees(onlyLLM);
1550
+
1551
+ if (dataSetsToSync.length === 0) return;
1552
+
1553
+ this.setSyncAllBackoffType(onlyLLM);
1554
+
1555
+ const delay = this.getWeightedBackoffTime(dataSetsToSync[0].backoff);
1556
+
1557
+ LoggerProxy.logger.info(
1558
+ `HashTreeParser#syncAllDatasets --> ${this.debugId} starting backoff delay of ${delay}ms (onlyLLM=${onlyLLM})`
1559
+ );
1560
+
1561
+ // delay the start of the syncs - this is a Locus requirement to avoid thundering herd issues
1562
+ await sleep(delay);
1563
+
1564
+ // read the (possibly upgraded) scope and clear the backoff flag
1565
+ const effectiveBackoffType = this.syncAllBackoffType;
1566
+ const skippedDataSets = this.dataSetsSyncedDuringBackoff;
1567
+
1568
+ this.syncAllBackoffType = SyncAllBackoffType.NONE;
1569
+ this.dataSetsSyncedDuringBackoff = new Set();
1470
1570
 
1471
- const sorted = sortByInitPriority(dataSetsWithHashTrees, DATA_SET_INIT_PRIORITY);
1571
+ if ((this.state as string) === 'stopped') return;
1472
1572
 
1573
+ // re-evaluate the dataset list after the sleep, since the scope may have been upgraded
1574
+ // and exclude datasets that received messages during the backoff sleep
1575
+ const effectiveDataSetsToSync = this.getSortedDataSetsWithHashTrees(
1576
+ effectiveBackoffType === SyncAllBackoffType.ONLY_LLM
1577
+ ).filter((ds) => !skippedDataSets.has(ds.name));
1578
+
1579
+ if (skippedDataSets.size > 0) {
1473
1580
  LoggerProxy.logger.info(
1474
- `HashTreeParser#syncAllDatasets --> ${this.debugId} syncing datasets: ${sorted
1475
- .map((ds) => ds.name)
1476
- .join(', ')}`
1581
+ `HashTreeParser#syncAllDatasets --> ${
1582
+ this.debugId
1583
+ } skipping datasets that received messages during backoff: ${[...skippedDataSets].join(
1584
+ ', '
1585
+ )}`
1477
1586
  );
1587
+ }
1478
1588
 
1479
- for (const ds of sorted) {
1480
- this.enqueueSyncForDataset(ds.name, 'syncAllDatasets');
1481
- }
1589
+ LoggerProxy.logger.info(
1590
+ `HashTreeParser#syncAllDatasets --> ${this.debugId} syncing ${
1591
+ effectiveBackoffType === SyncAllBackoffType.ONLY_LLM ? 'only LLM' : 'all'
1592
+ } datasets: ${effectiveDataSetsToSync.map((ds) => ds.name).join(', ')}`
1593
+ );
1482
1594
 
1483
- await this.syncQueueProcessingPromise;
1484
- } finally {
1485
- this.isSyncAllInProgress = false;
1595
+ for (const ds of effectiveDataSetsToSync) {
1596
+ this.enqueueSyncForDataset(ds.name, 'syncAllDatasets');
1597
+ }
1598
+
1599
+ await this.syncQueueProcessingPromise;
1600
+ }
1601
+
1602
+ /**
1603
+ * Returns the list of data sets that have hash trees, sorted by the priority order provided by sortByInitPriority().
1604
+ *
1605
+ * @param {boolean} onlyLLM - Whether to include only LLM based data sets
1606
+ * @returns {Array<{name: string, backoff: {maxMs: number, exponent: number}}>} The sorted list of data sets with their backoff configurations
1607
+ */
1608
+ private getSortedDataSetsWithHashTrees(onlyLLM: boolean) {
1609
+ let dataSets = Object.values(this.dataSets)
1610
+ .filter((dataSet) => dataSet?.hashTree)
1611
+ .map((dataSet) => ({name: dataSet.name, backoff: dataSet.backoff}));
1612
+
1613
+ if (onlyLLM) {
1614
+ dataSets = dataSets.filter((ds) => LLM_DATASET_NAMES.includes(ds.name));
1486
1615
  }
1616
+
1617
+ return sortByInitPriority(dataSets, DATA_SET_INIT_PRIORITY);
1487
1618
  }
1488
1619
 
1489
1620
  /**
@@ -1623,6 +1754,8 @@ class HashTreeParser {
1623
1754
  );
1624
1755
  this.stopAllTimers();
1625
1756
  this.syncQueue = [];
1757
+ this.syncAllBackoffType = SyncAllBackoffType.NONE;
1758
+ this.dataSetsSyncedDuringBackoff = new Set();
1626
1759
  Object.values(this.dataSets).forEach((dataSet) => {
1627
1760
  dataSet.syncAbortController?.abort();
1628
1761
  dataSet.syncAbortController = undefined;
@@ -77,3 +77,22 @@ export function sortByInitPriority<T extends {name: string}>(items: T[], priorit
77
77
 
78
78
  return [...prioritized, ...rest];
79
79
  }
80
+
81
+ /**
82
+ * Sleeps for the specified amount of milliseconds
83
+ *
84
+ * @param {number} ms amount of milliseconds to sleep
85
+ * @returns {Promise<void>} A promise that resolves after the specified delay
86
+ */
87
+ export function sleep(ms: number): Promise<void> {
88
+ if (ms <= 0) {
89
+ return Promise.resolve();
90
+ }
91
+
92
+ return new Promise((resolve) => {
93
+ // start a timer that will resolve the promise after the specified delay
94
+ setTimeout(() => {
95
+ resolve();
96
+ }, ms);
97
+ });
98
+ }
@@ -1203,13 +1203,15 @@ export default class LocusInfo extends EventsScope {
1203
1203
  * Triggers a sync of all hash tree datasets for all hash tree parsers associated with this meeting.
1204
1204
  * The syncs are executed sequentially within each parser.
1205
1205
  *
1206
+ * @param {Object} [options={}] - Options for syncing
1207
+ * @param {boolean} [options.onlyLLM=false] - Whether to sync only LLM based data sets
1206
1208
  * @returns {Promise<void>}
1207
1209
  */
1208
- async syncAllHashTreeDatasets(): Promise<void> {
1210
+ async syncAllHashTreeDatasets(options: {onlyLLM?: boolean} = {}): Promise<void> {
1209
1211
  for (const [, entry] of this.hashTreeParsers) {
1210
1212
  if (entry.parser) {
1211
1213
  // eslint-disable-next-line no-await-in-loop
1212
- await entry.parser.syncAllDatasets();
1214
+ await entry.parser.syncAllDatasets(options);
1213
1215
  }
1214
1216
  }
1215
1217
  }
@@ -6135,6 +6135,7 @@ export default class Meeting extends StatelessWebexPlugin {
6135
6135
  */
6136
6136
  private handleLLMOnline = (): void => {
6137
6137
  this.restoreLLMSubscriptionsIfNeeded();
6138
+ this.locusInfo.syncAllHashTreeDatasets({onlyLLM: true});
6138
6139
 
6139
6140
  Trigger.trigger(
6140
6141
  this,
@@ -6687,6 +6688,7 @@ export default class Meeting extends StatelessWebexPlugin {
6687
6688
  return this.webex.internal.llm
6688
6689
  .registerAndConnect(url, dataChannelUrl, datachannelToken)
6689
6690
  .then((registerAndConnectResult) => {
6691
+ this.locusInfo.syncAllHashTreeDatasets({onlyLLM: true});
6690
6692
  // Record ownership of the default LLM session for this meeting so
6691
6693
  // subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
6692
6694
  // calls can detect and skip work that doesn't belong to them.