@webex/plugin-meetings 3.12.0-next.35 → 3.12.0-next.36

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.
@@ -129,6 +129,7 @@ export default class LocusInfo extends EventsScope {
129
129
  private createHashTreeParser;
130
130
  /**
131
131
  * @param {Object} data - data to initialize locus info with. It may be from a join or GET /loci response or from a Mercury event that triggers a creation of meeting object
132
+ * @param {Function} [onLocusSynced] - optional callback that will be called at the end of initial setup, when locus info is fully synced. It will be called with the full locus snapshot as an argument (which may be null if we haven't received any full locus DTOs during the initial setup, for example in case we receive only hash tree messages without full locus DTOs)
132
133
  * @returns {undefined}
133
134
  * @memberof LocusInfo
134
135
  */
@@ -144,7 +145,13 @@ export default class LocusInfo extends EventsScope {
144
145
  } | {
145
146
  trigger: 'get-loci-response';
146
147
  locus?: LocusDTO;
147
- }): Promise<void>;
148
+ }, onLocusSynced?: (locus: LocusDTO) => void): Promise<void>;
149
+ /**
150
+ * Builds a full locus DTO snapshot from current internal locus state.
151
+ *
152
+ * @returns {LocusDTO}
153
+ */
154
+ private getCurrentLocusSnapshot;
148
155
  /**
149
156
  * Handles HTTP response from Locus API call.
150
157
  * @param {Meeting} meeting meeting object
@@ -374,7 +374,7 @@ export default class Meeting extends StatelessWebexPlugin {
374
374
  webinar: any;
375
375
  conversationUrl: string;
376
376
  callStateForMetrics: CallStateForMetrics;
377
- destination: string;
377
+ destination: string | LocusDTO;
378
378
  destinationType: DESTINATION_TYPE;
379
379
  deviceUrl: string;
380
380
  hostId: string;
@@ -1051,6 +1051,13 @@ export default class Meeting extends StatelessWebexPlugin {
1051
1051
  * @memberof Meeting
1052
1052
  */
1053
1053
  setSipUri(sipUri: string): void;
1054
+ /**
1055
+ * After initial locus setup, refreshes destination with synced locus data and optionally
1056
+ * performs deferred meeting info fetch when initial locus was incomplete.
1057
+ * @param {LocusDTO} locus
1058
+ * @returns {void}
1059
+ */
1060
+ finalizeMeetingAfterInitialLocusSetup(locus: LocusDTO): Promise<void>;
1054
1061
  /**
1055
1062
  * Set the locus info the class instance. Should be called with the parsed locus
1056
1063
  * we got in the join response.
@@ -723,7 +723,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
723
723
  }, _callee1);
724
724
  }))();
725
725
  },
726
- version: "3.12.0-next.35"
726
+ version: "3.12.0-next.36"
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.35"
97
+ "version": "3.12.0-next.36"
98
98
  }
@@ -582,6 +582,7 @@ export default class LocusInfo extends EventsScope {
582
582
 
583
583
  /**
584
584
  * @param {Object} data - data to initialize locus info with. It may be from a join or GET /loci response or from a Mercury event that triggers a creation of meeting object
585
+ * @param {Function} [onLocusSynced] - optional callback that will be called at the end of initial setup, when locus info is fully synced. It will be called with the full locus snapshot as an argument (which may be null if we haven't received any full locus DTOs during the initial setup, for example in case we receive only hash tree messages without full locus DTOs)
585
586
  * @returns {undefined}
586
587
  * @memberof LocusInfo
587
588
  */
