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