@webex/plugin-meetings 3.12.0-next.1 → 3.12.0-next.11
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.
- package/dist/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/hashTree/constants.js +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +56 -31
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +38 -14
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meeting/index.js +427 -323
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +1 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/metrics/constants.js +5 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +116 -2
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +12 -2
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/locus-info/index.d.ts +8 -3
- package/dist/types/meeting/index.d.ts +24 -1
- package/dist/types/metrics/constants.d.ts +4 -0
- package/dist/types/multistream/sendSlotManager.d.ts +23 -1
- package/dist/webinar/index.js +325 -220
- package/dist/webinar/index.js.map +1 -1
- package/package.json +15 -15
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +60 -36
- package/src/hashTree/utils.ts +17 -0
- package/src/locus-info/index.ts +48 -24
- package/src/meeting/index.ts +165 -57
- package/src/meeting/util.ts +1 -0
- package/src/metrics/constants.ts +5 -0
- package/src/multistream/sendSlotManager.ts +97 -3
- package/src/webinar/index.ts +120 -18
- package/test/unit/spec/hashTree/hashTreeParser.ts +295 -30
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/locus-info/index.js +47 -22
- package/test/unit/spec/meeting/index.js +179 -48
- package/test/unit/spec/meeting/utils.js +4 -0
- package/test/unit/spec/meetings/index.js +3 -3
- package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
- package/test/unit/spec/webinar/index.ts +193 -8
package/src/webinar/index.ts
CHANGED
|
@@ -131,6 +131,12 @@ const Webinar = WebexPlugin.extend({
|
|
|
131
131
|
* @returns {Promise<void>}
|
|
132
132
|
*/
|
|
133
133
|
async cleanupPSDataChannel() {
|
|
134
|
+
if (this._pendingOnlineListener) {
|
|
135
|
+
// @ts-ignore - Fix type
|
|
136
|
+
this.webex.internal.llm.off('online', this._pendingOnlineListener);
|
|
137
|
+
this._pendingOnlineListener = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
134
140
|
const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
|
|
135
141
|
|
|
136
142
|
// @ts-ignore - Fix type
|
|
@@ -148,12 +154,63 @@ const Webinar = WebexPlugin.extend({
|
|
|
148
154
|
);
|
|
149
155
|
},
|
|
150
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Ensures practice-session token exists before registering the practice LLM channel.
|
|
159
|
+
* @param {object} meeting
|
|
160
|
+
* @returns {Promise<string|undefined>}
|
|
161
|
+
*/
|
|
162
|
+
async ensurePracticeSessionDatachannelToken(meeting) {
|
|
163
|
+
// @ts-ignore
|
|
164
|
+
const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
|
|
165
|
+
|
|
166
|
+
if (!isDataChannelTokenEnabled) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// @ts-ignore
|
|
171
|
+
const cachedToken = this.webex.internal.llm.getDatachannelToken(
|
|
172
|
+
DataChannelTokenType.PracticeSession
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (cachedToken) {
|
|
176
|
+
return cachedToken;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const refreshResponse = await meeting.refreshDataChannelToken();
|
|
181
|
+
const {datachannelToken, dataChannelTokenType} = refreshResponse?.body ?? {};
|
|
182
|
+
|
|
183
|
+
if (!datachannelToken) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
this.webex.internal.llm.setDatachannelToken(
|
|
189
|
+
datachannelToken,
|
|
190
|
+
dataChannelTokenType || DataChannelTokenType.PracticeSession
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return datachannelToken;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
LoggerProxy.logger.warn(
|
|
196
|
+
`Webinar:index#ensurePracticeSessionDatachannelToken --> failed to proactively refresh practice-session token: ${
|
|
197
|
+
error?.message || String(error)
|
|
198
|
+
}`
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
151
205
|
/**
|
|
152
206
|
* Connects to low latency mercury and reconnects if the address has changed
|
|
153
207
|
* It will also disconnect if called when the meeting has ended
|
|
154
208
|
* @returns {Promise}
|
|
155
209
|
*/
|
|
156
210
|
async updatePSDataChannel() {
|
|
211
|
+
this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
|
|
212
|
+
const invocationSequence = this._updatePSDataChannelSequence;
|
|
213
|
+
|
|
157
214
|
const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
|
|
158
215
|
const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
|
|
159
216
|
|
|
@@ -164,29 +221,16 @@ const Webinar = WebexPlugin.extend({
|
|
|
164
221
|
}
|
|
165
222
|
|
|
166
223
|
// @ts-ignore - Fix type
|
|
167
|
-
const {
|
|
168
|
-
|
|
169
|
-
info: {practiceSessionDatachannelUrl = undefined} = {},
|
|
170
|
-
self: {practiceSessionDatachannelToken = undefined} = {},
|
|
171
|
-
} = meeting?.locusInfo || {};
|
|
224
|
+
const {url = undefined, info: {practiceSessionDatachannelUrl = undefined} = {}} =
|
|
225
|
+
meeting?.locusInfo || {};
|
|
172
226
|
|
|
173
227
|
// @ts-ignore
|
|
174
|
-
|
|
228
|
+
let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
|
|
175
229
|
DataChannelTokenType.PracticeSession
|
|
176
230
|
);
|
|
177
231
|
|
|
178
|
-
const finalToken = currentToken ?? practiceSessionDatachannelToken;
|
|
179
|
-
|
|
180
232
|
const isCaptionBoxOn = this.webex.internal.voicea.getIsCaptionBoxOn();
|
|
181
233
|
|
|
182
|
-
if (!currentToken && practiceSessionDatachannelToken) {
|
|
183
|
-
// @ts-ignore
|
|
184
|
-
this.webex.internal.llm.setDatachannelToken(
|
|
185
|
-
practiceSessionDatachannelToken,
|
|
186
|
-
DataChannelTokenType.PracticeSession
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
234
|
if (!practiceSessionDatachannelUrl) {
|
|
191
235
|
return undefined;
|
|
192
236
|
}
|
|
@@ -205,9 +249,68 @@ const Webinar = WebexPlugin.extend({
|
|
|
205
249
|
await this.cleanupPSDataChannel();
|
|
206
250
|
}
|
|
207
251
|
|
|
252
|
+
// Ensure the default session data channel is connected before connecting the practice session.
|
|
253
|
+
// Subscribe before checking isConnected() to avoid a race where the 'online' event fires
|
|
254
|
+
// between the check and the subscription — Mercury does not replay missed events.
|
|
255
|
+
if (!this._pendingOnlineListener) {
|
|
256
|
+
const onDefaultSessionConnected = () => {
|
|
257
|
+
this._pendingOnlineListener = null;
|
|
258
|
+
// @ts-ignore - Fix type
|
|
259
|
+
this.webex.internal.llm.off('online', onDefaultSessionConnected);
|
|
260
|
+
this.updatePSDataChannel();
|
|
261
|
+
};
|
|
262
|
+
this._pendingOnlineListener = onDefaultSessionConnected;
|
|
263
|
+
// @ts-ignore - Fix type
|
|
264
|
+
this.webex.internal.llm.on('online', onDefaultSessionConnected);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// @ts-ignore - Fix type
|
|
268
|
+
if (!this.webex.internal.llm.isConnected()) {
|
|
269
|
+
LoggerProxy.logger.info(
|
|
270
|
+
'Webinar:index#updatePSDataChannel --> default session not yet connected, deferring practice session connect.'
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Default session is already connected — cancel the pending listener and proceed
|
|
277
|
+
if (this._pendingOnlineListener) {
|
|
278
|
+
// @ts-ignore - Fix type
|
|
279
|
+
this.webex.internal.llm.off('online', this._pendingOnlineListener);
|
|
280
|
+
this._pendingOnlineListener = null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const refreshedPracticeSessionToken = await this.ensurePracticeSessionDatachannelToken(meeting);
|
|
284
|
+
|
|
285
|
+
const latestPracticeSessionDatachannelUrl = get(
|
|
286
|
+
meeting,
|
|
287
|
+
'locusInfo.info.practiceSessionDatachannelUrl'
|
|
288
|
+
);
|
|
289
|
+
const isStillPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
|
|
290
|
+
|
|
291
|
+
// Skip stale invocations after async refresh to avoid reconnecting a session
|
|
292
|
+
// that was already updated/cleaned by a newer state transition.
|
|
293
|
+
if (
|
|
294
|
+
invocationSequence !== this._updatePSDataChannelSequence ||
|
|
295
|
+
!isStillPracticeSession ||
|
|
296
|
+
!latestPracticeSessionDatachannelUrl ||
|
|
297
|
+
latestPracticeSessionDatachannelUrl !== practiceSessionDatachannelUrl
|
|
298
|
+
) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (refreshedPracticeSessionToken) {
|
|
303
|
+
practiceSessionDatachannelToken = refreshedPracticeSessionToken;
|
|
304
|
+
}
|
|
305
|
+
|
|
208
306
|
// @ts-ignore - Fix type
|
|
209
307
|
return this.webex.internal.llm
|
|
210
|
-
.registerAndConnect(
|
|
308
|
+
.registerAndConnect(
|
|
309
|
+
url,
|
|
310
|
+
practiceSessionDatachannelUrl,
|
|
311
|
+
practiceSessionDatachannelToken,
|
|
312
|
+
LLM_PRACTICE_SESSION
|
|
313
|
+
)
|
|
211
314
|
.then((registerAndConnectResult) => {
|
|
212
315
|
// @ts-ignore - Fix type
|
|
213
316
|
this.webex.internal.llm.off(
|
|
@@ -366,7 +469,6 @@ const Webinar = WebexPlugin.extend({
|
|
|
366
469
|
|
|
367
470
|
/**
|
|
368
471
|
* view all webcast attendees
|
|
369
|
-
* @param {string} queryString
|
|
370
472
|
* @returns {Promise}
|
|
371
473
|
*/
|
|
372
474
|
async viewAllWebcastAttendees() {
|
|
@@ -553,7 +553,7 @@ describe('HashTreeParser', () => {
|
|
|
553
553
|
);
|
|
554
554
|
|
|
555
555
|
// Verify callback was called with OBJECTS_UPDATED and correct updatedObjects list
|
|
556
|
-
assert.calledWith(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
556
|
+
assert.calledWith(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
557
557
|
updatedObjects: [
|
|
558
558
|
{
|
|
559
559
|
htMeta: {
|
|
@@ -596,6 +596,41 @@ describe('HashTreeParser', () => {
|
|
|
596
596
|
});
|
|
597
597
|
});
|
|
598
598
|
|
|
599
|
+
it('initializes "main" before "self" regardless of order from Locus', async () => {
|
|
600
|
+
const parser = createHashTreeParser({dataSets: [], locus: null}, null);
|
|
601
|
+
|
|
602
|
+
// Locus returns datasets in non-priority order: atd-active, main, self
|
|
603
|
+
const atdActiveDataSet = createDataSet('atd-active', 4, 500);
|
|
604
|
+
const mainDataSet = createDataSet('main', 16, 1100);
|
|
605
|
+
const selfDataSet = createDataSet('self', 1, 2100);
|
|
606
|
+
|
|
607
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
|
|
608
|
+
atdActiveDataSet,
|
|
609
|
+
mainDataSet,
|
|
610
|
+
selfDataSet,
|
|
611
|
+
]);
|
|
612
|
+
|
|
613
|
+
mockSyncRequest(webexRequest, selfDataSet.url);
|
|
614
|
+
mockSyncRequest(webexRequest, mainDataSet.url);
|
|
615
|
+
mockSyncRequest(webexRequest, atdActiveDataSet.url);
|
|
616
|
+
|
|
617
|
+
await parser.initializeFromMessage({
|
|
618
|
+
dataSets: [],
|
|
619
|
+
visibleDataSetsUrl,
|
|
620
|
+
locusUrl,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Verify sync requests were sent in priority order: main, self, then atd-active
|
|
624
|
+
const syncCalls = webexRequest
|
|
625
|
+
.getCalls()
|
|
626
|
+
.filter((call) => call.args[0]?.method === 'POST' && call.args[0]?.uri?.endsWith('/sync'));
|
|
627
|
+
|
|
628
|
+
expect(syncCalls).to.have.lengthOf(3);
|
|
629
|
+
expect(syncCalls[0].args[0].uri).to.equal(`${mainDataSet.url}/sync`);
|
|
630
|
+
expect(syncCalls[1].args[0].uri).to.equal(`${selfDataSet.url}/sync`);
|
|
631
|
+
expect(syncCalls[2].args[0].uri).to.equal(`${atdActiveDataSet.url}/sync`);
|
|
632
|
+
});
|
|
633
|
+
|
|
599
634
|
it('handles sync response that has locusStateElements undefined', async () => {
|
|
600
635
|
const minimalInitialLocus = {
|
|
601
636
|
dataSets: [],
|
|
@@ -788,7 +823,7 @@ describe('HashTreeParser', () => {
|
|
|
788
823
|
expect(parser.dataSets.self.version).to.equal(2100);
|
|
789
824
|
expect(parser.dataSets['atd-unmuted'].version).to.equal(3100);
|
|
790
825
|
|
|
791
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
826
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
792
827
|
updatedObjects: [
|
|
793
828
|
{
|
|
794
829
|
htMeta: {
|
|
@@ -861,6 +896,116 @@ describe('HashTreeParser', () => {
|
|
|
861
896
|
});
|
|
862
897
|
});
|
|
863
898
|
|
|
899
|
+
it('handles updates to control entries correctly', () => {
|
|
900
|
+
const parser = createHashTreeParser();
|
|
901
|
+
|
|
902
|
+
const mainPutItemsSpy = sinon.spy(parser.dataSets.main.hashTree, 'putItems');
|
|
903
|
+
|
|
904
|
+
// Create a locus update with new htMeta information for some things
|
|
905
|
+
const locusUpdate = {
|
|
906
|
+
dataSets: [
|
|
907
|
+
createDataSet('main', 16, 1100),
|
|
908
|
+
],
|
|
909
|
+
locus: {
|
|
910
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f',
|
|
911
|
+
htMeta: {
|
|
912
|
+
elementId: {
|
|
913
|
+
type: 'locus',
|
|
914
|
+
id: 0,
|
|
915
|
+
version: 200, // same version
|
|
916
|
+
},
|
|
917
|
+
dataSetNames: ['main'],
|
|
918
|
+
},
|
|
919
|
+
participants: [],
|
|
920
|
+
controls: {
|
|
921
|
+
lock: {
|
|
922
|
+
locked: true,
|
|
923
|
+
htMeta: {
|
|
924
|
+
elementId: {
|
|
925
|
+
type: 'ControlEntry',
|
|
926
|
+
id: 10100,
|
|
927
|
+
version: 100,
|
|
928
|
+
},
|
|
929
|
+
dataSetNames: ['main'],
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
stream: {
|
|
933
|
+
streaming: true,
|
|
934
|
+
htMeta: {
|
|
935
|
+
elementId: {
|
|
936
|
+
type: 'ControlEntry',
|
|
937
|
+
id: 10101,
|
|
938
|
+
version: 100,
|
|
939
|
+
},
|
|
940
|
+
dataSetNames: ['main'],
|
|
941
|
+
},
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// Call handleLocusUpdate
|
|
948
|
+
parser.handleLocusUpdate(locusUpdate);
|
|
949
|
+
|
|
950
|
+
// Verify putItems was called on main hash tree with correct data
|
|
951
|
+
assert.calledOnceWithExactly(mainPutItemsSpy, [
|
|
952
|
+
{type: 'locus', id: 0, version: 200},
|
|
953
|
+
{type: 'ControlEntry', id: 10100, version: 100},
|
|
954
|
+
{type: 'ControlEntry', id: 10101, version: 100}
|
|
955
|
+
]);
|
|
956
|
+
|
|
957
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
958
|
+
updatedObjects: [
|
|
959
|
+
{
|
|
960
|
+
htMeta: {
|
|
961
|
+
elementId: {
|
|
962
|
+
type: 'ControlEntry',
|
|
963
|
+
id: 10100,
|
|
964
|
+
version: 100,
|
|
965
|
+
},
|
|
966
|
+
dataSetNames: ['main'],
|
|
967
|
+
},
|
|
968
|
+
data: {
|
|
969
|
+
lock: {
|
|
970
|
+
locked: true,
|
|
971
|
+
htMeta: {
|
|
972
|
+
elementId: {
|
|
973
|
+
type: 'ControlEntry',
|
|
974
|
+
id: 10100,
|
|
975
|
+
version: 100,
|
|
976
|
+
},
|
|
977
|
+
dataSetNames: ['main'],
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
htMeta: {
|
|
984
|
+
elementId: {
|
|
985
|
+
type: 'ControlEntry',
|
|
986
|
+
id: 10101,
|
|
987
|
+
version: 100,
|
|
988
|
+
},
|
|
989
|
+
dataSetNames: ['main'],
|
|
990
|
+
},
|
|
991
|
+
data: {
|
|
992
|
+
stream: {
|
|
993
|
+
streaming: true,
|
|
994
|
+
htMeta: {
|
|
995
|
+
elementId: {
|
|
996
|
+
type: 'ControlEntry',
|
|
997
|
+
id: 10101,
|
|
998
|
+
version: 100,
|
|
999
|
+
},
|
|
1000
|
+
dataSetNames: ['main'],
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
1004
|
+
}
|
|
1005
|
+
],
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
|
|
864
1009
|
it('handles unknown datasets gracefully', () => {
|
|
865
1010
|
const parser = createHashTreeParser();
|
|
866
1011
|
|
|
@@ -899,7 +1044,7 @@ describe('HashTreeParser', () => {
|
|
|
899
1044
|
assert.calledOnceWithExactly(mainPutItemsSpy, [{type: 'locus', id: 0, version: 201}]);
|
|
900
1045
|
|
|
901
1046
|
// Verify callback was called only for known dataset
|
|
902
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1047
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
903
1048
|
updatedObjects: [
|
|
904
1049
|
{
|
|
905
1050
|
htMeta: {
|
|
@@ -999,7 +1144,7 @@ describe('HashTreeParser', () => {
|
|
|
999
1144
|
assert.calledOnceWithExactly(selfPutItemSpy, {type: 'metadata', id: 5, version: 51});
|
|
1000
1145
|
|
|
1001
1146
|
// Verify callback was called with metadata object and removed dataset objects
|
|
1002
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1147
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1003
1148
|
updatedObjects: [
|
|
1004
1149
|
// updated metadata object:
|
|
1005
1150
|
{
|
|
@@ -1160,7 +1305,7 @@ describe('HashTreeParser', () => {
|
|
|
1160
1305
|
assert.notCalled(atdUnmutedPutItemsSpy);
|
|
1161
1306
|
|
|
1162
1307
|
// Verify callback was called with the updated object
|
|
1163
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1308
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1164
1309
|
updatedObjects: [
|
|
1165
1310
|
{
|
|
1166
1311
|
htMeta: {
|
|
@@ -1388,7 +1533,7 @@ describe('HashTreeParser', () => {
|
|
|
1388
1533
|
]);
|
|
1389
1534
|
|
|
1390
1535
|
// Verify callback was called with OBJECTS_UPDATED and all updated objects
|
|
1391
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1536
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1392
1537
|
updatedObjects: [
|
|
1393
1538
|
{
|
|
1394
1539
|
htMeta: {
|
|
@@ -1453,9 +1598,7 @@ describe('HashTreeParser', () => {
|
|
|
1453
1598
|
parser.handleMessage(sentinelMessage, 'sentinel message');
|
|
1454
1599
|
|
|
1455
1600
|
// Verify callback was called with MEETING_ENDED
|
|
1456
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
1457
|
-
updatedObjects: undefined,
|
|
1458
|
-
});
|
|
1601
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1459
1602
|
|
|
1460
1603
|
// Verify that all timers were stopped
|
|
1461
1604
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
@@ -1477,9 +1620,7 @@ describe('HashTreeParser', () => {
|
|
|
1477
1620
|
parser.handleMessage(sentinelMessage, 'sentinel message');
|
|
1478
1621
|
|
|
1479
1622
|
// Verify callback was called with MEETING_ENDED
|
|
1480
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
1481
|
-
updatedObjects: undefined,
|
|
1482
|
-
});
|
|
1623
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1483
1624
|
|
|
1484
1625
|
// Verify that all timers were stopped
|
|
1485
1626
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
@@ -1575,7 +1716,7 @@ describe('HashTreeParser', () => {
|
|
|
1575
1716
|
);
|
|
1576
1717
|
|
|
1577
1718
|
// Verify that callback was called with synced objects
|
|
1578
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1719
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1579
1720
|
updatedObjects: [
|
|
1580
1721
|
{
|
|
1581
1722
|
htMeta: {
|
|
@@ -1637,9 +1778,7 @@ describe('HashTreeParser', () => {
|
|
|
1637
1778
|
await clock.tickAsync(1000);
|
|
1638
1779
|
|
|
1639
1780
|
// Verify callback was called with MEETING_ENDED
|
|
1640
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
1641
|
-
updatedObjects: undefined,
|
|
1642
|
-
});
|
|
1781
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1643
1782
|
|
|
1644
1783
|
// Verify all timers are stopped
|
|
1645
1784
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
@@ -1702,9 +1841,7 @@ describe('HashTreeParser', () => {
|
|
|
1702
1841
|
await clock.tickAsync(1000);
|
|
1703
1842
|
|
|
1704
1843
|
// Verify callback was called with MEETING_ENDED
|
|
1705
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
1706
|
-
updatedObjects: undefined,
|
|
1707
|
-
});
|
|
1844
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
1708
1845
|
|
|
1709
1846
|
// Verify all timers are stopped
|
|
1710
1847
|
Object.values(parser.dataSets).forEach((ds: any) => {
|
|
@@ -1942,7 +2079,7 @@ describe('HashTreeParser', () => {
|
|
|
1942
2079
|
assert.equal(parser.dataSets.attendees.hashTree.numLeaves, 8);
|
|
1943
2080
|
|
|
1944
2081
|
// Verify callback was called with the metadata update (appears twice - processed once for visible dataset changes, once in main loop)
|
|
1945
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2082
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
1946
2083
|
updatedObjects: [
|
|
1947
2084
|
{
|
|
1948
2085
|
htMeta: {
|
|
@@ -2062,6 +2199,98 @@ describe('HashTreeParser', () => {
|
|
|
2062
2199
|
await checkAsyncDatasetInitialization(parser, newDataSet);
|
|
2063
2200
|
});
|
|
2064
2201
|
|
|
2202
|
+
it('initializes new visible data sets in priority order', async () => {
|
|
2203
|
+
// Create a parser that only has "self" as visible (no "main")
|
|
2204
|
+
const initialLocusWithoutMain = {
|
|
2205
|
+
dataSets: [createDataSet('self', 1, 2000)],
|
|
2206
|
+
locus: {
|
|
2207
|
+
...exampleInitialLocus.locus,
|
|
2208
|
+
},
|
|
2209
|
+
};
|
|
2210
|
+
const metadataWithoutMain = {
|
|
2211
|
+
...exampleMetadata,
|
|
2212
|
+
visibleDataSets: [
|
|
2213
|
+
{
|
|
2214
|
+
name: 'self',
|
|
2215
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2216
|
+
},
|
|
2217
|
+
],
|
|
2218
|
+
};
|
|
2219
|
+
const parser = createHashTreeParser(initialLocusWithoutMain, metadataWithoutMain);
|
|
2220
|
+
|
|
2221
|
+
// Verify "main" is not visible initially
|
|
2222
|
+
expect(parser.visibleDataSets.some((vds) => vds.name === 'main')).to.be.false;
|
|
2223
|
+
|
|
2224
|
+
// Stub updateItems on self hash tree to return true
|
|
2225
|
+
sinon.stub(parser.dataSets.self.hashTree, 'updateItems').returns([true]);
|
|
2226
|
+
|
|
2227
|
+
// Send a message that adds "main" and "atd-active" as new visible datasets.
|
|
2228
|
+
// Neither has info in dataSets, so both require async initialization.
|
|
2229
|
+
const newMainDataSet = createDataSet('main', 16, 6000);
|
|
2230
|
+
const newAtdActiveDataSet = createDataSet('atd-active', 4, 7000);
|
|
2231
|
+
|
|
2232
|
+
const message = {
|
|
2233
|
+
dataSets: [createDataSet('self', 1, 2100)],
|
|
2234
|
+
visibleDataSetsUrl,
|
|
2235
|
+
locusUrl,
|
|
2236
|
+
locusStateElements: [
|
|
2237
|
+
{
|
|
2238
|
+
htMeta: {
|
|
2239
|
+
elementId: {
|
|
2240
|
+
type: 'metadata' as const,
|
|
2241
|
+
id: 5,
|
|
2242
|
+
version: 51,
|
|
2243
|
+
},
|
|
2244
|
+
dataSetNames: ['self'],
|
|
2245
|
+
},
|
|
2246
|
+
data: {
|
|
2247
|
+
visibleDataSets: [
|
|
2248
|
+
{
|
|
2249
|
+
name: 'self',
|
|
2250
|
+
url: 'https://locus-a.wbx2.com/locus/api/v1/loci/97d64a5f/participant/713e9f99/datasets/self',
|
|
2251
|
+
},
|
|
2252
|
+
// listed in non-priority order: atd-active before main
|
|
2253
|
+
{name: 'atd-active', url: newAtdActiveDataSet.url},
|
|
2254
|
+
{name: 'main', url: newMainDataSet.url},
|
|
2255
|
+
],
|
|
2256
|
+
},
|
|
2257
|
+
},
|
|
2258
|
+
],
|
|
2259
|
+
};
|
|
2260
|
+
|
|
2261
|
+
// Mock getAllVisibleDataSetsFromLocus to return both new datasets (in non-priority order)
|
|
2262
|
+
mockGetAllDataSetsMetadata(webexRequest, visibleDataSetsUrl, [
|
|
2263
|
+
newAtdActiveDataSet,
|
|
2264
|
+
newMainDataSet,
|
|
2265
|
+
]);
|
|
2266
|
+
mockSyncRequest(webexRequest, newMainDataSet.url);
|
|
2267
|
+
mockSyncRequest(webexRequest, newAtdActiveDataSet.url);
|
|
2268
|
+
|
|
2269
|
+
parser.handleMessage(message, 'add main and atd-active datasets');
|
|
2270
|
+
|
|
2271
|
+
// Wait for the async initialization (queueMicrotask) to complete
|
|
2272
|
+
await clock.tickAsync(0);
|
|
2273
|
+
|
|
2274
|
+
// Verify both datasets are initialized
|
|
2275
|
+
expect(parser.dataSets.main?.hashTree).to.exist;
|
|
2276
|
+
expect(parser.dataSets['atd-active']?.hashTree).to.exist;
|
|
2277
|
+
|
|
2278
|
+
// Verify sync requests were sent in priority order: "main" before "atd-active",
|
|
2279
|
+
// even though atd-active was listed first in both the message and the Locus response
|
|
2280
|
+
const syncCalls = webexRequest
|
|
2281
|
+
.getCalls()
|
|
2282
|
+
.filter(
|
|
2283
|
+
(call) =>
|
|
2284
|
+
call.args[0]?.method === 'POST' &&
|
|
2285
|
+
call.args[0]?.uri?.endsWith('/sync') &&
|
|
2286
|
+
(call.args[0]?.uri?.includes('/main/') || call.args[0]?.uri?.includes('/atd-active/'))
|
|
2287
|
+
);
|
|
2288
|
+
|
|
2289
|
+
expect(syncCalls).to.have.lengthOf(2);
|
|
2290
|
+
expect(syncCalls[0].args[0].uri).to.equal(`${newMainDataSet.url}/sync`);
|
|
2291
|
+
expect(syncCalls[1].args[0].uri).to.equal(`${newAtdActiveDataSet.url}/sync`);
|
|
2292
|
+
});
|
|
2293
|
+
|
|
2065
2294
|
it('emits MEETING_ENDED if async init of a new visible dataset fails with 404', async () => {
|
|
2066
2295
|
const parser = createHashTreeParser();
|
|
2067
2296
|
|
|
@@ -2128,9 +2357,7 @@ describe('HashTreeParser', () => {
|
|
|
2128
2357
|
await clock.tickAsync(0);
|
|
2129
2358
|
|
|
2130
2359
|
// Verify callback was called with MEETING_ENDED
|
|
2131
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
2132
|
-
updatedObjects: undefined,
|
|
2133
|
-
});
|
|
2360
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
2134
2361
|
});
|
|
2135
2362
|
|
|
2136
2363
|
it('handles removal of visible data set', async () => {
|
|
@@ -2193,7 +2420,7 @@ describe('HashTreeParser', () => {
|
|
|
2193
2420
|
assert.isUndefined(parser.dataSets['atd-unmuted'].timer);
|
|
2194
2421
|
|
|
2195
2422
|
// Verify callback was called with the metadata update and the removed objects (metadata appears twice - processed once for dataset changes, once in main loop)
|
|
2196
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2423
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2197
2424
|
updatedObjects: [
|
|
2198
2425
|
{
|
|
2199
2426
|
htMeta: {
|
|
@@ -2812,7 +3039,7 @@ describe('HashTreeParser', () => {
|
|
|
2812
3039
|
parser.handleMessage(updateMessage, 'update with newer version');
|
|
2813
3040
|
|
|
2814
3041
|
// Callback should be called with the update
|
|
2815
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
3042
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2816
3043
|
updatedObjects: [
|
|
2817
3044
|
{
|
|
2818
3045
|
htMeta: {
|
|
@@ -2883,7 +3110,7 @@ describe('HashTreeParser', () => {
|
|
|
2883
3110
|
parser.handleMessage(removalMessage, 'removal of non-existent object');
|
|
2884
3111
|
|
|
2885
3112
|
// Callback should be called with the removal
|
|
2886
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
3113
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
2887
3114
|
updatedObjects: [
|
|
2888
3115
|
{
|
|
2889
3116
|
htMeta: {
|
|
@@ -3018,7 +3245,7 @@ describe('HashTreeParser', () => {
|
|
|
3018
3245
|
parser.handleMessage(mixedMessage, 'mixed updates');
|
|
3019
3246
|
|
|
3020
3247
|
// Callback should be called with only the valid updates (participant 1 v110 and participant 3 v10)
|
|
3021
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
3248
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.OBJECTS_UPDATED,
|
|
3022
3249
|
updatedObjects: [
|
|
3023
3250
|
{
|
|
3024
3251
|
htMeta: {
|
|
@@ -3196,9 +3423,7 @@ describe('HashTreeParser', () => {
|
|
|
3196
3423
|
parser.handleMessage(sentinelMessage as any, 'sentinel message');
|
|
3197
3424
|
|
|
3198
3425
|
// Callback should be called with MEETING_ENDED
|
|
3199
|
-
assert.calledOnceWithExactly(callback, LocusInfoUpdateType.MEETING_ENDED
|
|
3200
|
-
updatedObjects: undefined,
|
|
3201
|
-
});
|
|
3426
|
+
assert.calledOnceWithExactly(callback, {updateType: LocusInfoUpdateType.MEETING_ENDED});
|
|
3202
3427
|
});
|
|
3203
3428
|
});
|
|
3204
3429
|
|
|
@@ -3557,4 +3782,44 @@ describe('HashTreeParser', () => {
|
|
|
3557
3782
|
assert.notCalled(callback);
|
|
3558
3783
|
});
|
|
3559
3784
|
});
|
|
3785
|
+
|
|
3786
|
+
describe('#cleanUp', () => {
|
|
3787
|
+
it('should stop the parser, clear all timers and clear all dataSets', () => {
|
|
3788
|
+
const parser = createHashTreeParser();
|
|
3789
|
+
|
|
3790
|
+
// Send a message to set up sync timers via runSyncAlgorithm
|
|
3791
|
+
const message = {
|
|
3792
|
+
dataSets: [
|
|
3793
|
+
{
|
|
3794
|
+
...createDataSet('main', 16, 1100),
|
|
3795
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
|
|
3796
|
+
},
|
|
3797
|
+
],
|
|
3798
|
+
visibleDataSetsUrl,
|
|
3799
|
+
locusUrl,
|
|
3800
|
+
heartbeatIntervalMs: 5000,
|
|
3801
|
+
locusStateElements: [
|
|
3802
|
+
{
|
|
3803
|
+
htMeta: {
|
|
3804
|
+
elementId: {type: 'locus' as const, id: 0, version: 201},
|
|
3805
|
+
dataSetNames: ['main'],
|
|
3806
|
+
},
|
|
3807
|
+
data: {someData: 'value'},
|
|
3808
|
+
},
|
|
3809
|
+
],
|
|
3810
|
+
};
|
|
3811
|
+
|
|
3812
|
+
parser.handleMessage(message, 'setup timers');
|
|
3813
|
+
|
|
3814
|
+
// Verify timers were set by handleMessage
|
|
3815
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
3816
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
3817
|
+
|
|
3818
|
+
parser.cleanUp();
|
|
3819
|
+
|
|
3820
|
+
expect(parser.state).to.equal('stopped');
|
|
3821
|
+
expect(parser.visibleDataSets).to.deep.equal([]);
|
|
3822
|
+
expect(parser.dataSets).to.deep.equal({});
|
|
3823
|
+
});
|
|
3824
|
+
});
|
|
3560
3825
|
});
|