@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.60

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 (158) 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 +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +6 -3
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +38 -24
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +10 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +716 -370
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +22 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/locusRetry.js +23 -8
  27. package/dist/interceptors/locusRetry.js.map +1 -1
  28. package/dist/interpretation/index.js +10 -1
  29. package/dist/interpretation/index.js.map +1 -1
  30. package/dist/interpretation/siLanguage.js +1 -1
  31. package/dist/locus-info/controlsUtils.js +4 -1
  32. package/dist/locus-info/controlsUtils.js.map +1 -1
  33. package/dist/locus-info/index.js +289 -87
  34. package/dist/locus-info/index.js.map +1 -1
  35. package/dist/locus-info/types.js +19 -0
  36. package/dist/locus-info/types.js.map +1 -1
  37. package/dist/media/index.js +3 -1
  38. package/dist/media/index.js.map +1 -1
  39. package/dist/media/properties.js +1 -0
  40. package/dist/media/properties.js.map +1 -1
  41. package/dist/meeting/in-meeting-actions.js +3 -1
  42. package/dist/meeting/in-meeting-actions.js.map +1 -1
  43. package/dist/meeting/index.js +907 -535
  44. package/dist/meeting/index.js.map +1 -1
  45. package/dist/meeting/util.js +19 -2
  46. package/dist/meeting/util.js.map +1 -1
  47. package/dist/meetings/index.js +231 -78
  48. package/dist/meetings/index.js.map +1 -1
  49. package/dist/meetings/meetings.types.js +6 -1
  50. package/dist/meetings/meetings.types.js.map +1 -1
  51. package/dist/meetings/request.js +39 -0
  52. package/dist/meetings/request.js.map +1 -1
  53. package/dist/meetings/util.js +79 -5
  54. package/dist/meetings/util.js.map +1 -1
  55. package/dist/member/index.js +10 -0
  56. package/dist/member/index.js.map +1 -1
  57. package/dist/member/types.js.map +1 -1
  58. package/dist/member/util.js +3 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +4 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/codec/constants.js +63 -0
  63. package/dist/multistream/codec/constants.js.map +1 -0
  64. package/dist/multistream/mediaRequestManager.js +62 -15
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/receiveSlot.js +9 -0
  67. package/dist/multistream/receiveSlot.js.map +1 -1
  68. package/dist/reactions/reactions.type.js.map +1 -1
  69. package/dist/recording-controller/index.js +1 -3
  70. package/dist/recording-controller/index.js.map +1 -1
  71. package/dist/types/config.d.ts +2 -0
  72. package/dist/types/constants.d.ts +2 -0
  73. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  74. package/dist/types/controls-options-manager/index.d.ts +10 -0
  75. package/dist/types/hashTree/constants.d.ts +1 -0
  76. package/dist/types/hashTree/hashTreeParser.d.ts +92 -16
  77. package/dist/types/hashTree/utils.d.ts +11 -0
  78. package/dist/types/index.d.ts +2 -0
  79. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  80. package/dist/types/locus-info/index.d.ts +46 -6
  81. package/dist/types/locus-info/types.d.ts +21 -1
  82. package/dist/types/media/properties.d.ts +1 -0
  83. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  84. package/dist/types/meeting/index.d.ts +87 -3
  85. package/dist/types/meeting/util.d.ts +8 -0
  86. package/dist/types/meetings/index.d.ts +30 -2
  87. package/dist/types/meetings/meetings.types.d.ts +15 -0
  88. package/dist/types/meetings/request.d.ts +14 -0
  89. package/dist/types/member/index.d.ts +1 -0
  90. package/dist/types/member/types.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +1 -0
  92. package/dist/types/metrics/constants.d.ts +3 -0
  93. package/dist/types/multistream/codec/constants.d.ts +7 -0
  94. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  95. package/dist/types/reactions/reactions.type.d.ts +3 -0
  96. package/dist/webinar/index.js +361 -235
  97. package/dist/webinar/index.js.map +1 -1
  98. package/package.json +22 -22
  99. package/src/aiEnableRequest/index.ts +16 -0
  100. package/src/breakouts/breakout.ts +3 -1
  101. package/src/breakouts/index.ts +31 -0
  102. package/src/config.ts +2 -0
  103. package/src/constants.ts +5 -1
  104. package/src/controls-options-manager/constants.ts +14 -1
  105. package/src/controls-options-manager/index.ts +47 -24
  106. package/src/controls-options-manager/util.ts +81 -1
  107. package/src/hashTree/constants.ts +9 -0
  108. package/src/hashTree/hashTreeParser.ts +429 -183
  109. package/src/hashTree/utils.ts +17 -0
  110. package/src/index.ts +5 -0
  111. package/src/interceptors/locusRetry.ts +25 -4
  112. package/src/interpretation/index.ts +25 -8
  113. package/src/locus-info/controlsUtils.ts +3 -1
  114. package/src/locus-info/index.ts +291 -97
  115. package/src/locus-info/types.ts +25 -1
  116. package/src/media/index.ts +3 -0
  117. package/src/media/properties.ts +1 -0
  118. package/src/meeting/in-meeting-actions.ts +4 -0
  119. package/src/meeting/index.ts +388 -33
  120. package/src/meeting/util.ts +20 -2
  121. package/src/meetings/index.ts +134 -44
  122. package/src/meetings/meetings.types.ts +19 -0
  123. package/src/meetings/request.ts +43 -0
  124. package/src/meetings/util.ts +97 -1
  125. package/src/member/index.ts +10 -0
  126. package/src/member/types.ts +1 -0
  127. package/src/member/util.ts +3 -0
  128. package/src/metrics/constants.ts +3 -0
  129. package/src/multistream/codec/constants.ts +58 -0
  130. package/src/multistream/mediaRequestManager.ts +119 -28
  131. package/src/multistream/receiveSlot.ts +18 -0
  132. package/src/reactions/reactions.type.ts +3 -0
  133. package/src/recording-controller/index.ts +1 -2
  134. package/src/webinar/index.ts +162 -21
  135. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  136. package/test/unit/spec/breakouts/breakout.ts +9 -3
  137. package/test/unit/spec/breakouts/index.ts +49 -0
  138. package/test/unit/spec/controls-options-manager/index.js +140 -29
  139. package/test/unit/spec/controls-options-manager/util.js +165 -0
  140. package/test/unit/spec/hashTree/hashTreeParser.ts +1508 -149
  141. package/test/unit/spec/hashTree/utils.ts +88 -1
  142. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  143. package/test/unit/spec/interpretation/index.ts +26 -4
  144. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  145. package/test/unit/spec/locus-info/index.js +475 -81
  146. package/test/unit/spec/media/index.ts +31 -0
  147. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  148. package/test/unit/spec/meeting/index.js +1131 -49
  149. package/test/unit/spec/meeting/muteState.js +3 -0
  150. package/test/unit/spec/meeting/utils.js +33 -0
  151. package/test/unit/spec/meetings/index.js +360 -10
  152. package/test/unit/spec/meetings/request.js +141 -0
  153. package/test/unit/spec/meetings/utils.js +189 -0
  154. package/test/unit/spec/member/index.js +7 -0
  155. package/test/unit/spec/member/util.js +24 -0
  156. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  157. package/test/unit/spec/recording-controller/index.js +9 -8
  158. package/test/unit/spec/webinar/index.ts +141 -16
