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