@webex/plugin-meetings 3.12.0-next.38 → 3.12.0-next.39

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.
@@ -412,6 +412,8 @@ export default class Meeting extends StatelessWebexPlugin {
412
412
  floorGrantPending: boolean;
413
413
  hasJoinedOnce: boolean;
414
414
  hasWebsocketConnected: boolean;
415
+ private mercuryOnlineHandler?;
416
+ private mercuryOfflineHandler?;
415
417
  inMeetingActions: InMeetingActions;
416
418
  isLocalShareLive: boolean;
417
419
  isRoapInProgress: boolean;
@@ -1170,6 +1172,22 @@ export default class Meeting extends StatelessWebexPlugin {
1170
1172
  * @memberof Meeting
1171
1173
  */
1172
1174
  setMercuryListener(): void;
1175
+ /**
1176
+ * Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
1177
+ * by setMercuryListener(). Must be called before Locus /leave to avoid
1178
+ * unnecessary syncs/metrics triggered by events received while leaving
1179
+ * (per Locus team recommendation).
1180
+ *
1181
+ * Mercury is a process-wide singleton shared with other plugins, so we
1182
+ * pass the bound handler refs to .off() to avoid clearing every listener
1183
+ * for ONLINE/OFFLINE on the shared emitter.
1184
+ *
1185
+ * Idempotent: subsequent calls are no-ops because the handler refs are
1186
+ * cleared after detaching.
1187
+ * @private
1188
+ * @returns {void}
1189
+ */
1190
+ private stopListeningForMercuryEvents;
1173
1191
  /**
1174
1192
  * Close the peer connections and remove them from the class.
1175
1193
  * Cleanup any media connection related things.
@@ -1363,6 +1381,33 @@ export default class Meeting extends StatelessWebexPlugin {
1363
1381
  * @returns {void}
1364
1382
  */
1365
1383
  private clearLLMHealthCheckTimer;
1384
+ /**
1385
+ * Removes LLM event listeners and clears the health check timer.
1386
+ * Must be called before Locus /leave to avoid unnecessary syncs triggered
1387
+ * by events received while leaving (per Locus team recommendation).
1388
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
1389
+ * matching listener is registered.
1390
+ * @private
1391
+ * @returns {void}
1392
+ */
1393
+ private stopListeningForLLMEvents;
1394
+ /**
1395
+ * Stops listening on every event bus (LLM, Mercury, voicea/transcription,
1396
+ * annotation) that could otherwise deliver events to this meeting while
1397
+ * Locus is processing /leave or /end. Per the Locus team recommendation,
1398
+ * this must run before the Locus request is dispatched to avoid
1399
+ * unnecessary syncs triggered by in-flight events.
1400
+ *
1401
+ * Voicea (transcription) subscribes to llm 'event:relay.event' internally,
1402
+ * and the annotation plugin subscribes to both mercury and llm, so both
1403
+ * must be torn down alongside the direct LLM/Mercury listeners.
1404
+ *
1405
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
1406
+ * matching listener is registered, and stopTranscription is guarded.
1407
+ * @private
1408
+ * @returns {void}
1409
+ */
1410
+ private stopListeningForMeetingEvents;
1366
1411
  /**
1367
1412
  * Disconnects and cleans up the default LLM session listeners/timers.
1368
1413
  * @param {Object} options
@@ -723,7 +723,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
723
723
  }, _callee1);
724
724
  }))();
725
725
  },
726
- version: "3.12.0-next.38"
726
+ version: "3.12.0-next.39"
727
727
  });
728
728
  var _default = exports.default = Webinar;
729
729
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -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.38"
97
+ "version": "3.12.0-next.39"
98
98
  }
@@ -651,6 +651,8 @@ export default class Meeting extends StatelessWebexPlugin {
651
651
  floorGrantPending: boolean;
652
652
  hasJoinedOnce: boolean;
653
653
  hasWebsocketConnected: boolean;
654
+ private mercuryOnlineHandler?: () => void;
655
+ private mercuryOfflineHandler?: () => void;
654
656
  inMeetingActions: InMeetingActions;
655
657
  isLocalShareLive: boolean;
656
658
  isRoapInProgress: boolean;
@@ -5157,8 +5159,7 @@ export default class Meeting extends StatelessWebexPlugin {
5157
5159
  public setMercuryListener() {
5158
5160
  // Client will have a socket manager and handle reconnecting to mercury, when we reconnect to mercury
5159
5161
  // if the meeting has active peer connections, it should try to reconnect.
5160
- // @ts-ignore
5161
- this.webex.internal.mercury.on(ONLINE, () => {
5162
+ this.mercuryOnlineHandler = () => {
5162
5163
  LoggerProxy.logger.info('Meeting:index#setMercuryListener --> Web socket online');
5163
5164
 
5164
5165
  // Only send restore event when it was disconnected before and for connected later
@@ -5168,15 +5169,47 @@ export default class Meeting extends StatelessWebexPlugin {
5168
5169
  });
5169
5170
  }
5170
5171
  this.hasWebsocketConnected = true;
5171
- });
5172
+ };
5172
5173
 
5173
- // @ts-ignore
5174
- this.webex.internal.mercury.on(OFFLINE, () => {
5174
+ this.mercuryOfflineHandler = () => {
5175
5175
  LoggerProxy.logger.error('Meeting:index#setMercuryListener --> Web socket offline');
5176
5176
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_FAILURE, {
5177
5177
  correlation_id: this.correlationId,
5178
5178
  });
5179
- });
5179
+ };
5180
+
5181
+ // @ts-ignore
5182
+ this.webex.internal.mercury.on(ONLINE, this.mercuryOnlineHandler);
5183
+ // @ts-ignore
5184
+ this.webex.internal.mercury.on(OFFLINE, this.mercuryOfflineHandler);
5185
+ }
5186
+
5187
+ /**
5188
+ * Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
5189
+ * by setMercuryListener(). Must be called before Locus /leave to avoid
5190
+ * unnecessary syncs/metrics triggered by events received while leaving
5191
+ * (per Locus team recommendation).
5192
+ *
5193
+ * Mercury is a process-wide singleton shared with other plugins, so we
5194
+ * pass the bound handler refs to .off() to avoid clearing every listener
5195
+ * for ONLINE/OFFLINE on the shared emitter.
5196
+ *
5197
+ * Idempotent: subsequent calls are no-ops because the handler refs are
5198
+ * cleared after detaching.
5199
+ * @private
5200
+ * @returns {void}
5201
+ */
5202
+ private stopListeningForMercuryEvents() {
5203
+ if (this.mercuryOnlineHandler) {
5204
+ // @ts-ignore
5205
+ this.webex.internal.mercury.off(ONLINE, this.mercuryOnlineHandler);
5206
+ this.mercuryOnlineHandler = undefined;
5207
+ }
5208
+ if (this.mercuryOfflineHandler) {
5209
+ // @ts-ignore
5210
+ this.webex.internal.mercury.off(OFFLINE, this.mercuryOfflineHandler);
5211
+ this.mercuryOfflineHandler = undefined;
5212
+ }
5180
5213
  }
5181
5214
 
5182
5215
  /**
@@ -6328,6 +6361,49 @@ export default class Meeting extends StatelessWebexPlugin {
6328
6361
  }
6329
6362
  }
6330
6363
 
6364
+ /**
6365
+ * Removes LLM event listeners and clears the health check timer.
6366
+ * Must be called before Locus /leave to avoid unnecessary syncs triggered
6367
+ * by events received while leaving (per Locus team recommendation).
6368
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
6369
+ * matching listener is registered.
6370
+ * @private
6371
+ * @returns {void}
6372
+ */
6373
+ private stopListeningForLLMEvents() {
6374
+ // @ts-ignore - fix types
6375
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6376
+ // @ts-ignore - fix types
6377
+ this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
6378
+ this.clearLLMHealthCheckTimer();
6379
+ }
6380
+
6381
+ /**
6382
+ * Stops listening on every event bus (LLM, Mercury, voicea/transcription,
6383
+ * annotation) that could otherwise deliver events to this meeting while
6384
+ * Locus is processing /leave or /end. Per the Locus team recommendation,
6385
+ * this must run before the Locus request is dispatched to avoid
6386
+ * unnecessary syncs triggered by in-flight events.
6387
+ *
6388
+ * Voicea (transcription) subscribes to llm 'event:relay.event' internally,
6389
+ * and the annotation plugin subscribes to both mercury and llm, so both
6390
+ * must be torn down alongside the direct LLM/Mercury listeners.
6391
+ *
6392
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
6393
+ * matching listener is registered, and stopTranscription is guarded.
6394
+ * @private
6395
+ * @returns {void}
6396
+ */
6397
+ private stopListeningForMeetingEvents() {
6398
+ this.stopListeningForLLMEvents();
6399
+ this.stopListeningForMercuryEvents();
6400
+ if (this.transcription) {
6401
+ this.stopTranscription();
6402
+ this.transcription = undefined;
6403
+ }
6404
+ this.annotation.deregisterEvents();
6405
+ }
6406
+
6331
6407
  /**
6332
6408
  * Disconnects and cleans up the default LLM session listeners/timers.
6333
6409
  * @param {Object} options
@@ -6362,12 +6438,7 @@ export default class Meeting extends StatelessWebexPlugin {
6362
6438
  // @ts-ignore - Fix type
6363
6439
  this.webex.internal.llm.off('online', this.handleLLMOnline);
6364
6440
  }
6365
- // @ts-ignore - fix types
6366
- this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6367
- // @ts-ignore - Fix type
6368
- this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
6369
-
6370
- this.clearLLMHealthCheckTimer();
6441
+ this.stopListeningForLLMEvents();
6371
6442
  }
6372
6443
  };
6373
6444
 
@@ -8824,6 +8895,8 @@ export default class Meeting extends StatelessWebexPlugin {
8824
8895
  });
8825
8896
  LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
8826
8897
 
8898
+ this.stopListeningForMeetingEvents();
8899
+
8827
8900
  return MeetingUtil.leaveMeeting(this, options)
8828
8901
  .then(async (leave) => {
8829
8902
  // CA team recommends submitting this *after* locus /leave
@@ -9688,6 +9761,8 @@ export default class Meeting extends StatelessWebexPlugin {
9688
9761
  locus_id: this.locusId,
9689
9762
  });
9690
9763
 
9764
+ this.stopListeningForMeetingEvents();
9765
+
9691
9766
  return MeetingUtil.endMeetingForAll(this)
9692
9767
  .then(async (end) => {
9693
9768
  this.meetingFiniteStateMachine.end();
@@ -9749,11 +9824,11 @@ export default class Meeting extends StatelessWebexPlugin {
9749
9824
  }
9750
9825
  this.queuedMediaUpdates = [];
9751
9826
 
9752
- this.stopTranscription();
9753
- this.transcription = undefined;
9754
-
9755
- this.annotation.deregisterEvents();
9756
-
9827
+ // Listener teardown (transcription, annotation, llm/mercury) runs in
9828
+ // stopListeningForMeetingEvents() before /leave and /end so events
9829
+ // received mid-teardown do not trigger Locus syncs. Calling it here
9830
+ // again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
9831
+ // because stopTranscription() always fires its trigger.
9757
9832
  this.clearDataChannelToken();
9758
9833
  await this.cleanupLLMConneciton({throwOnError: false});
9759
9834
  };
@@ -34,6 +34,7 @@ import {
34
34
  ONLINE,
35
35
  OFFLINE,
36
36
  ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
37
+ LOCUS_LLM_EVENT,
37
38
  } from '@webex/plugin-meetings/src/constants';
38
39
  import {
39
40
  ConnectionState,
@@ -6429,6 +6430,9 @@ describe('plugin-meetings', () => {
6429
6430
 
6430
6431
  meeting.annotation.deregisterEvents = sinon.stub();
6431
6432
  webex.internal.llm.off = sinon.stub();
6433
+ webex.internal.mercury.off = sinon.stub();
6434
+ meeting.mercuryOnlineHandler = sinon.stub();
6435
+ meeting.mercuryOfflineHandler = sinon.stub();
6432
6436
 
6433
6437
  // A meeting needs to be joined to leave
6434
6438
  meeting.meetingState = 'ACTIVE';
@@ -6452,6 +6456,67 @@ describe('plugin-meetings', () => {
6452
6456
  assert.calledOnce(meeting.clearMeetingData);
6453
6457
  });
6454
6458
 
6459
+ it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /leave', async () => {
6460
+ const onlineHandler = meeting.mercuryOnlineHandler;
6461
+ const offlineHandler = meeting.mercuryOfflineHandler;
6462
+
6463
+ await meeting.leave();
6464
+
6465
+ // All llm/mercury consumers (direct listeners, voicea transcription,
6466
+ // annotation) must be detached before the /leave request so that
6467
+ // in-flight events do not trigger unnecessary Locus syncs
6468
+ // (per Locus team recommendation).
6469
+ assert.callOrder(
6470
+ webex.internal.llm.off,
6471
+ webex.internal.mercury.off,
6472
+ meeting.stopTranscription,
6473
+ meeting.annotation.deregisterEvents,
6474
+ meeting.meetingRequest.leaveMeeting
6475
+ );
6476
+ assert.calledWithExactly(
6477
+ webex.internal.llm.off,
6478
+ 'event:relay.event',
6479
+ meeting.processRelayEvent
6480
+ );
6481
+ assert.calledWithExactly(
6482
+ webex.internal.llm.off,
6483
+ LOCUS_LLM_EVENT,
6484
+ meeting.processLocusLLMEvent
6485
+ );
6486
+ assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
6487
+ assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
6488
+ assert.isUndefined(meeting.mercuryOnlineHandler);
6489
+ assert.isUndefined(meeting.mercuryOfflineHandler);
6490
+ assert.calledOnceWithExactly(meeting.stopTranscription);
6491
+ assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
6492
+ assert.isUndefined(meeting.transcription);
6493
+ });
6494
+
6495
+ it('tears down llm/mercury/transcription/annotation even when /leave rejects', async () => {
6496
+ const onlineHandler = meeting.mercuryOnlineHandler;
6497
+ const offlineHandler = meeting.mercuryOfflineHandler;
6498
+ meeting.meetingRequest.leaveMeeting = sinon
6499
+ .stub()
6500
+ .returns(Promise.reject(new Error('leave failed')));
6501
+
6502
+ await meeting.leave().catch(() => {});
6503
+
6504
+ assert.calledWithExactly(
6505
+ webex.internal.llm.off,
6506
+ 'event:relay.event',
6507
+ meeting.processRelayEvent
6508
+ );
6509
+ assert.calledWithExactly(
6510
+ webex.internal.llm.off,
6511
+ LOCUS_LLM_EVENT,
6512
+ meeting.processLocusLLMEvent
6513
+ );
6514
+ assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
6515
+ assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
6516
+ assert.calledOnceWithExactly(meeting.stopTranscription);
6517
+ assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
6518
+ });
6519
+
6455
6520
  it('should reset call diagnostic latencies correctly', async () => {
6456
6521
  const leave = meeting.leave();
6457
6522
 
@@ -8459,6 +8524,9 @@ describe('plugin-meetings', () => {
8459
8524
 
8460
8525
  meeting.annotation.deregisterEvents = sinon.stub();
8461
8526
  webex.internal.llm.off = sinon.stub();
8527
+ webex.internal.mercury.off = sinon.stub();
8528
+ meeting.mercuryOnlineHandler = sinon.stub();
8529
+ meeting.mercuryOfflineHandler = sinon.stub();
8462
8530
 
8463
8531
  // A meeting needs to be joined to end
8464
8532
  meeting.meetingState = 'ACTIVE';
@@ -8481,6 +8549,66 @@ describe('plugin-meetings', () => {
8481
8549
  assert.calledOnce(meeting?.unsetPeerConnections);
8482
8550
  assert.calledOnce(meeting?.clearMeetingData);
8483
8551
  });
8552
+
8553
+ it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /end', async () => {
8554
+ const onlineHandler = meeting.mercuryOnlineHandler;
8555
+ const offlineHandler = meeting.mercuryOfflineHandler;
8556
+
8557
+ await meeting.endMeetingForAll();
8558
+
8559
+ // All llm/mercury consumers (direct listeners, voicea transcription,
8560
+ // annotation) must be detached before the /end request so that
8561
+ // in-flight events do not trigger unnecessary Locus syncs
8562
+ // (per Locus team recommendation).
8563
+ assert.callOrder(
8564
+ webex.internal.llm.off,
8565
+ webex.internal.mercury.off,
8566
+ meeting.stopTranscription,
8567
+ meeting.annotation.deregisterEvents,
8568
+ meeting.meetingRequest.endMeetingForAll
8569
+ );
8570
+ assert.calledWithExactly(
8571
+ webex.internal.llm.off,
8572
+ 'event:relay.event',
8573
+ meeting.processRelayEvent
8574
+ );
8575
+ assert.calledWithExactly(
8576
+ webex.internal.llm.off,
8577
+ LOCUS_LLM_EVENT,
8578
+ meeting.processLocusLLMEvent
8579
+ );
8580
+ assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
8581
+ assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
8582
+ assert.isUndefined(meeting.mercuryOnlineHandler);
8583
+ assert.isUndefined(meeting.mercuryOfflineHandler);
8584
+ assert.calledOnceWithExactly(meeting.stopTranscription);
8585
+ assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
8586
+ });
8587
+
8588
+ it('tears down llm/mercury/transcription/annotation even when /end rejects', async () => {
8589
+ const onlineHandler = meeting.mercuryOnlineHandler;
8590
+ const offlineHandler = meeting.mercuryOfflineHandler;
8591
+ meeting.meetingRequest.endMeetingForAll = sinon
8592
+ .stub()
8593
+ .returns(Promise.reject(new Error('end failed')));
8594
+
8595
+ await meeting.endMeetingForAll().catch(() => {});
8596
+
8597
+ assert.calledWithExactly(
8598
+ webex.internal.llm.off,
8599
+ 'event:relay.event',
8600
+ meeting.processRelayEvent
8601
+ );
8602
+ assert.calledWithExactly(
8603
+ webex.internal.llm.off,
8604
+ LOCUS_LLM_EVENT,
8605
+ meeting.processLocusLLMEvent
8606
+ );
8607
+ assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
8608
+ assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
8609
+ assert.calledOnceWithExactly(meeting.stopTranscription);
8610
+ assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
8611
+ });
8484
8612
  });
8485
8613
 
8486
8614
  describe('#moveTo', () => {
@@ -13311,10 +13439,13 @@ describe('plugin-meetings', () => {
13311
13439
  meeting.processLocusLLMEvent
13312
13440
  );
13313
13441
  assert.calledOnce(meeting.clearLLMHealthCheckTimer);
13314
- assert.calledOnce(meeting.stopTranscription);
13315
- assert.isUndefined(meeting.transcription);
13316
13442
  assert.calledOnce(meeting.clearDataChannelToken);
13317
- assert.calledOnce(meeting.annotation.deregisterEvents);
13443
+ // stopTranscription and annotation.deregisterEvents are not
13444
+ // called here: they run in stopListeningForMeetingEvents()
13445
+ // before /leave to avoid double-emitting
13446
+ // MEETING_STOPPED_RECEIVING_TRANSCRIPTION.
13447
+ assert.notCalled(meeting.stopTranscription);
13448
+ assert.notCalled(meeting.annotation.deregisterEvents);
13318
13449
  });
13319
13450
  it('continues cleanup when disconnectLLM fails during meeting data cleanup', async () => {
13320
13451
  webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
@@ -13333,19 +13464,9 @@ describe('plugin-meetings', () => {
13333
13464
  meeting.processLocusLLMEvent
13334
13465
  );
13335
13466
  assert.calledOnce(meeting.clearLLMHealthCheckTimer);
13336
- assert.calledOnce(meeting.stopTranscription);
13337
- assert.isUndefined(meeting.transcription);
13338
- assert.calledOnce(meeting.clearDataChannelToken);
13339
- assert.calledOnce(meeting.annotation.deregisterEvents);
13340
- });
13341
- it('always calls stopTranscription even when transcription is undefined', async () => {
13342
- meeting.transcription = undefined;
13343
-
13344
- await meeting.clearMeetingData();
13345
-
13346
- assert.calledOnce(meeting.stopTranscription);
13347
- assert.isUndefined(meeting.transcription);
13348
13467
  assert.calledOnce(meeting.clearDataChannelToken);
13468
+ assert.notCalled(meeting.stopTranscription);
13469
+ assert.notCalled(meeting.annotation.deregisterEvents);
13349
13470
  });
13350
13471
  });
13351
13472
  });