@webex/plugin-meetings 3.11.0-next.32 → 3.11.0-next.34

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.
@@ -310,7 +310,6 @@ export declare const HEADERS: {
310
310
  };
311
311
  export declare const MEETING_REMOVED_REASON: {
312
312
  SELF_REMOVED: string;
313
- FULLSTATE_REMOVED: string;
314
313
  MEETING_INACTIVE_TERMINATING: string;
315
314
  CLIENT_LEAVE_REQUEST: string;
316
315
  CLIENT_LEAVE_REQUEST_TAB_CLOSED: string;
@@ -5,4 +5,5 @@ export declare const DataSetNames: {
5
5
  ATD_ACTIVE: string;
6
6
  ATD_UNMUTED: string;
7
7
  SELF: string;
8
+ UNJOINED: string;
8
9
  };
@@ -48,6 +48,12 @@ export type LocusInfoUpdateType = Enum<typeof LocusInfoUpdateType>;
48
48
  export type LocusInfoUpdateCallback = (updateType: LocusInfoUpdateType, data?: {
49
49
  updatedObjects: HashTreeObject[];
50
50
  }) => void;
51
+ /**
52
+ * This error is thrown if we receive information that the meeting has ended while we're processing some hash messages.
53
+ * It's handled internally by HashTreeParser and results in MEETING_ENDED being sent up.
54
+ */
55
+ export declare class MeetingEndedError extends Error {
56
+ }
51
57
  /**
52
58
  * Parses hash tree eventing locus data
53
59
  */
@@ -177,6 +183,13 @@ declare class HashTreeParser {
177
183
  * @returns {void}
178
184
  */
179
185
  private handleRootHashHeartBeatMessage;
186
+ /**
187
+ * Asynchronously initializes new visible data sets
188
+ *
189
+ * @param {VisibleDataSetInfo[]} dataSetsRequiringInitialization list of datasets to initialize
190
+ * @returns {void}
191
+ */
192
+ private queueInitForNewVisibleDataSets;
180
193
  /**
181
194
  * Handles updates to Metadata object that we receive from Locus via other means than messages. Right now
182
195
  * that means only in the API response alongside locus object.
@@ -302,6 +315,7 @@ declare class HashTreeParser {
302
315
  * @returns {void}
303
316
  */
304
317
  private stopAllTimers;
318
+ private checkForSentinelHttpResponse;
305
319
  /**
306
320
  * Gets the current hashes from the locus for a specific data set.
307
321
  * @param {string} dataSetName
@@ -506,7 +506,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
506
506
  }, _callee8);
507
507
  }))();
508
508
  },
509
- version: "3.11.0-next.32"
509
+ version: "3.11.0-next.34"
510
510
  });
511
511
  var _default = exports.default = Webinar;
512
512
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -93,5 +93,5 @@
93
93
  "//": [
94
94
  "TODO: upgrade jwt-decode when moving to node 18"
95
95
  ],
96
- "version": "3.11.0-next.32"
96
+ "version": "3.11.0-next.34"
97
97
  }
@@ -51,7 +51,13 @@ const AIEnableRequest = WebexPlugin.extend({
51
51
  const isApprover = !!approverId && approverId === this.selfParticipantId;
52
52
  const initiatorId = initiator?.participantId;
53
53
  const isInitiator = !!initiatorId && initiatorId === this.selfParticipantId;
54
- if (!isApprover && !isInitiator) {
54
+ if (
55
+ !isApprover &&
56
+ !isInitiator &&
57
+ // Not just the initiator needs to know about declined all because
58
+ // all future requests will be rejected if the meeting is in the declined all state
59
+ actionType !== AI_ENABLE_REQUEST.ACTION_TYPE.DECLINED_ALL
60
+ ) {
55
61
  return;
56
62
  }
57
63
  this.trigger(AI_ENABLE_REQUEST.EVENTS.APPROVAL_REQUEST_ARRIVED, {
package/src/constants.ts CHANGED
@@ -414,7 +414,6 @@ export const HEADERS = {
414
414
  // Meeting actually ended
415
415
  export const MEETING_REMOVED_REASON = {
416
416
  SELF_REMOVED: 'SELF_REMOVED', // server or host removed you from the meeting
417
- FULLSTATE_REMOVED: 'FULLSTATE_REMOVED', // meeting got dropped ? not sure
418
417
  MEETING_INACTIVE_TERMINATING: 'MEETING_INACTIVE_TERMINATING', // Meeting got ended or everyone left the meeting
419
418
  CLIENT_LEAVE_REQUEST: 'CLIENT_LEAVE_REQUEST', // You triggered leave meeting
420
419
  CLIENT_LEAVE_REQUEST_TAB_CLOSED: 'CLIENT_LEAVE_REQUEST_TAB_CLOSED', // You triggered leave meeting, such as closing the browser tab directly
@@ -6,4 +6,5 @@ export const DataSetNames = {
6
6
  ATD_ACTIVE: 'atd-active', // only sent to panelists, over LLM; the attendees that have their hands raised or are allowed to unmute themselves
7
7
  ATD_UNMUTED: 'atd-unmuted', // sent to web client, over LLM, not sent to panelists; the attendees that are unmuted
8
8
  SELF: 'self', // sent to web client, over Mercury
9
+ UNJOINED: 'unjoined', // sent when you are not joined, but can still see some stuff from the meeting (mutually exclusive with "main")
9
10
  };
@@ -73,13 +73,19 @@ interface LeafInfo {
73
73
  * This error is thrown if we receive information that the meeting has ended while we're processing some hash messages.
74
74
  * It's handled internally by HashTreeParser and results in MEETING_ENDED being sent up.
75
75
  */
76
- class MeetingEndedError extends Error {}
76
+ export class MeetingEndedError extends Error {}
77
77
 
78
78
  /* Currently Locus always sends Metadata objects only in the "self" dataset.
79
79
  * If this ever changes, update all the code that relies on this constant.
80
80
  */
81
81
  const MetadataDataSetName = DataSetNames.SELF;
82
82
 
83
+ const PossibleSentinelMessageDataSetNames = [
84
+ DataSetNames.MAIN,
85
+ DataSetNames.SELF,
86
+ DataSetNames.UNJOINED,
87
+ ];
88
+
83
89
  /**
84
90
  * Parses hash tree eventing locus data
85
91
  */
@@ -285,9 +291,15 @@ class HashTreeParser {
285
291
  return this.webexRequest({
286
292
  method: HTTP_VERBS.GET,
287
293
  uri: this.visibleDataSetsUrl,
288
- }).then((response) => {
289
- return response.body.dataSets as Array<DataSet>;
290
- });
294
+ })
295
+ .then((response) => {
296
+ return response.body.dataSets as Array<DataSet>;
297
+ })
298
+ .catch((error) => {
299
+ this.checkForSentinelHttpResponse(error);
300
+
301
+ throw error;
302
+ });
291
303
  }
