@webex/internal-plugin-mercury 3.8.1 → 3.9.0-multiple-llm.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 -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 +280 -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,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,41 +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});
|
|
495
|
+
this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(true);
|
|
361
496
|
|
|
362
497
|
return resolve();
|
|
363
498
|
};
|
|
364
|
-
|
|
365
499
|
// eslint-disable-next-line prefer-reflect
|
|
366
|
-
call = backoff.call(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
);
|
|
370
511
|
|
|
371
512
|
call.setStrategy(
|
|
372
513
|
new backoff.ExponentialStrategy({
|
|
@@ -381,9 +522,12 @@ const Mercury = WebexPlugin.extend({
|
|
|
381
522
|
call.failAfter(this.config.maxRetries);
|
|
382
523
|
}
|
|
383
524
|
|
|
525
|
+
// Store the call BEFORE setting up event handlers to prevent race conditions
|
|
526
|
+
this.backoffCalls.set(sessionId, call);
|
|
527
|
+
|
|
384
528
|
call.on('abort', () => {
|
|
385
|
-
this.logger.info(`${this.namespace}: connection aborted`);
|
|
386
|
-
reject(new Error(
|
|
529
|
+
this.logger.info(`${this.namespace}: connection aborted for ${sessionId}`);
|
|
530
|
+
reject(new Error(`Mercury Connection Aborted for ${sessionId}`));
|
|
387
531
|
});
|
|
388
532
|
|
|
389
533
|
call.on('callback', (err) => {
|
|
@@ -392,7 +536,9 @@ const Mercury = WebexPlugin.extend({
|
|
|
392
536
|
const delay = Math.min(call.strategy_.nextBackoffDelay_, this.config.backoffTimeMax);
|
|
393
537
|
|
|
394
538
|
this.logger.info(
|
|
395
|
-
`${this.namespace}: failed to connect; attempting retry ${
|
|
539
|
+
`${this.namespace}: failed to connect ${sessionId}; attempting retry ${
|
|
540
|
+
number + 1
|
|
541
|
+
} in ${delay} ms`
|
|
396
542
|
);
|
|
397
543
|
/* istanbul ignore if */
|
|
398
544
|
if (process.env.NODE_ENV === 'development') {
|
|
@@ -401,25 +547,32 @@ const Mercury = WebexPlugin.extend({
|
|
|
401
547
|
|
|
402
548
|
return;
|
|
403
549
|
}
|
|
404
|
-
this.logger.info(`${this.namespace}: connected`);
|
|
550
|
+
this.logger.info(`${this.namespace}: connected ${sessionId}`);
|
|
405
551
|
});
|
|
406
552
|
|
|
407
553
|
call.start();
|
|
408
|
-
|
|
409
|
-
this.backoffCall = call;
|
|
410
554
|
});
|
|
411
555
|
},
|
|
412
556
|
|
|
413
557
|
_emit(...args) {
|
|
414
558
|
try {
|
|
415
|
-
|
|
559
|
+
// Validate args before processing
|
|
560
|
+
if (args && args.length > 0) {
|
|
561
|
+
this.trigger(...args);
|
|
562
|
+
}
|
|
416
563
|
} catch (error) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
error
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
this.logger.error('Mercury _emit error handling failed:', logError);
|
|
575
|
+
}
|
|
423
576
|
}
|
|
424
577
|
},
|
|
425
578
|
|
|
@@ -443,77 +596,94 @@ const Mercury = WebexPlugin.extend({
|
|
|
443
596
|
return handlers;
|
|
444
597
|
},
|
|
445
598
|
|
|
446
|
-
_onclose(event) {
|
|
599
|
+
_onclose(sessionId, event) {
|
|
447
600
|
// I don't see any way to avoid the complexity or statement count in here.
|
|
448
601
|
/* eslint complexity: [0] */
|
|
449
602
|
|
|
450
603
|
try {
|
|
451
604
|
const reason = event.reason && event.reason.toLowerCase();
|
|
452
|
-
|
|
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
|
+
}
|
|
453
616
|
|
|
454
|
-
|
|
455
|
-
this.
|
|
456
|
-
this.connected =
|
|
457
|
-
|
|
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
|
+
}
|
|
458
624
|
|
|
459
625
|
switch (event.code) {
|
|
460
626
|
case 1003:
|
|
461
627
|
// metric: disconnect
|
|
462
628
|
this.logger.info(
|
|
463
|
-
`${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}`
|
|
464
630
|
);
|
|
465
|
-
this._emit(
|
|
631
|
+
this._emit(`offline.permanent${suffix}`, event);
|
|
466
632
|
break;
|
|
467
633
|
case 4000:
|
|
468
634
|
// metric: disconnect
|
|
469
|
-
this.logger.info(`${this.namespace}: socket replaced; will not reconnect`);
|
|
470
|
-
this._emit(
|
|
635
|
+
this.logger.info(`${this.namespace}: socket ${sessionId} replaced; will not reconnect`);
|
|
636
|
+
this._emit(`offline.replaced${suffix}`, event);
|
|
471
637
|
break;
|
|
472
638
|
case 1001:
|
|
473
639
|
case 1005:
|
|
474
640
|
case 1006:
|
|
475
641
|
case 1011:
|
|
476
|
-
this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
|
|
477
|
-
this._emit(
|
|
478
|
-
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);
|
|
479
645
|
// metric: disconnect
|
|
480
646
|
// if (code == 1011 && reason !== ping error) metric: unexpected disconnect
|
|
481
647
|
break;
|
|
482
648
|
case 1000:
|
|
483
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
|
|
484
650
|
if (normalReconnectReasons.includes(reason)) {
|
|
485
|
-
this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
|
|
486
|
-
this._emit(
|
|
487
|
-
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);
|
|
488
654
|
// metric: disconnect
|
|
489
655
|
// if (reason === done forced) metric: force closure
|
|
490
656
|
} else {
|
|
491
657
|
this.logger.info(
|
|
492
|
-
`${this.namespace}: socket disconnected; will not reconnect: ${event.reason}`
|
|
658
|
+
`${this.namespace}: socket ${sessionId} disconnected; will not reconnect: ${event.reason}`
|
|
493
659
|
);
|
|
494
|
-
this._emit(
|
|
660
|
+
this._emit(`offline.permanent${suffix}`, event);
|
|
495
661
|
}
|
|
496
662
|
break;
|
|
497
663
|
default:
|
|
498
664
|
this.logger.info(
|
|
499
|
-
`${this.namespace}: socket disconnected unexpectedly; will not reconnect`
|
|
665
|
+
`${this.namespace}: socket ${sessionId} disconnected unexpectedly; will not reconnect`
|
|
500
666
|
);
|
|
501
667
|
// unexpected disconnect
|
|
502
|
-
this._emit(
|
|
668
|
+
this._emit(`offline.permanent${suffix}`, event);
|
|
503
669
|
}
|
|
504
670
|
} catch (error) {
|
|
505
|
-
this.logger.error(
|
|
671
|
+
this.logger.error(
|
|
672
|
+
`${this.namespace}: error occurred in close handler for ${sessionId}`,
|
|
673
|
+
error
|
|
674
|
+
);
|
|
506
675
|
}
|
|
507
676
|
},
|
|
508
677
|
|
|
509
|
-
_onmessage(event) {
|
|
510
|
-
this._setTimeOffset(event);
|
|
678
|
+
_onmessage(sessionId, event) {
|
|
679
|
+
this._setTimeOffset(sessionId, event);
|
|
511
680
|
const envelope = event.data;
|
|
512
681
|
|
|
513
682
|
if (process.env.ENABLE_MERCURY_LOGGING) {
|
|
514
|
-
this.logger.debug(`${this.namespace}: message envelope: `, envelope);
|
|
683
|
+
this.logger.debug(`${this.namespace}: message envelope from ${sessionId}: `, envelope);
|
|
515
684
|
}
|
|
516
685
|
|
|
686
|
+
envelope.sessionId = sessionId;
|
|
517
687
|
const {data} = envelope;
|
|
518
688
|
|
|
519
689
|
this._applyOverrides(data);
|
|
@@ -528,7 +698,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
528
698
|
resolve((this.webex[namespace] || this.webex.internal[namespace])[name](data))
|
|
529
699
|
).catch((reason) =>
|
|
530
700
|
this.logger.error(
|
|
531
|
-
`${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}`,
|
|
532
702
|
reason
|
|
533
703
|
)
|
|
534
704
|
);
|
|
@@ -536,32 +706,37 @@ const Mercury = WebexPlugin.extend({
|
|
|
536
706
|
Promise.resolve()
|
|
537
707
|
)
|
|
538
708
|
.then(() => {
|
|
539
|
-
this.
|
|
709
|
+
const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
|
|
710
|
+
|
|
711
|
+
this._emit(`event${suffix}`, envelope);
|
|
540
712
|
const [namespace] = data.eventType.split('.');
|
|
541
713
|
|
|
542
714
|
if (namespace === data.eventType) {
|
|
543
|
-
this._emit(`event:${namespace}`, envelope);
|
|
715
|
+
this._emit(`event:${namespace}${suffix}`, envelope);
|
|
544
716
|
} else {
|
|
545
|
-
this._emit(`event:${namespace}`, envelope);
|
|
546
|
-
this._emit(`event:${data.eventType}`, envelope);
|
|
717
|
+
this._emit(`event:${namespace}${suffix}`, envelope);
|
|
718
|
+
this._emit(`event:${data.eventType}${suffix}`, envelope);
|
|
547
719
|
}
|
|
548
720
|
})
|
|
549
721
|
.catch((reason) => {
|
|
550
|
-
this.logger.error(
|
|
722
|
+
this.logger.error(
|
|
723
|
+
`${this.namespace}: error occurred processing socket message from ${sessionId}`,
|
|
724
|
+
reason
|
|
725
|
+
);
|
|
551
726
|
});
|
|
552
727
|
},
|
|
553
728
|
|
|
554
|
-
_setTimeOffset(event) {
|
|
729
|
+
_setTimeOffset(sessionId, event) {
|
|
555
730
|
const {wsWriteTimestamp} = event.data;
|
|
556
731
|
if (typeof wsWriteTimestamp === 'number' && wsWriteTimestamp > 0) {
|
|
557
732
|
this.mercuryTimeOffset = Date.now() - wsWriteTimestamp;
|
|
558
733
|
}
|
|
559
734
|
},
|
|
560
735
|
|
|
561
|
-
_reconnect(webSocketUrl) {
|
|
562
|
-
this.logger.info(`${this.namespace}: reconnecting`);
|
|
736
|
+
_reconnect(webSocketUrl, sessionId = this.defaultSessionId) {
|
|
737
|
+
this.logger.info(`${this.namespace}: reconnecting ${sessionId}`);
|
|
563
738
|
|
|
564
|
-
return this.connect(webSocketUrl);
|
|
739
|
+
return this.connect(webSocketUrl, sessionId);
|
|
565
740
|
},
|
|
566
741
|
});
|
|
567
742
|
|