@webex/internal-plugin-mercury 3.10.0-multi-llms.1 → 3.10.0-next.10

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