@webex/internal-plugin-mercury 3.12.0-next.9 → 3.12.0-task-refactor.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/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} from '@webex/common';
9
+ import {deprecated, oneFlight} from '@webex/common';
10
10
  import {camelCase, get, set} from 'lodash';
11
11
  import backoff from 'backoff';
12
12
 
@@ -25,7 +25,6 @@ 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',
29
28
 
30
29
  session: {
31
30
  connected: {
@@ -40,18 +39,7 @@ const Mercury = WebexPlugin.extend({
40
39
  default: false,
41
40
  type: 'boolean',
42
41
  },
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
- },
42
+ socket: 'object',
55
43
  localClusterServiceUrls: 'object',
56
44
  mercuryTimeOffset: {
57
45
  default: undefined,
@@ -111,63 +99,50 @@ const Mercury = WebexPlugin.extend({
111
99
  /**
112
100
  * Attach event listeners to a socket.
113
101
  * @param {Socket} socket - The socket to attach listeners to
114
- * @param {sessionId} sessionId - The socket related session ID
115
102
  * @returns {void}
116
103
  */
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
- );
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) => this._emit('sequence-mismatch', ...args));
109
+ socket.on('ping-pong-latency', (...args) => this._emit('ping-pong-latency', ...args));
127
110
  },
128
111
 
129
112
  /**
130
113
  * Handle imminent shutdown by establishing a new connection while keeping
131
114
  * the current one alive (make-before-break).
132
115
  * Idempotent: will no-op if already in progress.
133
- * @param {string} sessionId - The session ID for which the shutdown is imminent
134
116
  * @returns {void}
135
117
  */
136
- _handleImminentShutdown(sessionId) {
137
- const oldSocket = this.sockets.get(sessionId);
138
-
118
+ _handleImminentShutdown() {
139
119
  try {
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
- );
120
+ if (this._shutdownSwitchoverInProgress) {
121
+ this.logger.info(`${this.namespace}: [shutdown] switchover already in progress`);
146
122
 
147
123
  return;
148
124
  }
149
-
125
+ this._shutdownSwitchoverInProgress = true;
150
126
  this._shutdownSwitchoverId = `${Date.now()}`;
151
127
  this.logger.info(
152
- `${this.namespace}: [shutdown] switchover start, id=${this._shutdownSwitchoverId} for ${sessionId}`
128
+ `${this.namespace}: [shutdown] switchover start, id=${this._shutdownSwitchoverId}`
153
129
  );
154
130
 
155
- this._connectWithBackoff(undefined, sessionId, {
131
+ this._connectWithBackoff(undefined, {
156
132
  isShutdownSwitchover: true,
157
133
  attemptOptions: {
158
134
  isShutdownSwitchover: true,
159
135
  onSuccess: (newSocket, webSocketUrl) => {
160
136
  this.logger.info(
161
- `${this.namespace}: [shutdown] switchover connected, url: ${webSocketUrl} for ${sessionId}`
137
+ `${this.namespace}: [shutdown] switchover connected, url: ${webSocketUrl}`
162
138
  );
163
139
 
140
+ const oldSocket = this.socket;
164
141
  // Atomically switch active socket reference
165
- this.socket = this.sockets.get(this.defaultSessionId);
166
- this.connected = this.hasConnectedSockets(); // remain connected throughout
142
+ this.socket = newSocket;
143
+ this.connected = true; // remain connected throughout
167
144
 
168
- this._emit(sessionId, 'event:mercury_shutdown_switchover_complete', {
169
- url: webSocketUrl,
170
- });
145
+ this._emit('event:mercury_shutdown_switchover_complete', {url: webSocketUrl});
171
146
 
172
147
  if (oldSocket) {
173
148
  this.logger.info(
@@ -178,25 +153,20 @@ const Mercury = WebexPlugin.extend({
178
153
  },
179
154
  })
180
155
  .then(() => {
181
- this.logger.info(
182
- `${this.namespace}: [shutdown] switchover completed successfully for ${sessionId}`
183
- );
156
+ this.logger.info(`${this.namespace}: [shutdown] switchover completed successfully`);
184
157
  })
185
158
  .catch((err) => {
186
159
  this.logger.info(
187
- `${this.namespace}: [shutdown] switchover exhausted retries; will fall back to normal reconnection for ${sessionId}: `,
160
+ `${this.namespace}: [shutdown] switchover exhausted retries; will fall back to normal reconnection`,
188
161
  err
189
162
  );
190
- this._emit(sessionId, 'event:mercury_shutdown_switchover_failed', {reason: err});
163
+ this._emit('event:mercury_shutdown_switchover_failed', {reason: err});
191
164
  // Old socket will eventually close with 4001, triggering normal reconnection
192
165
  });
193
166
  } catch (e) {
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});
167
+ this.logger.error(`${this.namespace}: [shutdown] error during switchover`, e);
168
+ this._shutdownSwitchoverInProgress = false;
169
+ this._emit('event:mercury_shutdown_switchover_failed', {reason: e});
200
170
  }
201
171
  },
202
172
 
@@ -208,95 +178,29 @@ const Mercury = WebexPlugin.extend({
208
178
  return this.lastError;
209
179
  },
210
180
 
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
- );
181
+ @oneFlight
182
+ connect(webSocketUrl) {
183
+ if (this.connected) {
184
+ this.logger.info(`${this.namespace}: already connected, will not connect again`);
273
185
 
274
186
  return Promise.resolve();
275
187
  }
276
188
 
277
189
  this.connecting = true;
278
190
 
279
- this.logger.info(`${this.namespace}: starting connection attempt for ${sessionId}`);
191
+ this.logger.info(`${this.namespace}: starting connection attempt`);
280
192
  this.logger.info(
281
193
  `${this.namespace}: debug_mercury_logging stack: `,
282
194
  new Error('debug_mercury_logging').stack
283
195
  );
284
196
 
285
- const connectPromise = Promise.resolve(
197
+ return Promise.resolve(
286
198
  this.webex.internal.device.registered || this.webex.internal.device.register()
287
- )
288
- .then(() => {
289
- this.logger.info(`${this.namespace}: connecting ${sessionId}`);
199
+ ).then(() => {
200
+ this.logger.info(`${this.namespace}: connecting`);
290
201
 
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;
202
+ return this._connectWithBackoff(webSocketUrl);
203
+ });
300
204
  },
301
205
 
302
206
  logout() {
@@ -306,7 +210,7 @@ const Mercury = WebexPlugin.extend({
306
210
  new Error('debug_mercury_logging').stack
307
211
  );
308
212
 
309
- return this.disconnectAll(
213
+ return this.disconnect(
310
214
  this.config.beforeLogoutOptionsCloseReason &&
311
215
  !normalReconnectReasons.includes(this.config.beforeLogoutOptionsCloseReason)
312
216
  ? {code: 3050, reason: this.config.beforeLogoutOptionsCloseReason}
@@ -314,74 +218,26 @@ const Mercury = WebexPlugin.extend({
314
218
  );
315
219
  },
316
220
 
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
-
221
+ @oneFlight
222
+ disconnect(options) {
330
223
  return new Promise((resolve) => {
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);
336
- }
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);
342
- }
343
- // Clean up any pending connection promises
344
- if (this._connectPromises) {
345
- this._connectPromises.delete(sessionId);
224
+ if (this.backoffCall) {
225
+ this.logger.info(`${this.namespace}: aborting connection`);
226
+ this.backoffCall.abort();
346
227
  }
347
228
 
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));
229
+ if (this._shutdownSwitchoverBackoffCall) {
230
+ this.logger.info(`${this.namespace}: aborting shutdown switchover`);
231
+ this._shutdownSwitchoverBackoffCall.abort();
357
232
  }
358
- resolve();
359
233
 
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();
234
+ if (this.socket) {
235
+ this.socket.removeAllListeners('message');
236
+ this.once('offline', resolve);
237
+ resolve(this.socket.close(options || undefined));
384
238
  }
239
+
240
+ resolve();
385
241
  });
386
242
  },
387
243
 
@@ -473,29 +329,34 @@ const Mercury = WebexPlugin.extend({
473
329
  }
474
330
 
475
331
  webSocketUrl.query.clientTimestamp = Date.now();
476
- delete webSocketUrl.search;
477
332
 
478
333
  return url.format(webSocketUrl);
479
334
  });
480
335
  },
481
336
 
482
- _attemptConnection(socketUrl, sessionId, callback, options = {}) {
337
+ _attemptConnection(socketUrl, callback, options = {}) {
483
338
  const {isShutdownSwitchover = false, onSuccess = null} = options;
484
339
 
485
340
  const socket = new Socket();
486
- socket.connecting = true;
487
341
  let newWSUrl;
488
342
 
489
- this._attachSocketEventListeners(socket, sessionId);
490
-
491
- const backoffCall = isShutdownSwitchover
492
- ? this._shutdownSwitchoverBackoffCalls.get(sessionId)
493
- : this.backoffCalls.get(sessionId);
343
+ this._attachSocketEventListeners(socket);
494
344
 
495
345
  // 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}`;
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);
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`;
499
360
  const err = new Error(msg);
500
361
 
501
362
  this.logger.info(msg);
@@ -509,17 +370,16 @@ const Mercury = WebexPlugin.extend({
509
370
  // For shutdown switchover, don't set socket yet (make-before-break)
510
371
  // For normal connection, set socket before opening to allow disconnect() to close it
511
372
  if (!isShutdownSwitchover) {
512
- this.sockets.set(sessionId, socket);
373
+ this.socket = socket;
513
374
  }
514
375
 
515
- return this._prepareAndOpenSocket(socket, socketUrl, sessionId, isShutdownSwitchover)
376
+ return this._prepareAndOpenSocket(socket, socketUrl, isShutdownSwitchover)
516
377
  .then((webSocketUrl) => {
517
378
  newWSUrl = webSocketUrl;
518
-
519
379
  this.logger.info(
520
380
  `${this.namespace}: ${
521
381
  isShutdownSwitchover ? '[shutdown] switchover' : ''
522
- } connected to mercury, success, action: connected for ${sessionId}, url: ${newWSUrl}`
382
+ } connected to mercury, success, action: connected, url: ${newWSUrl}`
523
383
  );
524
384
 
525
385
  // Custom success handler for shutdown switchover
@@ -546,10 +406,7 @@ const Mercury = WebexPlugin.extend({
546
406
  .catch((reason) => {
547
407
  // For shutdown, simpler error handling - just callback for retry
548
408
  if (isShutdownSwitchover) {
549
- this.logger.info(
550
- `${this.namespace}: [shutdown] switchover attempt failed for ${sessionId}`,
551
- reason
552
- );
409
+ this.logger.info(`${this.namespace}: [shutdown] switchover attempt failed`, reason);
553
410
 
554
411
  return callback(reason);
555
412
  }
@@ -557,36 +414,30 @@ const Mercury = WebexPlugin.extend({
557
414
  // Normal connection error handling (existing complex logic)
558
415
  this.lastError = reason; // remember the last error
559
416
 
560
- const backoffCallNormal = this.backoffCalls.get(sessionId);
561
417
  // Suppress connection errors that appear to be network related. This
562
418
  // may end up suppressing metrics during outages, but we might not care
563
419
  // (especially since many of our outages happen in a way that client
564
420
  // metrics can't be trusted).
565
- if (reason.code !== 1006 && backoffCallNormal && backoffCallNormal?.getNumRetries() > 0) {
566
- this._emit(sessionId, 'connection_failed', reason, {
567
- sessionId,
568
- retries: backoffCallNormal?.getNumRetries(),
569
- });
421
+ if (reason.code !== 1006 && this.backoffCall && this.backoffCall?.getNumRetries() > 0) {
422
+ this._emit('connection_failed', reason, {retries: this.backoffCall?.getNumRetries()});
570
423
  }
571
424
  this.logger.info(
572
- `${this.namespace}: connection attempt failed for ${sessionId}`,
425
+ `${this.namespace}: connection attempt failed`,
573
426
  reason,
574
- backoffCallNormal?.getNumRetries() === 0 ? reason.stack : ''
427
+ this.backoffCall?.getNumRetries() === 0 ? reason.stack : ''
575
428
  );
576
429
  // UnknownResponse is produced by IE for any 4XXX; treated it like a bad
577
430
  // web socket url and let WDM handle the token checking
578
431
  if (reason instanceof UnknownResponse) {
579
432
  this.logger.info(
580
- `${this.namespace}: received unknown response code for ${sessionId}, refreshing device registration`
433
+ `${this.namespace}: received unknown response code, refreshing device registration`
581
434
  );
582
435
 
583
436
  return this.webex.internal.device.refresh().then(() => callback(reason));
584
437
  }
585
438
  // NotAuthorized implies expired token
586
439
  if (reason instanceof NotAuthorized) {
587
- this.logger.info(
588
- `${this.namespace}: received authorization error for ${sessionId}, reauthorizing`
589
- );
440
+ this.logger.info(`${this.namespace}: received authorization error, reauthorizing`);
590
441
 
591
442
  return this.webex.credentials.refresh({force: true}).then(() => callback(reason));
592
443
  }
@@ -599,10 +450,8 @@ const Mercury = WebexPlugin.extend({
599
450
  // BadRequest implies current credentials are for a Service Account
600
451
  // Forbidden implies current user is not entitle for Webex
601
452
  if (reason instanceof BadRequest || reason instanceof Forbidden) {
602
- this.logger.warn(
603
- `${this.namespace}: received unrecoverable response from mercury for ${sessionId}`
604
- );
605
- backoffCallNormal?.abort();
453
+ this.logger.warn(`${this.namespace}: received unrecoverable response from mercury`);
454
+ this.backoffCall.abort();
606
455
 
607
456
  return callback(reason);
608
457
  }
@@ -612,7 +461,7 @@ const Mercury = WebexPlugin.extend({
612
461
  .then((haMessagingEnabled) => {
613
462
  if (haMessagingEnabled) {
614
463
  this.logger.info(
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}`
464
+ `${this.namespace}: received a generic connection error, will try to connect to another datacenter. failed, action: 'failed', url: ${newWSUrl} error: ${reason.message}`
616
465
  );
617
466
 
618
467
  return this.webex.internal.services.markFailedUrl(newWSUrl);
@@ -626,15 +475,12 @@ const Mercury = WebexPlugin.extend({
626
475
  return callback(reason);
627
476
  })
628
477
  .catch((reason) => {
629
- this.logger.error(
630
- `${this.namespace}: failed to handle connection failure for ${sessionId}`,
631
- reason
632
- );
478
+ this.logger.error(`${this.namespace}: failed to handle connection failure`, reason);
633
479
  callback(reason);
634
480
  });
635
481
  },
636
482
 
637
- _prepareAndOpenSocket(socket, socketUrl, sessionId, isShutdownSwitchover = false) {
483
+ _prepareAndOpenSocket(socket, socketUrl, isShutdownSwitchover = false) {
638
484
  const logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
639
485
 
640
486
  return Promise.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()]).then(
@@ -657,32 +503,30 @@ const Mercury = WebexPlugin.extend({
657
503
  options = {...options, ...this.webex.config.defaultMercuryOptions};
658
504
  }
659
505
 
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}`);
506
+ this.logger.info(`${this.namespace}: ${logPrefix} url: ${webSocketUrl}`);
666
507
 
667
508
  return socket.open(webSocketUrl, options).then(() => webSocketUrl);
668
509
  }
669
510
  );
670
511
  },
671
512
 
672
- _connectWithBackoff(webSocketUrl, sessionId, context = {}) {
513
+ _connectWithBackoff(webSocketUrl, context = {}) {
673
514
  const {isShutdownSwitchover = false, attemptOptions = {}} = context;
674
515
 
675
516
  return new Promise((resolve, reject) => {
676
- // eslint gets confused about whether call is actually used
517
+ // eslint gets confused about whether or not call is actually used
677
518
  // eslint-disable-next-line prefer-const
678
519
  let call;
679
- const onComplete = (err, sid = sessionId) => {
520
+ const onComplete = (err) => {
521
+ // Clear state flags based on connection type
680
522
  if (isShutdownSwitchover) {
681
- this._shutdownSwitchoverBackoffCalls.delete(sid);
523
+ this._shutdownSwitchoverInProgress = false;
524
+ this._shutdownSwitchoverBackoffCall = undefined;
682
525
  } else {
683
- this.backoffCalls.delete(sid);
526
+ this.connecting = false;
527
+ this.backoffCall = undefined;
684
528
  }
685
- const sessionSocket = this.sockets.get(sid);
529
+
686
530
  if (err) {
687
531
  const msg = isShutdownSwitchover
688
532
  ? `[shutdown] switchover failed after ${call.getNumRetries()} retries`
@@ -691,45 +535,29 @@ const Mercury = WebexPlugin.extend({
691
535
  this.logger.info(
692
536
  `${this.namespace}: ${msg}; log statement about next retry was inaccurate; ${err}`
693
537
  );
694
- if (sessionSocket) {
695
- sessionSocket.connecting = false;
696
- sessionSocket.connected = false;
697
- }
698
538
 
699
539
  return reject(err);
700
540
  }
701
541
 
702
- // Update overall connected status
703
- if (sessionSocket) {
704
- sessionSocket.connecting = false;
705
- sessionSocket.connected = true;
706
- }
707
542
  // Default success handling for normal connections
708
543
  if (!isShutdownSwitchover) {
709
- this.connecting = this.hasConnectingSockets();
710
- this.connected = this.hasConnectedSockets();
544
+ this.connected = true;
711
545
  this.hasEverConnected = true;
712
- this._emit(sid, 'online');
713
- if (this.connected) {
714
- this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(true);
715
- }
546
+ this._emit('online');
547
+ this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(true);
716
548
  }
717
549
 
718
550
  return resolve();
719
551
  };
552
+
720
553
  // eslint-disable-next-line prefer-reflect
721
- call = backoff.call(
722
- (callback) => {
723
- const attemptNum = call.getNumRetries();
724
- const logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
554
+ call = backoff.call((callback) => {
555
+ const attemptNum = call.getNumRetries();
556
+ const logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : 'connection';
725
557
 
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
- );
558
+ this.logger.info(`${this.namespace}: executing ${logPrefix} attempt ${attemptNum}`);
559
+ this._attemptConnection(webSocketUrl, callback, attemptOptions);
560
+ }, onComplete);
733
561
 
734
562
  call.setStrategy(
735
563
  new backoff.ExponentialStrategy({
@@ -748,32 +576,23 @@ const Mercury = WebexPlugin.extend({
748
576
  call.failAfter(this.config.maxRetries);
749
577
  }
750
578
 
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
-
759
579
  call.on('abort', () => {
760
580
  const msg = isShutdownSwitchover ? 'Shutdown Switchover' : 'Connection';
761
581
 
762
- this.logger.info(`${this.namespace}: ${msg} aborted for ${sessionId}`);
763
- reject(new Error(`Mercury ${msg} Aborted for ${sessionId}`));
582
+ this.logger.info(`${this.namespace}: ${msg} aborted`);
583
+ reject(new Error(`Mercury ${msg} Aborted`));
764
584
  });
765
585
 
766
586
  call.on('callback', (err) => {
767
587
  if (err) {
768
588
  const number = call.getNumRetries();
769
589
  const delay = Math.min(call.strategy_.nextBackoffDelay_, this.config.backoffTimeMax);
770
-
771
590
  const logPrefix = isShutdownSwitchover ? '[shutdown] switchover' : '';
772
591
 
773
592
  this.logger.info(
774
593
  `${this.namespace}: ${logPrefix} failed to connect; attempting retry ${
775
594
  number + 1
776
- } in ${delay} ms for ${sessionId}`
595
+ } in ${delay} ms`
777
596
  );
778
597
  /* istanbul ignore if */
779
598
  if (process.env.NODE_ENV === 'development') {
@@ -782,55 +601,34 @@ const Mercury = WebexPlugin.extend({
782
601
 
783
602
  return;
784
603
  }
785
- this.logger.info(`${this.namespace}: connected ${sessionId}`);
604
+ this.logger.info(`${this.namespace}: connected`);
786
605
  });
787
606
 
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
+
788
614
  call.start();
789
615
  });
790
616
  },
791
617
 
792
618
  _emit(...args) {
793
619
  try {
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
- }
620
+ this.trigger(...args);
813
621
  } catch (error) {
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
- }
622
+ this.logger.error(
623
+ `${this.namespace}: error occurred in event handler:`,
624
+ error,
625
+ ' with args: ',
626
+ args
627
+ );
827
628
  }
828
629
  },
829
630
 
830
631
  _getEventHandlers(eventType) {
831
- if (!eventType) {
832
- return [];
833
- }
834
632
  const [namespace, name] = eventType.split('.');
835
633
  const handlers = [];
836
634
 
@@ -850,40 +648,36 @@ const Mercury = WebexPlugin.extend({
850
648
  return handlers;
851
649
  },
852
650
 
853
- _onclose(sessionId, event, sourceSocket) {
651
+ _onclose(event, sourceSocket) {
854
652
  // I don't see any way to avoid the complexity or statement count in here.
855
653
  /* eslint complexity: [0] */
856
654
 
857
655
  try {
656
+ const isActiveSocket = sourceSocket === this.socket;
858
657
  const reason = event.reason && event.reason.toLowerCase();
859
- const sessionSocket = this.sockets.get(sessionId);
860
- let socketUrl;
861
- event.sessionId = sessionId;
862
658
 
863
- const isActiveSocket = sourceSocket === sessionSocket;
864
- if (sourceSocket) {
659
+ let socketUrl;
660
+ if (isActiveSocket && this.socket) {
661
+ // Active socket closed - get URL from current socket reference
662
+ socketUrl = this.socket.url;
663
+ } else if (sourceSocket) {
664
+ // Old socket closed - get URL from the closed socket
865
665
  socketUrl = sourceSocket.url;
866
666
  }
867
- this.sockets.delete(sessionId);
868
667
 
869
668
  if (isActiveSocket) {
870
669
  // Only tear down state if the currently active socket closed
871
- if (sessionSocket) {
872
- sessionSocket.removeAllListeners();
873
- if (sessionId === this.defaultSessionId) this.unset('socket');
874
- this._emit(sessionId, 'offline', event);
875
- }
876
- // Update overall connected status
877
- this.connecting = this.hasConnectingSockets();
878
- this.connected = this.hasConnectedSockets();
879
-
880
- if (!this.connected) {
881
- this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(false);
670
+ if (this.socket) {
671
+ this.socket.removeAllListeners();
882
672
  }
673
+ this.unset('socket');
674
+ this.connected = false;
675
+ this._emit('offline', event);
676
+ this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(false);
883
677
  } else {
884
678
  // Old socket closed; do not flip connection state
885
679
  this.logger.info(
886
- `${this.namespace}: [shutdown] non-active socket closed, code=${event.code} for ${sessionId}`
680
+ `${this.namespace}: [shutdown] non-active socket closed, code=${event.code}`
887
681
  );
888
682
  // Clean up listeners from old socket now that it's closed
889
683
  if (sourceSocket) {
@@ -895,14 +689,14 @@ const Mercury = WebexPlugin.extend({
895
689
  case 1003:
896
690
  // metric: disconnect
897
691
  this.logger.info(
898
- `${this.namespace}: Mercury service rejected last message for ${sessionId}; will not reconnect: ${event.reason}`
692
+ `${this.namespace}: Mercury service rejected last message; will not reconnect: ${event.reason}`
899
693
  );
900
- if (isActiveSocket) this._emit(sessionId, 'offline.permanent', event);
694
+ if (isActiveSocket) this._emit('offline.permanent', event);
901
695
  break;
902
696
  case 4000:
903
697
  // metric: disconnect
904
- this.logger.info(`${this.namespace}: socket ${sessionId} replaced; will not reconnect`);
905
- if (isActiveSocket) this._emit(sessionId, 'offline.replaced', event);
698
+ this.logger.info(`${this.namespace}: socket replaced; will not reconnect`);
699
+ if (isActiveSocket) this._emit('offline.replaced', event);
906
700
  // If not active, nothing to do
907
701
  break;
908
702
  case 4001:
@@ -912,28 +706,26 @@ const Mercury = WebexPlugin.extend({
912
706
  // to be replaced, but the switchover in _handleImminentShutdown failed.
913
707
  // This is a permanent failure - do not reconnect.
914
708
  this.logger.warn(
915
- `${this.namespace}: active socket closed with 4001; shutdown switchover failed for ${sessionId}`
709
+ `${this.namespace}: active socket closed with 4001; shutdown switchover failed`
916
710
  );
917
- this._emit(sessionId, 'offline.permanent', event);
711
+ this._emit('offline.permanent', event);
918
712
  } else {
919
713
  // Expected: old socket closed after successful switchover
920
714
  this.logger.info(
921
- `${this.namespace}: old socket closed with 4001 (replaced during shutdown); no reconnect needed for ${sessionId}`
715
+ `${this.namespace}: old socket closed with 4001 (replaced during shutdown); no reconnect needed`
922
716
  );
923
- this._emit(sessionId, 'offline.replaced', event);
717
+ this._emit('offline.replaced', event);
924
718
  }
925
719
  break;
926
720
  case 1001:
927
721
  case 1005:
928
722
  case 1006:
929
723
  case 1011:
930
- this.logger.info(`${this.namespace}: socket ${sessionId} disconnected; reconnecting`);
724
+ this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
931
725
  if (isActiveSocket) {
932
- this._emit(sessionId, 'offline.transient', event);
933
- this.logger.info(
934
- `${this.namespace}: [shutdown] reconnecting active socket to recover for ${sessionId}`
935
- );
936
- this._reconnect(socketUrl, sessionId);
726
+ this._emit('offline.transient', event);
727
+ this.logger.info(`${this.namespace}: [shutdown] reconnecting active socket to recover`);
728
+ this._reconnect(socketUrl);
937
729
  }
938
730
  // metric: disconnect
939
731
  // if (code == 1011 && reason !== ping error) metric: unexpected disconnect
@@ -941,71 +733,55 @@ const Mercury = WebexPlugin.extend({
941
733
  case 1000:
942
734
  case 3050: // 3050 indicates logout form of closure, default to old behavior, use config reason defined by consumer to proceed with the permanent block
943
735
  if (normalReconnectReasons.includes(reason)) {
944
- this.logger.info(`${this.namespace}: socket ${sessionId} disconnected; reconnecting`);
736
+ this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
945
737
  if (isActiveSocket) {
946
- this._emit(sessionId, 'offline.transient', event);
947
- this.logger.info(
948
- `${this.namespace}: [shutdown] reconnecting due to normal close for ${sessionId}`
949
- );
950
- this._reconnect(socketUrl, sessionId);
738
+ this._emit('offline.transient', event);
739
+ this.logger.info(`${this.namespace}: [shutdown] reconnecting due to normal close`);
740
+ this._reconnect(socketUrl);
951
741
  }
952
742
  // metric: disconnect
953
743
  // if (reason === done forced) metric: force closure
954
744
  } else {
955
745
  this.logger.info(
956
- `${this.namespace}: socket ${sessionId} disconnected; will not reconnect: ${event.reason}`
746
+ `${this.namespace}: socket disconnected; will not reconnect: ${event.reason}`
957
747
  );
958
- if (isActiveSocket) this._emit(sessionId, 'offline.permanent', event);
748
+ if (isActiveSocket) this._emit('offline.permanent', event);
959
749
  }
960
750
  break;
961
751
  default:
962
752
  this.logger.info(
963
- `${this.namespace}: socket ${sessionId} disconnected unexpectedly; will not reconnect`
753
+ `${this.namespace}: socket disconnected unexpectedly; will not reconnect`
964
754
  );
965
755
  // unexpected disconnect
966
- if (isActiveSocket) this._emit(sessionId, 'offline.permanent', event);
756
+ if (isActiveSocket) this._emit('offline.permanent', event);
967
757
  }
968
758
  } catch (error) {
969
- this.logger.error(
970
- `${this.namespace}: error occurred in close handler for ${sessionId}`,
971
- error
972
- );
759
+ this.logger.error(`${this.namespace}: error occurred in close handler`, error);
973
760
  }
974
761
  },
975
762
 
976
- _onmessage(sessionId, event) {
977
- this._setTimeOffset(sessionId, event);
763
+ _onmessage(event) {
764
+ this._setTimeOffset(event);
978
765
  const envelope = event.data;
979
766
 
980
767
  if (process.env.ENABLE_MERCURY_LOGGING) {
981
- this.logger.debug(`${this.namespace}: message envelope from ${sessionId}: `, envelope);
768
+ this.logger.debug(`${this.namespace}: message envelope: `, envelope);
982
769
  }
983
770
 
984
- envelope.sessionId = sessionId;
985
-
986
771
  // Handle shutdown message shape: { type: 'shutdown' }
987
772
  if (envelope && envelope.type === 'shutdown') {
988
- this.logger.info(
989
- `${this.namespace}: [shutdown] imminent shutdown message received for ${sessionId}`
990
- );
991
- this._emit(sessionId, 'event:mercury_shutdown_imminent', envelope);
773
+ this.logger.info(`${this.namespace}: [shutdown] imminent shutdown message received`);
774
+ this._emit('event:mercury_shutdown_imminent', envelope);
992
775
 
993
- this._handleImminentShutdown(sessionId);
776
+ this._handleImminentShutdown();
994
777
 
995
778
  return Promise.resolve();
996
779
  }
997
780
 
998
- envelope.sessionId = sessionId;
999
781
  const {data} = envelope;
1000
782
 
1001
783
  this._applyOverrides(data);
1002
784
 
1003
- if (!data || !data.eventType) {
1004
- this._emit(sessionId, 'event', envelope);
1005
-
1006
- return Promise.resolve();
1007
- }
1008
-
1009
785
  return this._getEventHandlers(data.eventType)
1010
786
  .reduce(
1011
787
  (promise, handler) =>
@@ -1016,7 +792,7 @@ const Mercury = WebexPlugin.extend({
1016
792
  resolve((this.webex[namespace] || this.webex.internal[namespace])[name](data))
1017
793
  ).catch((reason) =>
1018
794
  this.logger.error(
1019
- `${this.namespace}: error occurred in autowired event handler for ${data.eventType} from ${sessionId}`,
795
+ `${this.namespace}: error occurred in autowired event handler for ${data.eventType}`,
1020
796
  reason
1021
797
  )
1022
798
  );
@@ -1024,35 +800,32 @@ const Mercury = WebexPlugin.extend({
1024
800
  Promise.resolve()
1025
801
  )
1026
802
  .then(() => {
1027
- this._emit(sessionId, 'event', envelope);
803
+ this._emit('event', event.data);
1028
804
  const [namespace] = data.eventType.split('.');
1029
805
 
1030
806
  if (namespace === data.eventType) {
1031
- this._emit(sessionId, `event:${namespace}`, envelope);
807
+ this._emit(`event:${namespace}`, envelope);
1032
808
  } else {
1033
- this._emit(sessionId, `event:${namespace}`, envelope);
1034
- this._emit(sessionId, `event:${data.eventType}`, envelope);
809
+ this._emit(`event:${namespace}`, envelope);
810
+ this._emit(`event:${data.eventType}`, envelope);
1035
811
  }
1036
812
  })
1037
813
  .catch((reason) => {
1038
- this.logger.error(
1039
- `${this.namespace}: error occurred processing socket message from ${sessionId}`,
1040
- reason
1041
- );
814
+ this.logger.error(`${this.namespace}: error occurred processing socket message`, reason);
1042
815
  });
1043
816
  },
1044
817
 
1045
- _setTimeOffset(sessionId, event) {
818
+ _setTimeOffset(event) {
1046
819
  const {wsWriteTimestamp} = event.data;
1047
820
  if (typeof wsWriteTimestamp === 'number' && wsWriteTimestamp > 0) {
1048
821
  this.mercuryTimeOffset = Date.now() - wsWriteTimestamp;
1049
822
  }
1050
823
  },
1051
824
 
1052
- _reconnect(webSocketUrl, sessionId = this.defaultSessionId) {
1053
- this.logger.info(`${this.namespace}: reconnecting ${sessionId}`);
825
+ _reconnect(webSocketUrl) {
826
+ this.logger.info(`${this.namespace}: reconnecting`);
1054
827
 
1055
- return this.connect(webSocketUrl, sessionId);
828
+ return this.connect(webSocketUrl);
1056
829
  },
1057
830
  });
1058
831