@webex/internal-plugin-mercury 3.10.0 → 3.11.0-next.10
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/README.md +54 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.js +18 -24
- package/dist/errors.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mercury.js +404 -196
- package/dist/mercury.js.map +1 -1
- package/dist/socket/constants.js +16 -0
- package/dist/socket/constants.js.map +1 -0
- package/dist/socket/index.js.map +1 -1
- package/dist/socket/socket-base.js +47 -18
- package/dist/socket/socket-base.js.map +1 -1
- package/dist/socket/socket.js.map +1 -1
- package/dist/socket/socket.shim.js.map +1 -1
- package/package.json +17 -17
- package/src/mercury.js +412 -172
- package/src/socket/constants.js +6 -0
- package/src/socket/socket-base.js +40 -3
- package/test/unit/spec/mercury-events.js +20 -2
- package/test/unit/spec/mercury.js +211 -139
- package/test/unit/spec/socket.js +61 -0
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,18 @@ 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
|
+
},
|
|
51
|
+
_shutdownSwitchoverBackoffCalls: {
|
|
52
|
+
default: () => new Map(),
|
|
53
|
+
type: 'object',
|
|
54
|
+
},
|
|
43
55
|
localClusterServiceUrls: 'object',
|
|
44
56
|
mercuryTimeOffset: {
|
|
45
57
|
default: undefined,
|
|
@@ -99,50 +111,63 @@ const Mercury = WebexPlugin.extend({
|
|
|
99
111
|
/**
|
|
100
112
|
* Attach event listeners to a socket.
|
|
101
113
|
* @param {Socket} socket - The socket to attach listeners to
|
|
114
|
+
* @param {sessionId} sessionId - The socket related session ID
|
|
102
115
|
* @returns {void}
|
|
103
116
|
*/
|
|
104
|
-
_attachSocketEventListeners(socket) {
|
|
105
|
-
socket.on('close', (event) => this._onclose(event, socket));
|
|
106
|
-
socket.on('message', (...args) => this._onmessage(...args));
|
|
107
|
-
socket.on('pong', (...args) => this._setTimeOffset(...args));
|
|
108
|
-
socket.on('sequence-mismatch', (...args) =>
|
|
109
|
-
|
|
117
|
+
_attachSocketEventListeners(socket, sessionId) {
|
|
118
|
+
socket.on('close', (event) => this._onclose(sessionId, event, socket));
|
|
119
|
+
socket.on('message', (...args) => this._onmessage(sessionId, ...args));
|
|
120
|
+
socket.on('pong', (...args) => this._setTimeOffset(sessionId, ...args));
|
|
121
|
+
socket.on('sequence-mismatch', (...args) =>
|
|
122
|
+
this._emit(sessionId, 'sequence-mismatch', ...args)
|
|
123
|
+
);
|
|
124
|
+
socket.on('ping-pong-latency', (...args) =>
|
|
125
|
+
this._emit(sessionId, 'ping-pong-latency', ...args)
|
|
126
|
+
);
|
|
110
127
|
},
|
|
111
128
|
|
|
112
129
|
/**
|
|
113
130
|
* Handle imminent shutdown by establishing a new connection while keeping
|
|
114
131
|
* the current one alive (make-before-break).
|
|
115
132
|
* Idempotent: will no-op if already in progress.
|
|
133
|
+
* @param {string} sessionId - The session ID for which the shutdown is imminent
|
|
116
134
|
* @returns {void}
|
|
117
135
|
*/
|
|
118
|
-
_handleImminentShutdown() {
|
|
136
|
+
_handleImminentShutdown(sessionId) {
|
|
137
|
+
const oldSocket = this.sockets.get(sessionId);
|
|
138
|
+
|
|
119
139
|
try {
|
|
120
|
-
if
|
|
121
|
-
|
|
140
|
+
// Idempotent: if we already have a switchover backoff call for this session,
|
|
141
|
+
// a switchover is in progress – do nothing.
|
|
142
|
+
if (this._shutdownSwitchoverBackoffCalls.get(sessionId)) {
|
|
143
|
+
this.logger.info(
|
|
144
|
+
`${this.namespace}: [shutdown] switchover already in progress for ${sessionId}`
|
|
145
|
+
);
|
|
122
146
|
|
|
123
147
|
return;
|
|
124
148
|
}
|
|
125
|
-
|
|
149
|
+
|
|
126
150
|
this._shutdownSwitchoverId = `${Date.now()}`;
|
|
127
151
|
this.logger.info(
|
|
128
|
-
`${this.namespace}: [shutdown] switchover start, id=${this._shutdownSwitchoverId}`
|
|
152
|
+
`${this.namespace}: [shutdown] switchover start, id=${this._shutdownSwitchoverId} for ${sessionId}`
|
|
129
153
|
);
|
|
130
154
|
|
|
131
|
-
this._connectWithBackoff(undefined, {
|
|
155
|
+
this._connectWithBackoff(undefined, sessionId, {
|
|
132
156
|
isShutdownSwitchover: true,
|
|
133
157
|
attemptOptions: {
|
|
134
158
|
isShutdownSwitchover: true,
|
|
135
159
|
onSuccess: (newSocket, webSocketUrl) => {
|
|
136
160
|
this.logger.info(
|
|
137
|
-
`${this.namespace}: [shutdown] switchover connected, url: ${webSocketUrl}`
|
|
161
|
+
`${this.namespace}: [shutdown] switchover connected, url: ${webSocketUrl} for ${sessionId}`
|
|
138
162
|
);
|
|
139
163
|
|
|
140
|
-
const oldSocket = this.socket;
|
|
141
164
|
// Atomically switch active socket reference
|
|
142
|
-
this.socket =
|
|
143
|
-
this.connected =
|
|
165
|
+
this.socket = this.sockets.get(this.defaultSessionId);
|
|
166
|
+
this.connected = this.hasConnectedSockets(); // remain connected throughout
|
|
144
167
|
|
|
145
|
-
this._emit('event:mercury_shutdown_switchover_complete', {
|
|
168
|
+
this._emit(sessionId, 'event:mercury_shutdown_switchover_complete', {
|
|
169
|
+
url: webSocketUrl,
|
|
170
|
+
});
|
|
146
171
|
|
|
147
172
|
if (oldSocket) {
|
|
148
173
|
this.logger.info(
|
|
@@ -153,20 +178,25 @@ const Mercury = WebexPlugin.extend({
|
|
|
153
178
|
},
|
|
154
179
|
})
|
|
155
180
|
.then(() => {
|
|
156
|
-
this.logger.info(
|
|
181
|
+
this.logger.info(
|
|
182
|
+
`${this.namespace}: [shutdown] switchover completed successfully for ${sessionId}`
|
|
183
|
+
);
|
|
157
184
|
})
|
|
158
185
|
.catch((err) => {
|
|
159
186
|
this.logger.info(
|
|
160
|
-
`${this.namespace}: [shutdown] switchover exhausted retries; will fall back to normal reconnection`,
|
|
187
|
+
`${this.namespace}: [shutdown] switchover exhausted retries; will fall back to normal reconnection for ${sessionId}: `,
|
|
161
188
|
err
|
|
162
189
|
);
|
|
163
|
-
this._emit('event:mercury_shutdown_switchover_failed', {reason: err});
|
|
190
|
+
this._emit(sessionId, 'event:mercury_shutdown_switchover_failed', {reason: err});
|
|
164
191
|
// Old socket will eventually close with 4001, triggering normal reconnection
|
|
165
192
|
});
|
|
166
193
|
} catch (e) {
|
|
167
|
-
this.logger.error(
|
|
168
|
-
|
|
169
|
-
|
|
194
|
+
this.logger.error(
|
|
195
|
+
`${this.namespace}: [shutdown] error during switchover for ${sessionId}`,
|
|
196
|
+
e
|
|
197
|
+
);
|
|
198
|
+
this._shutdownSwitchoverBackoffCalls.delete(sessionId);
|
|
199
|
+
this._emit(sessionId, 'event:mercury_shutdown_switchover_failed', {reason: e});
|
|
170
200
|
}
|
|
171
201
|
},
|
|
172
202
|
|
|
@@ -178,29 +208,95 @@ const Mercury = WebexPlugin.extend({
|
|
|
178
208
|
return this.lastError;
|
|
179
209
|
},
|
|
180
210
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Get all active socket connections
|
|
213
|
+
* @returns {Map} Map of sessionId to socket instances
|
|
214
|
+
*/
|
|
215
|
+
getSockets() {
|
|
216
|
+
return this.sockets;
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get a specific socket by connection ID
|
|
221
|
+
* @param {string} sessionId - The connection identifier
|
|
222
|
+
* @returns {Socket|undefined} The socket instance or undefined if not found
|
|
223
|
+
*/
|
|
224
|
+
getSocket(sessionId = this.defaultSessionId) {
|
|
225
|
+
return this.sockets.get(sessionId);
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check if a socket is connected
|
|
230
|
+
* @param {string} [sessionId=this.defaultSessionId] - The session identifier
|
|
231
|
+
* @returns {boolean|undefined} True if the socket is connected
|
|
232
|
+
*/
|
|
233
|
+
hasConnectedSockets(sessionId = this.defaultSessionId) {
|
|
234
|
+
const socket = this.sockets.get(sessionId || this.defaultSessionId);
|
|
235
|
+
|
|
236
|
+
return socket?.connected;
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if any sockets are connecting
|
|
241
|
+
* @param {string} [sessionId=this.defaultSessionId] - The session identifier
|
|
242
|
+
* @returns {boolean|undefined} True if the socket is connecting
|
|
243
|
+
*/
|
|
244
|
+
hasConnectingSockets(sessionId = this.defaultSessionId) {
|
|
245
|
+
const socket = this.sockets.get(sessionId || this.defaultSessionId);
|
|
246
|
+
|
|
247
|
+
return socket?.connecting;
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Connect to Mercury for a specific session.
|
|
252
|
+
* @param {string} [webSocketUrl] - Optional websocket URL override. Falls back to the device websocket URL.
|
|
253
|
+
* @param {string} [sessionId=this.defaultSessionId] - The session identifier for this connection.
|
|
254
|
+
* @returns {Promise<void>} Resolves when connection flow completes for the session.
|
|
255
|
+
*/
|
|
256
|
+
connect(webSocketUrl, sessionId = this.defaultSessionId) {
|
|
257
|
+
if (!this._connectPromises) this._connectPromises = new Map();
|
|
258
|
+
|
|
259
|
+
// First check if there's already a connection promise for this session
|
|
260
|
+
if (this._connectPromises.has(sessionId)) {
|
|
261
|
+
this.logger.info(
|
|
262
|
+
`${this.namespace}: connection ${sessionId} already in progress, returning existing promise`
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
return this._connectPromises.get(sessionId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const sessionSocket = this.sockets.get(sessionId);
|
|
269
|
+
if (sessionSocket?.connected || sessionSocket?.connecting) {
|
|
270
|
+
this.logger.info(
|
|
271
|
+
`${this.namespace}: connection ${sessionId} already connected, will not connect again`
|
|
272
|
+
);
|
|
185
273
|
|
|
186
274
|
return Promise.resolve();
|
|
187
275
|
}
|
|
188
276
|
|
|
189
277
|
this.connecting = true;
|
|
190
278
|
|
|
191
|
-
this.logger.info(`${this.namespace}: starting connection attempt`);
|
|
279
|
+
this.logger.info(`${this.namespace}: starting connection attempt for ${sessionId}`);
|
|
192
280
|
this.logger.info(
|
|
193
281
|
`${this.namespace}: debug_mercury_logging stack: `,
|
|
194
282
|
new Error('debug_mercury_logging').stack
|
|
195
283
|
);
|
|
196
284
|
|
|
197
|
-
|
|
285
|
+
const connectPromise = Promise.resolve(
|
|
198
286
|
this.webex.internal.device.registered || this.webex.internal.device.register()
|
|
199
|
-
)
|
|
200
|
-
|
|
287
|
+
)
|
|
288
|
+
.then(() => {
|
|
289
|
+
this.logger.info(`${this.namespace}: connecting ${sessionId}`);
|
|
201
290
|
|
|
202
|
-
|
|
203
|
-
|
|
291
|
+
return this._connectWithBackoff(webSocketUrl, sessionId);
|
|
292
|
+
})
|
|
293
|
+
.finally(() => {
|
|
294
|
+
this._connectPromises.delete(sessionId);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
this._connectPromises.set(sessionId, connectPromise);
|
|
298
|
+
|
|
299
|
+
return connectPromise;
|
|
204
300
|
},
|
|
205
301
|
|
|
206
302
|
logout() {
|
|
@@ -210,7 +306,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
210
306
|
new Error('debug_mercury_logging').stack
|
|
211
307
|
);
|
|
212
308
|
|
|
213
|
-
return this.
|
|
309
|
+
return this.disconnectAll(
|
|
214
310
|
this.config.beforeLogoutOptionsCloseReason &&
|
|
215
311
|
!normalReconnectReasons.includes(this.config.beforeLogoutOptionsCloseReason)
|
|
216
312
|
? {code: 3050, reason: this.config.beforeLogoutOptionsCloseReason}
|
|
@@ -218,26 +314,74 @@ const Mercury = WebexPlugin.extend({
|
|
|
218
314
|
);
|
|
219
315
|
},
|
|
220
316
|
|
|
221
|
-
|
|
222
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Disconnect a Mercury socket for a specific session.
|
|
319
|
+
* @param {object} [options] - Optional websocket close options (for example: `{code, reason}`).
|
|
320
|
+
* @param {string} [sessionId=this.defaultSessionId] - The session identifier to disconnect.
|
|
321
|
+
* @returns {Promise<void>} Resolves after disconnect cleanup and close handling are initiated/completed.
|
|
322
|
+
*/
|
|
323
|
+
disconnect(options, sessionId = this.defaultSessionId) {
|
|
324
|
+
this.logger.info(
|
|
325
|
+
`${this.namespace}#disconnect: connecting state: ${this.connecting}, connected state: ${
|
|
326
|
+
this.connected
|
|
327
|
+
}, socket exists: ${!!this.socket}, options: ${JSON.stringify(options)}`
|
|
328
|
+
);
|
|
329
|
+
|
|
223
330
|
return new Promise((resolve) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.
|
|
331
|
+
const backoffCall = this.backoffCalls.get(sessionId);
|
|
332
|
+
if (backoffCall) {
|
|
333
|
+
this.logger.info(`${this.namespace}: aborting connection ${sessionId}`);
|
|
334
|
+
backoffCall.abort();
|
|
335
|
+
this.backoffCalls.delete(sessionId);
|
|
227
336
|
}
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
this.logger.info(`${this.namespace}: aborting shutdown switchover`);
|
|
231
|
-
|
|
337
|
+
const shutdownSwitchoverBackoffCall = this._shutdownSwitchoverBackoffCalls.get(sessionId);
|
|
338
|
+
if (shutdownSwitchoverBackoffCall) {
|
|
339
|
+
this.logger.info(`${this.namespace}: aborting shutdown switchover connection ${sessionId}`);
|
|
340
|
+
shutdownSwitchoverBackoffCall.abort();
|
|
341
|
+
this._shutdownSwitchoverBackoffCalls.delete(sessionId);
|
|
232
342
|
}
|
|
233
|
-
|
|
234
|
-
if (this.
|
|
235
|
-
this.
|
|
236
|
-
this.once('offline', resolve);
|
|
237
|
-
resolve(this.socket.close(options || undefined));
|
|
343
|
+
// Clean up any pending connection promises
|
|
344
|
+
if (this._connectPromises) {
|
|
345
|
+
this._connectPromises.delete(sessionId);
|
|
238
346
|
}
|
|
239
347
|
|
|
348
|
+
const sessionSocket = this.sockets.get(sessionId);
|
|
349
|
+
const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
|
|
350
|
+
|
|
351
|
+
if (sessionSocket) {
|
|
352
|
+
sessionSocket.removeAllListeners('message');
|
|
353
|
+
sessionSocket.connecting = false;
|
|
354
|
+
sessionSocket.connected = false;
|
|
355
|
+
this.once(sessionId === this.defaultSessionId ? 'offline' : `offline${suffix}`, resolve);
|
|
356
|
+
resolve(sessionSocket.close(options || undefined));
|
|
357
|
+
}
|
|
240
358
|
resolve();
|
|
359
|
+
|
|
360
|
+
// Update overall connected status
|
|
361
|
+
this.connected = this.hasConnectedSockets();
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Disconnect all socket connections
|
|
367
|
+
* @param {object} options - Close options
|
|
368
|
+
* @returns {Promise} Promise that resolves when all connections are closed
|
|
369
|
+
*/
|
|
370
|
+
disconnectAll(options) {
|
|
371
|
+
const disconnectPromises = [];
|
|
372
|
+
|
|
373
|
+
for (const sessionId of this.sockets.keys()) {
|
|
374
|
+
disconnectPromises.push(this.disconnect(options, sessionId));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return Promise.all(disconnectPromises).then(() => {
|
|
378
|
+
this.connected = false;
|
|
379
|
+
this.sockets.clear();
|
|
380
|
+
this.backoffCalls.clear();
|
|
381
|
+
// Clear connection promises to prevent stale promises
|
|
382
|
+
if (this._connectPromises) {
|
|
383
|
+
this._connectPromises.clear();
|
|
384
|
+
}
|
|
241
385
|
});
|
|
242
386
|
},
|
|
243
387
|
|
|
@@ -277,7 +421,26 @@ const Mercury = WebexPlugin.extend({
|
|
|
277
421
|
.getFeature('developer', 'web-high-availability')
|
|
278
422
|
.then((haMessagingEnabled) => {
|
|
279
423
|
if (haMessagingEnabled) {
|
|
280
|
-
|
|
424
|
+
let highPrioritySocketUrl;
|
|
425
|
+
try {
|
|
426
|
+
highPrioritySocketUrl =
|
|
427
|
+
this.webex.internal.services.convertUrlToPriorityHostUrl(webSocketUrl);
|
|
428
|
+
} catch (e) {
|
|
429
|
+
this.logger.warn(`${this.namespace}: error converting to high priority url`, e);
|
|
430
|
+
}
|
|
431
|
+
if (!highPrioritySocketUrl) {
|
|
432
|
+
const hostFromUrl = url.parse(webSocketUrl, true)?.host;
|
|
433
|
+
const isValidHost = this.webex.internal.services.isValidHost(hostFromUrl);
|
|
434
|
+
if (!isValidHost) {
|
|
435
|
+
this.logger.error(
|
|
436
|
+
`${this.namespace}: host ${hostFromUrl} is not a valid host from host catalog`
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return '';
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return highPrioritySocketUrl || webSocketUrl;
|
|
281
444
|
}
|
|
282
445
|
|
|
283
446
|
return webSocketUrl;
|
|
@@ -287,6 +450,9 @@ const Mercury = WebexPlugin.extend({
|
|
|
287
450
|
})
|
|
288
451
|
.then(() => this.webex.internal.feature.getFeature('developer', 'web-shared-mercury'))
|
|
289
452
|
.then((webSharedMercury) => {
|
|
453
|
+
if (!webSocketUrl) {
|
|
454
|
+
return '';
|
|
455
|
+
}
|
|
290
456
|
webSocketUrl = url.parse(webSocketUrl, true);
|
|
291
457
|
Object.assign(webSocketUrl.query, {
|
|
292
458
|
outboundWireFormat: 'text',
|
|
@@ -307,34 +473,29 @@ const Mercury = WebexPlugin.extend({
|
|
|
307
473
|
}
|
|
308
474
|
|
|
309
475
|
webSocketUrl.query.clientTimestamp = Date.now();
|
|
476
|
+
delete webSocketUrl.search;
|
|
310
477
|
|
|
311
478
|
return url.format(webSocketUrl);
|
|
312
479
|
});
|
|
313
480
|
},
|
|
314
481
|
|
|
315
|
-
_attemptConnection(socketUrl, callback, options = {}) {
|
|
482
|
+
_attemptConnection(socketUrl, sessionId, callback, options = {}) {
|
|
316
483
|
const {isShutdownSwitchover = false, onSuccess = null} = options;
|
|
317
484
|
|
|
318
485
|
const socket = new Socket();
|
|
486
|
+
socket.connecting = true;
|
|
319
487
|
let newWSUrl;
|
|
320
488
|
|
|
321
|
-
this._attachSocketEventListeners(socket);
|
|
322
|
-
|
|
323
|
-
// Check appropriate backoff call based on connection type
|
|
324
|
-
if (isShutdownSwitchover && !this._shutdownSwitchoverBackoffCall) {
|
|
325
|
-
const msg = `${this.namespace}: prevent socket open when switchover backoff call no longer defined`;
|
|
326
|
-
const err = new Error(msg);
|
|
327
|
-
|
|
328
|
-
this.logger.info(msg);
|
|
329
|
-
|
|
330
|
-
// Call the callback with the error before rejecting
|
|
331
|
-
callback(err);
|
|
489
|
+
this._attachSocketEventListeners(socket, sessionId);
|
|
332
490
|
|
|
333
|
-
|
|
334
|
-
|
|
491
|
+
const backoffCall = isShutdownSwitchover
|
|
492
|
+
? this._shutdownSwitchoverBackoffCalls.get(sessionId)
|
|
493
|
+
: this.backoffCalls.get(sessionId);
|
|
335
494
|
|
|
336
|
-
|
|
337
|
-
|
|
495
|
+
// Check appropriate backoff call based on connection type
|
|
496
|
+
if (!backoffCall) {
|
|
497
|
+
const mode = isShutdownSwitchover ? 'switchover backoff call' : 'backoffCall';
|
|
498
|
+
const msg = `${this.namespace}: prevent socket open when ${mode} no longer defined for ${sessionId}`;
|
|
338
499
|
const err = new Error(msg);
|
|
339
500
|
|
|
340
501
|
this.logger.info(msg);
|
|
@@ -348,16 +509,17 @@ const Mercury = WebexPlugin.extend({
|
|
|
348
509
|
// For shutdown switchover, don't set socket yet (make-before-break)
|
|
349
510
|
// For normal connection, set socket before opening to allow disconnect() to close it
|
|
350
511
|
if (!isShutdownSwitchover) {
|
|
351
|
-
this.
|
|
512
|
+
this.sockets.set(sessionId, socket);
|
|
352
513
|
}
|
|
353
514
|
|
|
354
|
-
return this._prepareAndOpenSocket(socket, socketUrl, isShutdownSwitchover)
|
|
515
|
+
return this._prepareAndOpenSocket(socket, socketUrl, sessionId, isShutdownSwitchover)
|
|
355
516
|
.then((webSocketUrl) => {
|
|
356
517
|
newWSUrl = webSocketUrl;
|
|
518
|
+
|
|
357
519
|
this.logger.info(
|
|
358
520
|
`${this.namespace}: ${
|
|
359
521
|
isShutdownSwitchover ? '[shutdown] switchover' : ''
|
|
360
|
-
} connected to mercury, success, action: connected, url: ${newWSUrl}`
|
|
522
|
+
} connected to mercury, success, action: connected for ${sessionId}, url: ${newWSUrl}`
|
|
361
523
|
);
|
|
362
524
|
|
|
363
525
|
// Custom success handler for shutdown switchover
|
|
@@ -384,7 +546,10 @@ const Mercury = WebexPlugin.extend({
|
|
|
384
546
|
.catch((reason) => {
|
|
385
547
|
// For shutdown, simpler error handling - just callback for retry
|
|
386
548
|
if (isShutdownSwitchover) {
|
|
387
|
-
this.logger.info(
|
|
549
|
+
this.logger.info(
|
|
550
|
+
`${this.namespace}: [shutdown] switchover attempt failed for ${sessionId}`,
|
|
551
|
+
reason
|
|
552
|
+
);
|
|
388
553
|
|
|
389
554
|
return callback(reason);
|
|
390
555
|
}
|
|
@@ -392,30 +557,36 @@ const Mercury = WebexPlugin.extend({
|
|
|
392
557
|
// Normal connection error handling (existing complex logic)
|
|
393
558
|
this.lastError = reason; // remember the last error
|
|
394
559
|
|
|
560
|
+
const backoffCallNormal = this.backoffCalls.get(sessionId);
|
|
395
561
|
// Suppress connection errors that appear to be network related. This
|
|
396
562
|
// may end up suppressing metrics during outages, but we might not care
|
|
397
563
|
// (especially since many of our outages happen in a way that client
|
|
398
564
|
// metrics can't be trusted).
|
|
399
|
-
if (reason.code !== 1006 &&
|
|
400
|
-
this._emit('connection_failed', reason, {
|
|
565
|
+
if (reason.code !== 1006 && backoffCallNormal && backoffCallNormal?.getNumRetries() > 0) {
|
|
566
|
+
this._emit(sessionId, 'connection_failed', reason, {
|
|
567
|
+
sessionId,
|
|
568
|
+
retries: backoffCallNormal?.getNumRetries(),
|
|
569
|
+
});
|
|
401
570
|
}
|
|
402
571
|
this.logger.info(
|
|
403
|
-
`${this.namespace}: connection attempt failed`,
|
|
572
|
+
`${this.namespace}: connection attempt failed for ${sessionId}`,
|
|
404
573
|
reason,
|
|
405
|
-
|
|
574
|
+
backoffCallNormal?.getNumRetries() === 0 ? reason.stack : ''
|
|
406
575
|
);
|
|
407
576
|
// UnknownResponse is produced by IE for any 4XXX; treated it like a bad
|
|
408
577
|
// web socket url and let WDM handle the token checking
|
|
409
578
|
if (reason instanceof UnknownResponse) {
|
|
410
579
|
this.logger.info(
|
|
411
|
-
`${this.namespace}: received unknown response code, refreshing device registration`
|
|
580
|
+
`${this.namespace}: received unknown response code for ${sessionId}, refreshing device registration`
|
|
412
581
|
);
|
|
413
582
|
|
|
414
583
|
return this.webex.internal.device.refresh().then(() => callback(reason));
|
|
415
584
|
}
|
|
416
585
|
// NotAuthorized implies expired token
|
|
417
586
|
if (reason instanceof NotAuthorized) {
|
|
418
|
-
this.logger.info(
|
|
587
|
+
this.logger.info(
|
|
588
|
+
`${this.namespace}: received authorization error for ${sessionId}, reauthorizing`
|
|
589
|
+
);
|
|
419
590
|
|
|
420
591
|
return this.webex.credentials.refresh({force: true}).then(() => callback(reason));
|
|
421
592
|
}
|
|
@@ -428,8 +599,10 @@ const Mercury = WebexPlugin.extend({
|
|
|
428
599
|
// BadRequest implies current credentials are for a Service Account
|
|
429
600
|
// Forbidden implies current user is not entitle for Webex
|
|
430
601
|
if (reason instanceof BadRequest || reason instanceof Forbidden) {
|
|
431
|
-
this.logger.warn(
|
|
432
|
-
|
|
602
|
+
this.logger.warn(
|
|
603
|
+
`${this.namespace}: received unrecoverable response from mercury for ${sessionId}`
|
|
604
|
+
);
|
|
605
|
+
backoffCallNormal?.abort();
|
|
433
606
|
|
|
434
607
|
return callback(reason);
|
|
435
608
|
}
|
|
@@ -439,7 +612,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
439
612
|
.then((haMessagingEnabled) => {
|
|
440
613
|
if (haMessagingEnabled) {
|
|
441
614
|
this.logger.info(
|
|
442
|
-
`${this.namespace}: received a generic connection error, will try to connect to another datacenter. failed, action: 'failed', url: ${newWSUrl} error: ${reason.message}`
|
|
615
|
+
`${this.namespace}: received a generic connection error for ${sessionId}, will try to connect to another datacenter. failed, action: 'failed', url: ${newWSUrl} error: ${reason.message}`
|
|
443
616
|
);
|
|
444
617
|
|
|
445
618
|
return this.webex.internal.services.markFailedUrl(newWSUrl);
|
|
@@ -453,12 +626,15 @@ const Mercury = WebexPlugin.extend({
|
|
|
453
626
|
return callback(reason);
|
|
454
627
|
})
|
|
455
628
|
.catch((reason) => {
|
|
456
|
-
this.logger.error(
|
|
629
|
+
this.logger.error(
|
|
630
|
+
`${this.namespace}: failed to handle connection failure for ${sessionId}`,
|
|
631
|
+
reason
|
|
632
|
+
);
|
|
457
633
|
callback(reason);
|
|
458
634
|
});
|
|
459
635
|
},
|
|
460
636
|
|
|
461
|
-
_prepareAndOpenSocket(socket, socketUrl, isShutdownSwitchover = false) {
|
|
637
|
+
_prepareAndOpenSocket(socket, socketUrl, sessionId, isShutdownSwitchover = false) {
|
|
462
638
|
const logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
|
|
463
639
|
|
|
464
640
|
return Promise.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()]).then(
|
|
@@ -481,30 +657,32 @@ const Mercury = WebexPlugin.extend({
|
|
|
481
657
|
options = {...options, ...this.webex.config.defaultMercuryOptions};
|
|
482
658
|
}
|
|
483
659
|
|
|
484
|
-
|
|
660
|
+
// Set the socket before opening it. This allows a disconnect() to close
|
|
661
|
+
// the socket if it is in the process of being opened.
|
|
662
|
+
this.sockets.set(sessionId, socket);
|
|
663
|
+
this.socket = this.sockets.get(this.defaultSessionId);
|
|
664
|
+
|
|
665
|
+
this.logger.info(`${this.namespace} ${logPrefix} url for ${sessionId}: ${webSocketUrl}`);
|
|
485
666
|
|
|
486
667
|
return socket.open(webSocketUrl, options).then(() => webSocketUrl);
|
|
487
668
|
}
|
|
488
669
|
);
|
|
489
670
|
},
|
|
490
671
|
|
|
491
|
-
_connectWithBackoff(webSocketUrl, context = {}) {
|
|
672
|
+
_connectWithBackoff(webSocketUrl, sessionId, context = {}) {
|
|
492
673
|
const {isShutdownSwitchover = false, attemptOptions = {}} = context;
|
|
493
674
|
|
|
494
675
|
return new Promise((resolve, reject) => {
|
|
495
|
-
// eslint gets confused about whether
|
|
676
|
+
// eslint gets confused about whether call is actually used
|
|
496
677
|
// eslint-disable-next-line prefer-const
|
|
497
678
|
let call;
|
|
498
|
-
const onComplete = (err) => {
|
|
499
|
-
// Clear state flags based on connection type
|
|
679
|
+
const onComplete = (err, sid = sessionId) => {
|
|
500
680
|
if (isShutdownSwitchover) {
|
|
501
|
-
this.
|
|
502
|
-
this._shutdownSwitchoverBackoffCall = undefined;
|
|
681
|
+
this._shutdownSwitchoverBackoffCalls.delete(sid);
|
|
503
682
|
} else {
|
|
504
|
-
this.
|
|
505
|
-
this.backoffCall = undefined;
|
|
683
|
+
this.backoffCalls.delete(sid);
|
|
506
684
|
}
|
|
507
|
-
|
|
685
|
+
const sessionSocket = this.sockets.get(sid);
|
|
508
686
|
if (err) {
|
|
509
687
|
const msg = isShutdownSwitchover
|
|
510
688
|
? `[shutdown] switchover failed after ${call.getNumRetries()} retries`
|
|
@@ -513,29 +691,45 @@ const Mercury = WebexPlugin.extend({
|
|
|
513
691
|
this.logger.info(
|
|
514
692
|
`${this.namespace}: ${msg}; log statement about next retry was inaccurate; ${err}`
|
|
515
693
|
);
|
|
694
|
+
if (sessionSocket) {
|
|
695
|
+
sessionSocket.connecting = false;
|
|
696
|
+
sessionSocket.connected = false;
|
|
697
|
+
}
|
|
516
698
|
|
|
517
699
|
return reject(err);
|
|
518
700
|
}
|
|
519
701
|
|
|
702
|
+
// Update overall connected status
|
|
703
|
+
if (sessionSocket) {
|
|
704
|
+
sessionSocket.connecting = false;
|
|
705
|
+
sessionSocket.connected = true;
|
|
706
|
+
}
|
|
520
707
|
// Default success handling for normal connections
|
|
521
708
|
if (!isShutdownSwitchover) {
|
|
522
|
-
this.
|
|
709
|
+
this.connecting = this.hasConnectingSockets();
|
|
710
|
+
this.connected = this.hasConnectedSockets();
|
|
523
711
|
this.hasEverConnected = true;
|
|
524
|
-
this._emit('online');
|
|
525
|
-
this.
|
|
712
|
+
this._emit(sid, 'online');
|
|
713
|
+
if (this.connected) {
|
|
714
|
+
this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(true);
|
|
715
|
+
}
|
|
526
716
|
}
|
|
527
717
|
|
|
528
718
|
return resolve();
|
|
529
719
|
};
|
|
530
|
-
|
|
531
720
|
// eslint-disable-next-line prefer-reflect
|
|
532
|
-
call = backoff.call(
|
|
533
|
-
|
|
534
|
-
|
|
721
|
+
call = backoff.call(
|
|
722
|
+
(callback) => {
|
|
723
|
+
const attemptNum = call.getNumRetries();
|
|
724
|
+
const logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
|
|
535
725
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
726
|
+
this.logger.info(
|
|
727
|
+
`${this.namespace}: executing ${logPrefix} attempt ${attemptNum} for ${sessionId}`
|
|
728
|
+
);
|
|
729
|
+
this._attemptConnection(webSocketUrl, sessionId, callback, attemptOptions);
|
|
730
|
+
},
|
|
731
|
+
(err) => onComplete(err, sessionId)
|
|
732
|
+
);
|
|
539
733
|
|
|
540
734
|
call.setStrategy(
|
|
541
735
|
new backoff.ExponentialStrategy({
|
|
@@ -554,23 +748,32 @@ const Mercury = WebexPlugin.extend({
|
|
|
554
748
|
call.failAfter(this.config.maxRetries);
|
|
555
749
|
}
|
|
556
750
|
|
|
751
|
+
// Store the call BEFORE setting up event handlers to prevent race conditions
|
|
752
|
+
// Store backoff call reference BEFORE starting (so it's available in _attemptConnection)
|
|
753
|
+
if (isShutdownSwitchover) {
|
|
754
|
+
this._shutdownSwitchoverBackoffCalls.set(sessionId, call);
|
|
755
|
+
} else {
|
|
756
|
+
this.backoffCalls.set(sessionId, call);
|
|
757
|
+
}
|
|
758
|
+
|
|
557
759
|
call.on('abort', () => {
|
|
558
760
|
const msg = isShutdownSwitchover ? 'Shutdown Switchover' : 'Connection';
|
|
559
761
|
|
|
560
|
-
this.logger.info(`${this.namespace}: ${msg} aborted`);
|
|
561
|
-
reject(new Error(`Mercury ${msg} Aborted`));
|
|
762
|
+
this.logger.info(`${this.namespace}: ${msg} aborted for ${sessionId}`);
|
|
763
|
+
reject(new Error(`Mercury ${msg} Aborted for ${sessionId}`));
|
|
562
764
|
});
|
|
563
765
|
|
|
564
766
|
call.on('callback', (err) => {
|
|
565
767
|
if (err) {
|
|
566
768
|
const number = call.getNumRetries();
|
|
567
769
|
const delay = Math.min(call.strategy_.nextBackoffDelay_, this.config.backoffTimeMax);
|
|
770
|
+
|
|
568
771
|
const logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : '';
|
|
569
772
|
|
|
570
773
|
this.logger.info(
|
|
571
774
|
`${this.namespace}: ${logPrefix} failed to connect; attempting retry ${
|
|
572
775
|
number + 1
|
|
573
|
-
} in ${delay} ms`
|
|
776
|
+
} in ${delay} ms for ${sessionId}`
|
|
574
777
|
);
|
|
575
778
|
/* istanbul ignore if */
|
|
576
779
|
if (process.env.NODE_ENV === 'development') {
|
|
@@ -579,30 +782,48 @@ const Mercury = WebexPlugin.extend({
|
|
|
579
782
|
|
|
580
783
|
return;
|
|
581
784
|
}
|
|
582
|
-
this.logger.info(`${this.namespace}: connected`);
|
|
785
|
+
this.logger.info(`${this.namespace}: connected ${sessionId}`);
|
|
583
786
|
});
|
|
584
787
|
|
|
585
|
-
// Store backoff call reference BEFORE starting (so it's available in _attemptConnection)
|
|
586
|
-
if (isShutdownSwitchover) {
|
|
587
|
-
this._shutdownSwitchoverBackoffCall = call;
|
|
588
|
-
} else {
|
|
589
|
-
this.backoffCall = call;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
788
|
call.start();
|
|
593
789
|
});
|
|
594
790
|
},
|
|
595
791
|
|
|
596
792
|
_emit(...args) {
|
|
597
793
|
try {
|
|
598
|
-
|
|
794
|
+
if (!args || args.length === 0) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// New signature: _emit(sessionId, eventName, ...rest)
|
|
799
|
+
// Backwards compatibility: if the first arg isn't a known sessionId (or defaultSessionId),
|
|
800
|
+
// treat the call as the old signature and forward directly to trigger(...)
|
|
801
|
+
const [first, second, ...rest] = args;
|
|
802
|
+
|
|
803
|
+
if (typeof first === 'string' && typeof second === 'string') {
|
|
804
|
+
const sessionId = first;
|
|
805
|
+
const eventName = second;
|
|
806
|
+
const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
|
|
807
|
+
|
|
808
|
+
this.trigger(`${eventName}${suffix}`, ...rest);
|
|
809
|
+
} else {
|
|
810
|
+
// Old usage: _emit(eventName, ...args)
|
|
811
|
+
this.trigger(...args);
|
|
812
|
+
}
|
|
599
813
|
} catch (error) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
error
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
814
|
+
// Safely handle errors without causing additional issues during cleanup
|
|
815
|
+
try {
|
|
816
|
+
this.logger.error(
|
|
817
|
+
`${this.namespace}: error occurred in event handler:`,
|
|
818
|
+
error,
|
|
819
|
+
' with args: ',
|
|
820
|
+
args
|
|
821
|
+
);
|
|
822
|
+
} catch (logError) {
|
|
823
|
+
// If even logging fails, just ignore to prevent cascading errors during cleanup
|
|
824
|
+
// eslint-disable-next-line no-console
|
|
825
|
+
console.error('Mercury _emit error handling failed:', logError);
|
|
826
|
+
}
|
|
606
827
|
}
|
|
607
828
|
},
|
|
608
829
|
|
|
@@ -626,36 +847,40 @@ const Mercury = WebexPlugin.extend({
|
|
|
626
847
|
return handlers;
|
|
627
848
|
},
|
|
628
849
|
|
|
629
|
-
_onclose(event, sourceSocket) {
|
|
850
|
+
_onclose(sessionId, event, sourceSocket) {
|
|
630
851
|
// I don't see any way to avoid the complexity or statement count in here.
|
|
631
852
|
/* eslint complexity: [0] */
|
|
632
853
|
|
|
633
854
|
try {
|
|
634
|
-
const isActiveSocket = sourceSocket === this.socket;
|
|
635
855
|
const reason = event.reason && event.reason.toLowerCase();
|
|
636
|
-
|
|
856
|
+
const sessionSocket = this.sockets.get(sessionId);
|
|
637
857
|
let socketUrl;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
// Old socket closed - get URL from the closed socket
|
|
858
|
+
event.sessionId = sessionId;
|
|
859
|
+
|
|
860
|
+
const isActiveSocket = sourceSocket === sessionSocket;
|
|
861
|
+
if (sourceSocket) {
|
|
643
862
|
socketUrl = sourceSocket.url;
|
|
644
863
|
}
|
|
864
|
+
this.sockets.delete(sessionId);
|
|
645
865
|
|
|
646
866
|
if (isActiveSocket) {
|
|
647
867
|
// Only tear down state if the currently active socket closed
|
|
648
|
-
if (
|
|
649
|
-
|
|
868
|
+
if (sessionSocket) {
|
|
869
|
+
sessionSocket.removeAllListeners();
|
|
870
|
+
if (sessionId === this.defaultSessionId) this.unset('socket');
|
|
871
|
+
this._emit(sessionId, 'offline', event);
|
|
872
|
+
}
|
|
873
|
+
// Update overall connected status
|
|
874
|
+
this.connecting = this.hasConnectingSockets();
|
|
875
|
+
this.connected = this.hasConnectedSockets();
|
|
876
|
+
|
|
877
|
+
if (!this.connected) {
|
|
878
|
+
this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(false);
|
|
650
879
|
}
|
|
651
|
-
this.unset('socket');
|
|
652
|
-
this.connected = false;
|
|
653
|
-
this._emit('offline', event);
|
|
654
|
-
this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(false);
|
|
655
880
|
} else {
|
|
656
881
|
// Old socket closed; do not flip connection state
|
|
657
882
|
this.logger.info(
|
|
658
|
-
`${this.namespace}: [shutdown] non-active socket closed, code=${event.code}`
|
|
883
|
+
`${this.namespace}: [shutdown] non-active socket closed, code=${event.code} for ${sessionId}`
|
|
659
884
|
);
|
|
660
885
|
// Clean up listeners from old socket now that it's closed
|
|
661
886
|
if (sourceSocket) {
|
|
@@ -667,14 +892,14 @@ const Mercury = WebexPlugin.extend({
|
|
|
667
892
|
case 1003:
|
|
668
893
|
// metric: disconnect
|
|
669
894
|
this.logger.info(
|
|
670
|
-
`${this.namespace}: Mercury service rejected last message; will not reconnect: ${event.reason}`
|
|
895
|
+
`${this.namespace}: Mercury service rejected last message for ${sessionId}; will not reconnect: ${event.reason}`
|
|
671
896
|
);
|
|
672
|
-
if (isActiveSocket) this._emit('offline.permanent', event);
|
|
897
|
+
if (isActiveSocket) this._emit(sessionId, 'offline.permanent', event);
|
|
673
898
|
break;
|
|
674
899
|
case 4000:
|
|
675
900
|
// metric: disconnect
|
|
676
|
-
this.logger.info(`${this.namespace}: socket replaced; will not reconnect`);
|
|
677
|
-
if (isActiveSocket) this._emit('offline.replaced', event);
|
|
901
|
+
this.logger.info(`${this.namespace}: socket ${sessionId} replaced; will not reconnect`);
|
|
902
|
+
if (isActiveSocket) this._emit(sessionId, 'offline.replaced', event);
|
|
678
903
|
// If not active, nothing to do
|
|
679
904
|
break;
|
|
680
905
|
case 4001:
|
|
@@ -684,26 +909,28 @@ const Mercury = WebexPlugin.extend({
|
|
|
684
909
|
// to be replaced, but the switchover in _handleImminentShutdown failed.
|
|
685
910
|
// This is a permanent failure - do not reconnect.
|
|
686
911
|
this.logger.warn(
|
|
687
|
-
`${this.namespace}: active socket closed with 4001; shutdown switchover failed`
|
|
912
|
+
`${this.namespace}: active socket closed with 4001; shutdown switchover failed for ${sessionId}`
|
|
688
913
|
);
|
|
689
|
-
this._emit('offline.permanent', event);
|
|
914
|
+
this._emit(sessionId, 'offline.permanent', event);
|
|
690
915
|
} else {
|
|
691
916
|
// Expected: old socket closed after successful switchover
|
|
692
917
|
this.logger.info(
|
|
693
|
-
`${this.namespace}: old socket closed with 4001 (replaced during shutdown); no reconnect needed`
|
|
918
|
+
`${this.namespace}: old socket closed with 4001 (replaced during shutdown); no reconnect needed for ${sessionId}`
|
|
694
919
|
);
|
|
695
|
-
this._emit('offline.replaced', event);
|
|
920
|
+
this._emit(sessionId, 'offline.replaced', event);
|
|
696
921
|
}
|
|
697
922
|
break;
|
|
698
923
|
case 1001:
|
|
699
924
|
case 1005:
|
|
700
925
|
case 1006:
|
|
701
926
|
case 1011:
|
|
702
|
-
this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
|
|
927
|
+
this.logger.info(`${this.namespace}: socket ${sessionId} disconnected; reconnecting`);
|
|
703
928
|
if (isActiveSocket) {
|
|
704
|
-
this._emit('offline.transient', event);
|
|
705
|
-
this.logger.info(
|
|
706
|
-
|
|
929
|
+
this._emit(sessionId, 'offline.transient', event);
|
|
930
|
+
this.logger.info(
|
|
931
|
+
`${this.namespace}: [shutdown] reconnecting active socket to recover for ${sessionId}`
|
|
932
|
+
);
|
|
933
|
+
this._reconnect(socketUrl, sessionId);
|
|
707
934
|
}
|
|
708
935
|
// metric: disconnect
|
|
709
936
|
// if (code == 1011 && reason !== ping error) metric: unexpected disconnect
|
|
@@ -711,51 +938,61 @@ const Mercury = WebexPlugin.extend({
|
|
|
711
938
|
case 1000:
|
|
712
939
|
case 3050: // 3050 indicates logout form of closure, default to old behavior, use config reason defined by consumer to proceed with the permanent block
|
|
713
940
|
if (normalReconnectReasons.includes(reason)) {
|
|
714
|
-
this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
|
|
941
|
+
this.logger.info(`${this.namespace}: socket ${sessionId} disconnected; reconnecting`);
|
|
715
942
|
if (isActiveSocket) {
|
|
716
|
-
this._emit('offline.transient', event);
|
|
717
|
-
this.logger.info(
|
|
718
|
-
|
|
943
|
+
this._emit(sessionId, 'offline.transient', event);
|
|
944
|
+
this.logger.info(
|
|
945
|
+
`${this.namespace}: [shutdown] reconnecting due to normal close for ${sessionId}`
|
|
946
|
+
);
|
|
947
|
+
this._reconnect(socketUrl, sessionId);
|
|
719
948
|
}
|
|
720
949
|
// metric: disconnect
|
|
721
950
|
// if (reason === done forced) metric: force closure
|
|
722
951
|
} else {
|
|
723
952
|
this.logger.info(
|
|
724
|
-
`${this.namespace}: socket disconnected; will not reconnect: ${event.reason}`
|
|
953
|
+
`${this.namespace}: socket ${sessionId} disconnected; will not reconnect: ${event.reason}`
|
|
725
954
|
);
|
|
726
|
-
if (isActiveSocket) this._emit('offline.permanent', event);
|
|
955
|
+
if (isActiveSocket) this._emit(sessionId, 'offline.permanent', event);
|
|
727
956
|
}
|
|
728
957
|
break;
|
|
729
958
|
default:
|
|
730
959
|
this.logger.info(
|
|
731
|
-
`${this.namespace}: socket disconnected unexpectedly; will not reconnect`
|
|
960
|
+
`${this.namespace}: socket ${sessionId} disconnected unexpectedly; will not reconnect`
|
|
732
961
|
);
|
|
733
962
|
// unexpected disconnect
|
|
734
|
-
if (isActiveSocket) this._emit('offline.permanent', event);
|
|
963
|
+
if (isActiveSocket) this._emit(sessionId, 'offline.permanent', event);
|
|
735
964
|
}
|
|
736
965
|
} catch (error) {
|
|
737
|
-
this.logger.error(
|
|
966
|
+
this.logger.error(
|
|
967
|
+
`${this.namespace}: error occurred in close handler for ${sessionId}`,
|
|
968
|
+
error
|
|
969
|
+
);
|
|
738
970
|
}
|
|
739
971
|
},
|
|
740
972
|
|
|
741
|
-
_onmessage(event) {
|
|
742
|
-
this._setTimeOffset(event);
|
|
973
|
+
_onmessage(sessionId, event) {
|
|
974
|
+
this._setTimeOffset(sessionId, event);
|
|
743
975
|
const envelope = event.data;
|
|
744
976
|
|
|
745
977
|
if (process.env.ENABLE_MERCURY_LOGGING) {
|
|
746
|
-
this.logger.debug(`${this.namespace}: message envelope: `, envelope);
|
|
978
|
+
this.logger.debug(`${this.namespace}: message envelope from ${sessionId}: `, envelope);
|
|
747
979
|
}
|
|
748
980
|
|
|
981
|
+
envelope.sessionId = sessionId;
|
|
982
|
+
|
|
749
983
|
// Handle shutdown message shape: { type: 'shutdown' }
|
|
750
984
|
if (envelope && envelope.type === 'shutdown') {
|
|
751
|
-
this.logger.info(
|
|
752
|
-
|
|
985
|
+
this.logger.info(
|
|
986
|
+
`${this.namespace}: [shutdown] imminent shutdown message received for ${sessionId}`
|
|
987
|
+
);
|
|
988
|
+
this._emit(sessionId, 'event:mercury_shutdown_imminent', envelope);
|
|
753
989
|
|
|
754
|
-
this._handleImminentShutdown();
|
|
990
|
+
this._handleImminentShutdown(sessionId);
|
|
755
991
|
|
|
756
992
|
return Promise.resolve();
|
|
757
993
|
}
|
|
758
994
|
|
|
995
|
+
envelope.sessionId = sessionId;
|
|
759
996
|
const {data} = envelope;
|
|
760
997
|
|
|
761
998
|
this._applyOverrides(data);
|
|
@@ -770,7 +1007,7 @@ const Mercury = WebexPlugin.extend({
|
|
|
770
1007
|
resolve((this.webex[namespace] || this.webex.internal[namespace])[name](data))
|
|
771
1008
|
).catch((reason) =>
|
|
772
1009
|
this.logger.error(
|
|
773
|
-
`${this.namespace}: error occurred in autowired event handler for ${data.eventType}`,
|
|
1010
|
+
`${this.namespace}: error occurred in autowired event handler for ${data.eventType} from ${sessionId}`,
|
|
774
1011
|
reason
|
|
775
1012
|
)
|
|
776
1013
|
);
|
|
@@ -778,32 +1015,35 @@ const Mercury = WebexPlugin.extend({
|
|
|
778
1015
|
Promise.resolve()
|
|
779
1016
|
)
|
|
780
1017
|
.then(() => {
|
|
781
|
-
this._emit('event',
|
|
1018
|
+
this._emit(sessionId, 'event', envelope);
|
|
782
1019
|
const [namespace] = data.eventType.split('.');
|
|
783
1020
|
|
|
784
1021
|
if (namespace === data.eventType) {
|
|
785
|
-
this._emit(`event:${namespace}`, envelope);
|
|
1022
|
+
this._emit(sessionId, `event:${namespace}`, envelope);
|
|
786
1023
|
} else {
|
|
787
|
-
this._emit(`event:${namespace}`, envelope);
|
|
788
|
-
this._emit(`event:${data.eventType}`, envelope);
|
|
1024
|
+
this._emit(sessionId, `event:${namespace}`, envelope);
|
|
1025
|
+
this._emit(sessionId, `event:${data.eventType}`, envelope);
|
|
789
1026
|
}
|
|
790
1027
|
})
|
|
791
1028
|
.catch((reason) => {
|
|
792
|
-
this.logger.error(
|
|
1029
|
+
this.logger.error(
|
|
1030
|
+
`${this.namespace}: error occurred processing socket message from ${sessionId}`,
|
|
1031
|
+
reason
|
|
1032
|
+
);
|
|
793
1033
|
});
|
|
794
1034
|
},
|
|
795
1035
|
|
|
796
|
-
_setTimeOffset(event) {
|
|
1036
|
+
_setTimeOffset(sessionId, event) {
|
|
797
1037
|
const {wsWriteTimestamp} = event.data;
|
|
798
1038
|
if (typeof wsWriteTimestamp === 'number' && wsWriteTimestamp > 0) {
|
|
799
1039
|
this.mercuryTimeOffset = Date.now() - wsWriteTimestamp;
|
|
800
1040
|
}
|
|
801
1041
|
},
|
|
802
1042
|
|
|
803
|
-
_reconnect(webSocketUrl) {
|
|
804
|
-
this.logger.info(`${this.namespace}: reconnecting`);
|
|
1043
|
+
_reconnect(webSocketUrl, sessionId = this.defaultSessionId) {
|
|
1044
|
+
this.logger.info(`${this.namespace}: reconnecting ${sessionId}`);
|
|
805
1045
|
|
|
806
|
-
return this.connect(webSocketUrl);
|
|
1046
|
+
return this.connect(webSocketUrl, sessionId);
|
|
807
1047
|
},
|
|
808
1048
|
});
|
|
809
1049
|
|