@webex/internal-plugin-mercury 3.8.1-web-workers-keepalive.1 → 3.9.0-multipleLLM.1
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/mercury.js +321 -132
- package/dist/mercury.js.map +1 -1
- package/dist/socket/socket-base.js +15 -0
- package/dist/socket/socket-base.js.map +1 -1
- package/package.json +17 -17
- package/src/mercury.js +279 -106
- package/src/socket/socket-base.js +13 -0
- package/test/unit/spec/mercury-events.js +20 -2
- package/test/unit/spec/mercury.js +53 -22
package/src/mercury.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import url from 'url';
|
|
7
7
|
|
|
8
8
|
import {WebexPlugin} from '@webex/webex-core';
|
|
9
|
-
import {deprecated
|
|
9
|
+
import {deprecated} from '@webex/common';
|
|
10
10
|
import {camelCase, get, set} from 'lodash';
|
|
11
11
|
import backoff from 'backoff';
|
|
12
12
|
|
|
@@ -25,6 +25,7 @@ const normalReconnectReasons = ['idle', 'done (forced)', 'pong not received', 'p
|
|
|
25
25
|
const Mercury = WebexPlugin.extend({
|
|
26
26
|
namespace: 'Mercury',
|
|
27
27
|
lastError: undefined,
|
|
28
|
+
defaultSessionId: 'mercury-default-session',
|
|
28
29
|
|
|
29
30
|
session: {
|
|
30
31
|
connected: {
|
|
@@ -39,7 +40,14 @@ const Mercury = WebexPlugin.extend({
|
|
|
39
40
|
default: false,
|
|
40
41
|
type: 'boolean',
|
|
41
42
|
},
|
|
42
|
-
|
|
43
|
+
sockets: {
|
|
44
|
+
default: () => new Map(),
|
|
45
|
+
type: 'object',
|
|
46
|
+
},
|
|
47
|
+
backoffCalls: {
|
|
48
|
+
default: () => new Map(),
|
|
49
|
+
type: 'object',
|
|
50
|
+
},
|
|
43
51
|
localClusterServiceUrls: 'object',
|
|
44
52
|
mercuryTimeOffset: {
|
|
45
53
|
default: undefined,
|
|
@@ -78,29 +86,96 @@ const Mercury = WebexPlugin.extend({
|
|
|
78
86
|
return this.lastError;
|
|
79
87
|
},
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Get all active socket connections
|
|
91
|
+
* @returns {Map} Map of sessionId to socket instances
|
|
92
|
+
*/
|
|
93
|
+
getSockets() {
|
|
94
|
+
return this.sockets;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a specific socket by connection ID
|
|
99
|
+
* @param {string} sessionId - The connection identifier
|
|
100
|
+
* @returns {Socket|undefined} The socket instance or undefined if not found
|
|
101
|
+
*/
|
|
102
|
+
getSocket(sessionId = this.defaultSessionId) {
|
|
103
|
+
return this.sockets.get(sessionId);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if any sockets are connected
|
|
108
|
+
* @returns {boolean} True if at least one socket is connected
|
|
109
|
+
*/
|
|
110
|
+
hasConnectedSockets() {
|
|
111
|
+
for (const socket of this.sockets.values()) {
|
|
112
|
+
if (socket && socket.connected) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if any sockets are connecting
|
|
122
|
+
* @returns {boolean} True if at least one socket is connected
|
|
123
|
+
*/
|
|
124
|
+
hasConnectingSockets() {
|
|
125
|
+
for (const socket of this.sockets.values()) {
|
|
126
|
+
if (socket && socket.connecting) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return false;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// @oneFlight
|
|
135
|
+
connect(webSocketUrl, sessionId = this.defaultSessionId) {
|
|
136
|
+
if (!this._connectPromises) this._connectPromises = new Map();
|
|
137
|
+
|
|
138
|
+
// First check if there's already a connection promise for this session
|
|
139
|
+
if (this._connectPromises.has(sessionId)) {
|
|
140
|
+
this.logger.info(
|
|
141
|
+
`${this.namespace}: connection ${sessionId} already in progress, returning existing promise`
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return this._connectPromises.get(sessionId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sessionSocket = this.sockets.get(sessionId);
|
|
148
|
+
if (sessionSocket?.connected || sessionSocket?.connecting) {
|
|
149
|
+
this.logger.info(
|
|
150
|
+
`${this.namespace}: connection ${sessionId} already connected, will not connect again`
|
|
151
|
+
);
|
|
85
152
|
|
|
86
153
|
return Promise.resolve();
|
|
87
154
|
}
|
|
88
155
|
|
|
89
156
|
this.connecting = true;
|
|
90
157
|
|
|
91
|
-
this.logger.info(`${this.namespace}: starting connection attempt`);
|
|
158
|
+
this.logger.info(`${this.namespace}: starting connection attempt for ${sessionId}`);
|
|
92
159
|
this.logger.info(
|
|
93
160
|
`${this.namespace}: debug_mercury_logging stack: `,
|
|
94
161
|
new Error('debug_mercury_logging').stack
|
|
95
162
|
);
|
|
96
163
|
|
|
97
|
-
|
|
164
|
+
const connectPromise = Promise.resolve(
|
|
98
165
|
this.webex.internal.device.registered || this.webex.internal.device.register()
|
|
99
|
-
)
|
|
100
|
-
|
|
166
|
+
)
|
|
167
|
+
.then(() => {
|
|
168
|
+
this.logger.info(`${this.namespace}: connecting ${sessionId}`);
|
|
101
169
|
|
|
102
|
-
|
|
103
|
-
|
|
170
|
+
return this._connectWithBackoff(webSocketUrl, sessionId);
|
|
171
|
+
})
|
|
172
|
+
.finally(() => {
|
|
173
|
+
this._connectPromises.delete(sessionId);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this._connectPromises.set(sessionId, connectPromise);
|
|
177
|
+
|
|
178
|
+
return connectPromise;
|
|
104
179
|
},
|
|
105
180
|
|
|
106
181
|
logout() {
|
|
@@ -110,7 +185,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
110
185
|
new Error('debug_mercury_logging').stack
|
|
111
186
|
);
|
|
112
187
|
|
|
113
|
-
return this.
|
|
188
|
+
return this.disconnectAll(
|
|
114
189
|
this.config.beforeLogoutOptionsCloseReason &&
|
|
115
190
|
!normalReconnectReasons.includes(this.config.beforeLogoutOptionsCloseReason)
|
|
116
191
|
? {code: 3050, reason: this.config.beforeLogoutOptionsCloseReason}
|
|
@@ -118,21 +193,58 @@ const Mercury = WebexPlugin.extend({
|
|
|
118
193
|
);
|
|
119
194
|
},
|
|
120
195
|
|
|
121
|
-
@oneFlight
|
|
122
|
-
disconnect(options) {
|
|
196
|
+
// @oneFlight
|
|
197
|
+
disconnect(options, sessionId = this.defaultSessionId) {
|
|
123
198
|
return new Promise((resolve) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.
|
|
199
|
+
const backoffCall = this.backoffCalls.get(sessionId);
|
|
200
|
+
if (backoffCall) {
|
|
201
|
+
this.logger.info(`${this.namespace}: aborting connection ${sessionId}`);
|
|
202
|
+
backoffCall.abort();
|
|
203
|
+
this.backoffCalls.delete(sessionId);
|
|
127
204
|
}
|
|
128
205
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.
|
|
132
|
-
resolve(this.socket.close(options || undefined));
|
|
206
|
+
// Clean up any pending connection promises
|
|
207
|
+
if (this._connectPromises) {
|
|
208
|
+
this._connectPromises.delete(sessionId);
|
|
133
209
|
}
|
|
134
210
|
|
|
211
|
+
const sessionSocket = this.sockets.get(sessionId);
|
|
212
|
+
const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
|
|
213
|
+
|
|
214
|
+
if (sessionSocket) {
|
|
215
|
+
sessionSocket.removeAllListeners('message');
|
|
216
|
+
sessionSocket.connecting = false;
|
|
217
|
+
sessionSocket.connected = false;
|
|
218
|
+
this.once(sessionId === this.defaultSessionId ? 'offline' : `offline${suffix}`, resolve);
|
|
219
|
+
resolve(sessionSocket.close(options || undefined));
|
|
220
|
+
}
|
|
135
221
|
resolve();
|
|
222
|
+
|
|
223
|
+
// Update overall connected status
|
|
224
|
+
this.connected = this.hasConnectedSockets();
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Disconnect all socket connections
|
|
230
|
+
* @param {object} options - Close options
|
|
231
|
+
* @returns {Promise} Promise that resolves when all connections are closed
|
|
232
|
+
*/
|
|
233
|
+
disconnectAll(options) {
|
|
234
|
+
const disconnectPromises = [];
|
|
235
|
+
|
|
236
|
+
for (const sessionId of this.sockets.keys()) {
|
|
237
|
+
disconnectPromises.push(this.disconnect(options, sessionId));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return Promise.all(disconnectPromises).then(() => {
|
|
241
|
+
this.connected = false;
|
|
242
|
+
this.sockets.clear();
|
|
243
|
+
this.backoffCalls.clear();
|
|
244
|
+
// Clear connection promises to prevent stale promises
|
|
245
|
+
if (this._connectPromises) {
|
|
246
|
+
this._connectPromises.clear();
|
|
247
|
+
}
|
|
136
248
|
});
|
|
137
249
|
},
|
|
138
250
|
|
|
@@ -207,20 +319,23 @@ const Mercury = WebexPlugin.extend({
|
|
|
207
319
|
});
|
|
208
320
|
},
|
|
209
321
|
|
|
210
|
-
_attemptConnection(socketUrl, callback) {
|
|
322
|
+
_attemptConnection(socketUrl, sessionId, callback) {
|
|
211
323
|
const socket = new Socket();
|
|
324
|
+
socket.connecting = true;
|
|
212
325
|
let attemptWSUrl;
|
|
326
|
+
const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
|
|
213
327
|
|
|
214
|
-
socket.on('close', (...args) => this._onclose(...args));
|
|
215
|
-
socket.on('message', (...args) => this._onmessage(...args));
|
|
216
|
-
socket.on('pong', (...args) => this._setTimeOffset(...args));
|
|
217
|
-
socket.on('sequence-mismatch', (...args) => this._emit(
|
|
218
|
-
socket.on('ping-pong-latency', (...args) => this._emit(
|
|
328
|
+
socket.on('close', (...args) => this._onclose(sessionId, ...args));
|
|
329
|
+
socket.on('message', (...args) => this._onmessage(sessionId, ...args));
|
|
330
|
+
socket.on('pong', (...args) => this._setTimeOffset(sessionId, ...args));
|
|
331
|
+
socket.on('sequence-mismatch', (...args) => this._emit(`sequence-mismatch${suffix}`, ...args));
|
|
332
|
+
socket.on('ping-pong-latency', (...args) => this._emit(`ping-pong-latency${suffix}`, ...args));
|
|
219
333
|
|
|
220
334
|
Promise.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()])
|
|
221
335
|
.then(([webSocketUrl, token]) => {
|
|
222
|
-
|
|
223
|
-
|
|
336
|
+
const backoffCall = this.backoffCalls.get(sessionId);
|
|
337
|
+
if (!backoffCall) {
|
|
338
|
+
const msg = `${this.namespace}: prevent socket open when backoffCall no longer defined for ${sessionId}`;
|
|
224
339
|
|
|
225
340
|
this.logger.info(msg);
|
|
226
341
|
|
|
@@ -234,27 +349,28 @@ const Mercury = WebexPlugin.extend({
|
|
|
234
349
|
pingInterval: this.config.pingInterval,
|
|
235
350
|
pongTimeout: this.config.pongTimeout,
|
|
236
351
|
token: token.toString(),
|
|
237
|
-
trackingId: `${this.webex.sessionId}_${Date.now()}`,
|
|
352
|
+
trackingId: `${this.webex.sessionId}_${sessionId}_${Date.now()}`,
|
|
238
353
|
logger: this.logger,
|
|
239
354
|
};
|
|
240
355
|
|
|
241
356
|
// if the consumer has supplied request options use them
|
|
242
357
|
if (this.webex.config.defaultMercuryOptions) {
|
|
243
|
-
this.logger.info(`${this.namespace}: setting custom options`);
|
|
358
|
+
this.logger.info(`${this.namespace}: setting custom options for ${sessionId}`);
|
|
244
359
|
options = {...options, ...this.webex.config.defaultMercuryOptions};
|
|
245
360
|
}
|
|
246
361
|
|
|
247
362
|
// Set the socket before opening it. This allows a disconnect() to close
|
|
248
363
|
// the socket if it is in the process of being opened.
|
|
249
|
-
this.
|
|
364
|
+
this.sockets.set(sessionId, socket);
|
|
365
|
+
this.socket = this.sockets.get(this.defaultSessionId) || socket;
|
|
250
366
|
|
|
251
|
-
this.logger.info(`${this.namespace} connection url: ${webSocketUrl}`);
|
|
367
|
+
this.logger.info(`${this.namespace} connection url for ${sessionId}: ${webSocketUrl}`);
|
|
252
368
|
|
|
253
369
|
return socket.open(webSocketUrl, options);
|
|
254
370
|
})
|
|
255
371
|
.then(() => {
|
|
256
372
|
this.logger.info(
|
|
257
|
-
`${this.namespace}: connected to mercury, success, action: connected, url: ${attemptWSUrl}`
|
|
373
|
+
`${this.namespace}: connected to mercury, success, action: connected, sessionId: ${sessionId}, url: ${attemptWSUrl}`
|
|
258
374
|
);
|
|
259
375
|
callback();
|
|
260
376
|
|
|
@@ -271,30 +387,36 @@ const Mercury = WebexPlugin.extend({
|
|
|
271
387
|
.catch((reason) => {
|
|
272
388
|
this.lastError = reason; // remember the last error
|
|
273
389
|
|
|
390
|
+
const backoffCall = this.backoffCalls.get(sessionId);
|
|
274
391
|
// Suppress connection errors that appear to be network related. This
|
|
275
392
|
// may end up suppressing metrics during outages, but we might not care
|
|
276
393
|
// (especially since many of our outages happen in a way that client
|
|
277
394
|
// metrics can't be trusted).
|
|
278
|
-
if (reason.code !== 1006 &&
|
|
279
|
-
this._emit(
|
|
395
|
+
if (reason.code !== 1006 && backoffCall && backoffCall?.getNumRetries() > 0) {
|
|
396
|
+
this._emit(`connection_failed${suffix}`, reason, {
|
|
397
|
+
sessionId,
|
|
398
|
+
retries: backoffCall?.getNumRetries(),
|
|
399
|
+
});
|
|
280
400
|
}
|
|
281
401
|
this.logger.info(
|
|
282
|
-
`${this.namespace}: connection attempt failed`,
|
|
402
|
+
`${this.namespace}: connection attempt failed for ${sessionId}`,
|
|
283
403
|
reason,
|
|
284
|
-
|
|
404
|
+
backoffCall?.getNumRetries() === 0 ? reason.stack : ''
|
|
285
405
|
);
|
|
286
406
|
// UnknownResponse is produced by IE for any 4XXX; treated it like a bad
|
|
287
407
|
// web socket url and let WDM handle the token checking
|
|
288
408
|
if (reason instanceof UnknownResponse) {
|
|
289
409
|
this.logger.info(
|
|
290
|
-
`${this.namespace}: received unknown response code, refreshing device registration`
|
|
410
|
+
`${this.namespace}: received unknown response code for ${sessionId}, refreshing device registration`
|
|
291
411
|
);
|
|
292
412
|
|
|
293
413
|
return this.webex.internal.device.refresh().then(() => callback(reason));
|
|
294
414
|
}
|
|
295
415
|
// NotAuthorized implies expired token
|
|
296
416
|
if (reason instanceof NotAuthorized) {
|
|
297
|
-
this.logger.info(
|
|
417
|
+
this.logger.info(
|
|
418
|
+
`${this.namespace}: received authorization error for ${sessionId}, reauthorizing`
|
|
419
|
+
);
|
|
298
420
|
|
|
299
421
|
return this.webex.credentials.refresh({force: true}).then(() => callback(reason));
|
|
300
422
|
}
|
|
@@ -307,8 +429,10 @@ const Mercury = WebexPlugin.extend({
|
|
|
307
429
|
// BadRequest implies current credentials are for a Service Account
|
|
308
430
|
// Forbidden implies current user is not entitle for Webex
|
|
309
431
|
if (reason instanceof BadRequest || reason instanceof Forbidden) {
|
|
310
|
-
this.logger.warn(
|
|
311
|
-
|
|
432
|
+
this.logger.warn(
|
|
433
|
+
`${this.namespace}: received unrecoverable response from mercury for ${sessionId}`
|
|
434
|
+
);
|
|
435
|
+
backoffCall?.abort();
|
|
312
436
|
|
|
313
437
|
return callback(reason);
|
|
314
438
|
}
|
|
@@ -318,7 +442,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
318
442
|
.then((haMessagingEnabled) => {
|
|
319
443
|
if (haMessagingEnabled) {
|
|
320
444
|
this.logger.info(
|
|
321
|
-
`${this.namespace}: received a generic connection error, will try to connect to another datacenter. failed, action: 'failed', url: ${attemptWSUrl} error: ${reason.message}`
|
|
445
|
+
`${this.namespace}: received a generic connection error for ${sessionId}, will try to connect to another datacenter. failed, action: 'failed', url: ${attemptWSUrl} error: ${reason.message}`
|
|
322
446
|
);
|
|
323
447
|
|
|
324
448
|
return this.webex.internal.services.markFailedUrl(attemptWSUrl);
|
|
@@ -332,42 +456,58 @@ const Mercury = WebexPlugin.extend({
|
|
|
332
456
|
return callback(reason);
|
|
333
457
|
})
|
|
334
458
|
.catch((reason) => {
|
|
335
|
-
this.logger.error(
|
|
459
|
+
this.logger.error(
|
|
460
|
+
`${this.namespace}: failed to handle connection failure for ${sessionId}`,
|
|
461
|
+
reason
|
|
462
|
+
);
|
|
336
463
|
callback(reason);
|
|
337
464
|
});
|
|
338
465
|
},
|
|
339
466
|
|
|
340
|
-
_connectWithBackoff(webSocketUrl) {
|
|
467
|
+
_connectWithBackoff(webSocketUrl, sessionId) {
|
|
341
468
|
return new Promise((resolve, reject) => {
|
|
342
469
|
// eslint gets confused about whether or not call is actually used
|
|
343
470
|
// eslint-disable-next-line prefer-const
|
|
344
471
|
let call;
|
|
345
|
-
const onComplete = (err) => {
|
|
346
|
-
this.
|
|
347
|
-
|
|
348
|
-
this.backoffCall = undefined;
|
|
472
|
+
const onComplete = (err, sid = sessionId) => {
|
|
473
|
+
this.backoffCalls.delete(sid);
|
|
349
474
|
if (err) {
|
|
350
475
|
this.logger.info(
|
|
351
476
|
`${
|
|
352
477
|
this.namespace
|
|
353
|
-
}: failed to connect after ${call.getNumRetries()} retries; log statement about next retry was inaccurate; ${err}`
|
|
478
|
+
}: failed to connect ${sid} after ${call.getNumRetries()} retries; log statement about next retry was inaccurate; ${err}`
|
|
354
479
|
);
|
|
355
480
|
|
|
356
481
|
return reject(err);
|
|
357
482
|
}
|
|
358
|
-
|
|
483
|
+
// Update overall connected status
|
|
484
|
+
const sessionSocket = this.sockets.get(sid);
|
|
485
|
+
if (sessionSocket) {
|
|
486
|
+
sessionSocket.connecting = false;
|
|
487
|
+
sessionSocket.connected = true;
|
|
488
|
+
}
|
|
489
|
+
// @ts-ignore
|
|
490
|
+
this.connecting = this.hasConnectingSockets();
|
|
491
|
+
this.connected = this.hasConnectedSockets();
|
|
359
492
|
this.hasEverConnected = true;
|
|
360
|
-
this.
|
|
493
|
+
const suffix = sid === this.defaultSessionId ? '' : `:${sid}`;
|
|
494
|
+
this._emit(`online${suffix}`, {sessionId: sid});
|
|
361
495
|
this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(true);
|
|
362
496
|
|
|
363
497
|
return resolve();
|
|
364
498
|
};
|
|
365
|
-
|
|
366
499
|
// eslint-disable-next-line prefer-reflect
|
|
367
|
-
call = backoff.call(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
500
|
+
call = backoff.call(
|
|
501
|
+
(callback) => {
|
|
502
|
+
this.logger.info(
|
|
503
|
+
`${
|
|
504
|
+
this.namespace
|
|
505
|
+
}: executing connection attempt ${call.getNumRetries()} for ${sessionId}`
|
|
506
|
+
);
|
|
507
|
+
this._attemptConnection(webSocketUrl, sessionId, callback);
|
|
508
|
+
},
|
|
509
|
+
(err) => onComplete(err, sessionId)
|
|
510
|
+
);
|
|
371
511
|
|
|
372
512
|
call.setStrategy(
|
|
373
513
|
new backoff.ExponentialStrategy({
|
|
@@ -382,9 +522,12 @@ const Mercury = WebexPlugin.extend({
|
|
|
382
522
|
call.failAfter(this.config.maxRetries);
|
|
383
523
|
}
|
|
384
524
|
|
|
525
|
+
// Store the call BEFORE setting up event handlers to prevent race conditions
|
|
526
|
+
this.backoffCalls.set(sessionId, call);
|
|
527
|
+
|
|
385
528
|
call.on('abort', () => {
|
|
386
|
-
this.logger.info(`${this.namespace}: connection aborted`);
|
|
387
|
-
reject(new Error(
|
|
529
|
+
this.logger.info(`${this.namespace}: connection aborted for ${sessionId}`);
|
|
530
|
+
reject(new Error(`Mercury Connection Aborted for ${sessionId}`));
|
|
388
531
|
});
|
|
389
532
|
|
|
390
533
|
call.on('callback', (err) => {
|
|
@@ -393,7 +536,9 @@ const Mercury = WebexPlugin.extend({
|
|
|
393
536
|
const delay = Math.min(call.strategy_.nextBackoffDelay_, this.config.backoffTimeMax);
|
|
394
537
|
|
|
395
538
|
this.logger.info(
|
|
396
|
-
`${this.namespace}: failed to connect; attempting retry ${
|
|
539
|
+
`${this.namespace}: failed to connect ${sessionId}; attempting retry ${
|
|
540
|
+
number + 1
|
|
541
|
+
} in ${delay} ms`
|
|
397
542
|
);
|
|
398
543
|
/* istanbul ignore if */
|
|
399
544
|
if (process.env.NODE_ENV === 'development') {
|
|
@@ -402,25 +547,32 @@ const Mercury = WebexPlugin.extend({
|
|
|
402
547
|
|
|
403
548
|
return;
|
|
404
549
|
}
|
|
405
|
-
this.logger.info(`${this.namespace}: connected`);
|
|
550
|
+
this.logger.info(`${this.namespace}: connected ${sessionId}`);
|
|
406
551
|
});
|
|
407
552
|
|
|
408
553
|
call.start();
|
|
409
|
-
|
|
410
|
-
this.backoffCall = call;
|
|
411
554
|
});
|
|
412
555
|
},
|
|
413
556
|
|
|
414
557
|
_emit(...args) {
|
|
415
558
|
try {
|
|
416
|
-
|
|
559
|
+
// Validate args before processing
|
|
560
|
+
if (args && args.length > 0) {
|
|
561
|
+
this.trigger(...args);
|
|
562
|
+
}
|
|
417
563
|
} catch (error) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
error
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
564
|
+
// Safely handle errors without causing additional issues during cleanup
|
|
565
|
+
try {
|
|
566
|
+
this.logger.error(
|
|
567
|
+
`${this.namespace}: error occurred in event handler:`,
|
|
568
|
+
error,
|
|
569
|
+
' with args: ',
|
|
570
|
+
args
|
|
571
|
+
);
|
|
572
|
+
} catch (logError) {
|
|
573
|
+
// If even logging fails, just ignore to prevent cascading errors during cleanup
|
|
574
|
+
console.error('Mercury _emit error handling failed:', logError);
|
|
575
|
+
}
|
|
424
576
|
}
|
|
425
577
|
},
|
|
426
578
|
|
|
@@ -444,78 +596,94 @@ const Mercury = WebexPlugin.extend({
|
|
|
444
596
|
return handlers;
|
|
445
597
|
},
|
|
446
598
|
|
|
447
|
-
_onclose(event) {
|
|
599
|
+
_onclose(sessionId, event) {
|
|
448
600
|
// I don't see any way to avoid the complexity or statement count in here.
|
|
449
601
|
/* eslint complexity: [0] */
|
|
450
602
|
|
|
451
603
|
try {
|
|
452
604
|
const reason = event.reason && event.reason.toLowerCase();
|
|
453
|
-
|
|
605
|
+
let sessionSocket = this.sockets.get(sessionId);
|
|
606
|
+
const socketUrl = sessionSocket?.url;
|
|
607
|
+
const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
|
|
608
|
+
event.sessionId = sessionId;
|
|
609
|
+
this.sockets.delete(sessionId);
|
|
610
|
+
|
|
611
|
+
if (sessionSocket) {
|
|
612
|
+
sessionSocket.removeAllListeners();
|
|
613
|
+
sessionSocket = null;
|
|
614
|
+
this._emit(`offline${suffix}`, event);
|
|
615
|
+
}
|
|
454
616
|
|
|
455
|
-
|
|
456
|
-
this.
|
|
457
|
-
this.connected =
|
|
458
|
-
|
|
459
|
-
this.
|
|
617
|
+
// Update overall connected status
|
|
618
|
+
this.connecting = this.hasConnectingSockets();
|
|
619
|
+
this.connected = this.hasConnectedSockets();
|
|
620
|
+
|
|
621
|
+
if (!this.connected) {
|
|
622
|
+
this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(false);
|
|
623
|
+
}
|
|
460
624
|
|
|
461
625
|
switch (event.code) {
|
|
462
626
|
case 1003:
|
|
463
627
|
// metric: disconnect
|
|
464
628
|
this.logger.info(
|
|
465
|
-
`${this.namespace}: Mercury service rejected last message; will not reconnect: ${event.reason}`
|
|
629
|
+
`${this.namespace}: Mercury service rejected last message for ${sessionId}; will not reconnect: ${event.reason}`
|
|
466
630
|
);
|
|
467
|
-
this._emit(
|
|
631
|
+
this._emit(`offline.permanent${suffix}`, event);
|
|
468
632
|
break;
|
|
469
633
|
case 4000:
|
|
470
634
|
// metric: disconnect
|
|
471
|
-
this.logger.info(`${this.namespace}: socket replaced; will not reconnect`);
|
|
472
|
-
this._emit(
|
|
635
|
+
this.logger.info(`${this.namespace}: socket ${sessionId} replaced; will not reconnect`);
|
|
636
|
+
this._emit(`offline.replaced${suffix}`, event);
|
|
473
637
|
break;
|
|
474
638
|
case 1001:
|
|
475
639
|
case 1005:
|
|
476
640
|
case 1006:
|
|
477
641
|
case 1011:
|
|
478
|
-
this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
|
|
479
|
-
this._emit(
|
|
480
|
-
this._reconnect(socketUrl);
|
|
642
|
+
this.logger.info(`${this.namespace}: socket ${sessionId} disconnected; reconnecting`);
|
|
643
|
+
this._emit(`offline.transient${suffix}`, event);
|
|
644
|
+
this._reconnect(socketUrl, sessionId);
|
|
481
645
|
// metric: disconnect
|
|
482
646
|
// if (code == 1011 && reason !== ping error) metric: unexpected disconnect
|
|
483
647
|
break;
|
|
484
648
|
case 1000:
|
|
485
649
|
case 3050: // 3050 indicates logout form of closure, default to old behavior, use config reason defined by consumer to proceed with the permanent block
|
|
486
650
|
if (normalReconnectReasons.includes(reason)) {
|
|
487
|
-
this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
|
|
488
|
-
this._emit(
|
|
489
|
-
this._reconnect(socketUrl);
|
|
651
|
+
this.logger.info(`${this.namespace}: socket ${sessionId} disconnected; reconnecting`);
|
|
652
|
+
this._emit(`offline.transient${suffix}`, event);
|
|
653
|
+
this._reconnect(socketUrl, sessionId);
|
|
490
654
|
// metric: disconnect
|
|
491
655
|
// if (reason === done forced) metric: force closure
|
|
492
656
|
} else {
|
|
493
657
|
this.logger.info(
|
|
494
|
-
`${this.namespace}: socket disconnected; will not reconnect: ${event.reason}`
|
|
658
|
+
`${this.namespace}: socket ${sessionId} disconnected; will not reconnect: ${event.reason}`
|
|
495
659
|
);
|
|
496
|
-
this._emit(
|
|
660
|
+
this._emit(`offline.permanent${suffix}`, event);
|
|
497
661
|
}
|
|
498
662
|
break;
|
|
499
663
|
default:
|
|
500
664
|
this.logger.info(
|
|
501
|
-
`${this.namespace}: socket disconnected unexpectedly; will not reconnect`
|
|
665
|
+
`${this.namespace}: socket ${sessionId} disconnected unexpectedly; will not reconnect`
|
|
502
666
|
);
|
|
503
667
|
// unexpected disconnect
|
|
504
|
-
this._emit(
|
|
668
|
+
this._emit(`offline.permanent${suffix}`, event);
|
|
505
669
|
}
|
|
506
670
|
} catch (error) {
|
|
507
|
-
this.logger.error(
|
|
671
|
+
this.logger.error(
|
|
672
|
+
`${this.namespace}: error occurred in close handler for ${sessionId}`,
|
|
673
|
+
error
|
|
674
|
+
);
|
|
508
675
|
}
|
|
509
676
|
},
|
|
510
677
|
|
|
511
|
-
_onmessage(event) {
|
|
512
|
-
this._setTimeOffset(event);
|
|
678
|
+
_onmessage(sessionId, event) {
|
|
679
|
+
this._setTimeOffset(sessionId, event);
|
|
513
680
|
const envelope = event.data;
|
|
514
681
|
|
|
515
682
|
if (process.env.ENABLE_MERCURY_LOGGING) {
|
|
516
|
-
this.logger.debug(`${this.namespace}: message envelope: `, envelope);
|
|
683
|
+
this.logger.debug(`${this.namespace}: message envelope from ${sessionId}: `, envelope);
|
|
517
684
|
}
|
|
518
685
|
|
|
686
|
+
envelope.sessionId = sessionId;
|
|
519
687
|
const {data} = envelope;
|
|
520
688
|
|
|
521
689
|
this._applyOverrides(data);
|
|
@@ -530,7 +698,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
530
698
|
resolve((this.webex[namespace] || this.webex.internal[namespace])[name](data))
|
|
531
699
|
).catch((reason) =>
|
|
532
700
|
this.logger.error(
|
|
533
|
-
`${this.namespace}: error occurred in autowired event handler for ${data.eventType}`,
|
|
701
|
+
`${this.namespace}: error occurred in autowired event handler for ${data.eventType} from ${sessionId}`,
|
|
534
702
|
reason
|
|
535
703
|
)
|
|
536
704
|
);
|
|
@@ -538,32 +706,37 @@ const Mercury = WebexPlugin.extend({
|
|
|
538
706
|
Promise.resolve()
|
|
539
707
|
)
|
|
540
708
|
.then(() => {
|
|
541
|
-
this.
|
|
709
|
+
const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
|
|
710
|
+
|
|
711
|
+
this._emit(`event${suffix}`, envelope);
|
|
542
712
|
const [namespace] = data.eventType.split('.');
|
|
543
713
|
|
|
544
714
|
if (namespace === data.eventType) {
|
|
545
|
-
this._emit(`event:${namespace}`, envelope);
|
|
715
|
+
this._emit(`event:${namespace}${suffix}`, envelope);
|
|
546
716
|
} else {
|
|
547
|
-
this._emit(`event:${namespace}`, envelope);
|
|
548
|
-
this._emit(`event:${data.eventType}`, envelope);
|
|
717
|
+
this._emit(`event:${namespace}${suffix}`, envelope);
|
|
718
|
+
this._emit(`event:${data.eventType}${suffix}`, envelope);
|
|
549
719
|
}
|
|
550
720
|
})
|
|
551
721
|
.catch((reason) => {
|
|
552
|
-
this.logger.error(
|
|
722
|
+
this.logger.error(
|
|
723
|
+
`${this.namespace}: error occurred processing socket message from ${sessionId}`,
|
|
724
|
+
reason
|
|
725
|
+
);
|
|
553
726
|
});
|
|
554
727
|
},
|
|
555
728
|
|
|
556
|
-
_setTimeOffset(event) {
|
|
729
|
+
_setTimeOffset(sessionId, event) {
|
|
557
730
|
const {wsWriteTimestamp} = event.data;
|
|
558
731
|
if (typeof wsWriteTimestamp === 'number' && wsWriteTimestamp > 0) {
|
|
559
732
|
this.mercuryTimeOffset = Date.now() - wsWriteTimestamp;
|
|
560
733
|
}
|
|
561
734
|
},
|
|
562
735
|
|
|
563
|
-
_reconnect(webSocketUrl) {
|
|
564
|
-
this.logger.info(`${this.namespace}: reconnecting`);
|
|
736
|
+
_reconnect(webSocketUrl, sessionId = this.defaultSessionId) {
|
|
737
|
+
this.logger.info(`${this.namespace}: reconnecting ${sessionId}`);
|
|
565
738
|
|
|
566
|
-
return this.connect(webSocketUrl);
|
|
739
|
+
return this.connect(webSocketUrl, sessionId);
|
|
567
740
|
},
|
|
568
741
|
});
|
|
569
742
|
|