@webex/plugin-meetings 3.12.0-next.4 → 3.12.0-next.40

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 (90) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +6 -2
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +1 -1
  7. package/dist/constants.js +1 -1
  8. package/dist/constants.js.map +1 -1
  9. package/dist/controls-options-manager/constants.js +11 -1
  10. package/dist/controls-options-manager/constants.js.map +1 -1
  11. package/dist/controls-options-manager/index.js +23 -21
  12. package/dist/controls-options-manager/index.js.map +1 -1
  13. package/dist/controls-options-manager/util.js +91 -0
  14. package/dist/controls-options-manager/util.js.map +1 -1
  15. package/dist/hashTree/constants.js +10 -1
  16. package/dist/hashTree/constants.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +554 -350
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/utils.js +22 -0
  20. package/dist/hashTree/utils.js.map +1 -1
  21. package/dist/interceptors/locusRetry.js +23 -8
  22. package/dist/interceptors/locusRetry.js.map +1 -1
  23. package/dist/interpretation/index.js +1 -1
  24. package/dist/interpretation/siLanguage.js +1 -1
  25. package/dist/locus-info/index.js +274 -85
  26. package/dist/locus-info/index.js.map +1 -1
  27. package/dist/locus-info/types.js +16 -0
  28. package/dist/locus-info/types.js.map +1 -1
  29. package/dist/meeting/index.js +710 -499
  30. package/dist/meeting/index.js.map +1 -1
  31. package/dist/meeting/util.js +1 -0
  32. package/dist/meeting/util.js.map +1 -1
  33. package/dist/meetings/index.js +174 -77
  34. package/dist/meetings/index.js.map +1 -1
  35. package/dist/meetings/util.js +49 -5
  36. package/dist/meetings/util.js.map +1 -1
  37. package/dist/member/index.js +10 -0
  38. package/dist/member/index.js.map +1 -1
  39. package/dist/member/types.js.map +1 -1
  40. package/dist/member/util.js +3 -0
  41. package/dist/member/util.js.map +1 -1
  42. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  43. package/dist/types/hashTree/constants.d.ts +1 -0
  44. package/dist/types/hashTree/hashTreeParser.d.ts +53 -15
  45. package/dist/types/hashTree/utils.d.ts +11 -0
  46. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  47. package/dist/types/locus-info/index.d.ts +46 -6
  48. package/dist/types/locus-info/types.d.ts +17 -1
  49. package/dist/types/meeting/index.d.ts +64 -1
  50. package/dist/types/member/index.d.ts +1 -0
  51. package/dist/types/member/types.d.ts +1 -0
  52. package/dist/types/member/util.d.ts +1 -0
  53. package/dist/webinar/index.js +301 -226
  54. package/dist/webinar/index.js.map +1 -1
  55. package/package.json +22 -22
  56. package/src/aiEnableRequest/index.ts +16 -0
  57. package/src/breakouts/breakout.ts +2 -1
  58. package/src/constants.ts +1 -1
  59. package/src/controls-options-manager/constants.ts +14 -1
  60. package/src/controls-options-manager/index.ts +26 -19
  61. package/src/controls-options-manager/util.ts +81 -1
  62. package/src/hashTree/constants.ts +9 -0
  63. package/src/hashTree/hashTreeParser.ts +278 -160
  64. package/src/hashTree/utils.ts +17 -0
  65. package/src/interceptors/locusRetry.ts +25 -4
  66. package/src/locus-info/index.ts +274 -93
  67. package/src/locus-info/types.ts +19 -1
  68. package/src/meeting/index.ts +206 -22
  69. package/src/meeting/util.ts +1 -0
  70. package/src/meetings/index.ts +77 -43
  71. package/src/meetings/util.ts +56 -1
  72. package/src/member/index.ts +10 -0
  73. package/src/member/types.ts +1 -0
  74. package/src/member/util.ts +3 -0
  75. package/src/webinar/index.ts +75 -1
  76. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  77. package/test/unit/spec/breakouts/breakout.ts +7 -3
  78. package/test/unit/spec/controls-options-manager/index.js +114 -6
  79. package/test/unit/spec/controls-options-manager/util.js +165 -0
  80. package/test/unit/spec/hashTree/hashTreeParser.ts +996 -51
  81. package/test/unit/spec/hashTree/utils.ts +88 -1
  82. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  83. package/test/unit/spec/locus-info/index.js +397 -81
  84. package/test/unit/spec/meeting/index.js +271 -44
  85. package/test/unit/spec/meeting/utils.js +4 -0
  86. package/test/unit/spec/meetings/index.js +195 -13
  87. package/test/unit/spec/meetings/utils.js +137 -0
  88. package/test/unit/spec/member/index.js +7 -0
  89. package/test/unit/spec/member/util.js +24 -0
  90. package/test/unit/spec/webinar/index.ts +60 -0
