@webex/plugin-meetings 3.12.0-next.63 → 3.12.0-next.65

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.
@@ -117,6 +117,7 @@ function createDataSet(name: string, leafCount: number, version = 1) {
117
117
  name,
118
118
  idleMs: 1000,
119
119
  backoff: {maxMs: 1000, exponent: 2},
120
+ heartbeatIntervalMs: 5000,
120
121
  };
121
122
  }
122
123
 
@@ -3042,7 +3043,6 @@ describe('HashTreeParser', () => {
3042
3043
  ],
3043
3044
  visibleDataSetsUrl,
3044
3045
  locusUrl,
3045
- heartbeatIntervalMs,
3046
3046
  };
3047
3047
 
3048
3048
  parser.handleMessage(heartbeatMessage, 'initial heartbeat');
@@ -3112,7 +3112,6 @@ describe('HashTreeParser', () => {
3112
3112
  ],
3113
3113
  visibleDataSetsUrl,
3114
3114
  locusUrl,
3115
- heartbeatIntervalMs,
3116
3115
  };
3117
3116
 
3118
3117
  parser.handleMessage(heartbeatMessage, 'self heartbeat');
@@ -3148,7 +3147,6 @@ describe('HashTreeParser', () => {
3148
3147
 
3149
3148
  it('sets watchdog timers for each data set in the message', async () => {
3150
3149
  const parser = createHashTreeParser();
3151
- const heartbeatIntervalMs = 5000;
3152
3150
 
3153
3151
  // Send heartbeat with multiple datasets
3154
3152
  const heartbeatMessage = {
@@ -3165,7 +3163,6 @@ describe('HashTreeParser', () => {
3165
3163
  ],
3166
3164
  visibleDataSetsUrl,
3167
3165
  locusUrl,
3168
- heartbeatIntervalMs,
3169
3166
  };
3170
3167
 
3171
3168
  parser.handleMessage(heartbeatMessage, 'multi-dataset heartbeat');
@@ -3179,7 +3176,6 @@ describe('HashTreeParser', () => {
3179
3176
 
3180
3177
  it('resets the watchdog timer for a specific data set when a new heartbeat for it is received', async () => {
3181
3178
  const parser = createHashTreeParser();
3182
- const heartbeatIntervalMs = 5000;
3183
3179
 
3184
3180
  // Send first heartbeat for 'main'
3185
3181
  const heartbeat1 = {
@@ -3191,7 +3187,6 @@ describe('HashTreeParser', () => {
3191
3187
  ],
3192
3188
  visibleDataSetsUrl,
3193
3189
  locusUrl,
3194
- heartbeatIntervalMs,
3195
3190
  };
3196
3191
 
3197
3192
  parser.handleMessage(heartbeat1, 'first heartbeat');
@@ -3212,7 +3207,6 @@ describe('HashTreeParser', () => {
3212
3207
  ],
3213
3208
  visibleDataSetsUrl,
3214
3209
  locusUrl,
3215
- heartbeatIntervalMs,
3216
3210
  };
3217
3211
 
3218
3212
  parser.handleMessage(heartbeat2, 'second heartbeat');
@@ -3231,7 +3225,6 @@ describe('HashTreeParser', () => {
3231
3225
 
3232
3226
  it('resets the watchdog timer when a normal message (with locusStateElements) is received', async () => {
3233
3227
  const parser = createHashTreeParser();
3234
- const heartbeatIntervalMs = 5000;
3235
3228
 
3236
3229
  // Send initial heartbeat to start the watchdog for 'main'
3237
3230
  const heartbeat = {
@@ -3243,7 +3236,6 @@ describe('HashTreeParser', () => {
3243
3236
  ],
3244
3237
  visibleDataSetsUrl,
3245
3238
  locusUrl,
3246
- heartbeatIntervalMs,
3247
3239
  };
3248
3240
 
3249
3241
  parser.handleMessage(heartbeat, 'initial heartbeat');
@@ -3271,7 +3263,6 @@ describe('HashTreeParser', () => {
3271
3263
  data: {someData: 'value'},
3272
3264
  },
3273
3265
  ],
3274
- heartbeatIntervalMs,
3275
3266
  };
3276
3267
 
3277
3268
  parser.handleMessage(normalMessage, 'normal message');
@@ -3285,12 +3276,17 @@ describe('HashTreeParser', () => {
3285
3276
  const parser = createHashTreeParser();
3286
3277
 
3287
3278
  // Send a heartbeat message without heartbeatIntervalMs
3288
- const heartbeatMessage = createHeartbeatMessage(
3289
- 'main',
3290
- 16,
3291
- 1100,
3292
- parser.dataSets.main.hashTree.getRootHash()
3293
- );
3279
+ const heartbeatMessage = {
3280
+ dataSets: [
3281
+ {
3282
+ ...createDataSet('main', 16, 1100),
3283
+ root: parser.dataSets.main.hashTree.getRootHash(),
3284
+ heartbeatIntervalMs: undefined,
3285
+ },
3286
+ ],
3287
+ visibleDataSetsUrl,
3288
+ locusUrl,
3289
+ };
3294
3290
 
3295
3291
  parser.handleMessage(heartbeatMessage, 'heartbeat without interval');
3296
3292
 
@@ -3299,7 +3295,6 @@ describe('HashTreeParser', () => {
3299
3295
 
3300
3296
  it('stops all watchdog timers when meeting ends via sentinel message', async () => {
3301
3297
  const parser = createHashTreeParser();
3302
- const heartbeatIntervalMs = 5000;
3303
3298
 
3304
3299
  // Send heartbeat for multiple datasets
3305
3300
  const heartbeat = {
@@ -3316,7 +3311,6 @@ describe('HashTreeParser', () => {
3316
3311
  ],
3317
3312
  visibleDataSetsUrl,
3318
3313
  locusUrl,
3319
- heartbeatIntervalMs,
3320
3314
  };
3321
3315
 
3322
3316
  parser.handleMessage(heartbeat, 'initial heartbeat');
@@ -3367,7 +3361,6 @@ describe('HashTreeParser', () => {
3367
3361
  };
3368
3362
 
3369
3363
  const parser = createHashTreeParser(initialLocus, metadata);
3370
- const heartbeatIntervalMs = 5000;
3371
3364
 
3372
3365
  // Set Math.random to return 1 so that backoff = 1^exponent * maxMs = maxMs
3373
3366
  mathRandomStub.returns(1);
@@ -3389,7 +3382,6 @@ describe('HashTreeParser', () => {
3389
3382
  ],
3390
3383
  visibleDataSetsUrl,
3391
3384
  locusUrl,
3392
- heartbeatIntervalMs,
3393
3385
  };
3394
3386
 
3395
3387
  parser.handleMessage(heartbeat, 'heartbeat');
@@ -3439,7 +3431,6 @@ describe('HashTreeParser', () => {
3439
3431
 
3440
3432
  it('does not set watchdog for data sets without a hash tree', async () => {
3441
3433
  const parser = createHashTreeParser();
3442
- const heartbeatIntervalMs = 5000;
3443
3434
 
3444
3435
  // 'atd-active' is in the initial locus but is not visible (no hash tree)
3445
3436
  // Send heartbeat mentioning a non-visible dataset
@@ -3453,7 +3444,6 @@ describe('HashTreeParser', () => {
3453
3444
  ],
3454
3445
  visibleDataSetsUrl,
3455
3446
  locusUrl,
3456
- heartbeatIntervalMs,
3457
3447
  };
3458
3448
 
3459
3449
  parser.handleMessage(heartbeatMessage, 'heartbeat with non-visible dataset');
@@ -3477,7 +3467,6 @@ describe('HashTreeParser', () => {
3477
3467
  ],
3478
3468
  visibleDataSetsUrl,
3479
3469
  locusUrl,
3480
- heartbeatIntervalMs,
3481
3470
  };
3482
3471
 
3483
3472
  parser.handleMessage(heartbeatMessage, 'initial heartbeat');
@@ -3531,6 +3520,122 @@ describe('HashTreeParser', () => {
3531
3520
  // And the watchdog should still be running
3532
3521
  expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
3533
3522
  });
3523
+
3524
+ it('uses dataset-level heartbeatIntervalMs over top-level value', async () => {
3525
+ const parser = createHashTreeParser();
3526
+ const datasetLevelInterval = 3000;
3527
+ const topLevelInterval = 8000;
3528
+
3529
+ // Send heartbeat with both top-level and dataset-level heartbeatIntervalMs
3530
+ const heartbeatMessage = {
3531
+ dataSets: [
3532
+ {
3533
+ ...createDataSet('main', 16, 1100),
3534
+ root: parser.dataSets.main.hashTree.getRootHash(),
3535
+ heartbeatIntervalMs: datasetLevelInterval,
3536
+ },
3537
+ ],
3538
+ visibleDataSetsUrl,
3539
+ locusUrl,
3540
+ heartbeatIntervalMs: topLevelInterval,
3541
+ };
3542
+
3543
+ parser.handleMessage(heartbeatMessage, 'heartbeat with both levels');
3544
+
3545
+ expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
3546
+
3547
+ // Mock sync responses
3548
+ const mainDataSetUrl = parser.dataSets.main.url;
3549
+ mockGetHashesFromLocusResponse(
3550
+ mainDataSetUrl,
3551
+ new Array(16).fill('00000000000000000000000000000000'),
3552
+ createDataSet('main', 16, 1101)
3553
+ );
3554
+ mockSendSyncRequestResponse(mainDataSetUrl, null);
3555
+
3556
+ // Watchdog should NOT fire at the top-level interval (8000ms)
3557
+ // It should fire at the dataset-level interval (3000ms)
3558
+ await clock.tickAsync(datasetLevelInterval - 1);
3559
+ assert.notCalled(webexRequest);
3560
+
3561
+ await clock.tickAsync(1);
3562
+ // Now at datasetLevelInterval, watchdog should have fired
3563
+ assert.calledWith(
3564
+ webexRequest,
3565
+ sinon.match({
3566
+ method: 'GET',
3567
+ uri: `${mainDataSetUrl}/hashtree`,
3568
+ })
3569
+ );
3570
+ });
3571
+
3572
+ it('falls back to top-level heartbeatIntervalMs when dataset-level is missing', async () => {
3573
+ const parser = createHashTreeParser();
3574
+ const topLevelInterval = 7000;
3575
+
3576
+ // Send heartbeat with top-level heartbeatIntervalMs but no dataset-level
3577
+ const heartbeatMessage = {
3578
+ dataSets: [
3579
+ {
3580
+ ...createDataSet('main', 16, 1100),
3581
+ root: parser.dataSets.main.hashTree.getRootHash(),
3582
+ heartbeatIntervalMs: undefined,
3583
+ },
3584
+ ],
3585
+ visibleDataSetsUrl,
3586
+ locusUrl,
3587
+ heartbeatIntervalMs: topLevelInterval,
3588
+ };
3589
+
3590
+ parser.handleMessage(heartbeatMessage, 'heartbeat with top-level only');
3591
+
3592
+ expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
3593
+
3594
+ // Mock sync responses
3595
+ const mainDataSetUrl = parser.dataSets.main.url;
3596
+ mockGetHashesFromLocusResponse(
3597
+ mainDataSetUrl,
3598
+ new Array(16).fill('00000000000000000000000000000000'),
3599
+ createDataSet('main', 16, 1101)
3600
+ );
3601
+ mockSendSyncRequestResponse(mainDataSetUrl, null);
3602
+
3603
+ // Should fire at the top-level interval
3604
+ await clock.tickAsync(topLevelInterval - 1);
3605
+ assert.notCalled(webexRequest);
3606
+
3607
+ await clock.tickAsync(1);
3608
+ assert.calledWith(
3609
+ webexRequest,
3610
+ sinon.match({
3611
+ method: 'GET',
3612
+ uri: `${mainDataSetUrl}/hashtree`,
3613
+ })
3614
+ );
3615
+ });
3616
+
3617
+ it('does not start watchdog when dataset-level heartbeatIntervalMs is 0 even if top-level is set', async () => {
3618
+ const parser = createHashTreeParser();
3619
+
3620
+ // Send heartbeat with dataset-level 0 and a top-level value
3621
+ const heartbeatMessage = {
3622
+ dataSets: [
3623
+ {
3624
+ ...createDataSet('main', 16, 1100),
3625
+ root: parser.dataSets.main.hashTree.getRootHash(),
3626
+ heartbeatIntervalMs: 0,
3627
+ },
3628
+ ],
3629
+ visibleDataSetsUrl,
3630
+ locusUrl,
3631
+ heartbeatIntervalMs: 5000,
3632
+ };
3633
+
3634
+ parser.handleMessage(heartbeatMessage, 'heartbeat with dataset-level 0');
3635
+
3636
+ // Dataset-level 0 means no watchdog, should NOT fall back to top-level
3637
+ expect(parser.dataSets.main.heartbeatWatchdogTimer).to.be.undefined;
3638
+ });
3534
3639
  });
3535
3640
 
3536
3641
  });
@@ -5196,7 +5301,6 @@ describe('HashTreeParser', () => {
5196
5301
  ],
5197
5302
  visibleDataSetsUrl,
5198
5303
  locusUrl,
5199
- heartbeatIntervalMs: 5000,
5200
5304
  locusStateElements: [
5201
5305
  {
5202
5306
  htMeta: {
@@ -5,7 +5,7 @@ import MockWebex from '@webex/test-helper-mock-webex';
5
5
  import uuid from 'uuid';
6
6
  import sinon from 'sinon';
7
7
  import {DataChannelTokenType} from '@webex/internal-plugin-llm';
8
- import {LLM_PRACTICE_SESSION, SHARE_STATUS} from '@webex/plugin-meetings/src/constants';
8
+ import {LLM_PRACTICE_SESSION, LOCUS_LLM_EVENT, SHARE_STATUS} from '@webex/plugin-meetings/src/constants';
9
9
 
10
10
  describe('plugin-meetings', () => {
11
11
  describe('Webinar', () => {
@@ -227,7 +227,7 @@ describe('plugin-meetings', () => {
227
227
 
228
228
  beforeEach(() => {
229
229
  relayListener = sinon.stub();
230
- webinar._practiceSessionRelayListener = relayListener;
230
+ webinar.llmListeners = {relay: relayListener, locusLLM: null};
231
231
  });
232
232
 
233
233
  it('disconnects the practice session channel and removes the tracked relay listener', async () => {
@@ -238,16 +238,16 @@ describe('plugin-meetings', () => {
238
238
  {code: 3050, reason: 'done (permanent)'},
239
239
  LLM_PRACTICE_SESSION
240
240
  );
241
- assert.calledOnceWithExactly(
241
+ assert.calledWithExactly(
242
242
  webex.internal.llm.off,
243
243
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
244
244
  relayListener
245
245
  );
246
- assert.isNull(webinar._practiceSessionRelayListener);
246
+ assert.isNull(webinar.llmListeners.relay);
247
247
  });
248
248
 
249
249
  it('skips relay listener removal when no listener has been tracked', async () => {
250
- webinar._practiceSessionRelayListener = null;
250
+ webinar.llmListeners.relay = null;
251
251
 
252
252
  await webinar.cleanupPSDataChannel();
253
253
 
@@ -257,6 +257,31 @@ describe('plugin-meetings', () => {
257
257
  assert.equal(relayOffCalls.length, 0);
258
258
  });
259
259
 
260
+ it('disconnects and removes the tracked locusLLM listener', async () => {
261
+ const locusLLMListener = sinon.stub();
262
+ webinar.llmListeners.locusLLM = locusLLMListener;
263
+
264
+ await webinar.cleanupPSDataChannel();
265
+
266
+ assert.calledWithExactly(
267
+ webex.internal.llm.off,
268
+ `${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`,
269
+ locusLLMListener
270
+ );
271
+ assert.isNull(webinar.llmListeners.locusLLM);
272
+ });
273
+
274
+ it('skips locusLLM listener removal when no listener has been tracked', async () => {
275
+ webinar.llmListeners.locusLLM = null;
276
+
277
+ await webinar.cleanupPSDataChannel();
278
+
279
+ const locusLLMOffCalls = webex.internal.llm.off.args.filter(
280
+ ([event]) => event === `${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`
281
+ );
282
+ assert.equal(locusLLMOffCalls.length, 0);
283
+ });
284
+
260
285
  it('does not consult the meeting collection during cleanup', async () => {
261
286
  webex.meetings.getMeetingByType = sinon.stub();
262
287
 
@@ -289,13 +314,16 @@ describe('plugin-meetings', () => {
289
314
  describe('#updatePSDataChannel', () => {
290
315
  let meeting;
291
316
  let processRelayEvent;
317
+ let processLocusLLMEvent;
292
318
 
293
319
  beforeEach(() => {
294
320
  processRelayEvent = sinon.stub();
321
+ processLocusLLMEvent = sinon.stub();
295
322
  meeting = {
296
323
  locusUrl: 'locusUrl',
297
324
  isJoined: sinon.stub().returns(true),
298
325
  processRelayEvent,
326
+ processLocusLLMEvent,
299
327
  locusInfo: {
300
328
  url: 'locus-url',
301
329
  info: {practiceSessionDatachannelUrl: 'dc-url'},
@@ -476,7 +504,7 @@ describe('plugin-meetings', () => {
476
504
  await webinar.updatePSDataChannel();
477
505
 
478
506
  // Stores the exact listener reference for deterministic cleanup
479
- assert.equal(webinar._practiceSessionRelayListener, processRelayEvent);
507
+ assert.equal(webinar.llmListeners.relay, processRelayEvent);
480
508
  assert.calledWith(
481
509
  webex.internal.llm.on,
482
510
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
@@ -486,7 +514,7 @@ describe('plugin-meetings', () => {
486
514
 
487
515
  it('removes a previously tracked relay listener before re-binding on reconnect', async () => {
488
516
  const previousListener = sinon.stub();
489
- webinar._practiceSessionRelayListener = previousListener;
517
+ webinar.llmListeners = {relay: previousListener, locusLLM: null};
490
518
 
491
519
  await webinar.updatePSDataChannel();
492
520
 
@@ -495,7 +523,32 @@ describe('plugin-meetings', () => {
495
523
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
496
524
  previousListener
497
525
  );
498
- assert.equal(webinar._practiceSessionRelayListener, processRelayEvent);
526
+ assert.equal(webinar.llmListeners.relay, processRelayEvent);
527
+ });
528
+
529
+ it('tracks and binds the locusLLM listener after successful connect', async () => {
530
+ await webinar.updatePSDataChannel();
531
+
532
+ assert.equal(webinar.llmListeners.locusLLM, processLocusLLMEvent);
533
+ assert.calledWith(
534
+ webex.internal.llm.on,
535
+ `${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`,
536
+ processLocusLLMEvent
537
+ );
538
+ });
539
+
540
+ it('removes a previously tracked locusLLM listener before re-binding on reconnect', async () => {
541
+ const previousListener = sinon.stub();
542
+ webinar.llmListeners = {relay: null, locusLLM: previousListener};
543
+
544
+ await webinar.updatePSDataChannel();
545
+
546
+ assert.calledWith(
547
+ webex.internal.llm.off,
548
+ `${LOCUS_LLM_EVENT}:${LLM_PRACTICE_SESSION}`,
549
+ previousListener
550
+ );
551
+ assert.equal(webinar.llmListeners.locusLLM, processLocusLLMEvent);
499
552
  });
500
553
 
501
554
  it('subscribes to transcription when caption intent is enabled', async () => {