292
304
 
293
305
  /**
@@ -511,21 +523,19 @@ class HashTreeParser {
511
523
  * @returns {boolean} - Returns true if the message indicates the end of the meeting, false otherwise
512
524
  */
513
525
  private isEndMessage(message: HashTreeMessage) {
514
- const mainDataSet = message.dataSets.find(
515
- (dataSet) => dataSet.name.toLowerCase() === DataSetNames.MAIN
516
- );
517
-
518
- if (
519
- mainDataSet &&
520
- mainDataSet.leafCount === 1 &&
521
- mainDataSet.root === EMPTY_HASH &&
522
- this.dataSets[DataSetNames.MAIN].version < mainDataSet.version
523
- ) {
524
- // this is a special way for Locus to indicate that this meeting has ended
525
- return true;
526
- }
526
+ return message.dataSets.some((dataSet) => {
527
+ if (
528
+ dataSet.leafCount === 1 &&
529
+ dataSet.root === EMPTY_HASH &&
530
+ (!this.dataSets[dataSet.name] || this.dataSets[dataSet.name].version < dataSet.version) &&
531
+ PossibleSentinelMessageDataSetNames.includes(dataSet.name.toLowerCase())
532
+ ) {
533
+ // this is a special way for Locus to indicate that this meeting has ended
534
+ return true;
535
+ }
527
536
 
528
- return false;
537
+ return false;
538
+ });
529
539
  }