@@ -1,15 +1,33 @@
1
+ import {Enum} from '../constants';
1
2
  import {HtMeta} from '../hashTree/types';
2
3
 
4
+ export const EndMeetingReason = {
5
+ maxMeetingDuration: 'MAX_MEETING_DURATION',
6
+ allParticipantsLeft: 'ALL_PARTICIPANTS_LEFT',
7
+ sipHostLeft: 'SIP_HOST_LEFT',
8
+ noHost: 'NO_HOST',
9
+ waitingForMpsEndMeetingTimeout: 'WAITING_FOR_MPS_END_MEETING_TIMEOUT',
10
+ fraudDetection: 'FRAUD_DETECTION',
11
+ meetingEndedByHost: 'MEETING_ENDED_BY_HOST',
12
+ meetingUpdated: 'MEETING_UPDATED', // Locus code has comment about EndMeetingIfPossible reason for this one
13
+ meetingCancelled: 'MEETING_CANCELLED', // Locus code has comment about EndMeetingIfPossible reason for this one
14
+ autoEndWithSingleParticipant: 'AUTO_END_WITH_SINGLE_PARTICIPANT',
15
+ breakoutEnded: 'BREAKOUT_ENDED', // indicates that only a breakout session ended, not the whole meeting
16
+ } as const;
17
+
18
+ export type EndMeetingReason = Enum<typeof EndMeetingReason>;
19
+
3
20
  export type LocusFullState = {
4
21
  active: boolean;
5
22
  count: number;
6
23
  lastActive: string;
7
24
  locked: boolean;
8
25
  sessionId: string;
9
- seessionIds: string[];
26
+ sessionIds: string[];
10
27
  startTime: number;
11
28
  state: string;
12
29
  type: string;
30
+ endMeetingReason?: EndMeetingReason;
13
31
  };
14
32
 
