@webex/plugin-meetings 3.12.0-next.37 → 3.12.0-next.39
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/hashTreeParser.js +4 -4
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meeting/index.js +97 -15
- package/dist/meeting/index.js.map +1 -1
- package/dist/types/meeting/index.d.ts +45 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +1 -1
- package/src/hashTree/hashTreeParser.ts +5 -6
- package/src/meeting/index.ts +92 -17
- package/test/unit/spec/hashTree/hashTreeParser.ts +143 -0
- package/test/unit/spec/meeting/index.js +136 -15
|
@@ -412,6 +412,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
412
412
|
floorGrantPending: boolean;
|
|
413
413
|
hasJoinedOnce: boolean;
|
|
414
414
|
hasWebsocketConnected: boolean;
|
|
415
|
+
private mercuryOnlineHandler?;
|
|
416
|
+
private mercuryOfflineHandler?;
|
|
415
417
|
inMeetingActions: InMeetingActions;
|
|
416
418
|
isLocalShareLive: boolean;
|
|
417
419
|
isRoapInProgress: boolean;
|
|
@@ -1170,6 +1172,22 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1170
1172
|
* @memberof Meeting
|
|
1171
1173
|
*/
|
|
1172
1174
|
setMercuryListener(): void;
|
|
1175
|
+
/**
|
|
1176
|
+
* Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
|
|
1177
|
+
* by setMercuryListener(). Must be called before Locus /leave to avoid
|
|
1178
|
+
* unnecessary syncs/metrics triggered by events received while leaving
|
|
1179
|
+
* (per Locus team recommendation).
|
|
1180
|
+
*
|
|
1181
|
+
* Mercury is a process-wide singleton shared with other plugins, so we
|
|
1182
|
+
* pass the bound handler refs to .off() to avoid clearing every listener
|
|
1183
|
+
* for ONLINE/OFFLINE on the shared emitter.
|
|
1184
|
+
*
|
|
1185
|
+
* Idempotent: subsequent calls are no-ops because the handler refs are
|
|
1186
|
+
* cleared after detaching.
|
|
1187
|
+
* @private
|
|
1188
|
+
* @returns {void}
|
|
1189
|
+
*/
|
|
1190
|
+
private stopListeningForMercuryEvents;
|
|
1173
1191
|
/**
|
|
1174
1192
|
* Close the peer connections and remove them from the class.
|
|
1175
1193
|
* Cleanup any media connection related things.
|
|
@@ -1363,6 +1381,33 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
1363
1381
|
* @returns {void}
|
|
1364
1382
|
*/
|
|
1365
1383
|
private clearLLMHealthCheckTimer;
|
|
1384
|
+
/**
|
|
1385
|
+
* Removes LLM event listeners and clears the health check timer.
|
|
1386
|
+
* Must be called before Locus /leave to avoid unnecessary syncs triggered
|
|
1387
|
+
* by events received while leaving (per Locus team recommendation).
|
|
1388
|
+
* Idempotent: safe to call multiple times; .off() is a no-op when no
|
|
1389
|
+
* matching listener is registered.
|
|
1390
|
+
* @private
|
|
1391
|
+
* @returns {void}
|
|
1392
|
+
*/
|
|
1393
|
+
private stopListeningForLLMEvents;
|
|
1394
|
+
/**
|
|
1395
|
+
* Stops listening on every event bus (LLM, Mercury, voicea/transcription,
|
|
1396
|
+
* annotation) that could otherwise deliver events to this meeting while
|
|
1397
|
+
* Locus is processing /leave or /end. Per the Locus team recommendation,
|
|
1398
|
+
* this must run before the Locus request is dispatched to avoid
|
|
1399
|
+
* unnecessary syncs triggered by in-flight events.
|
|
1400
|
+
*
|
|
1401
|
+
* Voicea (transcription) subscribes to llm 'event:relay.event' internally,
|
|
1402
|
+
* and the annotation plugin subscribes to both mercury and llm, so both
|
|
1403
|
+
* must be torn down alongside the direct LLM/Mercury listeners.
|
|
1404
|
+
*
|
|
1405
|
+
* Idempotent: safe to call multiple times; .off() is a no-op when no
|
|
1406
|
+
* matching listener is registered, and stopTranscription is guarded.
|
|
1407
|
+
* @private
|
|
1408
|
+
* @returns {void}
|
|
1409
|
+
*/
|
|
1410
|
+
private stopListeningForMeetingEvents;
|
|
1366
1411
|
/**
|
|
1367
1412
|
* Disconnects and cleans up the default LLM session listeners/timers.
|
|
1368
1413
|
* @param {Object} options
|
package/dist/webinar/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1249,7 +1249,11 @@ class HashTreeParser {
|
|
|
1249
1249
|
|
|
1250
1250
|
// sync API may return nothing (in that case data will arrive via messages)
|
|
1251
1251
|
// or it may return a response in the same format as messages
|
|
1252
|
+
// We still need to restart the sync timer as a safety net in case the messages don't arrive.
|
|
1253
|
+
this.runSyncAlgorithm(dataSet);
|
|
1254
|
+
|
|
1252
1255
|
if (syncResponse) {
|
|
1256
|
+
// the format of sync response is the same as messages, so we can reuse the same handler
|
|
1253
1257
|
this.handleMessage(syncResponse, 'via sync API');
|
|
1254
1258
|
}
|
|
1255
1259
|
} catch (error) {
|
|
@@ -1392,12 +1396,6 @@ class HashTreeParser {
|
|
|
1392
1396
|
|
|
1393
1397
|
dataSet.hashTree.resize(receivedDataSet.leafCount);
|
|
1394
1398
|
|
|
1395
|
-
// temporary log for the workshop // todo: remove
|
|
1396
|
-
const ourCurrentRootHash = dataSet.hashTree.getRootHash();
|
|
1397
|
-
LoggerProxy.logger.info(
|
|
1398
|
-
`HashTreeParser#runSyncAlgorithm --> ${this.debugId} dataSet="${dataSet.name}" version=${dataSet.version} hashes before starting timer: ours=${ourCurrentRootHash} Locus=${dataSet.root}`
|
|
1399
|
-
);
|
|
1400
|
-
|
|
1401
1399
|
const delay = dataSet.idleMs + this.getWeightedBackoffTime(dataSet.backoff);
|
|
1402
1400
|
|
|
1403
1401
|
if (delay > 0) {
|
|
@@ -1478,6 +1476,7 @@ class HashTreeParser {
|
|
|
1478
1476
|
);
|
|
1479
1477
|
|
|
1480
1478
|
this.enqueueSyncForDataset(dataSet.name, `heartbeat watchdog expired`);
|
|
1479
|
+
this.resetHeartbeatWatchdogs([dataSet]);
|
|
1481
1480
|
}, delay);
|
|
1482
1481
|
}
|
|
1483
1482
|
}
|
package/src/meeting/index.ts
CHANGED
|
@@ -651,6 +651,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
651
651
|
floorGrantPending: boolean;
|
|
652
652
|
hasJoinedOnce: boolean;
|
|
653
653
|
hasWebsocketConnected: boolean;
|
|
654
|
+
private mercuryOnlineHandler?: () => void;
|
|
655
|
+
private mercuryOfflineHandler?: () => void;
|
|
654
656
|
inMeetingActions: InMeetingActions;
|
|
655
657
|
isLocalShareLive: boolean;
|
|
656
658
|
isRoapInProgress: boolean;
|
|
@@ -5157,8 +5159,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5157
5159
|
public setMercuryListener() {
|
|
5158
5160
|
// Client will have a socket manager and handle reconnecting to mercury, when we reconnect to mercury
|
|
5159
5161
|
// if the meeting has active peer connections, it should try to reconnect.
|
|
5160
|
-
|
|
5161
|
-
this.webex.internal.mercury.on(ONLINE, () => {
|
|
5162
|
+
this.mercuryOnlineHandler = () => {
|
|
5162
5163
|
LoggerProxy.logger.info('Meeting:index#setMercuryListener --> Web socket online');
|
|
5163
5164
|
|
|
5164
5165
|
// Only send restore event when it was disconnected before and for connected later
|
|
@@ -5168,15 +5169,47 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
5168
5169
|
});
|
|
5169
5170
|
}
|
|
5170
5171
|
this.hasWebsocketConnected = true;
|
|
5171
|
-
}
|
|
5172
|
+
};
|
|
5172
5173
|
|
|
5173
|
-
|
|
5174
|
-
this.webex.internal.mercury.on(OFFLINE, () => {
|
|
5174
|
+
this.mercuryOfflineHandler = () => {
|
|
5175
5175
|
LoggerProxy.logger.error('Meeting:index#setMercuryListener --> Web socket offline');
|
|
5176
5176
|
Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_FAILURE, {
|
|
5177
5177
|
correlation_id: this.correlationId,
|
|
5178
5178
|
});
|
|
5179
|
-
}
|
|
5179
|
+
};
|
|
5180
|
+
|
|
5181
|
+
// @ts-ignore
|
|
5182
|
+
this.webex.internal.mercury.on(ONLINE, this.mercuryOnlineHandler);
|
|
5183
|
+
// @ts-ignore
|
|
5184
|
+
this.webex.internal.mercury.on(OFFLINE, this.mercuryOfflineHandler);
|
|
5185
|
+
}
|
|
5186
|
+
|
|
5187
|
+
/**
|
|
5188
|
+
* Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
|
|
5189
|
+
* by setMercuryListener(). Must be called before Locus /leave to avoid
|
|
5190
|
+
* unnecessary syncs/metrics triggered by events received while leaving
|
|
5191
|
+
* (per Locus team recommendation).
|
|
5192
|
+
*
|
|
5193
|
+
* Mercury is a process-wide singleton shared with other plugins, so we
|
|
5194
|
+
* pass the bound handler refs to .off() to avoid clearing every listener
|
|
5195
|
+
* for ONLINE/OFFLINE on the shared emitter.
|
|
5196
|
+
*
|
|
5197
|
+
* Idempotent: subsequent calls are no-ops because the handler refs are
|
|
5198
|
+
* cleared after detaching.
|
|
5199
|
+
* @private
|
|
5200
|
+
* @returns {void}
|
|
5201
|
+
*/
|
|
5202
|
+
private stopListeningForMercuryEvents() {
|
|
5203
|
+
if (this.mercuryOnlineHandler) {
|
|
5204
|
+
// @ts-ignore
|
|
5205
|
+
this.webex.internal.mercury.off(ONLINE, this.mercuryOnlineHandler);
|
|
5206
|
+
this.mercuryOnlineHandler = undefined;
|
|
5207
|
+
}
|
|
5208
|
+
if (this.mercuryOfflineHandler) {
|
|
5209
|
+
// @ts-ignore
|
|
5210
|
+
this.webex.internal.mercury.off(OFFLINE, this.mercuryOfflineHandler);
|
|
5211
|
+
this.mercuryOfflineHandler = undefined;
|
|
5212
|
+
}
|
|
5180
5213
|
}
|
|
5181
5214
|
|
|
5182
5215
|
/**
|
|
@@ -6328,6 +6361,49 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6328
6361
|
}
|
|
6329
6362
|
}
|
|
6330
6363
|
|
|
6364
|
+
/**
|
|
6365
|
+
* Removes LLM event listeners and clears the health check timer.
|
|
6366
|
+
* Must be called before Locus /leave to avoid unnecessary syncs triggered
|
|
6367
|
+
* by events received while leaving (per Locus team recommendation).
|
|
6368
|
+
* Idempotent: safe to call multiple times; .off() is a no-op when no
|
|
6369
|
+
* matching listener is registered.
|
|
6370
|
+
* @private
|
|
6371
|
+
* @returns {void}
|
|
6372
|
+
*/
|
|
6373
|
+
private stopListeningForLLMEvents() {
|
|
6374
|
+
// @ts-ignore - fix types
|
|
6375
|
+
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
6376
|
+
// @ts-ignore - fix types
|
|
6377
|
+
this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
|
|
6378
|
+
this.clearLLMHealthCheckTimer();
|
|
6379
|
+
}
|
|
6380
|
+
|
|
6381
|
+
/**
|
|
6382
|
+
* Stops listening on every event bus (LLM, Mercury, voicea/transcription,
|
|
6383
|
+
* annotation) that could otherwise deliver events to this meeting while
|
|
6384
|
+
* Locus is processing /leave or /end. Per the Locus team recommendation,
|
|
6385
|
+
* this must run before the Locus request is dispatched to avoid
|
|
6386
|
+
* unnecessary syncs triggered by in-flight events.
|
|
6387
|
+
*
|
|
6388
|
+
* Voicea (transcription) subscribes to llm 'event:relay.event' internally,
|
|
6389
|
+
* and the annotation plugin subscribes to both mercury and llm, so both
|
|
6390
|
+
* must be torn down alongside the direct LLM/Mercury listeners.
|
|
6391
|
+
*
|
|
6392
|
+
* Idempotent: safe to call multiple times; .off() is a no-op when no
|
|
6393
|
+
* matching listener is registered, and stopTranscription is guarded.
|
|
6394
|
+
* @private
|
|
6395
|
+
* @returns {void}
|
|
6396
|
+
*/
|
|
6397
|
+
private stopListeningForMeetingEvents() {
|
|
6398
|
+
this.stopListeningForLLMEvents();
|
|
6399
|
+
this.stopListeningForMercuryEvents();
|
|
6400
|
+
if (this.transcription) {
|
|
6401
|
+
this.stopTranscription();
|
|
6402
|
+
this.transcription = undefined;
|
|
6403
|
+
}
|
|
6404
|
+
this.annotation.deregisterEvents();
|
|
6405
|
+
}
|
|
6406
|
+
|
|
6331
6407
|
/**
|
|
6332
6408
|
* Disconnects and cleans up the default LLM session listeners/timers.
|
|
6333
6409
|
* @param {Object} options
|
|
@@ -6362,12 +6438,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
6362
6438
|
// @ts-ignore - Fix type
|
|
6363
6439
|
this.webex.internal.llm.off('online', this.handleLLMOnline);
|
|
6364
6440
|
}
|
|
6365
|
-
|
|
6366
|
-
this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
|
|
6367
|
-
// @ts-ignore - Fix type
|
|
6368
|
-
this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
|
|
6369
|
-
|
|
6370
|
-
this.clearLLMHealthCheckTimer();
|
|
6441
|
+
this.stopListeningForLLMEvents();
|
|
6371
6442
|
}
|
|
6372
6443
|
};
|
|
6373
6444
|
|
|
@@ -8824,6 +8895,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
8824
8895
|
});
|
|
8825
8896
|
LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
|
|
8826
8897
|
|
|
8898
|
+
this.stopListeningForMeetingEvents();
|
|
8899
|
+
|
|
8827
8900
|
return MeetingUtil.leaveMeeting(this, options)
|
|
8828
8901
|
.then(async (leave) => {
|
|
8829
8902
|
// CA team recommends submitting this *after* locus /leave
|
|
@@ -9688,6 +9761,8 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9688
9761
|
locus_id: this.locusId,
|
|
9689
9762
|
});
|
|
9690
9763
|
|
|
9764
|
+
this.stopListeningForMeetingEvents();
|
|
9765
|
+
|
|
9691
9766
|
return MeetingUtil.endMeetingForAll(this)
|
|
9692
9767
|
.then(async (end) => {
|
|
9693
9768
|
this.meetingFiniteStateMachine.end();
|
|
@@ -9749,11 +9824,11 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
9749
9824
|
}
|
|
9750
9825
|
this.queuedMediaUpdates = [];
|
|
9751
9826
|
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9827
|
+
// Listener teardown (transcription, annotation, llm/mercury) runs in
|
|
9828
|
+
// stopListeningForMeetingEvents() before /leave and /end so events
|
|
9829
|
+
// received mid-teardown do not trigger Locus syncs. Calling it here
|
|
9830
|
+
// again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
|
|
9831
|
+
// because stopTranscription() always fires its trigger.
|
|
9757
9832
|
this.clearDataChannelToken();
|
|
9758
9833
|
await this.cleanupLLMConneciton({throwOnError: false});
|
|
9759
9834
|
};
|
|
@@ -2052,6 +2052,79 @@ describe('HashTreeParser', () => {
|
|
|
2052
2052
|
},
|
|
2053
2053
|
});
|
|
2054
2054
|
});
|
|
2055
|
+
|
|
2056
|
+
it('restarts the sync timer when sync response is empty so that a future sync can be triggered', async () => {
|
|
2057
|
+
const parser = createHashTreeParser();
|
|
2058
|
+
|
|
2059
|
+
// Send a heartbeat with a mismatched root hash to trigger runSyncAlgorithm
|
|
2060
|
+
const heartbeatMessage = {
|
|
2061
|
+
dataSets: [
|
|
2062
|
+
{
|
|
2063
|
+
...createDataSet('main', 16, 1100),
|
|
2064
|
+
root: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', // different from ours
|
|
2065
|
+
},
|
|
2066
|
+
],
|
|
2067
|
+
visibleDataSetsUrl,
|
|
2068
|
+
locusUrl,
|
|
2069
|
+
};
|
|
2070
|
+
|
|
2071
|
+
parser.handleMessage(heartbeatMessage, 'heartbeat with mismatch');
|
|
2072
|
+
|
|
2073
|
+
// The sync timer should be set
|
|
2074
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
2075
|
+
|
|
2076
|
+
// Mock responses for the first sync - return null (204/empty body)
|
|
2077
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
2078
|
+
mockGetHashesFromLocusResponse(
|
|
2079
|
+
mainDataSetUrl,
|
|
2080
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
2081
|
+
{
|
|
2082
|
+
...createDataSet('main', 16, 1101),
|
|
2083
|
+
root: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', // still mismatched
|
|
2084
|
+
}
|
|
2085
|
+
);
|
|
2086
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
2087
|
+
|
|
2088
|
+
// Advance time to fire the sync timer (idleMs=1000 + backoff=0)
|
|
2089
|
+
await clock.tickAsync(1000);
|
|
2090
|
+
|
|
2091
|
+
// Verify sync was triggered
|
|
2092
|
+
assert.calledWith(
|
|
2093
|
+
webexRequest,
|
|
2094
|
+
sinon.match({
|
|
2095
|
+
method: 'POST',
|
|
2096
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
2097
|
+
})
|
|
2098
|
+
);
|
|
2099
|
+
|
|
2100
|
+
// After empty response, runSyncAlgorithm should have been called,
|
|
2101
|
+
// setting a new sync timer as a safety net
|
|
2102
|
+
expect(parser.dataSets.main.timer).to.not.be.undefined;
|
|
2103
|
+
|
|
2104
|
+
// Reset and set up mocks for the second sync
|
|
2105
|
+
webexRequest.resetHistory();
|
|
2106
|
+
mockGetHashesFromLocusResponse(
|
|
2107
|
+
mainDataSetUrl,
|
|
2108
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
2109
|
+
{
|
|
2110
|
+
...createDataSet('main', 16, 1102),
|
|
2111
|
+
root: 'cccccccccccccccccccccccccccccccc', // still mismatched
|
|
2112
|
+
}
|
|
2113
|
+
);
|
|
2114
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
2115
|
+
|
|
2116
|
+
// Advance time again to fire the second sync timer
|
|
2117
|
+
await clock.tickAsync(1000);
|
|
2118
|
+
|
|
2119
|
+
// Verify a second sync was triggered
|
|
2120
|
+
assert.calledWith(
|
|
2121
|
+
webexRequest,
|
|
2122
|
+
sinon.match({
|
|
2123
|
+
method: 'POST',
|
|
2124
|
+
uri: `${mainDataSetUrl}/sync`,
|
|
2125
|
+
})
|
|
2126
|
+
);
|
|
2127
|
+
});
|
|
2055
2128
|
});
|
|
2056
2129
|
|
|
2057
2130
|
describe('handles visible data sets changes correctly', () => {
|
|
@@ -3119,7 +3192,77 @@ describe('HashTreeParser', () => {
|
|
|
3119
3192
|
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
3120
3193
|
expect(parser.dataSets['atd-active']?.heartbeatWatchdogTimer).to.be.undefined;
|
|
3121
3194
|
});
|
|
3195
|
+
|
|
3196
|
+
it('restarts the watchdog timer after it fires so that future missed heartbeats still trigger syncs', async () => {
|
|
3197
|
+
const parser = createHashTreeParser();
|
|
3198
|
+
const heartbeatIntervalMs = 5000;
|
|
3199
|
+
|
|
3200
|
+
// Send initial heartbeat for 'main'
|
|
3201
|
+
const heartbeatMessage = {
|
|
3202
|
+
dataSets: [
|
|
3203
|
+
{
|
|
3204
|
+
...createDataSet('main', 16, 1100),
|
|
3205
|
+
root: parser.dataSets.main.hashTree.getRootHash(),
|
|
3206
|
+
},
|
|
3207
|
+
],
|
|
3208
|
+
visibleDataSetsUrl,
|
|
3209
|
+
locusUrl,
|
|
3210
|
+
heartbeatIntervalMs,
|
|
3211
|
+
};
|
|
3212
|
+
|
|
3213
|
+
parser.handleMessage(heartbeatMessage, 'initial heartbeat');
|
|
3214
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
3215
|
+
|
|
3216
|
+
// Mock responses for performSync - return null (204/empty body)
|
|
3217
|
+
const mainDataSetUrl = parser.dataSets.main.url;
|
|
3218
|
+
mockGetHashesFromLocusResponse(
|
|
3219
|
+
mainDataSetUrl,
|
|
3220
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
3221
|
+
createDataSet('main', 16, 1101)
|
|
3222
|
+
);
|
|
3223
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
3224
|
+
|
|
3225
|
+
// Advance time past heartbeatIntervalMs to fire the watchdog
|
|
3226
|
+
await clock.tickAsync(heartbeatIntervalMs);
|
|
3227
|
+
|
|
3228
|
+
// Verify sync was triggered
|
|
3229
|
+
assert.calledWith(
|
|
3230
|
+
webexRequest,
|
|
3231
|
+
sinon.match({
|
|
3232
|
+
method: 'GET',
|
|
3233
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
3234
|
+
})
|
|
3235
|
+
);
|
|
3236
|
+
|
|
3237
|
+
// The watchdog timer should have been restarted after firing
|
|
3238
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
3239
|
+
|
|
3240
|
+
// Reset call history and set up new mock responses for the second sync
|
|
3241
|
+
webexRequest.resetHistory();
|
|
3242
|
+
mockGetHashesFromLocusResponse(
|
|
3243
|
+
mainDataSetUrl,
|
|
3244
|
+
new Array(16).fill('00000000000000000000000000000000'),
|
|
3245
|
+
createDataSet('main', 16, 1102)
|
|
3246
|
+
);
|
|
3247
|
+
mockSendSyncRequestResponse(mainDataSetUrl, null);
|
|
3248
|
+
|
|
3249
|
+
// Advance time again to fire the watchdog a second time
|
|
3250
|
+
await clock.tickAsync(heartbeatIntervalMs);
|
|
3251
|
+
|
|
3252
|
+
// Verify a second sync was triggered
|
|
3253
|
+
assert.calledWith(
|
|
3254
|
+
webexRequest,
|
|
3255
|
+
sinon.match({
|
|
3256
|
+
method: 'GET',
|
|
3257
|
+
uri: `${mainDataSetUrl}/hashtree`,
|
|
3258
|
+
})
|
|
3259
|
+
);
|
|
3260
|
+
|
|
3261
|
+
// And the watchdog should still be running
|
|
3262
|
+
expect(parser.dataSets.main.heartbeatWatchdogTimer).to.not.be.undefined;
|
|
3263
|
+
});
|
|
3122
3264
|
});
|
|
3265
|
+
|
|
3123
3266
|
});
|
|
3124
3267
|
|
|
3125
3268
|
describe('#callLocusInfoUpdateCallback filtering', () => {
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
ONLINE,
|
|
35
35
|
OFFLINE,
|
|
36
36
|
ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
|
|
37
|
+
LOCUS_LLM_EVENT,
|
|
37
38
|
} from '@webex/plugin-meetings/src/constants';
|
|
38
39
|
import {
|
|
39
40
|
ConnectionState,
|
|
@@ -6429,6 +6430,9 @@ describe('plugin-meetings', () => {
|
|
|
6429
6430
|
|
|
6430
6431
|
meeting.annotation.deregisterEvents = sinon.stub();
|
|
6431
6432
|
webex.internal.llm.off = sinon.stub();
|
|
6433
|
+
webex.internal.mercury.off = sinon.stub();
|
|
6434
|
+
meeting.mercuryOnlineHandler = sinon.stub();
|
|
6435
|
+
meeting.mercuryOfflineHandler = sinon.stub();
|
|
6432
6436
|
|
|
6433
6437
|
// A meeting needs to be joined to leave
|
|
6434
6438
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -6452,6 +6456,67 @@ describe('plugin-meetings', () => {
|
|
|
6452
6456
|
assert.calledOnce(meeting.clearMeetingData);
|
|
6453
6457
|
});
|
|
6454
6458
|
|
|
6459
|
+
it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /leave', async () => {
|
|
6460
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
6461
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
6462
|
+
|
|
6463
|
+
await meeting.leave();
|
|
6464
|
+
|
|
6465
|
+
// All llm/mercury consumers (direct listeners, voicea transcription,
|
|
6466
|
+
// annotation) must be detached before the /leave request so that
|
|
6467
|
+
// in-flight events do not trigger unnecessary Locus syncs
|
|
6468
|
+
// (per Locus team recommendation).
|
|
6469
|
+
assert.callOrder(
|
|
6470
|
+
webex.internal.llm.off,
|
|
6471
|
+
webex.internal.mercury.off,
|
|
6472
|
+
meeting.stopTranscription,
|
|
6473
|
+
meeting.annotation.deregisterEvents,
|
|
6474
|
+
meeting.meetingRequest.leaveMeeting
|
|
6475
|
+
);
|
|
6476
|
+
assert.calledWithExactly(
|
|
6477
|
+
webex.internal.llm.off,
|
|
6478
|
+
'event:relay.event',
|
|
6479
|
+
meeting.processRelayEvent
|
|
6480
|
+
);
|
|
6481
|
+
assert.calledWithExactly(
|
|
6482
|
+
webex.internal.llm.off,
|
|
6483
|
+
LOCUS_LLM_EVENT,
|
|
6484
|
+
meeting.processLocusLLMEvent
|
|
6485
|
+
);
|
|
6486
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
6487
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
6488
|
+
assert.isUndefined(meeting.mercuryOnlineHandler);
|
|
6489
|
+
assert.isUndefined(meeting.mercuryOfflineHandler);
|
|
6490
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
6491
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
6492
|
+
assert.isUndefined(meeting.transcription);
|
|
6493
|
+
});
|
|
6494
|
+
|
|
6495
|
+
it('tears down llm/mercury/transcription/annotation even when /leave rejects', async () => {
|
|
6496
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
6497
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
6498
|
+
meeting.meetingRequest.leaveMeeting = sinon
|
|
6499
|
+
.stub()
|
|
6500
|
+
.returns(Promise.reject(new Error('leave failed')));
|
|
6501
|
+
|
|
6502
|
+
await meeting.leave().catch(() => {});
|
|
6503
|
+
|
|
6504
|
+
assert.calledWithExactly(
|
|
6505
|
+
webex.internal.llm.off,
|
|
6506
|
+
'event:relay.event',
|
|
6507
|
+
meeting.processRelayEvent
|
|
6508
|
+
);
|
|
6509
|
+
assert.calledWithExactly(
|
|
6510
|
+
webex.internal.llm.off,
|
|
6511
|
+
LOCUS_LLM_EVENT,
|
|
6512
|
+
meeting.processLocusLLMEvent
|
|
6513
|
+
);
|
|
6514
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
6515
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
6516
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
6517
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
6518
|
+
});
|
|
6519
|
+
|
|
6455
6520
|
it('should reset call diagnostic latencies correctly', async () => {
|
|
6456
6521
|
const leave = meeting.leave();
|
|
6457
6522
|
|
|
@@ -8459,6 +8524,9 @@ describe('plugin-meetings', () => {
|
|
|
8459
8524
|
|
|
8460
8525
|
meeting.annotation.deregisterEvents = sinon.stub();
|
|
8461
8526
|
webex.internal.llm.off = sinon.stub();
|
|
8527
|
+
webex.internal.mercury.off = sinon.stub();
|
|
8528
|
+
meeting.mercuryOnlineHandler = sinon.stub();
|
|
8529
|
+
meeting.mercuryOfflineHandler = sinon.stub();
|
|
8462
8530
|
|
|
8463
8531
|
// A meeting needs to be joined to end
|
|
8464
8532
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -8481,6 +8549,66 @@ describe('plugin-meetings', () => {
|
|
|
8481
8549
|
assert.calledOnce(meeting?.unsetPeerConnections);
|
|
8482
8550
|
assert.calledOnce(meeting?.clearMeetingData);
|
|
8483
8551
|
});
|
|
8552
|
+
|
|
8553
|
+
it('stops listening for LLM/Mercury and tears down transcription and annotation before calling Locus /end', async () => {
|
|
8554
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
8555
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
8556
|
+
|
|
8557
|
+
await meeting.endMeetingForAll();
|
|
8558
|
+
|
|
8559
|
+
// All llm/mercury consumers (direct listeners, voicea transcription,
|
|
8560
|
+
// annotation) must be detached before the /end request so that
|
|
8561
|
+
// in-flight events do not trigger unnecessary Locus syncs
|
|
8562
|
+
// (per Locus team recommendation).
|
|
8563
|
+
assert.callOrder(
|
|
8564
|
+
webex.internal.llm.off,
|
|
8565
|
+
webex.internal.mercury.off,
|
|
8566
|
+
meeting.stopTranscription,
|
|
8567
|
+
meeting.annotation.deregisterEvents,
|
|
8568
|
+
meeting.meetingRequest.endMeetingForAll
|
|
8569
|
+
);
|
|
8570
|
+
assert.calledWithExactly(
|
|
8571
|
+
webex.internal.llm.off,
|
|
8572
|
+
'event:relay.event',
|
|
8573
|
+
meeting.processRelayEvent
|
|
8574
|
+
);
|
|
8575
|
+
assert.calledWithExactly(
|
|
8576
|
+
webex.internal.llm.off,
|
|
8577
|
+
LOCUS_LLM_EVENT,
|
|
8578
|
+
meeting.processLocusLLMEvent
|
|
8579
|
+
);
|
|
8580
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
8581
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
8582
|
+
assert.isUndefined(meeting.mercuryOnlineHandler);
|
|
8583
|
+
assert.isUndefined(meeting.mercuryOfflineHandler);
|
|
8584
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
8585
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
8586
|
+
});
|
|
8587
|
+
|
|
8588
|
+
it('tears down llm/mercury/transcription/annotation even when /end rejects', async () => {
|
|
8589
|
+
const onlineHandler = meeting.mercuryOnlineHandler;
|
|
8590
|
+
const offlineHandler = meeting.mercuryOfflineHandler;
|
|
8591
|
+
meeting.meetingRequest.endMeetingForAll = sinon
|
|
8592
|
+
.stub()
|
|
8593
|
+
.returns(Promise.reject(new Error('end failed')));
|
|
8594
|
+
|
|
8595
|
+
await meeting.endMeetingForAll().catch(() => {});
|
|
8596
|
+
|
|
8597
|
+
assert.calledWithExactly(
|
|
8598
|
+
webex.internal.llm.off,
|
|
8599
|
+
'event:relay.event',
|
|
8600
|
+
meeting.processRelayEvent
|
|
8601
|
+
);
|
|
8602
|
+
assert.calledWithExactly(
|
|
8603
|
+
webex.internal.llm.off,
|
|
8604
|
+
LOCUS_LLM_EVENT,
|
|
8605
|
+
meeting.processLocusLLMEvent
|
|
8606
|
+
);
|
|
8607
|
+
assert.calledWithExactly(webex.internal.mercury.off, ONLINE, onlineHandler);
|
|
8608
|
+
assert.calledWithExactly(webex.internal.mercury.off, OFFLINE, offlineHandler);
|
|
8609
|
+
assert.calledOnceWithExactly(meeting.stopTranscription);
|
|
8610
|
+
assert.calledOnceWithExactly(meeting.annotation.deregisterEvents);
|
|
8611
|
+
});
|
|
8484
8612
|
});
|
|
8485
8613
|
|
|
8486
8614
|
describe('#moveTo', () => {
|
|
@@ -13311,10 +13439,13 @@ describe('plugin-meetings', () => {
|
|
|
13311
13439
|
meeting.processLocusLLMEvent
|
|
13312
13440
|
);
|
|
13313
13441
|
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13314
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13315
|
-
assert.isUndefined(meeting.transcription);
|
|
13316
13442
|
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13317
|
-
|
|
13443
|
+
// stopTranscription and annotation.deregisterEvents are not
|
|
13444
|
+
// called here: they run in stopListeningForMeetingEvents()
|
|
13445
|
+
// before /leave to avoid double-emitting
|
|
13446
|
+
// MEETING_STOPPED_RECEIVING_TRANSCRIPTION.
|
|
13447
|
+
assert.notCalled(meeting.stopTranscription);
|
|
13448
|
+
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13318
13449
|
});
|
|
13319
13450
|
it('continues cleanup when disconnectLLM fails during meeting data cleanup', async () => {
|
|
13320
13451
|
webex.internal.llm.disconnectLLM.rejects(new Error('disconnect failed'));
|
|
@@ -13333,19 +13464,9 @@ describe('plugin-meetings', () => {
|
|
|
13333
13464
|
meeting.processLocusLLMEvent
|
|
13334
13465
|
);
|
|
13335
13466
|
assert.calledOnce(meeting.clearLLMHealthCheckTimer);
|
|
13336
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13337
|
-
assert.isUndefined(meeting.transcription);
|
|
13338
|
-
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13339
|
-
assert.calledOnce(meeting.annotation.deregisterEvents);
|
|
13340
|
-
});
|
|
13341
|
-
it('always calls stopTranscription even when transcription is undefined', async () => {
|
|
13342
|
-
meeting.transcription = undefined;
|
|
13343
|
-
|
|
13344
|
-
await meeting.clearMeetingData();
|
|
13345
|
-
|
|
13346
|
-
assert.calledOnce(meeting.stopTranscription);
|
|
13347
|
-
assert.isUndefined(meeting.transcription);
|
|
13348
13467
|
assert.calledOnce(meeting.clearDataChannelToken);
|
|
13468
|
+
assert.notCalled(meeting.stopTranscription);
|
|
13469
|
+
assert.notCalled(meeting.annotation.deregisterEvents);
|
|
13349
13470
|
});
|
|
13350
13471
|
});
|
|
13351
13472
|
});
|