@@ -601,8 +602,10 @@ export default class LocusInfo extends EventsScope {
601
602
  | {
602
603
  trigger: 'get-loci-response';
603
604
  locus?: LocusDTO;
604
- }
605
+ },
606
+ onLocusSynced?: (locus: LocusDTO) => void
605
607
  ) {
608
+ let initialFullLocus: LocusDTO | null = null;
606
609
  switch (data.trigger) {
607
610
  case 'locus-message':
608
611
  if (data.hashTreeMessage) {
@@ -650,6 +653,7 @@ export default class LocusInfo extends EventsScope {
650
653
  case 'join-response':
651
654
  this.updateLocusCache(data.locus);
652
655
  this.onFullLocus('join response', data.locus, undefined, data.dataSets, data.metadata);
656
+ initialFullLocus = data.locus;
653
657
  break;
654
658
  case 'get-loci-response':
655
659
  if (data.locus?.links?.resources?.visibleDataSets?.url) {
@@ -672,12 +676,47 @@ export default class LocusInfo extends EventsScope {
672
676
  // "classic" Locus case, no hash trees involved
673
677
  this.updateLocusCache(data.locus);
674
678
  this.onFullLocus('classic get-loci-response', data.locus, undefined);
679
+ initialFullLocus = data.locus || null;
675
680
  }
676
681
  }
682
+
683
+ if (onLocusSynced) {
684
+ try {
685
+ onLocusSynced(initialFullLocus || this.getCurrentLocusSnapshot());
686
+ } catch (error) {
687
+ LoggerProxy.logger.warn(
688
+ `Locus-info:index#initialSetup --> onLocusSynced callback failed: ${error}`
689
+ );
690
+ }
691
+ }
692
+
677
693
  // Change it to true after it receives it first locus object
678
694
  this.emitChange = true;
679
695
  }
680
696
 
697
+ /**
698
+ * Builds a full locus DTO snapshot from current internal locus state.
699
+ *
700
+ * @returns {LocusDTO}
701
+ */
702
+ private getCurrentLocusSnapshot(): LocusDTO {
703
+ const locus: Record<string, any> = {};
704
+
705
+ LocusDtoTopLevelKeys.forEach((key) => {
706
+ const value = (this as Record<string, any>)[key];
707
+
708
+ if (value !== undefined && value !== null) {
709
+ locus[key] = cloneDeep(value);
710
+ }
711
+ });
712
+
713
+ if (!Array.isArray(locus.participants)) {
714
+ locus.participants = [];
715
+ }
716
+
717
+ return locus as LocusDTO;
718
+ }
719
+
681
720
  /**
682
721
  * Handles HTTP response from Locus API call.
683
722
  * @param {Meeting} meeting meeting object
@@ -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;
@@ -4659,6 +4659,33 @@ export default class Meeting extends StatelessWebexPlugin {
4659
4659
  this.sipUri = sipUri;
4660
4660
  }
4661
4661
 
4662
+ /**
4663
+ * After initial locus setup, refreshes destination with synced locus data and optionally
4664
+ * performs deferred meeting info fetch when initial locus was incomplete.
4665
+ * @param {LocusDTO} locus
4666
+ * @returns {void}
4667
+ */
4668
+ public async finalizeMeetingAfterInitialLocusSetup(locus: LocusDTO): Promise<void> {
4669
+ if (locus && this?.destinationType === DESTINATION_TYPE.LOCUS_ID) {
4670
+ // destination is initialized from the initial locus snapshot in constructor,
4671
+ // so refresh it after locus sync to avoid stale partial hash-tree data.
4672
+ this.destination = locus;
4673
+ }
4674
+ if (
4675
+ (!this.meetingInfo || isEmpty(this.meetingInfo)) &&
4676
+ (this.destination as LocusDTO)?.info &&
4677
+ !this.fetchMeetingInfoTimeoutId
4678
+ ) {
4679
+ try {
4680
+ await this.fetchMeetingInfo({});
4681
+ } catch (error: any) {
4682
+ LoggerProxy.logger.info(
4683
+ `Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: ${error.message}`
4684
+ );
4685
+ }
4686
+ }
4687
+ }
4688
+
4662
4689
  /**
4663
4690
  * Set the locus info the class instance. Should be called with the parsed locus
4664
4691
  * we got in the join response.
@@ -584,17 +584,21 @@ export default class Meetings extends WebexPlugin {
584
584
  this.create(data.locus, DESTINATION_TYPE.LOCUS_ID, useRandomDelayForInfo)
585
585
  .then(async (newMeeting) => {
586
586
  meeting = newMeeting;
587
-
588
587
  try {
589
588
  // It's a new meeting so initialize the locus data
590
- await meeting.locusInfo.initialSetup({
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
- });
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
+ );
598
602
  } catch (error) {
599
603
  LoggerProxy.logger.warn(
600
604
  `Meetings:index#handleLocusEvent --> Error initializing locus data: ${error.message}`
@@ -602,6 +606,7 @@ export default class Meetings extends WebexPlugin {
602
606
  // @ts-ignore
603
607
  this.destroy(meeting, MEETING_REMOVED_REASON.LOCUS_DTO_SYNC_FAILED);
604
608
  }
609
+
605
610
  this.checkHandleBreakoutLocus(data.locus);
606
611
  })
607
612
  .catch((e) => {
@@ -1762,6 +1767,7 @@ export default class Meetings extends WebexPlugin {
1762
1767
  extraParams: infoExtraParams,
1763
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
1764
1769
  };
1770
+ const shouldDeferMeetingInfoFetch = type === DESTINATION_TYPE.LOCUS_ID && !destination?.info;
1765
1771
 
1766
1772
  if (meetingInfo) {
1767
1773
  meeting.injectMeetingInfo(meetingInfo, meetingInfoOptions, meetingLookupUrl);
@@ -1773,8 +1779,12 @@ export default class Meetings extends WebexPlugin {
1773
1779
  waitingTime
1774
1780
  );
1775
1781
  meeting.parseMeetingInfo(undefined, destination);
1776
- } else {
1782
+ } else if (!shouldDeferMeetingInfoFetch) {
1777
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
+ );
1778
1788
  }
1779
1789
  }
1780
1790
  } catch (err) {
@@ -221,6 +221,47 @@ describe('plugin-meetings', () => {
221
221
  assert.isTrue(locusInfo.emitChange);
222
222
  });
223
223
 
224
+ it('calls onLocusSynced callback passed as second argument with full locus from join response', async () => {
225
+ const syncedLocus = {url: 'http://locus-url.com', participants: []};
226
+ const onLocusSynced = sinon.stub();
227
+
228
+ await locusInfo.initialSetup(
229
+ {
230
+ trigger: 'join-response',
231
+ locus: syncedLocus,
232
+ },
233
+ onLocusSynced
234
+ );
235
+
236
+ assert.calledOnceWithExactly(onLocusSynced, syncedLocus);
237
+ });
238
+
239
+ it('swallows onLocusSynced callback errors and logs warn', async () => {
240
+ const syncedLocus = {url: 'http://locus-url.com', participants: []};
241
+ const callbackError = new Error('onLocusSynced failed');
242
+ const onLocusSynced = sinon.stub().throws(callbackError);
243
+ const loggerWarnStub = LoggerProxy.logger.warn?.isSinonProxy
244
+ ? LoggerProxy.logger.warn
245
+ : sinon.stub(LoggerProxy.logger, 'warn');
246
+
247
+ loggerWarnStub.resetHistory();
248
+
249
+ await locusInfo.initialSetup(
250
+ {
251
+ trigger: 'join-response',
252
+ locus: syncedLocus,
253
+ },
254
+ onLocusSynced
255
+ );
256
+
257
+ assert.calledOnceWithExactly(onLocusSynced, syncedLocus);
258
+ assert.calledOnce(loggerWarnStub);
259
+ assert.match(
260
+ loggerWarnStub.firstCall.args[0],
261
+ /Locus-info:index#initialSetup --> onLocusSynced callback failed/
262
+ );
263
+ });
264
+
224
265
  it('should initialize the hash tree parser correctly when triggered from a get loci response containing visible datasets', async () => {
225
266
  const visibleDataSets = ['dataset1', 'dataset2'];
226
267
  const locus = createLocusWithVisibleDataSets(visibleDataSets);
@@ -11353,6 +11353,93 @@ describe('plugin-meetings', () => {
11353
11353
  });
11354
11354
  });
11355
11355
 
11356
+ describe('#finalizeMeetingAfterInitialLocusSetup', () => {
11357
+ it('refreshes destination from synced locus when destination type is LOCUS_ID', () => {
11358
+ const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11359
+
11360
+ meeting.destinationType = DESTINATION_TYPE.LOCUS_ID;
11361
+ meeting.destination = {info: {topic: 'old'}};
11362
+
11363
+ meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
11364
+
11365
+ assert.equal(meeting.destination, syncedLocus);
11366
+ });
11367
+
11368
+ it('does not refresh destination when destination type is not LOCUS_ID', () => {
11369
+ const syncedLocus = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11370
+ const originalDestination = {destination: 'original-destination'};
11371
+
11372
+ meeting.destinationType = DESTINATION_TYPE.CONVERSATION_URL;
11373
+ meeting.destination = originalDestination;
11374
+
11375
+ meeting.finalizeMeetingAfterInitialLocusSetup(syncedLocus);
11376
+
11377
+ assert.equal(meeting.destination, originalDestination);
11378
+ });
11379
+
11380
+ it('fetches meeting info when meetingInfo is empty and destination has info', () => {
11381
+ const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
11382
+
11383
+ meeting.meetingInfo = {};
11384
+ meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11385
+
11386
+ meeting.finalizeMeetingAfterInitialLocusSetup({});
11387
+
11388
+ assert.calledOnceWithExactly(fetchMeetingInfoStub, {});
11389
+ });
11390
+
11391
+ it('does not fetch meeting info when destination has no info', () => {
11392
+ const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
11393
+
11394
+ meeting.meetingInfo = {};
11395
+ meeting.destination = {url: 'https://locus.example.com/locus/123'};
11396
+
11397
+ meeting.finalizeMeetingAfterInitialLocusSetup({});
11398
+
11399
+ assert.notCalled(fetchMeetingInfoStub);
11400
+ });
11401
+
11402
+ it('does not fetch meeting info when meetingInfo is already populated', () => {
11403
+ const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
11404
+
11405
+ meeting.meetingInfo = {meetingJoinUrl: 'https://example.com/join/abc'};
11406
+ meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11407
+
11408
+ meeting.finalizeMeetingAfterInitialLocusSetup({});
11409
+
11410
+ assert.notCalled(fetchMeetingInfoStub);
11411
+ });
11412
+
11413
+ it('does not fetch meeting info when delayed fetch timer is already scheduled', () => {
11414
+ const fetchMeetingInfoStub = sinon.stub(meeting, 'fetchMeetingInfo').resolves();
11415
+
11416
+ meeting.meetingInfo = {};
11417
+ meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11418
+ meeting.fetchMeetingInfoTimeoutId = 42;
11419
+
11420
+ meeting.finalizeMeetingAfterInitialLocusSetup({});
11421
+
11422
+ assert.notCalled(fetchMeetingInfoStub);
11423
+ });
11424
+
11425
+ it('swallows async fetchMeetingInfo errors and logs info', async () => {
11426
+ const error = new Error('fetch failed');
11427
+
11428
+ meeting.meetingInfo = {};
11429
+ meeting.destination = {url: 'https://locus.example.com/locus/123', info: {topic: 'x'}};
11430
+ sinon.stub(meeting, 'fetchMeetingInfo').returns(Promise.reject(error));
11431
+ const loggerInfoStub = sinon.stub(LoggerProxy.logger, 'info');
11432
+
11433
+ await meeting.finalizeMeetingAfterInitialLocusSetup({});
11434
+
11435
+ assert.calledOnce(loggerInfoStub);
11436
+ assert.match(
11437
+ loggerInfoStub.firstCall.args[0],
11438
+ /Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: fetch failed/
11439
+ );
11440
+ });
11441
+ });
11442
+
11356
11443
  describe('#emailInput', () => {
11357
11444
  it('should set the email input', () => {
11358
11445
  assert.notOk(meeting.emailInput);
@@ -1489,7 +1489,7 @@ describe('plugin-meetings', () => {
1489
1489
  url: url1,
1490
1490
  },
1491
1491
  hashTreeMessage: undefined,
1492
- });
1492
+ }, sinon.match.func);
1493
1493
  });
1494
1494
  });
1495
1495
  describe('when destroying meeting is needed', () => {
@@ -2137,7 +2137,7 @@ describe('plugin-meetings', () => {
2137
2137
  },
2138
2138
  },
2139
2139
  hashTreeMessage: undefined,
2140
- });
2140
+ }, sinon.match.func);
2141
2141
  });
2142
2142
  it('should setup the meeting from a hash tree event', async () => {
2143
2143
  const selfData = {};
@@ -2171,7 +2171,7 @@ describe('plugin-meetings', () => {
2171
2171
  info: infoData,
2172
2172
  },
2173
2173
  hashTreeMessage,
2174
- });
2174
+ }, sinon.match.func);
2175
2175
  });
2176
2176
 
2177
2177
  it('should ignore hash tree event when created locus has INACTIVE fullState', async () => {
@@ -2251,7 +2251,7 @@ describe('plugin-meetings', () => {
2251
2251
  },
2252
2252
  },
2253
2253
  hashTreeMessage: undefined,
2254
- });
2254
+ }, sinon.match.func);
2255
2255
  });
2256
2256
 
2257
2257
  it('sends client event correctly on finally', async () => {
@@ -2327,7 +2327,7 @@ describe('plugin-meetings', () => {
2327
2327
  },
2328
2328
  },
2329
2329
  hashTreeMessage: undefined,
2330
- });
2330
+ }, sinon.match.func);
2331
2331
  });
2332
2332
 
2333
2333
  const generateFakeLocusData = (isUnifiedSpaceMeeting) => ({