@@ -35,6 +35,7 @@ describe('plugin-meetings', () => {
35
35
  beforeEach(() => {
36
36
  request = {
37
37
  request: sinon.stub().returns(Promise.resolve()),
38
+ locusDeltaRequest: sinon.stub().returns(Promise.resolve()),
38
39
  };
39
40
 
40
41
  controller = new RecordingController(request);
@@ -69,13 +70,13 @@ describe('plugin-meetings', () => {
69
70
 
70
71
  const result = controller.startRecording();
71
72
 
72
- assert.calledWith(request.request, {
73
+ assert.calledWith(request.locusDeltaRequest, {
73
74
  uri: `${locusUrl}/controls`,
74
75
  body: {record: {recording: true, paused: false}},
75
76
  method: HTTP_VERBS.PATCH,
76
77
  });
77
78
 
78
- assert.deepEqual(result, request.request.firstCall.returnValue);
79
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
79
80
  });
80
81
  });
81
82
 
@@ -103,13 +104,13 @@ describe('plugin-meetings', () => {
103
104
 
104
105
  const result = controller.stopRecording();
105
106
 
106
- assert.calledWith(request.request, {
107
+ assert.calledWith(request.locusDeltaRequest, {
107
108
  uri: `${locusUrl}/controls`,
108
109
  body: {record: {recording: false, paused: false}},
109
110
  method: HTTP_VERBS.PATCH,
110
111
  });
111
112
 
112
- assert.deepEqual(result, request.request.firstCall.returnValue);
113
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
113
114
  });
114
115
  });
115
116
 
@@ -139,13 +140,13 @@ describe('plugin-meetings', () => {
139
140
 
140
141
  const result = controller.pauseRecording();
141
142
 
142
- assert.calledWith(request.request, {
143
+ assert.calledWith(request.locusDeltaRequest, {
143
144
  uri: `${locusUrl}/controls`,
144
145
  body: {record: {recording: true, paused: true}},
145
146
  method: HTTP_VERBS.PATCH,
146
147
  });
147
148
 
148
- assert.deepEqual(result, request.request.firstCall.returnValue);
149
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
149
150
  });
150
151
  });
151
152
 
@@ -176,13 +177,13 @@ describe('plugin-meetings', () => {
176
177
 
177
178
  const result = controller.resumeRecording();
178
179
 
179
- assert.calledWith(request.request, {
180
+ assert.calledWith(request.locusDeltaRequest, {
180
181
  uri: `${locusUrl}/controls`,
181
182
  body: {record: {recording: true, paused: false}},
182
183
  method: HTTP_VERBS.PATCH,
183
184
  });
184
185
 
185
- assert.deepEqual(result, request.request.firstCall.returnValue);
186
+ assert.deepEqual(result, request.locusDeltaRequest.firstCall.returnValue);
186
187
  });
187
188
  });
188
189
  });
@@ -33,6 +33,7 @@ describe('plugin-meetings', () => {
33
33
  webex.internal.llm = {
34
34
  getDatachannelToken: sinon.stub().returns(undefined),
35
35
  setDatachannelToken: sinon.stub(),
36
+ isDataChannelTokenEnabled: sinon.stub().resolves(false),
36
37
  isConnected: sinon.stub().returns(false),
37
38
  disconnectLLM: sinon.stub().resolves(),
38
39
  off: sinon.stub(),
@@ -175,6 +176,42 @@ describe('plugin-meetings', () => {
175
176
  });
176
177
  });
177
178
 
179
+ describe('#getValidatedWebinarMeeting', () => {
180
+ it('returns the meeting when its locusUrl matches the webinar locusUrl', () => {
181
+ const meeting = {locusUrl: 'locusUrl'};
182
+ webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
183
+ webinar.locusUrl = 'locusUrl';
184
+
185
+ assert.equal(webinar.getValidatedWebinarMeeting(), meeting);
186
+ });
187
+
188
+ it('returns undefined and warns when the resolved meeting locusUrl does not match', () => {
189
+ const warnStub = sinon.stub(LoggerProxy.logger, 'warn');
190
+ const meeting = {locusUrl: 'other-locus-url'};
191
+ webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
192
+ webinar.locusUrl = 'locusUrl';
193
+
194
+ assert.isUndefined(webinar.getValidatedWebinarMeeting());
195
+ assert.calledOnce(warnStub);
196
+ });
197
+
198
+ it('returns undefined when no meeting is resolved', () => {
199
+ webex.meetings.getMeetingByType = sinon.stub().returns(undefined);
200
+
201
+ assert.isUndefined(webinar.getValidatedWebinarMeeting());
202
+ });
203
+
204
+ it('returns undefined and warns when webinar locusUrl is not yet initialized', () => {
205
+ const warnStub = sinon.stub(LoggerProxy.logger, 'warn');
206
+ const meeting = {locusUrl: 'some-url'};
207
+ webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
208
+ webinar.locusUrl = undefined;
209
+
210
+ assert.isUndefined(webinar.getValidatedWebinarMeeting());
211
+ assert.calledOnce(warnStub);
212
+ });
213
+ });
214
+
178
215
  describe('#cleanUp', () => {
179
216
  it('delegates to cleanupPSDataChannel', () => {
180
217
  const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
@@ -186,17 +223,14 @@ describe('plugin-meetings', () => {
186
223
  });
187
224
 
188
225
  describe('#cleanupPSDataChannel', () => {
189
- let meeting;
226
+ let relayListener;
190
227
 
191
228
  beforeEach(() => {
192
- meeting = {
193
- processRelayEvent: sinon.stub(),
194
- };
195
-
196
- webex.meetings.getMeetingByType = sinon.stub().returns(meeting);
229
+ relayListener = sinon.stub();
230
+ webinar._practiceSessionRelayListener = relayListener;
197
231
  });
198
232
 
199
- it('disconnects the practice session channel and removes the relay listener', async () => {
233
+ it('disconnects the practice session channel and removes the tracked relay listener', async () => {
200
234
  await webinar.cleanupPSDataChannel();
201
235
 
202
236
  assert.calledOnceWithExactly(
@@ -207,8 +241,28 @@ describe('plugin-meetings', () => {
207
241
  assert.calledOnceWithExactly(
208
242
  webex.internal.llm.off,
209
243
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
210
- meeting.processRelayEvent
244
+ relayListener
245
+ );
246
+ assert.isNull(webinar._practiceSessionRelayListener);
247
+ });
248
+
249
+ it('skips relay listener removal when no listener has been tracked', async () => {
250
+ webinar._practiceSessionRelayListener = null;
251
+
252
+ await webinar.cleanupPSDataChannel();
253
+
254
+ const relayOffCalls = webex.internal.llm.off.args.filter(
255
+ ([event]) => event === `event:relay.event:${LLM_PRACTICE_SESSION}`
211
256
  );
257
+ assert.equal(relayOffCalls.length, 0);
258
+ });
259
+
260
+ it('does not consult the meeting collection during cleanup', async () => {
261
+ webex.meetings.getMeetingByType = sinon.stub();
262
+
263
+ await webinar.cleanupPSDataChannel();
264
+
265
+ assert.notCalled(webex.meetings.getMeetingByType);
212
266
  });
213
267
 
214
268
  it('removes a pending online listener if one exists', async () => {
@@ -239,6 +293,7 @@ describe('plugin-meetings', () => {
239
293
  beforeEach(() => {
240
294
  processRelayEvent = sinon.stub();
241
295
  meeting = {
296
+ locusUrl: 'locusUrl',
242
297
  isJoined: sinon.stub().returns(true),
243
298
  processRelayEvent,
244
299
  locusInfo: {
@@ -267,6 +322,65 @@ describe('plugin-meetings', () => {
267
322
  webex.internal.voicea.updateSubchannelSubscriptions = sinon.stub();
268
323
  });
269
324
 
325
+ it('refreshes practice-session token before register when cached token is missing', async () => {
326
+ webex.internal.llm.isDataChannelTokenEnabled.resolves(true);
327
+ webex.internal.llm.getDatachannelToken = sinon.stub().callsFake((tokenType) => {
328
+ if (tokenType === DataChannelTokenType.PracticeSession) return undefined;
329
+
330
+ return undefined;
331
+ });
332
+ meeting.refreshDataChannelToken = sinon.stub().resolves({
333
+ body: {
334
+ datachannelToken: 'ps-token-from-refresh',
335
+ dataChannelTokenType: DataChannelTokenType.PracticeSession,
336
+ },
337
+ });
338
+
339
+ await webinar.updatePSDataChannel();
340
+
341
+ assert.calledOnceWithExactly(meeting.refreshDataChannelToken);
342
+ assert.calledWithExactly(
343
+ webex.internal.llm.setDatachannelToken,
344
+ 'ps-token-from-refresh',
345
+ DataChannelTokenType.PracticeSession
346
+ );
347
+ assert.calledWith(
348
+ webex.internal.llm.registerAndConnect,
349
+ 'locus-url',
350
+ 'dc-url',
351
+ 'ps-token-from-refresh',
352
+ LLM_PRACTICE_SESSION
353
+ );
354
+ });
355
+
356
+ it('does not reconnect if practice-session eligibility changes during async token refresh', async () => {
357
+ webex.internal.llm.isDataChannelTokenEnabled.resolves(true);
358
+ webex.internal.llm.getDatachannelToken = sinon.stub().returns(undefined);
359
+
360
+ let resolveRefresh;
361
+ meeting.refreshDataChannelToken = sinon.stub().returns(
362
+ new Promise((resolve) => {
363
+ resolveRefresh = resolve;
364
+ })
365
+ );
366
+
367
+ const updatePromise = webinar.updatePSDataChannel();
368
+
369
+ webinar.practiceSessionEnabled = false;
370
+
371
+ resolveRefresh({
372
+ body: {
373
+ datachannelToken: 'stale-ps-token',
374
+ dataChannelTokenType: DataChannelTokenType.PracticeSession,
375
+ },
376
+ });
377
+
378
+ const result = await updatePromise;
379
+
380
+ assert.isUndefined(result);
381
+ assert.notCalled(webex.internal.llm.registerAndConnect);
382
+ });
383
+
270
384
  it('no-ops when practice session join eligibility is false', async () => {
271
385
  webinar.practiceSessionEnabled = false;
272
386
  const cleanupPSDataChannelStub = sinon.stub(webinar, 'cleanupPSDataChannel').resolves();
@@ -358,19 +472,30 @@ describe('plugin-meetings', () => {
358
472
  assert.calledOnce(webex.internal.llm.registerAndConnect);
359
473
  });
360
474
 
361
- it('rebinds relay listener after successful connect', async () => {
475
+ it('tracks and binds the relay listener after successful connect', async () => {
362
476
  await webinar.updatePSDataChannel();
363
477
 
478
+ // Stores the exact listener reference for deterministic cleanup
479
+ assert.equal(webinar._practiceSessionRelayListener, processRelayEvent);
364
480
  assert.calledWith(
365
- webex.internal.llm.off,
481
+ webex.internal.llm.on,
366
482
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
367
483
  processRelayEvent
368
484
  );
485
+ });
486
+
487
+ it('removes a previously tracked relay listener before re-binding on reconnect', async () => {
488
+ const previousListener = sinon.stub();
489
+ webinar._practiceSessionRelayListener = previousListener;
490
+
491
+ await webinar.updatePSDataChannel();
492
+
369
493
  assert.calledWith(
370
- webex.internal.llm.on,
494
+ webex.internal.llm.off,
371
495
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
372
- processRelayEvent
496
+ previousListener
373
497
  );
498
+ assert.equal(webinar._practiceSessionRelayListener, processRelayEvent);
374
499
  });
375
500
 
376
501
  it('subscribes to transcription when caption intent is enabled', async () => {
@@ -491,7 +616,7 @@ describe('plugin-meetings', () => {
491
616
  updateMediaShares = sinon.stub()
492
617
  webinar.webex.meetings = {
493
618
  getMeetingByType: sinon.stub().returns({
494
- id: 'meeting-id',
619
+ id: 'meeting-id', locusUrl: 'locusUrl',
495
620
  isJoined: sinon.stub().returns(false),
496
621
  updateLLMConnection: sinon.stub(),
497
622
  shareStatus: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE,
@@ -546,7 +671,7 @@ describe('plugin-meetings', () => {
546
671
 
547
672
  webinar.webex.meetings = {
548
673
  getMeetingByType: sinon.stub().returns({
549
- id: 'meeting-id',
674
+ id: 'meeting-id', locusUrl: 'locusUrl',
550
675
  isJoined: sinon.stub().returns(false),
551
676
  updateLLMConnection: sinon.stub(),
552
677
  shareStatus: SHARE_STATUS.REMOTE_SHARE_ACTIVE,
@@ -974,7 +1099,7 @@ describe('plugin-meetings', () => {
974
1099
  // @ts-ignore
975
1100
  webinar.webex.meetings = {
976
1101
  getMeetingByType: sinon.stub().returns({
977
- id: 'meeting-id',
1102
+ id: 'meeting-id', locusUrl: 'locusUrl',
978
1103
  locusInfo: {
979
1104
  links:{
980
1105
  resources: {
@@ -991,7 +1116,7 @@ describe('plugin-meetings', () => {
991
1116
  it('throws an error if attendeeSearchUrl is not available', async () => {
992
1117
  webinar.webex.meetings = {
993
1118
  getMeetingByType: sinon.stub().returns({
994
- id: 'meeting-id',
1119
+ id: 'meeting-id', locusUrl: 'locusUrl',
995
1120
  locusInfo: {
996
1121
  links:{
997
1122
  resources: {