@webex/plugin-meetings 3.8.1-next.4 → 3.8.1-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 (94) hide show
  1. package/README.md +26 -13
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/constants.js +24 -2
  5. package/dist/constants.js.map +1 -1
  6. package/dist/interpretation/index.js +1 -1
  7. package/dist/interpretation/siLanguage.js +1 -1
  8. package/dist/locus-info/index.js +38 -84
  9. package/dist/locus-info/index.js.map +1 -1
  10. package/dist/media/index.js +2 -2
  11. package/dist/media/index.js.map +1 -1
  12. package/dist/meeting/brbState.js +14 -12
  13. package/dist/meeting/brbState.js.map +1 -1
  14. package/dist/meeting/in-meeting-actions.js +6 -0
  15. package/dist/meeting/in-meeting-actions.js.map +1 -1
  16. package/dist/meeting/index.js +213 -77
  17. package/dist/meeting/index.js.map +1 -1
  18. package/dist/meeting/request.js +19 -0
  19. package/dist/meeting/request.js.map +1 -1
  20. package/dist/meeting/request.type.js.map +1 -1
  21. package/dist/meeting/type.js +7 -0
  22. package/dist/meeting/type.js.map +1 -0
  23. package/dist/meeting/util.js +11 -0
  24. package/dist/meeting/util.js.map +1 -1
  25. package/dist/meetings/index.js +35 -33
  26. package/dist/meetings/index.js.map +1 -1
  27. package/dist/members/index.js +11 -9
  28. package/dist/members/index.js.map +1 -1
  29. package/dist/members/request.js +3 -3
  30. package/dist/members/request.js.map +1 -1
  31. package/dist/members/util.js +18 -6
  32. package/dist/members/util.js.map +1 -1
  33. package/dist/multistream/mediaRequestManager.js +1 -1
  34. package/dist/multistream/mediaRequestManager.js.map +1 -1
  35. package/dist/multistream/remoteMedia.js +34 -5
  36. package/dist/multistream/remoteMedia.js.map +1 -1
  37. package/dist/multistream/remoteMediaGroup.js +42 -2
  38. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  39. package/dist/multistream/sendSlotManager.js +32 -2
  40. package/dist/multistream/sendSlotManager.js.map +1 -1
  41. package/dist/reachability/index.js +5 -10
  42. package/dist/reachability/index.js.map +1 -1
  43. package/dist/types/constants.d.ts +22 -0
  44. package/dist/types/locus-info/index.d.ts +0 -9
  45. package/dist/types/meeting/brbState.d.ts +0 -1
  46. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  47. package/dist/types/meeting/index.d.ts +35 -18
  48. package/dist/types/meeting/request.d.ts +9 -1
  49. package/dist/types/meeting/request.type.d.ts +74 -0
  50. package/dist/types/meeting/type.d.ts +9 -0
  51. package/dist/types/meeting/util.d.ts +3 -0
  52. package/dist/types/members/index.d.ts +10 -7
  53. package/dist/types/members/request.d.ts +1 -1
  54. package/dist/types/members/util.d.ts +7 -3
  55. package/dist/types/multistream/remoteMedia.d.ts +20 -1
  56. package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
  57. package/dist/types/multistream/sendSlotManager.d.ts +16 -0
  58. package/dist/types/reachability/index.d.ts +2 -2
  59. package/dist/webinar/index.js +1 -1
  60. package/package.json +24 -25
  61. package/src/constants.ts +23 -2
  62. package/src/locus-info/index.ts +47 -82
  63. package/src/media/index.ts +2 -2
  64. package/src/meeting/brbState.ts +9 -7
  65. package/src/meeting/in-meeting-actions.ts +13 -0
  66. package/src/meeting/index.ts +168 -42
  67. package/src/meeting/request.ts +16 -0
  68. package/src/meeting/request.type.ts +64 -0
  69. package/src/meeting/type.ts +9 -0
  70. package/src/meeting/util.ts +13 -0
  71. package/src/meetings/index.ts +3 -2
  72. package/src/members/index.ts +13 -10
  73. package/src/members/request.ts +2 -2
  74. package/src/members/util.ts +16 -4
  75. package/src/multistream/mediaRequestManager.ts +7 -7
  76. package/src/multistream/remoteMedia.ts +34 -4
  77. package/src/multistream/remoteMediaGroup.ts +37 -2
  78. package/src/multistream/sendSlotManager.ts +34 -2
  79. package/src/reachability/index.ts +5 -13
  80. package/test/unit/spec/locus-info/index.js +177 -83
  81. package/test/unit/spec/media/index.ts +107 -0
  82. package/test/unit/spec/meeting/brbState.ts +9 -9
  83. package/test/unit/spec/meeting/in-meeting-actions.ts +6 -0
  84. package/test/unit/spec/meeting/index.js +694 -60
  85. package/test/unit/spec/meeting/request.js +71 -0
  86. package/test/unit/spec/meeting/utils.js +21 -0
  87. package/test/unit/spec/meetings/index.js +2 -0
  88. package/test/unit/spec/members/index.js +68 -9
  89. package/test/unit/spec/members/request.js +2 -2
  90. package/test/unit/spec/members/utils.js +27 -7
  91. package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
  92. package/test/unit/spec/multistream/remoteMedia.ts +66 -2
  93. package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
  94. package/test/unit/spec/reachability/index.ts +2 -6