530
540
 
531
541
  /**
@@ -556,6 +566,33 @@ class HashTreeParser {
556
566
  });
557
567
  }
558
568
 
569
+ /**
570
+ * Asynchronously initializes new visible data sets
571
+ *
572
+ * @param {VisibleDataSetInfo[]} dataSetsRequiringInitialization list of datasets to initialize
573
+ * @returns {void}
574
+ */
575
+ private queueInitForNewVisibleDataSets(dataSetsRequiringInitialization: VisibleDataSetInfo[]) {
576
+ queueMicrotask(() => {
577
+ this.initializeNewVisibleDataSets(dataSetsRequiringInitialization).catch((error) => {
578
+ if (error instanceof MeetingEndedError) {
579
+ this.callLocusInfoUpdateCallback({
580
+ updateType: LocusInfoUpdateType.MEETING_ENDED,
581
+ });
582
+ } else {
583
+ LoggerProxy.logger.warn(
584
+ `HashTreeParser#queueInitForNewVisibleDataSets --> ${
585
+ this.debugId
586
+ } error while initializing new visible datasets: ${dataSetsRequiringInitialization
587
+ .map((ds) => ds.name)
588
+ .join(', ')}: `,
589
+ error
590
+ );
591
+ }
592
+ });
593
+ });
594
+ }
595
+
559
596
  /**
560
597
  * Handles updates to Metadata object that we receive from Locus via other means than messages. Right now
561
598
  * that means only in the API response alongside locus object.
@@ -600,9 +637,7 @@ class HashTreeParser {
600
637
 
601
638
  if (dataSetsRequiringInitialization.length > 0) {
602
639
  // there are some data sets that we need to initialize asynchronously
603
- queueMicrotask(() => {
604
- this.initializeNewVisibleDataSets(dataSetsRequiringInitialization);
605
- });
640
+ this.queueInitForNewVisibleDataSets(dataSetsRequiringInitialization);
606
641
  }
607
642
  }
608
643
  }
@@ -931,15 +966,6 @@ class HashTreeParser {
931
966
  this.visibleDataSetsUrl = visibleDataSetsUrl;
932
967
  dataSets.forEach((dataSet) => this.updateDataSetInfo(dataSet));
933
968
 
934
- if (this.isEndMessage(message)) {
935
- LoggerProxy.logger.info(
936
- `HashTreeParser#parseMessage --> ${this.debugId} received END message`
937
- );
938
- this.stopAllTimers();
939
-
940
- return {updateType: LocusInfoUpdateType.MEETING_ENDED};
941
- }
942
-
943
969
  let isRosterDropped = false;
944
970
  const updatedObjects: HashTreeObject[] = [];
945
971
 
@@ -1039,9 +1065,7 @@ class HashTreeParser {
1039
1065
 
1040
1066
  if (dataSetsRequiringInitialization.length > 0) {
1041
1067
  // there are some data sets that we need to initialize asynchronously
1042
- queueMicrotask(() => {
1043
- this.initializeNewVisibleDataSets(dataSetsRequiringInitialization);
1044
- });
1068
+ this.queueInitForNewVisibleDataSets(dataSetsRequiringInitialization);
1045
1069
  }
1046
1070
 
1047
1071
  if (updatedObjects.length === 0) {
@@ -1064,7 +1088,14 @@ class HashTreeParser {
1064
1088
  if (message.heartbeatIntervalMs) {
1065
1089
  this.heartbeatIntervalMs = message.heartbeatIntervalMs;
1066
1090
  }
1067
- if (message.locusStateElements === undefined) {
1091
+ if (this.isEndMessage(message)) {
1092
+ LoggerProxy.logger.info(
1093
+ `HashTreeParser#parseMessage --> ${this.debugId} received sentinel END MEETING message`
1094
+ );
1095
+ this.stopAllTimers();
1096
+
1097
+ this.callLocusInfoUpdateCallback({updateType: LocusInfoUpdateType.MEETING_ENDED});
1098
+ } else if (message.locusStateElements === undefined) {
1068
1099
  this.handleRootHashHeartBeatMessage(message);
1069
1100
  this.resetHeartbeatWatchdogs(message.dataSets);
1070
1101
  } else {
@@ -1169,54 +1200,67 @@ class HashTreeParser {
1169
1200
  return;
1170
1201
  }
1171
1202
 
1172
- LoggerProxy.logger.info(
1173
- `HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
1174
- );
1203
+ try {
1204
+ LoggerProxy.logger.info(
1205
+ `HashTreeParser#performSync --> ${this.debugId} ${reason}, syncing data set "${dataSet.name}"`
1206
+ );
1175
1207
 
1176
- const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
1208
+ const mismatchedLeavesData: Record<number, LeafDataItem[]> = {};
1177
1209
 
1178
- if (dataSet.leafCount !== 1) {
1179
- let receivedHashes;
1210
+ if (dataSet.leafCount !== 1) {
1211
+ let receivedHashes;
1180
1212
 
1181
- try {
1182
- // request hashes from sender
1183
- const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
1184
- dataSet.name,
1185
- rootHash
1186
- );
1213
+ try {
1214
+ // request hashes from sender
1215
+ const {hashes, dataSet: latestDataSetInfo} = await this.getHashesFromLocus(
1216
+ dataSet.name,
1217
+ rootHash
1218
+ );
1187
1219
 
1188
- receivedHashes = hashes;
1220
+ receivedHashes = hashes;
1189
1221
 
1190
- dataSet.hashTree.resize(latestDataSetInfo.leafCount);
1191
- } catch (error) {
1192
- if (error.statusCode === 409) {
1193
- // this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
1194
- LoggerProxy.logger.info(
1195
- `HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
1196
- );
1222
+ dataSet.hashTree.resize(latestDataSetInfo.leafCount);
1223
+ } catch (error) {
1224
+ if (error.statusCode === 409) {
1225
+ // this is a leaf count mismatch, we should do nothing, just wait for another heartbeat message from Locus
1226
+ LoggerProxy.logger.info(
1227
+ `HashTreeParser#getHashesFromLocus --> ${this.debugId} Got 409 when fetching hashes for data set "${dataSet.name}": ${error.message}`
1228
+ );
1197
1229
 
1198
- return;
1230
+ return;
1231
+ }
1232
+ throw error;
1199
1233
  }
1200
- throw error;
1201
- }
1202
1234
 
1203
- // identify mismatched leaves
1204
- const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
1235
+ // identify mismatched leaves
1236
+ const mismatchedLeaveIndexes = dataSet.hashTree.diffHashes(receivedHashes);
1205
1237
 
1206
- mismatchedLeaveIndexes.forEach((index) => {
1207
- mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
1208
- });
1209
- } else {
1210
- mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
1211
- }
1212
- // request sync for mismatched leaves
1213
- if (Object.keys(mismatchedLeavesData).length > 0) {
1214
- const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
1215
-
1216
- // sync API may return nothing (in that case data will arrive via messages)
1217
- // or it may return a response in the same format as messages
1218
- if (syncResponse) {
1219
- this.handleMessage(syncResponse, 'via sync API');
1238
+ mismatchedLeaveIndexes.forEach((index) => {
1239
+ mismatchedLeavesData[index] = dataSet.hashTree.getLeafData(index);
1240
+ });
1241
+ } else {
1242
+ mismatchedLeavesData[0] = dataSet.hashTree.getLeafData(0);
1243
+ }
1244
+ // request sync for mismatched leaves
1245
+ if (Object.keys(mismatchedLeavesData).length > 0) {
1246
+ const syncResponse = await this.sendSyncRequestToLocus(dataSet, mismatchedLeavesData);
1247
+
1248
+ // sync API may return nothing (in that case data will arrive via messages)
1249
+ // or it may return a response in the same format as messages
1250
+ if (syncResponse) {
1251
+ this.handleMessage(syncResponse, 'via sync API');
1252
+ }
1253
+ }
1254
+ } catch (error) {
1255
+ if (error instanceof MeetingEndedError) {
1256
+ this.callLocusInfoUpdateCallback({
1257
+ updateType: LocusInfoUpdateType.MEETING_ENDED,
1258
+ });
1259
+ } else {
1260
+ LoggerProxy.logger.warn(
1261
+ `HashTreeParser#performSync --> ${this.debugId} error during sync for data set "${dataSet.name}":`,
1262
+ error
1263
+ );
1220
1264
  }
1221
1265
  }
1222
1266
  }
@@ -1360,6 +1404,25 @@ class HashTreeParser {
1360
1404
  });
1361
1405
  }
1362
1406
 
1407
+ private checkForSentinelHttpResponse(error: any, dataSetName?: string) {
1408
+ const isValidDataSetForSentinel =
1409
+ dataSetName === undefined ||
1410
+ PossibleSentinelMessageDataSetNames.includes(dataSetName.toLowerCase());
1411
+
1412
+ if (
1413
+ ((error.statusCode === 409 && error.body?.errorCode === 2403004) ||
1414
+ error.statusCode === 404) &&
1415
+ isValidDataSetForSentinel
1416
+ ) {
1417
+ LoggerProxy.logger.info(
1418
+ `HashTreeParser#checkForSentinelHttpResponse --> ${this.debugId} Received ${error.statusCode} for data set "${dataSetName}", indicating that the meeting has ended`
1419
+ );
1420
+ this.stopAllTimers();
1421
+
1422
+ throw new MeetingEndedError();
1423
+ }
1424
+ }
1425
+
1363
1426
  /**
1364
1427
  * Gets the current hashes from the locus for a specific data set.
1365
1428
  * @param {string} dataSetName
@@ -1410,6 +1473,8 @@ class HashTreeParser {
1410
1473
  `HashTreeParser#getHashesFromLocus --> ${this.debugId} Error ${error.statusCode} fetching hashes for data set "${dataSetName}":`,
1411
1474
  error
1412
1475
  );
1476
+ this.checkForSentinelHttpResponse(error, dataSet.name);
1477
+
1413
1478
  throw error;
1414
1479
  });
1415
1480
  }
@@ -1472,6 +1537,8 @@ class HashTreeParser {
1472
1537
  `HashTreeParser#sendSyncRequestToLocus --> ${this.debugId} Error ${error.statusCode} sending sync request for data set "${dataSet.name}":`,
1473
1538
  error
1474
1539
  );
1540
+ this.checkForSentinelHttpResponse(error, dataSet.name);
1541
+
1475
1542
  throw error;
1476
1543
  });
1477
1544
  }
@@ -35,6 +35,7 @@ import HashTreeParser, {
35
35
  DataSet,
36
36
  HashTreeMessage,
37
37
  LocusInfoUpdateType,
38
+ MeetingEndedError,
38
39
  Metadata,
39
40
  } from '../hashTree/hashTreeParser';
40
41
  import {HashTreeObject, ObjectType, ObjectTypeToLocusKeyMap} from '../hashTree/types';
@@ -416,10 +417,13 @@ export default class LocusInfo extends EventsScope {
416
417
  );
417
418
 
418
419
  if (!metadataObject?.data?.visibleDataSets) {
419
- LoggerProxy.logger.warn(
420
+ // this is a common case (not an error)
421
+ // it happens for example after we leave the meeting and still get some heartbeats or delayed messages
422
+ LoggerProxy.logger.info(
420
423
  `Locus-info:index#initialSetup --> cannot initialize HashTreeParser, Metadata object with visibleDataSets is missing in the message`
421
424
  );
422
425
 
426
+ // throw so that handleLocusEvent() catches it and destroys the partially created meeting object
423
427
  throw new Error('Metadata object with visibleDataSets is missing in the message');
424
428
  }
425
429
 
@@ -727,7 +731,11 @@ export default class LocusInfo extends EventsScope {
727
731
  * @param {HashTreeMessage} message incoming hash tree message
728
732
  * @returns {void}
729
733
  */