15
33
  export type Links = {
@@ -612,7 +612,7 @@ export default class Meeting extends StatelessWebexPlugin {
612
612
  webinar: any;
613
613
  conversationUrl: string;
614
614
  callStateForMetrics: CallStateForMetrics;
615
- destination: string;
615
+ destination: string | LocusDTO;
616
616
  destinationType: DESTINATION_TYPE;
617
617
  deviceUrl: string;
618
618
  hostId: string;
@@ -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;
@@ -935,7 +937,7 @@ export default class Meeting extends StatelessWebexPlugin {
935
937
  this.simultaneousInterpretation = new SimultaneousInterpretation({}, {parent: this.webex});
936
938
 
937
939
  // @ts-ignore
938
- this.aiEnableRequest = new AIEnableRequest({}, {parent: this.webex});
940
+ this.aiEnableRequest = new AIEnableRequest({locusUrl: this.locusUrl}, {parent: this.webex});
939
941
 
940
942
  /**
941
943
  * @instance
@@ -3459,6 +3461,7 @@ export default class Meeting extends StatelessWebexPlugin {
3459
3461
  this.breakouts.locusUrlUpdate(url);
3460
3462
  this.simultaneousInterpretation.locusUrlUpdate(url);
3461
3463
  this.annotation.locusUrlUpdate(url);
3464
+ this.aiEnableRequest.locusUrlUpdate(url);
3462
3465
  this.locusUrl = url;
3463
3466
  this.locusId = this.locusUrl?.split('/').pop();
3464
3467
  this.recordingController.setLocusUrl(this.locusUrl);
@@ -3734,7 +3737,7 @@ export default class Meeting extends StatelessWebexPlugin {
3734
3737
  });
3735
3738
  this.updateLLMConnection();
3736
3739
  });
3737
- this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
3740
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
3738
3741
  this.stopKeepAlive();
3739
3742
 
3740
3743
  if (payload) {
@@ -3760,6 +3763,15 @@ export default class Meeting extends StatelessWebexPlugin {
3760
3763
  });
3761
3764
  }
3762
3765
  this.rtcMetrics?.sendNextMetrics();
3766
+
3767
+ this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
3768
+ LoggerProxy.logger.warn(
3769
+ `Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
3770
+ error?.message || String(error)
3771
+ }`
3772
+ );
3773
+ });
3774
+
3763
3775
  this.updateLLMConnection();
3764
3776
  });
3765
3777
 
@@ -4650,6 +4662,33 @@ export default class Meeting extends StatelessWebexPlugin {
4650
4662
  this.sipUri = sipUri;
4651
4663
  }
4652
4664
 
4665
+ /**
4666
+ * After initial locus setup, refreshes destination with synced locus data and optionally
4667
+ * performs deferred meeting info fetch when initial locus was incomplete.
4668
+ * @param {LocusDTO} locus
4669
+ * @returns {void}
4670
+ */
4671
+ public async finalizeMeetingAfterInitialLocusSetup(locus: LocusDTO): Promise<void> {
4672
+ if (locus && this?.destinationType === DESTINATION_TYPE.LOCUS_ID) {
4673
+ // destination is initialized from the initial locus snapshot in constructor,
4674
+ // so refresh it after locus sync to avoid stale partial hash-tree data.
4675
+ this.destination = locus;
4676
+ }
4677
+ if (
4678
+ (!this.meetingInfo || isEmpty(this.meetingInfo)) &&
4679
+ (this.destination as LocusDTO)?.info &&
4680
+ !this.fetchMeetingInfoTimeoutId
4681
+ ) {
4682
+ try {
4683
+ await this.fetchMeetingInfo({});
4684
+ } catch (error: any) {
4685
+ LoggerProxy.logger.info(
4686
+ `Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: ${error.message}`
4687
+ );
4688
+ }
4689
+ }
4690
+ }
4691
+
4653
4692
  /**
4654
4693
  * Set the locus info the class instance. Should be called with the parsed locus
4655
4694
  * we got in the join response.
@@ -5121,8 +5160,7 @@ export default class Meeting extends StatelessWebexPlugin {
5121
5160
  public setMercuryListener() {
5122
5161
  // Client will have a socket manager and handle reconnecting to mercury, when we reconnect to mercury
5123
5162
  // if the meeting has active peer connections, it should try to reconnect.
5124
- // @ts-ignore
5125
- this.webex.internal.mercury.on(ONLINE, () => {
5163
+ this.mercuryOnlineHandler = () => {
5126
5164
  LoggerProxy.logger.info('Meeting:index#setMercuryListener --> Web socket online');
5127
5165
 
5128
5166
  // Only send restore event when it was disconnected before and for connected later
@@ -5132,15 +5170,47 @@ export default class Meeting extends StatelessWebexPlugin {
5132
5170
  });
5133
5171
  }
5134
5172
  this.hasWebsocketConnected = true;
5135
- });
5173
+ };
5136
5174
 
5137
- // @ts-ignore
5138
- this.webex.internal.mercury.on(OFFLINE, () => {
5175
+ this.mercuryOfflineHandler = () => {
5139
5176
  LoggerProxy.logger.error('Meeting:index#setMercuryListener --> Web socket offline');
5140
5177
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_FAILURE, {
5141
5178
  correlation_id: this.correlationId,
5142
5179
  });
5143
- });
5180
+ };
5181
+
5182
+ // @ts-ignore
5183
+ this.webex.internal.mercury.on(ONLINE, this.mercuryOnlineHandler);
5184
+ // @ts-ignore
5185
+ this.webex.internal.mercury.on(OFFLINE, this.mercuryOfflineHandler);
5186
+ }
5187
+
5188
+ /**
5189
+ * Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
5190
+ * by setMercuryListener(). Must be called before Locus /leave to avoid
5191
+ * unnecessary syncs/metrics triggered by events received while leaving
5192
+ * (per Locus team recommendation).
5193
+ *
5194
+ * Mercury is a process-wide singleton shared with other plugins, so we
5195
+ * pass the bound handler refs to .off() to avoid clearing every listener
5196
+ * for ONLINE/OFFLINE on the shared emitter.
5197
+ *
5198
+ * Idempotent: subsequent calls are no-ops because the handler refs are
5199
+ * cleared after detaching.
5200
+ * @private
5201
+ * @returns {void}
5202
+ */
5203
+ private stopListeningForMercuryEvents() {
5204
+ if (this.mercuryOnlineHandler) {
5205
+ // @ts-ignore
5206
+ this.webex.internal.mercury.off(ONLINE, this.mercuryOnlineHandler);
5207
+ this.mercuryOnlineHandler = undefined;
5208
+ }
5209
+ if (this.mercuryOfflineHandler) {
5210
+ // @ts-ignore
5211
+ this.webex.internal.mercury.off(OFFLINE, this.mercuryOfflineHandler);
5212
+ this.mercuryOfflineHandler = undefined;
5213
+ }
5144
5214
  }
5145
5215
 
5146
5216
  /**
@@ -5960,6 +6030,30 @@ export default class Meeting extends StatelessWebexPlugin {
5960
6030
  );
5961
6031
  }
5962
6032
 
6033
+ /**
6034
+ * Restores LLM subchannel subscriptions after reconnect when captions are active.
6035
+ * @returns {void}
6036
+ */
6037
+ private restoreLLMSubscriptionsIfNeeded(): void {
6038
+ try {
6039
+ // @ts-ignore
6040
+ const isCaptionBoxOn = this.webex.internal.voicea?.getIsCaptionBoxOn?.();
6041
+
6042
+ if (!isCaptionBoxOn) {
6043
+ return;
6044
+ }
6045
+
6046
+ // @ts-ignore
6047
+ this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
6048
+ } catch (error) {
6049
+ const msg = error?.message || String(error);
6050
+
6051
+ LoggerProxy.logger.warn(
6052
+ `Meeting:index#restoreLLMSubscriptionsIfNeeded --> failed to restore subscriptions after LLM online: ${msg}`
6053
+ );
6054
+ }
6055
+ }
6056
+
5963
6057
  /**
5964
6058
  * This is a callback for the LLM event that is triggered when it comes online
5965
6059
  * This method in turn will trigger an event to the developers that the LLM is connected
@@ -5968,8 +6062,8 @@ export default class Meeting extends StatelessWebexPlugin {
5968
6062
  * @returns {null}
5969
6063
  */
5970
6064
  private handleLLMOnline = (): void => {
5971
- // @ts-ignore
5972
- this.webex.internal.llm.off('online', this.handleLLMOnline);
6065
+ this.restoreLLMSubscriptionsIfNeeded();
6066
+
5973
6067
  Trigger.trigger(
5974
6068
  this,
5975
6069
  {
@@ -6200,6 +6294,8 @@ export default class Meeting extends StatelessWebexPlugin {
6200
6294
  this.saveDataChannelToken(join);
6201
6295
  // @ts-ignore - config coming from registerPlugin
6202
6296
  if (this.config.enableAutomaticLLM) {
6297
+ // @ts-ignore
6298
+ this.webex.internal.llm.off('online', this.handleLLMOnline);
6203
6299
  // @ts-ignore
6204
6300
  this.webex.internal.llm.on('online', this.handleLLMOnline);
6205
6301
  this.updateLLMConnection()
@@ -6266,6 +6362,49 @@ export default class Meeting extends StatelessWebexPlugin {
6266
6362
  }
6267
6363
  }
6268
6364
 
6365
+ /**
6366
+ * Removes LLM event listeners and clears the health check timer.
6367
+ * Must be called before Locus /leave to avoid unnecessary syncs triggered
6368
+ * by events received while leaving (per Locus team recommendation).
6369
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
6370
+ * matching listener is registered.
6371
+ * @private
6372
+ * @returns {void}
6373
+ */
6374
+ private stopListeningForLLMEvents() {
6375
+ // @ts-ignore - fix types
6376
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6377
+ // @ts-ignore - fix types
6378
+ this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
6379
+ this.clearLLMHealthCheckTimer();
6380
+ }
6381
+
6382
+ /**
6383
+ * Stops listening on every event bus (LLM, Mercury, voicea/transcription,
6384
+ * annotation) that could otherwise deliver events to this meeting while
6385
+ * Locus is processing /leave or /end. Per the Locus team recommendation,
6386
+ * this must run before the Locus request is dispatched to avoid
6387
+ * unnecessary syncs triggered by in-flight events.
6388
+ *
6389
+ * Voicea (transcription) subscribes to llm 'event:relay.event' internally,
6390
+ * and the annotation plugin subscribes to both mercury and llm, so both
6391
+ * must be torn down alongside the direct LLM/Mercury listeners.
6392
+ *
6393
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
6394
+ * matching listener is registered, and stopTranscription is guarded.
6395
+ * @private
6396
+ * @returns {void}
6397
+ */
6398
+ private stopListeningForMeetingEvents() {
6399
+ this.stopListeningForLLMEvents();
6400
+ this.stopListeningForMercuryEvents();
6401
+ if (this.transcription) {
6402
+ this.stopTranscription();
6403
+ this.transcription = undefined;
6404
+ }
6405
+ this.annotation.deregisterEvents();
6406
+ }
6407
+
6269
6408
  /**
6270
6409
  * Disconnects and cleans up the default LLM session listeners/timers.
6271
6410
  * @param {Object} options
@@ -6300,12 +6439,7 @@ export default class Meeting extends StatelessWebexPlugin {
6300
6439
  // @ts-ignore - Fix type
6301
6440
  this.webex.internal.llm.off('online', this.handleLLMOnline);
6302
6441
  }
6303
- // @ts-ignore - fix types
6304
- this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6305
- // @ts-ignore - Fix type
6306
- this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
6307
-
6308
- this.clearLLMHealthCheckTimer();
6442
+ this.stopListeningForLLMEvents();
6309
6443
  }
6310
6444
  };
6311
6445
 
@@ -6343,6 +6477,52 @@ export default class Meeting extends StatelessWebexPlugin {
6343
6477
  }
6344
6478
  }
6345
6479
 
6480
+ /**
6481
+ * Ensures default-session data channel token exists after lobby admission.
6482
+ * Some lobby users do not receive a token until they are admitted.
6483
+ * @returns {Promise<boolean>} true when a new token is fetched and cached
6484
+ */
6485
+ private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
6486
+ try {
6487
+ // @ts-ignore
6488
+ const datachannelToken = this.webex.internal.llm.getDatachannelToken();
6489
+ // @ts-ignore
6490
+ const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
6491
+
6492
+ if (!isDataChannelTokenEnabled || datachannelToken) {
6493
+ return false;
6494
+ }
6495
+
6496
+ const response = await this.meetingRequest.fetchDatachannelToken({
6497
+ locusUrl: this.locusUrl,
6498
+ requestingParticipantId: this.members.selfId,
6499
+ isPracticeSession: false,
6500
+ });
6501
+ const fetchedDatachannelToken = response?.body?.datachannelToken;
6502
+
6503
+ if (!fetchedDatachannelToken) {
6504
+ return false;
6505
+ }
6506
+
6507
+ // @ts-ignore
6508
+ this.webex.internal.llm.setDatachannelToken(
6509
+ fetchedDatachannelToken,
6510
+ DataChannelTokenType.Default
6511
+ );
6512
+
6513
+ return true;
6514
+ } catch (error) {
6515
+ const msg = error?.message || String(error);
6516
+
6517
+ LoggerProxy.logger.warn(
6518
+ `Meeting:index#ensureDefaultDatachannelTokenAfterAdmit --> failed to proactively fetch default data channel token after admit: ${msg}`,
6519
+ {statusCode: error?.statusCode}
6520
+ );
6521
+
6522
+ return false;
6523
+ }
6524
+ }
6525
+
6346
6526
  /**
6347
6527
  * Connects to low latency mercury and reconnects if the address has changed
6348
6528
  * It will also disconnect if called when the meeting has ended
@@ -8716,6 +8896,8 @@ export default class Meeting extends StatelessWebexPlugin {
8716
8896
  });
8717
8897
  LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
8718
8898
 
8899
+ this.stopListeningForMeetingEvents();
8900
+
8719
8901
  return MeetingUtil.leaveMeeting(this, options)
8720
8902
  .then(async (leave) => {
8721
8903
  // CA team recommends submitting this *after* locus /leave
@@ -9580,6 +9762,8 @@ export default class Meeting extends StatelessWebexPlugin {
9580
9762
  locus_id: this.locusId,
9581
9763
  });
9582
9764
 
9765
+ this.stopListeningForMeetingEvents();
9766
+
9583
9767
  return MeetingUtil.endMeetingForAll(this)
9584
9768
  .then(async (end) => {
9585
9769
  this.meetingFiniteStateMachine.end();
@@ -9641,11 +9825,11 @@ export default class Meeting extends StatelessWebexPlugin {
9641
9825
  }
9642
9826
  this.queuedMediaUpdates = [];
9643
9827
 
9644
- this.stopTranscription();
9645
- this.transcription = undefined;
9646
-
9647
- this.annotation.deregisterEvents();
9648
-
9828
+ // Listener teardown (transcription, annotation, llm/mercury) runs in
9829
+ // stopListeningForMeetingEvents() before /leave and /end so events
9830
+ // received mid-teardown do not trigger Locus syncs. Calling it here
9831
+ // again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
9832
+ // because stopTranscription() always fires its trigger.
9649
9833
  this.clearDataChannelToken();
9650
9834
  await this.cleanupLLMConneciton({throwOnError: false});
9651
9835
  };
@@ -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(
@@ -69,7 +69,9 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
69
69
  import {HashTreeMessage} from '../hashTree/hashTreeParser';
70
70
  import {HashTreeObject} from '../hashTree/types';
71
71
  import {isSelf} from '../hashTree/utils';
72
+
72
73
  import {createLocusFromHashTreeMessage, findMeetingForHashTreeMessage} from '../locus-info';
74
+ import {LocusDTO} from '../locus-info/types';
73
75
 
74
76
  let mediaLogger;
75
77
 
@@ -313,7 +315,7 @@ export default class Meetings extends WebexPlugin {
313
315
  const breakoutLocus = this.meetingCollection.getActiveBreakoutLocus(breakoutUrl);
314
316
 
315
317
  const isSelfJoined = newLocus?.self?.state === _JOINED_;
316
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
318
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
317
319
  // @ts-ignore
318
320
  const deviceFromNewLocus = MeetingsUtil.getThisDevice(newLocus, this.webex.internal.device.url);
319
321
  const isResourceMovedOnThisDevice =
@@ -390,7 +392,7 @@ export default class Meetings extends WebexPlugin {
390
392
  private isNeedHandleLocusDTO(meeting: any, newLocus: any) {
391
393
  if (newLocus) {
392
394
  const isNewLocusAsBreakout = MeetingsUtil.isBreakoutLocusDTO(newLocus);
393
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
395
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
394
396
  const isSelfMovedToLobby =
395
397
  newLocus?.self?.devices[0]?.intent?.reason === _ON_HOLD_LOBBY_ &&
396
398
  newLocus?.self?.devices[0]?.intent?.type === _WAIT_;
@@ -435,14 +437,11 @@ export default class Meetings extends WebexPlugin {
435
437
  if (existingMeeting) {
436
438
  return existingMeeting;
437
439
  }
438
-
439
440
  if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
440
441
  // need to check if maybe this event indicates a move to/from breakout
441
442
  const meetingForHashTreeMessage = findMeetingForHashTreeMessage(
442
- data.stateElementsMessage,
443
- this.meetingCollection,
444
- // @ts-ignore
445
- this.webex.internal.device.url
443
+ data?.stateElementsMessage,
444
+ this.meetingCollection
446
445
  );
447
446
 
448
447
  if (meetingForHashTreeMessage) {
@@ -492,7 +491,6 @@ export default class Meetings extends WebexPlugin {
492
491
  */
493
492
  private handleLocusEvent(data: LocusEvent, useRandomDelayForInfo = false) {
494
493
  let meeting = this.getCorrespondingMeetingByLocus(data);
495
-
496
494
  // @ts-ignore
497
495
  if (this.config.experimental.storeLocusHashTreeEventsForDebugging) {
498
496
  storeEventForDebugging('mercury', data);
@@ -586,17 +584,21 @@ export default class Meetings extends WebexPlugin {
586
584
  this.create(data.locus, DESTINATION_TYPE.LOCUS_ID, useRandomDelayForInfo)
587
585
  .then(async (newMeeting) => {
588
586
  meeting = newMeeting;
589
-
590
587
  try {
591
588
  // It's a new meeting so initialize the locus data
592
- await meeting.locusInfo.initialSetup({
593
- trigger:
594
- data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS
595
- ? 'get-loci-response'
596
- : 'locus-message',
597
- locus: data.locus,
598
- hashTreeMessage: data.stateElementsMessage,
599
- });
589
+ await meeting.locusInfo.initialSetup(
590
+ {
591
+ trigger:
592
+ data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS
593
+ ? 'get-loci-response'
594
+ : 'locus-message',
595
+ locus: data.locus,
596
+ hashTreeMessage: data.stateElementsMessage,
597
+ },
598
+ (locus: LocusDTO) => {
599
+ meeting.finalizeMeetingAfterInitialLocusSetup(locus);
600
+ }
601
+ );
600
602
  } catch (error) {
601
603
  LoggerProxy.logger.warn(
602
604
  `Meetings:index#handleLocusEvent --> Error initializing locus data: ${error.message}`
@@ -1765,6 +1767,7 @@ export default class Meetings extends WebexPlugin {
1765
1767
  extraParams: infoExtraParams,
1766
1768
  sendCAevents: !!callStateForMetrics?.correlationId, // if client sends correlation id as argument of public create(), then it means that this meeting creation is part of a pre-join intent from user
1767
1769
  };
1770
+ const shouldDeferMeetingInfoFetch = type === DESTINATION_TYPE.LOCUS_ID && !destination?.info;
1768
1771
 
1769
1772
  if (meetingInfo) {
1770
1773
  meeting.injectMeetingInfo(meetingInfo, meetingInfoOptions, meetingLookupUrl);
@@ -1776,8 +1779,12 @@ export default class Meetings extends WebexPlugin {
1776
1779
  waitingTime
1777
1780
  );
1778
1781
  meeting.parseMeetingInfo(undefined, destination);
1779
- } else {
1782
+ } else if (!shouldDeferMeetingInfoFetch) {
1780
1783
  await meeting.fetchMeetingInfo(meetingInfoOptions);
1784
+ } else {
1785
+ LoggerProxy.logger.info(
1786
+ 'Meetings:index#createMeeting --> defer fetchMeetingInfo for incomplete locus, will do it after locus initialSetup'
1787
+ );
1781
1788
  }
1782
1789
  }
1783
1790
  } catch (err) {
@@ -1811,7 +1818,11 @@ export default class Meetings extends WebexPlugin {
1811
1818
  // For type LOCUS_ID we need to parse the locus object to get the information
1812
1819
  // about the caller and callee
1813
1820
  // Meeting Added event will be created in `handleLocusEvent`
1814
- if (type !== DESTINATION_TYPE.LOCUS_ID) {
1821
+ // Only emit MEETING_ADDED if the meeting still exists in the collection.
1822
+ // If fetchMeetingInfo failed and the meeting was destroyed in the catch block,
1823
+ // skip emitting to prevent orphaned meeting references on the consumer side.
1824
+ // @ts-ignore - getMeetingByType types value as object but accepts strings (same as handleLocusEvent)
1825
+ if (type !== DESTINATION_TYPE.LOCUS_ID && this.getMeetingByType(_ID_, meeting.id)) {
1815
1826
  if (!meeting.sipUri) {
1816
1827
  meeting.setSipUri(destination);
1817
1828
  }
@@ -1886,23 +1897,20 @@ export default class Meetings extends WebexPlugin {
1886
1897
  * @public
1887
1898
  * @memberof Meetings
1888
1899
  */
1889
- public syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1900
+ public async syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1890
1901
  // @ts-ignore
1891
1902
  if (this.webex.credentials.isUnverifiedGuest) {
1892
1903
  LoggerProxy.logger.info(
1893
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1904
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1894
1905
  );
1895
-
1896
- return Promise.resolve();
1897
- }
1898
-
1899
- return this.request
1900
- .getActiveMeetings()
1901
- .then((locusArray) => {
1902
- const activeLocusUrl = [];
1906
+ } else {
1907
+ try {
1908
+ const locusArray = await this.request.getActiveMeetings();
1909
+ const activeLocusUrl: string[] = [];
1903
1910
 
1904
1911
  if (locusArray?.loci && locusArray.loci.length > 0) {
1905
1912
  const lociToUpdate = this.sortLocusArrayToUpdate(locusArray.loci);
1913
+
1906
1914
  lociToUpdate.forEach((locus) => {
1907
1915
  activeLocusUrl.push(locus.url);
1908
1916
  this.handleLocusEvent({
@@ -1920,21 +1928,48 @@ export default class Meetings extends WebexPlugin {
1920
1928
  // (they had a locusUrl previously but are no longer active) in the sync
1921
1929
  for (const meeting of Object.values(meetingsCollection)) {
1922
1930
  // @ts-ignore
1923
- const {locusUrl} = meeting;
1931
+ const {locusUrl, locusInfo} = meeting;
1924
1932
  if ((keepOnlyLocusMeetings || locusUrl) && !activeLocusUrl.includes(locusUrl)) {
1925
- // destroy function also uploads logs
1926
- // @ts-ignore
1927
- this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1933
+ const globalMeetingId = locusInfo?.info?.globalMeetingId;
1934
+
1935
+ if (
1936
+ globalMeetingId &&
1937
+ locusArray?.loci?.some(
1938
+ (locus: LocusDTO) => locus.info?.globalMeetingId === globalMeetingId
1939
+ )
1940
+ ) {
1941
+ // don't destroy the meeting as Locus API still returned some Locus that shares
1942
+ // the same globalMeetingId - that happens for example if a webinar user (who hasn't scheduled it)
1943
+ // is in a breakout and gets moved to a different breakout while we were offline
1944
+ } else {
1945
+ // destroy function also uploads logs
1946
+ // @ts-ignore
1947
+ this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1948
+ }
1928
1949
  }
1929
1950
  }
1930
1951
  }
1931
- })
1932
- .catch((error) => {
1952
+ } catch (error) {
1933
1953
  LoggerProxy.logger.error(
1934
1954
  `Meetings:index#syncMeetings --> failed to sync meetings, ${error}`
1935
1955
  );
1936
- throw new Error(error);
1937
- });
1956
+ throw error;
1957
+ }
1958
+ }
1959
+
1960
+ // Trigger hash tree syncs for all remaining meetings
1961
+ const remainingMeetings = this.meetingCollection.getAll();
1962
+ const syncPromises = [];
1963
+
1964
+ for (const meeting of Object.values(remainingMeetings) as any[]) {
1965
+ if (meeting.locusInfo) {
1966
+ syncPromises.push(meeting.locusInfo.syncAllHashTreeDatasets());
1967
+ }
1968
+ }
1969
+
1970
+ if (syncPromises.length > 0) {
1971
+ await Promise.all(syncPromises);
1972
+ }
1938
1973
  }
1939
1974
 
1940
1975
  /**
@@ -1950,8 +1985,8 @@ export default class Meetings extends WebexPlugin {
1950
1985
  this.breakoutLocusForHandleLater = [];
1951
1986
  const lociToUpdate = [...mainLoci];
1952
1987
  breakoutLoci.forEach((breakoutLocus) => {
1953
- const associateMainLocus = mainLoci.find(
1954
- (mainLocus) => mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
1988
+ const associateMainLocus = mainLoci.find((mainLocus) =>
1989
+ MeetingsUtil.isMainAssociatedWithBreakout(mainLocus, breakoutLocus)
1955
1990
  );
1956
1991
  const existCorrespondingMeeting = this.getCorrespondingMeetingByLocus({
1957
1992
  eventType: LOCUSEVENT.SDK_NO_EVENT,
@@ -1979,7 +2014,7 @@ export default class Meetings extends WebexPlugin {
1979
2014
  * @public
1980
2015
  * @memberof Meetings
1981
2016
  */
1982
- checkHandleBreakoutLocus(newCreatedLocus) {
2017
+ checkHandleBreakoutLocus(newCreatedLocus: any) {
1983
2018
  if (
1984
2019
  !newCreatedLocus ||
1985
2020
  !this.breakoutLocusForHandleLater ||
@@ -1990,9 +2025,8 @@ export default class Meetings extends WebexPlugin {
1990
2025
  if (MeetingsUtil.isBreakoutLocusDTO(newCreatedLocus)) {
1991
2026
  return;
1992
2027
  }
1993
- const existIndex = this.breakoutLocusForHandleLater.findIndex(
1994
- (breakoutLocus) =>
1995
- breakoutLocus.controls?.breakout?.url === newCreatedLocus.controls?.breakout?.url
2028
+ const existIndex = this.breakoutLocusForHandleLater.findIndex((breakoutLocus: any) =>
2029
+ MeetingsUtil.isMainAssociatedWithBreakout(newCreatedLocus, breakoutLocus)
1996
2030
  );
1997
2031
 
1998
2032
  if (existIndex < 0) {