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