@@ -15,7 +15,7 @@ import {cloneDeepWith, debounce, isEmpty} from 'lodash';
15
15
  import LoggerProxy from '../common/logs/logger-proxy';
16
16
 
17
17
  import {ReceiveSlot, ReceiveSlotEvents} from './receiveSlot';
18
- import {getMaxFs} from './remoteMedia';
18
+ import {MAX_FS_VALUES} from './remoteMedia';
19
19
 
20
20
  export interface ActiveSpeakerPolicyInfo {
21
21
  policy: 'active-speaker';
@@ -123,12 +123,12 @@ export class MediaRequestManager {
123
123
 
124
124
  private getDegradedClientRequests(clientRequests: ClientRequestsMap) {
125
125
  const maxFsLimits = [
126
- getMaxFs('best'),
127
- getMaxFs('large'),
128
- getMaxFs('medium'),
129
- getMaxFs('small'),
130
- getMaxFs('very small'),
131
- getMaxFs('thumbnail'),
126
+ MAX_FS_VALUES['1080p'],
127
+ MAX_FS_VALUES['720p'],
128
+ MAX_FS_VALUES['540p'],
129
+ MAX_FS_VALUES['360p'],
130
+ MAX_FS_VALUES['180p'],
131
+ MAX_FS_VALUES['90p'],
132
132
  ];
133
133
 
134
134
  // reduce max-fs until total macroblocks is below limit
@@ -19,17 +19,18 @@ export type RemoteVideoResolution =
19
19
  | 'large' // 1080p or less
20
20
  | 'best'; // highest possible resolution
21
21
 
22
- const MAX_FS_VALUES = {
22
+ export const MAX_FS_VALUES = {
23
23
  '90p': 60,
24
24
  '180p': 240,
25
25
  '360p': 920,
26
+ '540p': 2040,
26
27
  '720p': 3600,
27
28
  '1080p': 8192,
28
29
  };
29
30
 
30
31
  /**
31
32
  * Converts pane size into h264 maxFs
32
- * @param {PaneSize} paneSize
33
+ * @param {RemoteVideoResolution} paneSize
33
34
  * @returns {number}
34
35
  */
35
36
  export function getMaxFs(paneSize: RemoteVideoResolution): number {
@@ -89,6 +90,13 @@ export class RemoteMedia extends EventsScope {
89
90
 
90
91
  public readonly id: RemoteMediaId;
91
92
 
93
+ /**
94
+ * The max frame size of the media request, used for logging and media requests.
95
+ * Set by setSizeHint() based on video element dimensions.
96
+ * When > 0, this value takes precedence over options.resolution in sendMediaRequest().
97
+ */
98
+ private maxFrameSize = 0;
99
+
92
100
  /**
93
101
  * Constructs RemoteMedia instance
94
102
  *
@@ -136,15 +144,34 @@ export class RemoteMedia extends EventsScope {
136
144
  fs = MAX_FS_VALUES['180p'];
137
145
  } else if (height < getThresholdHeight(360)) {
138
146
  fs = MAX_FS_VALUES['360p'];
147
+ } else if (height < getThresholdHeight(540)) {
148
+ fs = MAX_FS_VALUES['540p'];
139
149
  } else if (height <= 720) {
140
150
  fs = MAX_FS_VALUES['720p'];
141
151
  } else {
142
152
  fs = MAX_FS_VALUES['1080p'];
143
153
  }
144
154
 
155
+ this.maxFrameSize = fs;
145
156
  this.receiveSlot?.setMaxFs(fs);
146
157
  }
147
158
 
159
+ /**
160
+ * Get the current effective maxFs value that would be used in media requests
161
+ * @returns {number | undefined} The maxFs value, or undefined if no constraints
162
+ */
163
+ public getEffectiveMaxFs(): number | undefined {
164
+ if (this.maxFrameSize > 0) {
165
+ return this.maxFrameSize;
166
+ }
167
+
168
+ if (this.options.resolution) {
169
+ return getMaxFs(this.options.resolution);
170
+ }
171
+
172
+ return undefined;
173
+ }
174
+
148
175
  /**
149
176
  * Invalidates the remote media by clearing the reference to a receive slot and
150
177
  * cancelling the media request.
@@ -185,6 +212,9 @@ export class RemoteMedia extends EventsScope {
185
212
  throw new Error('sendMediaRequest() called on an invalidated RemoteMedia instance');
186
213
  }
187
214
 
215
+ // Use maxFrameSize from setSizeHint if available, otherwise fallback to options.resolution
216
+ const maxFs = this.getEffectiveMaxFs();
217
+
188
218
  this.mediaRequestId = this.mediaRequestManager.addRequest(
189
219
  {
190
220
  policyInfo: {
@@ -192,9 +222,9 @@ export class RemoteMedia extends EventsScope {
192
222
  csi,
193
223
  },
194
224
  receiveSlots: [this.receiveSlot],
195
- codecInfo: this.options.resolution && {
225
+ codecInfo: maxFs && {
196
226
  codec: 'h264',
197
- maxFs: getMaxFs(this.options.resolution),
227
+ maxFs,
198
228
  },
199
229
  },
200
230
  commit
@@ -215,6 +215,9 @@ export class RemoteMediaGroup {
215
215
  private sendActiveSpeakerMediaRequest(commit: boolean) {
216
216
  this.cancelActiveSpeakerMediaRequest(false);
217
217
 
218
+ // Calculate the effective maxFs based on all unpinned RemoteMedia instances
219
+ const effectiveMaxFs = this.getEffectiveMaxFsForActiveSpeaker();
220
+
218
221
  this.mediaRequestId = this.mediaRequestManager.addRequest(
219
222
  {
220
223
  policyInfo: {
@@ -230,9 +233,9 @@ export class RemoteMediaGroup {
230
233
  receiveSlots: this.unpinnedRemoteMedia.map((remoteMedia) =>
231
234
  remoteMedia.getUnderlyingReceiveSlot()
232
235
  ) as ReceiveSlot[],
233
- codecInfo: this.options.resolution && {
236
+ codecInfo: effectiveMaxFs && {
234
237
  codec: 'h264',
235
- maxFs: getMaxFs(this.options.resolution),
238
+ maxFs: effectiveMaxFs,
236
239
  },
237
240
  },
238
241
  commit
@@ -300,4 +303,36 @@ export class RemoteMediaGroup {
300
303
  this.unpinnedRemoteMedia.includes(remoteMedia) || this.pinnedRemoteMedia.includes(remoteMedia)
301
304
  );
302
305
  }
306
+
307
+ /**
308
+ * Calculate the effective maxFs for the active speaker media request based on unpinned RemoteMedia instances
309
+ * @returns {number | undefined} The calculated maxFs value, or undefined if no constraints
310
+ * @private
311
+ */
312
+ private getEffectiveMaxFsForActiveSpeaker(): number | undefined {
313
+ // Get all effective maxFs values from unpinned RemoteMedia instances
314
+ const maxFsValues = this.unpinnedRemoteMedia
315
+ .map((remoteMedia) => remoteMedia.getEffectiveMaxFs())
316
+ .filter((maxFs) => maxFs !== undefined);
317
+
318
+ // Use the highest maxFs value to ensure we don't under-request resolution for any instance
319
+ if (maxFsValues.length > 0) {
320
+ return Math.max(...maxFsValues);
321
+ }
322
+
323
+ // Fall back to group's resolution option
324
+ if (this.options.resolution) {
325
+ return getMaxFs(this.options.resolution);
326
+ }
327
+
328
+ return undefined;
329
+ }
330
+
331
+ /**
332
+ * Get the current effective maxFs that would be used for the active speaker media request
333
+ * @returns {number | undefined} The effective maxFs value
334
+ */
335
+ public getEffectiveMaxFs(): number | undefined {
336
+ return this.getEffectiveMaxFsForActiveSpeaker();
337
+ }
303
338
  }
@@ -7,10 +7,20 @@ import {
7
7
  StreamState,
8
8
  } from '@webex/internal-media-core';
9
9
 
10
+ /**
11
+ * This class is used to manage the sendSlots for the given media types.
12
+ */
10
13
  export default class SendSlotManager {
11
14
  private readonly slots: Map<MediaType, SendSlot> = new Map();
12
15
  private readonly LoggerProxy: any;
16
+ private readonly sourceStateOverrides: Map<MediaType, StreamState> = new Map();
13
17
 
18
+ /**
19
+ * Constructor for SendSlotManager
20
+ *
21
+ * @param {any} LoggerProxy is used to log the messages
22
+ * @constructor
23
+ */
14
24
  constructor(LoggerProxy: any) {
15
25
  this.LoggerProxy = LoggerProxy;
16
26
  }
@@ -93,7 +103,7 @@ export default class SendSlotManager {
93
103
  public setSourceStateOverride(mediaType: MediaType, state: StreamState | null) {
94
104
  if (mediaType !== MediaType.VideoMain) {
95
105
  throw new Error(
96
- `sendSlotManager cannot set source state override which media type is ${mediaType}`
106
+ `Invalid media type '${mediaType}'. Source state overrides are only applicable to ${MediaType.VideoMain}.`
97
107
  );
98
108
  }
99
109
 
@@ -103,17 +113,39 @@ export default class SendSlotManager {
103
113
  throw new Error(`Slot for ${mediaType} does not exist`);
104
114
  }
105
115
 
116
+ const currentStateOverride = this.getSourceStateOverride(mediaType);
117
+ if (currentStateOverride === state) {
118
+ return;
119
+ }
120
+
106
121
  if (state) {
107
122
  slot.setSourceStateOverride(state);
123
+ this.sourceStateOverrides.set(mediaType, state);
108
124
  } else {
109
125
  slot.clearSourceStateOverride();
126
+ this.sourceStateOverrides.delete(mediaType);
110
127
  }
111
128
 
112
129
  this.LoggerProxy.logger.info(
113
- `SendSlotsManager->setSourceStateOverride#set source state override for ${mediaType} to ${state}`
130
+ `SendSlotManager->setSourceStateOverride#set source state override for ${mediaType} to ${state}`
114
131
  );
115
132
  }
116
133
 
134
+ /**
135
+ * Gets the source state override for the given media type.
136
+ * @param {MediaType} mediaType - The type of media to get the source state override for.
137
+ * @returns {StreamState | null} - The current source state override or null if not set.
138
+ */
139
+ private getSourceStateOverride(mediaType: MediaType): StreamState | null {
140
+ if (mediaType !== MediaType.VideoMain) {
141
+ throw new Error(
142
+ `Invalid media type '${mediaType}'. Source state overrides are only applicable to ${MediaType.VideoMain}.`
143
+ );
144
+ }
145
+
146
+ return this.sourceStateOverrides.get(mediaType) || null;
147
+ }
148
+
117
149
  /**
118
150
  * This method publishes the given stream to the sendSlot for the given mediaType
119
151
  * @param {MediaType} mediaType MediaType of the sendSlot to which a stream needs to be published (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
@@ -140,22 +140,14 @@ export default class Reachability extends EventsScope {
140
140
 
141
141
  /**
142
142
  * Checks if the given subnet is reachable
143
- * @param {string} mediaServerIp - media server ip
143
+ * @param {string} selectedSubnetFirstOctet - selected subnet first octet, e.g. "10" for "10.X.X.X"
144
144
  * @returns {boolean | null} true if reachable, false if not reachable, null if mediaServerIp is not provided
145
145
  * @public
146
146
  * @memberof Reachability
147
147
  */
148
- public isSubnetReachable(mediaServerIp?: string): boolean | null {
149
- if (!mediaServerIp) {
150
- LoggerProxy.logger.error(`Reachability:index#isSubnetReachable --> mediaServerIp is null`);
151
-
152
- return null;
153
- }
154
-
155
- const subnetFirstOctet = mediaServerIp.split('.')[0];
156
-
148
+ public isSubnetReachable(selectedSubnetFirstOctet: string): boolean | null {
157
149
  LoggerProxy.logger.info(
158
- `Reachability:index#isSubnetReachable --> Looking for subnet: ${subnetFirstOctet}.X.X.X`
150
+ `Reachability:index#isSubnetReachable --> Looking for subnet: ${selectedSubnetFirstOctet}.X.X.X`
159
151
  );
160
152
 
161
153
  const matchingReachedClusters = Object.values(this.clusterReachability).reduce(
@@ -167,7 +159,7 @@ export default class Reachability extends EventsScope {
167
159
  const subnet = reachedSubnetsArray[i];
168
160
  const reachedSubnetFirstOctet = subnet.split('.')[0];
169
161
 
170
- if (subnetFirstOctet === reachedSubnetFirstOctet) {
162
+ if (selectedSubnetFirstOctet === reachedSubnetFirstOctet) {
171
163
  acc.add(cluster.name);
172
164
  }
173
165
 
@@ -186,7 +178,7 @@ export default class Reachability extends EventsScope {
186
178
  );
187
179
 
188
180
  LoggerProxy.logger.info(
189
- `Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${subnetFirstOctet}.X.X.X`
181
+ `Reachability:index#isSubnetReachable --> Found ${matchingReachedClusters.size} clusters that use the subnet ${selectedSubnetFirstOctet}.X.X.X`
190
182
  );
191
183
 
192
184
  return matchingReachedClusters.size > 0;
@@ -305,7 +305,7 @@ describe('plugin-meetings', () => {
305
305
  {state: newControls.rdcControl}
306
306
  );
307
307
  });
308
-
308
+
309
309
  it('should trigger the CONTROLS_POLLING_QA_CHANGED event when necessary', () => {
310
310
  locusInfo.controls = {};
311
311
  locusInfo.emitScoped = sinon.stub();
@@ -834,39 +834,6 @@ describe('plugin-meetings', () => {
834
834
  );
835
835
  });
836
836
 
837
- it('should update the deltaParticipants object', () => {
838
- const prev = locusInfo.deltaParticipants;
839
-
840
- locusInfo.updateParticipantDeltas(newParticipants);
841
-
842
- assert.notEqual(locusInfo.deltaParticipants, prev);
843
- });
844
-
845
- it('should update the delta property on all changed states', () => {
846
- locusInfo.updateParticipantDeltas(newParticipants);
847
-
848
- const [exampleParticipant] = locusInfo.deltaParticipants;
849
-
850
- assert.isTrue(exampleParticipant.delta.audioStatus);
851
- assert.isTrue(exampleParticipant.delta.videoSlidesStatus);
852
- assert.isTrue(exampleParticipant.delta.videoStatus);
853
- });
854
-
855
- it('should include the person details of the changed participant', () => {
856
- locusInfo.updateParticipantDeltas(newParticipants);
857
-
858
- const [exampleParticipant] = locusInfo.deltaParticipants;
859
-
860
- assert.equal(exampleParticipant.person, newParticipants[0].person);
861
- });
862
-
863
- it('should clear deltaParticipants when no changes occured', () => {
864
- locusInfo.participants = [...newParticipants];
865
-
866
- locusInfo.updateParticipantDeltas(locusInfo.participants);
867
-
868
- assert.isTrue(locusInfo.deltaParticipants.length === 0);
869
- });
870
837
 
871
838
  it('should call with participant display name', () => {
872
839
  const failureParticipant = [
@@ -2108,6 +2075,38 @@ describe('plugin-meetings', () => {
2108
2075
  assert.isFunction(locusParser.onDeltaAction);
2109
2076
  });
2110
2077
 
2078
+ it("#updateLocusInfo invokes updateLocusUrl before updateMeetingInfo", () => {
2079
+ const callOrder = [];
2080
+ sinon.stub(locusInfo, "updateControls");
2081
+ sinon.stub(locusInfo, "updateConversationUrl");
2082
+ sinon.stub(locusInfo, "updateCreated");
2083
+ sinon.stub(locusInfo, "updateFullState");
2084
+ sinon.stub(locusInfo, "updateHostInfo");
2085
+ sinon.stub(locusInfo, "updateMeetingInfo").callsFake(() => {
2086
+ callOrder.push("updateMeetingInfo");
2087
+ });
2088
+ sinon.stub(locusInfo, "updateMediaShares");
2089
+ sinon.stub(locusInfo, "updateParticipantsUrl");
2090
+ sinon.stub(locusInfo, "updateReplace");
2091
+ sinon.stub(locusInfo, "updateSelf");
2092
+ sinon.stub(locusInfo, "updateLocusUrl").callsFake(() => {
2093
+ callOrder.push("updateLocusUrl");
2094
+ });
2095
+ sinon.stub(locusInfo, "updateAclUrl");
2096
+ sinon.stub(locusInfo, "updateBasequence");
2097
+ sinon.stub(locusInfo, "updateSequence");
2098
+ sinon.stub(locusInfo, "updateMemberShip");
2099
+ sinon.stub(locusInfo, "updateIdentifiers");
2100
+ sinon.stub(locusInfo, "updateEmbeddedApps");
2101
+ sinon.stub(locusInfo, "updateResources");
2102
+ sinon.stub(locusInfo, "compareAndUpdate");
2103
+
2104
+ locusInfo.updateLocusInfo(locus);
2105
+
2106
+ // Ensure updateLocusUrl is called before updateMeetingInfo if both are called
2107
+ assert.deepEqual(callOrder, ['updateLocusUrl', 'updateMeetingInfo']);
2108
+ });
2109
+
2111
2110
  it('#updateLocusInfo ignores breakout LEFT message', () => {
2112
2111
  const newLocus = {
2113
2112
  self: {
@@ -2159,10 +2158,11 @@ describe('plugin-meetings', () => {
2159
2158
  assert.notCalled(locusInfo.compareAndUpdate);
2160
2159
  });
2161
2160
 
2161
+
2162
+
2162
2163
  it('onFullLocus() updates the working-copy of locus parser', () => {
2163
2164
  const eventType = 'fakeEvent';
2164
2165
 
2165
- sandbox.stub(locusInfo, 'updateParticipantDeltas');
2166
2166
  sandbox.stub(locusInfo, 'updateLocusInfo');
2167
2167
  sandbox.stub(locusInfo, 'updateParticipants');
2168
2168
  sandbox.stub(locusInfo, 'isMeetingActive');
@@ -2182,7 +2182,6 @@ describe('plugin-meetings', () => {
2182
2182
  const oldWorkingCopy = locusParser.workingCopy;
2183
2183
 
2184
2184
  const spies = [
2185
- sandbox.stub(locusInfo, 'updateParticipantDeltas'),
2186
2185
  sandbox.stub(locusInfo, 'updateLocusInfo'),
2187
2186
  sandbox.stub(locusInfo, 'updateParticipants'),
2188
2187
  sandbox.stub(locusInfo, 'isMeetingActive'),
@@ -2257,7 +2256,7 @@ describe('plugin-meetings', () => {
2257
2256
 
2258
2257
  it('applyLocusDeltaData gets delta locus on DESYNC action if we have a syncUrl', () => {
2259
2258
  const {DESYNC} = LocusDeltaParser.loci;
2260
- const fakeDeltaLocus = {id: 'fake delta locus'};
2259
+ const fakeDeltaLocus = {baseSequence: {}, id: 'fake delta locus'};
2261
2260
  const meeting = {
2262
2261
  meetingRequest: {
2263
2262
  getLocusDTO: sandbox.stub().resolves({body: fakeDeltaLocus}),
@@ -2392,25 +2391,22 @@ describe('plugin-meetings', () => {
2392
2391
  };
2393
2392
  });
2394
2393
 
2395
- it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', () => {
2394
+ it('applyLocusDeltaData gets full locus on DESYNC action if we do not have a syncUrl and destroys the meeting if that fails', async () => {
2396
2395
  meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
2397
2396
 
2398
2397
  locusInfo.locusParser.workingCopy = {}; // no syncUrl
2399
2398
 
2400
- // Since we have a promise inside a function we want to test that's not returned,
2401
- // we will wait and stub it's last function to resolve this waiting promise.
2402
- return new Promise((resolve) => {
2403
- webex.meetings.destroy.callsFake(() => resolve());
2404
- locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2405
- }).then(() => {
2406
- assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
2399
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2407
2400
 
2408
- assert.notCalled(meeting.locusInfo.handleLocusDelta);
2409
- assert.notCalled(meeting.locusInfo.onFullLocus);
2410
- assert.notCalled(locusInfo.locusParser.resume);
2401
+ await testUtils.flushPromises();
2411
2402
 
2412
- assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2413
- });
2403
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'fullSyncUrl'});
2404
+
2405
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2406
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2407
+ assert.notCalled(locusInfo.locusParser.resume);
2408
+
2409
+ assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2414
2410
  });
2415
2411
 
2416
2412
  it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails, does a full locus sync', () => {
@@ -2447,39 +2443,62 @@ describe('plugin-meetings', () => {
2447
2443
  });
2448
2444
  });
2449
2445
 
2450
- it('applyLocusDeltaData destroys the meeting if both delta sync and full sync fail', () => {
2446
+ it('applyLocusDeltaData first tries a delta sync on DESYNC action and if that fails with 403, it does not do a full locus sync', async () => {
2447
+ const fake403Error = new Error('fake error');
2448
+ fake403Error.statusCode = 403;
2449
+
2450
+ meeting.meetingRequest.getLocusDTO.onCall(0).rejects(fake403Error);
2451
+
2452
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2453
+
2454
+ await testUtils.flushPromises();
2455
+
2456
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {url: 'deltaSyncUrl'});
2457
+
2458
+ assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2459
+ correlationId: meeting.correlationId,
2460
+ url: 'deltaSyncUrl',
2461
+ reason: 'fake error',
2462
+ errorName: 'Error',
2463
+ stack: sinon.match.any,
2464
+ code: sinon.match.any,
2465
+ });
2466
+
2467
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2468
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2469
+ assert.notCalled(locusInfo.locusParser.resume);
2470
+ });
2471
+
2472
+ it('applyLocusDeltaData destroys the meeting if both delta sync and full sync fail', async () => {
2451
2473
  meeting.meetingRequest.getLocusDTO.rejects(new Error('fake error'));
2452
2474
 
2453
- // Since we have a promise inside a function we want to test that's not returned,
2454
- // we will wait and stub it's last function to resolve this waiting promise.
2455
- return new Promise((resolve) => {
2456
- webex.meetings.destroy.callsFake(() => resolve());
2457
- locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2458
- }).then(() => {
2459
- assert.calledTwice(meeting.meetingRequest.getLocusDTO);
2475
+ locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2460
2476
 
2461
- assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [
2462
- {url: 'deltaSyncUrl'},
2463
- ]);
2464
- assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [
2465
- {url: 'fullSyncUrl'},
2466
- ]);
2477
+ await testUtils.flushPromises();
2467
2478
 
2468
- assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2469
- correlationId: meeting.correlationId,
2470
- url: 'deltaSyncUrl',
2471
- reason: 'fake error',
2472
- errorName: 'Error',
2473
- stack: sinon.match.any,
2474
- code: sinon.match.any,
2475
- });
2479
+ assert.calledTwice(meeting.meetingRequest.getLocusDTO);
2476
2480
 
2477
- assert.notCalled(meeting.locusInfo.handleLocusDelta);
2478
- assert.notCalled(meeting.locusInfo.onFullLocus);
2479
- assert.notCalled(locusInfo.locusParser.resume);
2481
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[0].args, [
2482
+ {url: 'deltaSyncUrl'},
2483
+ ]);
2484
+ assert.deepEqual(meeting.meetingRequest.getLocusDTO.getCalls()[1].args, [
2485
+ {url: 'fullSyncUrl'},
2486
+ ]);
2480
2487
 
2481
- assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2488
+ assert.calledWith(sendBehavioralMetricStub, 'js_sdk_locus_delta_sync_failed', {
2489
+ correlationId: meeting.correlationId,
2490
+ url: 'deltaSyncUrl',
2491
+ reason: 'fake error',
2492
+ errorName: 'Error',
2493
+ stack: sinon.match.any,
2494
+ code: sinon.match.any,
2482
2495
  });
2496
+
2497
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2498
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2499
+ assert.notCalled(locusInfo.locusParser.resume);
2500
+
2501
+ assert.calledOnceWithExactly(webex.meetings.destroy, meeting, 'LOCUS_DTO_SYNC_FAILED');
2483
2502
  });
2484
2503
  });
2485
2504
 
@@ -2509,9 +2528,7 @@ describe('plugin-meetings', () => {
2509
2528
  });
2510
2529
 
2511
2530
  it('onDeltaLocus merges delta participants with existing participants', () => {
2512
- const FAKE_DELTA_PARTICIPANTS = [
2513
- {id: '1111'}, {id: '2222'}
2514
- ]
2531
+ const FAKE_DELTA_PARTICIPANTS = [{id: '1111'}, {id: '2222'}];
2515
2532
  fakeLocus.participants = FAKE_DELTA_PARTICIPANTS;
2516
2533
 
2517
2534
  sinon.spy(locusInfo, 'mergeParticipants');
@@ -2519,9 +2536,87 @@ describe('plugin-meetings', () => {
2519
2536
  const existingParticipants = locusInfo.participants;
2520
2537
 
2521
2538
  locusInfo.onDeltaLocus(fakeLocus);
2522
- assert.calledOnceWithExactly(locusInfo.mergeParticipants, existingParticipants, FAKE_DELTA_PARTICIPANTS);
2539
+ assert.calledOnceWithExactly(
2540
+ locusInfo.mergeParticipants,
2541
+ existingParticipants,
2542
+ FAKE_DELTA_PARTICIPANTS
2543
+ );
2523
2544
  assert.calledWith(locusInfo.updateParticipants, FAKE_DELTA_PARTICIPANTS, false);
2524
2545
  });
2546
+
2547
+ [true, false].forEach((isDelta) =>
2548
+ it(`applyLocusDeltaData - handles empty ${
2549
+ isDelta ? 'delta' : 'full'
2550
+ } DTO in response`, async () => {
2551
+ const {DESYNC} = LocusDeltaParser.loci;
2552
+ const fakeFullLocusDto = {};
2553
+ const meeting = {
2554
+ meetingRequest: {
2555
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
2556
+ },
2557
+ locusInfo: {
2558
+ onFullLocus: sandbox.stub(),
2559
+ handleLocusDelta: sandbox.stub(),
2560
+ },
2561
+ locusUrl: 'fake locus FULL url',
2562
+ };
2563
+
2564
+ sinon.stub(locusInfo.locusParser, 'resume').resolves();
2565
+
2566
+ if (isDelta) {
2567
+ locusInfo.locusParser.workingCopy = {syncUrl: 'fake locus DELTA url'};
2568
+ } else {
2569
+ locusInfo.locusParser.workingCopy = {}; // no syncUrl (to trigger FULL DTO request)
2570
+ }
2571
+
2572
+ await locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2573
+
2574
+ await testUtils.flushPromises();
2575
+
2576
+ if (isDelta) {
2577
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2578
+ url: 'fake locus DELTA url',
2579
+ });
2580
+ } else {
2581
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2582
+ url: 'fake locus FULL url',
2583
+ });
2584
+ }
2585
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2586
+ assert.notCalled(meeting.locusInfo.onFullLocus);
2587
+ assert.calledOnce(locusInfo.locusParser.resume);
2588
+ })
2589
+ );
2590
+
2591
+ it(`applyLocusDeltaData - handles the case when we get FULL DTO when we asked for DELTA DTO`, async () => {
2592
+ const {DESYNC} = LocusDeltaParser.loci;
2593
+ const fakeFullLocusDto = {someStuff: 'data'}; // non-empty DTO, without baseSequence
2594
+ const meeting = {
2595
+ meetingRequest: {
2596
+ getLocusDTO: sandbox.stub().resolves({body: fakeFullLocusDto}),
2597
+ },
2598
+ locusInfo: {
2599
+ onFullLocus: sandbox.stub(),
2600
+ handleLocusDelta: sandbox.stub(),
2601
+ },
2602
+ locusUrl: 'fake locus FULL url',
2603
+ };
2604
+
2605
+ sinon.stub(locusInfo.locusParser, 'resume').resolves();
2606
+
2607
+ locusInfo.locusParser.workingCopy = {syncUrl: 'fake locus DELTA url'};
2608
+
2609
+ await locusInfo.applyLocusDeltaData(DESYNC, fakeLocus, meeting);
2610
+
2611
+ await testUtils.flushPromises();
2612
+
2613
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusDTO, {
2614
+ url: 'fake locus DELTA url',
2615
+ });
2616
+ assert.notCalled(meeting.locusInfo.handleLocusDelta);
2617
+ assert.calledOnceWithExactly(meeting.locusInfo.onFullLocus, fakeFullLocusDto);
2618
+ assert.calledOnce(locusInfo.locusParser.resume);
2619
+ });
2525
2620
  });
2526
2621
 
2527
2622
  describe('#updateLocusCache', () => {
@@ -2934,10 +3029,9 @@ describe('plugin-meetings', () => {
2934
3029
  beforeEach(() => {
2935
3030
  clock = sinon.useFakeTimers();
2936
3031
 
2937
- sinon.stub(locusInfo, 'updateParticipantDeltas');
2938
3032
  sinon.stub(locusInfo, 'updateParticipants');
2939
- sinon.stub(locusInfo, 'isMeetingActive'),
2940
- sinon.stub(locusInfo, 'handleOneOnOneEvent'),
3033
+ sinon.stub(locusInfo, 'isMeetingActive');
3034
+ sinon.stub(locusInfo, 'handleOneOnOneEvent');
2941
3035
  (updateLocusInfoStub = sinon.stub(locusInfo, 'updateLocusInfo'));
2942
3036
  syncRequestStub = sinon.stub().resolves({body: {}});
2943
3037