730
- private handleHashTreeMessage(meeting: any, eventType: LOCUSEVENT, message: HashTreeMessage) {
734
+ private async handleHashTreeMessage(
735
+ meeting: any,
736
+ eventType: LOCUSEVENT,
737
+ message: HashTreeMessage
738
+ ) {
731
739
  if (eventType !== LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
732
740
  this.sendClassicVsHashTreeMismatchMetric(
733
741
  meeting,
@@ -736,8 +744,15 @@ export default class LocusInfo extends EventsScope {
736
744
 
737
745
  return;
738
746
  }
739
-
740
- this.hashTreeParser.handleMessage(message);
747
+ try {
748
+ await this.hashTreeParser.handleMessage(message);
749
+ } catch (error) {
750
+ if (error instanceof MeetingEndedError) {
751
+ this.webex.meetings.destroy(meeting, MEETING_REMOVED_REASON.SELF_REMOVED);
752
+ } else {
753
+ throw error;
754
+ }
755
+ }
741
756
  }
742
757
 
743
758
  /**
@@ -1292,27 +1307,6 @@ export default class LocusInfo extends EventsScope {
1292
1307
  shouldLeave: false,
1293
1308
  }
1294
1309
  );
1295
- } else if (this.fullState && this.fullState.removed) {
1296
- // user has been dropped from a meeting
1297
-
1298
- // @ts-ignore
1299
- this.webex.internal.newMetrics.submitClientEvent({
1300
- name: 'client.call.remote-ended',
1301
- options: {
1302
- meetingId: this.meetingId,
1303
- },
1304
- });
1305
- this.emitScoped(
1306
- {
1307
- file: 'locus-info',
1308
- function: 'isMeetingActive',
1309
- },
1310
- EVENTS.DESTROY_MEETING,
1311
- {
1312
- reason: MEETING_REMOVED_REASON.FULLSTATE_REMOVED,
1313
- shouldLeave: false,
1314
- }
1315
- );
1316
1310
  }
1317
1311
  // If you are guest and you are removed from the meeting
1318
1312
  // You wont get any further events
@@ -206,6 +206,34 @@ describe('plugin-meetings', () => {
206
206
  sinon.assert.notCalled(triggerSpy);
207
207
  });
208
208
 
209
+ it('should trigger DECLINED_ALL event even when user is neither approver nor initiator', () => {
210
+ aiEnableRequest.listenToApprovalRequests();
211
+
212
+ const event = {
213
+ data: {
214
+ approval: {
215
+ resourceType: AI_ENABLE_REQUEST.RESOURCE_TYPE,
216
+ receivers: [{participantId: testApproverId}],
217
+ initiator: {participantId: testInitiatorId},
218
+ actionType: AI_ENABLE_REQUEST.ACTION_TYPE.DECLINED_ALL,
219
+ url: testUrl,
220
+ },
221
+ },
222
+ };
223
+
224
+ webex.internal.mercury.emit(`event:${LOCUSEVENT.APPROVAL_REQUEST}`, event);
225
+
226
+ sinon.assert.calledOnce(triggerSpy);
227
+ sinon.assert.calledWith(triggerSpy, AI_ENABLE_REQUEST.EVENTS.APPROVAL_REQUEST_ARRIVED, {
228
+ actionType: AI_ENABLE_REQUEST.ACTION_TYPE.DECLINED_ALL,
229
+ isApprover: false,
230
+ isInitiator: false,
231
+ initiatorId: testInitiatorId,
232
+ approverId: testApproverId,
233
+ url: testUrl,
234
+ });
235
+ });
236
+
209
237
  it('should not trigger event when resourceType does not match', () => {
210
238
  aiEnableRequest.listenToApprovalRequests();
211
239