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