@webex/internal-plugin-mercury 3.8.1 → 3.9.0-multiple-llm.2

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, oneFlight} from '@webex/common';
9
+ import {deprecated} from '@webex/common';
10
10
  import {camelCase, get, set} from 'lodash';
11
11
  import backoff from 'backoff';
12
12
 
@@ -25,6 +25,7 @@ const normalReconnectReasons = ['idle', 'done (forced)', 'pong not received', 'p
25
25
  const Mercury = WebexPlugin.extend({
26
26
  namespace: 'Mercury',
27
27
  lastError: undefined,
28
+ defaultSessionId: 'mercury-default-session',
28
29
 
29
30
  session: {
30
31
  connected: {
@@ -39,7 +40,14 @@ const Mercury = WebexPlugin.extend({
39
40
  default: false,
40
41
  type: 'boolean',
41
42
  },
42
- socket: 'object',
43
+ sockets: {
44
+ default: () => new Map(),
45
+ type: 'object',
46
+ },
47
+ backoffCalls: {
48
+ default: () => new Map(),
49
+ type: 'object',
50
+ },
43
51
  localClusterServiceUrls: 'object',
44
52
  mercuryTimeOffset: {
45
53
  default: undefined,
@@ -78,29 +86,97 @@ const Mercury = WebexPlugin.extend({
78
86
  return this.lastError;
79
87
  },
80
88
 
81
- @oneFlight
82
- connect(webSocketUrl) {
83
- if (this.connected) {
84
- this.logger.info(`${this.namespace}: already connected, will not connect again`);
89
+ /**
90
+ * Get all active socket connections
91
+ * @returns {Map} Map of sessionId to socket instances
92
+ */
93
+ getSockets() {
94
+ return this.sockets;
95
+ },
96
+
97
+ /**
98
+ * Get a specific socket by connection ID
99
+ * @param {string} sessionId - The connection identifier
100
+ * @returns {Socket|undefined} The socket instance or undefined if not found
101
+ */
102
+ getSocket(sessionId = this.defaultSessionId) {
103
+ return this.sockets.get(sessionId);
104
+ },
105
+
106
+ /**
107
+ * Check if any sockets are connected
108
+ * @returns {boolean} True if at least one socket is connected
109
+ */
110
+ hasConnectedSockets() {
111
+ for (const socket of this.sockets.values()) {
112
+ if (socket && socket.connected) {
113
+ return true;
114
+ }
115
+ }
116
+
117
+ return false;
118
+ },
119
+
120
+ /**
121
+ * Check if any sockets are connecting
122
+ * @returns {boolean} True if at least one socket is connected
123
+ */
124
+ hasConnectingSockets() {
125
+ for (const socket of this.sockets.values()) {
126
+ if (socket && socket.connecting) {
127
+ return true;
128
+ }
129
+ }
130
+
131
+ return false;
132
+ },
133
+
134
+ // @oneFlight
135
+ connect(webSocketUrl, sessionId = this.defaultSessionId) {
136
+ if (!this._connectPromises) this._connectPromises = new Map();
137
+
138
+ // First check if there's already a connection promise for this session
139
+ if (this._connectPromises.has(sessionId)) {
140
+ this.logger.info(
141
+ `${this.namespace}: connection ${sessionId} already in progress, returning existing promise`
142
+ );
143
+
144
+ return this._connectPromises.get(sessionId);
145
+ }
146
+
147
+ const sessionSocket = this.sockets.get(sessionId);
148
+ if (sessionSocket?.connected || sessionSocket?.connecting) {
149
+ this.logger.info(
150
+ `${this.namespace}: connection ${sessionId} already connected, will not connect again`
151
+ );
85
152
 
86
153
  return Promise.resolve();
87
154
  }
88
155
 
89
156
  this.connecting = true;
90
157
 
91
- this.logger.info(`${this.namespace}: starting connection attempt`);
158
+ this.logger.info(`${this.namespace}: starting connection attempt for ${sessionId}`);
159
+
92
160
  this.logger.info(
93
161
  `${this.namespace}: debug_mercury_logging stack: `,
94
162
  new Error('debug_mercury_logging').stack
95
163
  );
96
164
 
97
- return Promise.resolve(
165
+ const connectPromise = Promise.resolve(
98
166
  this.webex.internal.device.registered || this.webex.internal.device.register()
99
- ).then(() => {
100
- this.logger.info(`${this.namespace}: connecting`);
167
+ )
168
+ .then(() => {
169
+ this.logger.info(`${this.namespace}: connecting ${sessionId}`);
101
170
 
102
- return this._connectWithBackoff(webSocketUrl);
103
- });
171
+ return this._connectWithBackoff(webSocketUrl, sessionId);
172
+ })
173
+ .finally(() => {
174
+ this._connectPromises.delete(sessionId);
175
+ });
176
+
177
+ this._connectPromises.set(sessionId, connectPromise);
178
+
179
+ return connectPromise;
104
180
  },
105
181
 
106
182
  logout() {
@@ -110,7 +186,7 @@ const Mercury = WebexPlugin.extend({
110
186
  new Error('debug_mercury_logging').stack
111
187
  );
112
188
 
113
- return this.disconnect(
189
+ return this.disconnectAll(
114
190
  this.config.beforeLogoutOptionsCloseReason &&
115
191
  !normalReconnectReasons.includes(this.config.beforeLogoutOptionsCloseReason)
116
192
  ? {code: 3050, reason: this.config.beforeLogoutOptionsCloseReason}
@@ -118,21 +194,58 @@ const Mercury = WebexPlugin.extend({
118
194
  );
119
195
  },
120
196
 
121
- @oneFlight
122
- disconnect(options) {
197
+ // @oneFlight
198
+ disconnect(options, sessionId = this.defaultSessionId) {
123
199
  return new Promise((resolve) => {
124
- if (this.backoffCall) {
125
- this.logger.info(`${this.namespace}: aborting connection`);
126
- this.backoffCall.abort();
200
+ const backoffCall = this.backoffCalls.get(sessionId);
201
+ if (backoffCall) {
202
+ this.logger.info(`${this.namespace}: aborting connection ${sessionId}`);
203
+ backoffCall.abort();
204
+ this.backoffCalls.delete(sessionId);
127
205
  }
128
206
 
129
- if (this.socket) {
130
- this.socket.removeAllListeners('message');
131
- this.once('offline', resolve);
132
- resolve(this.socket.close(options || undefined));
207
+ // Clean up any pending connection promises
208
+ if (this._connectPromises) {
209
+ this._connectPromises.delete(sessionId);
133
210
  }
134
211
 
212
+ const sessionSocket = this.sockets.get(sessionId);
213
+ const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
214
+
215
+ if (sessionSocket) {
216
+ sessionSocket.removeAllListeners('message');
217
+ sessionSocket.connecting = false;
218
+ sessionSocket.connected = false;
219
+ this.once(sessionId === this.defaultSessionId ? 'offline' : `offline${suffix}`, resolve);
220
+ resolve(sessionSocket.close(options || undefined));
221
+ }
135
222
  resolve();
223
+
224
+ // Update overall connected status
225
+ this.connected = this.hasConnectedSockets();
226
+ });
227
+ },
228
+
229
+ /**
230
+ * Disconnect all socket connections
231
+ * @param {object} options - Close options
232
+ * @returns {Promise} Promise that resolves when all connections are closed
233
+ */
234
+ disconnectAll(options) {
235
+ const disconnectPromises = [];
236
+
237
+ for (const sessionId of this.sockets.keys()) {
238
+ disconnectPromises.push(this.disconnect(options, sessionId));
239
+ }
240
+
241
+ return Promise.all(disconnectPromises).then(() => {
242
+ this.connected = false;
243
+ this.sockets.clear();
244
+ this.backoffCalls.clear();
245
+ // Clear connection promises to prevent stale promises
246
+ if (this._connectPromises) {
247
+ this._connectPromises.clear();
248
+ }
136
249
  });
137
250
  },
138
251
 
@@ -207,20 +320,23 @@ const Mercury = WebexPlugin.extend({
207
320
  });
208
321
  },
209
322
 
210
- _attemptConnection(socketUrl, callback) {
323
+ _attemptConnection(socketUrl, sessionId, callback) {
211
324
  const socket = new Socket();
325
+ socket.connecting = true;
212
326
  let attemptWSUrl;
327
+ const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
213
328
 
214
- socket.on('close', (...args) => this._onclose(...args));
215
- socket.on('message', (...args) => this._onmessage(...args));
216
- socket.on('pong', (...args) => this._setTimeOffset(...args));
217
- socket.on('sequence-mismatch', (...args) => this._emit('sequence-mismatch', ...args));
218
- socket.on('ping-pong-latency', (...args) => this._emit('ping-pong-latency', ...args));
329
+ socket.on('close', (...args) => this._onclose(sessionId, ...args));
330
+ socket.on('message', (...args) => this._onmessage(sessionId, ...args));
331
+ socket.on('pong', (...args) => this._setTimeOffset(sessionId, ...args));
332
+ socket.on('sequence-mismatch', (...args) => this._emit(`sequence-mismatch${suffix}`, ...args));
333
+ socket.on('ping-pong-latency', (...args) => this._emit(`ping-pong-latency${suffix}`, ...args));
219
334
 
220
335
  Promise.all([this._prepareUrl(socketUrl), this.webex.credentials.getUserToken()])
221
336
  .then(([webSocketUrl, token]) => {
222
- if (!this.backoffCall) {
223
- const msg = `${this.namespace}: prevent socket open when backoffCall no longer defined`;
337
+ const backoffCall = this.backoffCalls.get(sessionId);
338
+ if (!backoffCall) {
339
+ const msg = `${this.namespace}: prevent socket open when backoffCall no longer defined for ${sessionId}`;
224
340
 
225
341
  this.logger.info(msg);
226
342
 
@@ -234,27 +350,28 @@ const Mercury = WebexPlugin.extend({
234
350
  pingInterval: this.config.pingInterval,
235
351
  pongTimeout: this.config.pongTimeout,
236
352
  token: token.toString(),
237
- trackingId: `${this.webex.sessionId}_${Date.now()}`,
353
+ trackingId: `${this.webex.sessionId}_${sessionId}_${Date.now()}`,
238
354
  logger: this.logger,
239
355
  };
240
356
 
241
357
  // if the consumer has supplied request options use them
242
358
  if (this.webex.config.defaultMercuryOptions) {
243
- this.logger.info(`${this.namespace}: setting custom options`);
359
+ this.logger.info(`${this.namespace}: setting custom options for ${sessionId}`);
244
360
  options = {...options, ...this.webex.config.defaultMercuryOptions};
245
361
  }
246
362
 
247
363
  // Set the socket before opening it. This allows a disconnect() to close
248
364
  // the socket if it is in the process of being opened.
249
- this.socket = socket;
365
+ this.sockets.set(sessionId, socket);
366
+ this.socket = this.sockets.get(this.defaultSessionId) || socket;
250
367
 
251
- this.logger.info(`${this.namespace} connection url: ${webSocketUrl}`);
368
+ this.logger.info(`${this.namespace} connection url for ${sessionId}: ${webSocketUrl}`);
252
369
 
253
370
  return socket.open(webSocketUrl, options);
254
371
  })
255
372
  .then(() => {
256
373
  this.logger.info(
257
- `${this.namespace}: connected to mercury, success, action: connected, url: ${attemptWSUrl}`
374
+ `${this.namespace}: connected to mercury, success, action: connected, sessionId: ${sessionId}, url: ${attemptWSUrl}`
258
375
  );
259
376
  callback();
260
377
 
@@ -271,30 +388,36 @@ const Mercury = WebexPlugin.extend({
271
388
  .catch((reason) => {
272
389
  this.lastError = reason; // remember the last error
273
390
 
391
+ const backoffCall = this.backoffCalls.get(sessionId);
274
392
  // Suppress connection errors that appear to be network related. This
275
393
  // may end up suppressing metrics during outages, but we might not care
276
394
  // (especially since many of our outages happen in a way that client
277
395
  // metrics can't be trusted).
278
- if (reason.code !== 1006 && this.backoffCall && this.backoffCall?.getNumRetries() > 0) {
279
- this._emit('connection_failed', reason, {retries: this.backoffCall?.getNumRetries()});
396
+ if (reason.code !== 1006 && backoffCall && backoffCall?.getNumRetries() > 0) {
397
+ this._emit(`connection_failed${suffix}`, reason, {
398
+ sessionId,
399
+ retries: backoffCall?.getNumRetries(),
400
+ });
280
401
  }
281
402
  this.logger.info(
282
- `${this.namespace}: connection attempt failed`,
403
+ `${this.namespace}: connection attempt failed for ${sessionId}`,
283
404
  reason,
284
- this.backoffCall?.getNumRetries() === 0 ? reason.stack : ''
405
+ backoffCall?.getNumRetries() === 0 ? reason.stack : ''
285
406
  );
286
407
  // UnknownResponse is produced by IE for any 4XXX; treated it like a bad
287
408
  // web socket url and let WDM handle the token checking
288
409
  if (reason instanceof UnknownResponse) {
289
410
  this.logger.info(
290
- `${this.namespace}: received unknown response code, refreshing device registration`
411
+ `${this.namespace}: received unknown response code for ${sessionId}, refreshing device registration`
291
412
  );
292
413
 
293
414
  return this.webex.internal.device.refresh().then(() => callback(reason));
294
415
  }
295
416
  // NotAuthorized implies expired token
296
417
  if (reason instanceof NotAuthorized) {
297
- this.logger.info(`${this.namespace}: received authorization error, reauthorizing`);
418
+ this.logger.info(
419
+ `${this.namespace}: received authorization error for ${sessionId}, reauthorizing`
420
+ );
298
421
 
299
422
  return this.webex.credentials.refresh({force: true}).then(() => callback(reason));
300
423
  }
@@ -307,8 +430,10 @@ const Mercury = WebexPlugin.extend({
307
430
  // BadRequest implies current credentials are for a Service Account
308
431
  // Forbidden implies current user is not entitle for Webex
309
432
  if (reason instanceof BadRequest || reason instanceof Forbidden) {
310
- this.logger.warn(`${this.namespace}: received unrecoverable response from mercury`);
311
- this.backoffCall.abort();
433
+ this.logger.warn(
434
+ `${this.namespace}: received unrecoverable response from mercury for ${sessionId}`
435
+ );
436
+ backoffCall?.abort();
312
437
 
313
438
  return callback(reason);
314
439
  }
@@ -318,7 +443,7 @@ const Mercury = WebexPlugin.extend({
318
443
  .then((haMessagingEnabled) => {
319
444
  if (haMessagingEnabled) {
320
445
  this.logger.info(
321
- `${this.namespace}: received a generic connection error, will try to connect to another datacenter. failed, action: 'failed', url: ${attemptWSUrl} error: ${reason.message}`
446
+ `${this.namespace}: received a generic connection error for ${sessionId}, will try to connect to another datacenter. failed, action: 'failed', url: ${attemptWSUrl} error: ${reason.message}`
322
447
  );
323
448
 
324
449
  return this.webex.internal.services.markFailedUrl(attemptWSUrl);
@@ -332,41 +457,58 @@ const Mercury = WebexPlugin.extend({
332
457
  return callback(reason);
333
458
  })
334
459
  .catch((reason) => {
335
- this.logger.error(`${this.namespace}: failed to handle connection failure`, reason);
460
+ this.logger.error(
461
+ `${this.namespace}: failed to handle connection failure for ${sessionId}`,
462
+ reason
463
+ );
336
464
  callback(reason);
337
465
  });
338
466
  },
339
467
 
340
- _connectWithBackoff(webSocketUrl) {
468
+ _connectWithBackoff(webSocketUrl, sessionId) {
341
469
  return new Promise((resolve, reject) => {
342
470
  // eslint gets confused about whether or not call is actually used
343
471
  // eslint-disable-next-line prefer-const
344
472
  let call;
345
- const onComplete = (err) => {
346
- this.connecting = false;
347
-
348
- this.backoffCall = undefined;
473
+ const onComplete = (err, sid = sessionId) => {
474
+ this.backoffCalls.delete(sid);
349
475
  if (err) {
350
476
  this.logger.info(
351
477
  `${
352
478
  this.namespace
353
- }: failed to connect after ${call.getNumRetries()} retries; log statement about next retry was inaccurate; ${err}`
479
+ }: failed to connect ${sid} after ${call.getNumRetries()} retries; log statement about next retry was inaccurate; ${err}`
354
480
  );
355
481
 
356
482
  return reject(err);
357
483
  }
358
- this.connected = true;
484
+ // Update overall connected status
485
+ const sessionSocket = this.sockets.get(sid);
486
+ if (sessionSocket) {
487
+ sessionSocket.connecting = false;
488
+ sessionSocket.connected = true;
489
+ }
490
+ // @ts-ignore
491
+ this.connecting = this.hasConnectingSockets();
492
+ this.connected = this.hasConnectedSockets();
359
493
  this.hasEverConnected = true;
360
- this._emit('online');
494
+ const suffix = sid === this.defaultSessionId ? '' : `:${sid}`;
495
+ this._emit(`online${suffix}`, {sessionId: sid});
496
+ this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(true);
361
497
 
362
498
  return resolve();
363
499
  };
364
-
365
500
  // eslint-disable-next-line prefer-reflect
366
- call = backoff.call((callback) => {
367
- this.logger.info(`${this.namespace}: executing connection attempt ${call.getNumRetries()}`);
368
- this._attemptConnection(webSocketUrl, callback);
369
- }, onComplete);
501
+ call = backoff.call(
502
+ (callback) => {
503
+ this.logger.info(
504
+ `${
505
+ this.namespace
506
+ }: executing connection attempt ${call.getNumRetries()} for ${sessionId}`
507
+ );
508
+ this._attemptConnection(webSocketUrl, sessionId, callback);
509
+ },
510
+ (err) => onComplete(err, sessionId)
511
+ );
370
512
 
371
513
  call.setStrategy(
372
514
  new backoff.ExponentialStrategy({
@@ -381,9 +523,12 @@ const Mercury = WebexPlugin.extend({
381
523
  call.failAfter(this.config.maxRetries);
382
524
  }
383
525
 
526
+ // Store the call BEFORE setting up event handlers to prevent race conditions
527
+ this.backoffCalls.set(sessionId, call);
528
+
384
529
  call.on('abort', () => {
385
- this.logger.info(`${this.namespace}: connection aborted`);
386
- reject(new Error('Mercury Connection Aborted'));
530
+ this.logger.info(`${this.namespace}: connection aborted for ${sessionId}`);
531
+ reject(new Error(`Mercury Connection Aborted for ${sessionId}`));
387
532
  });
388
533
 
389
534
  call.on('callback', (err) => {
@@ -392,7 +537,9 @@ const Mercury = WebexPlugin.extend({
392
537
  const delay = Math.min(call.strategy_.nextBackoffDelay_, this.config.backoffTimeMax);
393
538
 
394
539
  this.logger.info(
395
- `${this.namespace}: failed to connect; attempting retry ${number + 1} in ${delay} ms`
540
+ `${this.namespace}: failed to connect ${sessionId}; attempting retry ${
541
+ number + 1
542
+ } in ${delay} ms`
396
543
  );
397
544
  /* istanbul ignore if */
398
545
  if (process.env.NODE_ENV === 'development') {
@@ -401,25 +548,32 @@ const Mercury = WebexPlugin.extend({
401
548
 
402
549
  return;
403
550
  }
404
- this.logger.info(`${this.namespace}: connected`);
551
+ this.logger.info(`${this.namespace}: connected ${sessionId}`);
405
552
  });
406
553
 
407
554
  call.start();
408
-
409
- this.backoffCall = call;
410
555
  });
411
556
  },
412
557
 
413
558
  _emit(...args) {
414
559
  try {
415
- this.trigger(...args);
560
+ // Validate args before processing
561
+ if (args && args.length > 0) {
562
+ this.trigger(...args);
563
+ }
416
564
  } catch (error) {
417
- this.logger.error(
418
- `${this.namespace}: error occurred in event handler:`,
419
- error,
420
- ' with args: ',
421
- args
422
- );
565
+ // Safely handle errors without causing additional issues during cleanup
566
+ try {
567
+ this.logger.error(
568
+ `${this.namespace}: error occurred in event handler:`,
569
+ error,
570
+ ' with args: ',
571
+ args
572
+ );
573
+ } catch (logError) {
574
+ // If even logging fails, just ignore to prevent cascading errors during cleanup
575
+ this.logger.error('Mercury _emit error handling failed:', logError);
576
+ }
423
577
  }
424
578
  },
425
579
 
@@ -443,77 +597,94 @@ const Mercury = WebexPlugin.extend({
443
597
  return handlers;
444
598
  },
445
599
 
446
- _onclose(event) {
600
+ _onclose(sessionId, event) {
447
601
  // I don't see any way to avoid the complexity or statement count in here.
448
602
  /* eslint complexity: [0] */
449
603
 
450
604
  try {
451
605
  const reason = event.reason && event.reason.toLowerCase();
452
- const socketUrl = this.socket.url;
606
+ let sessionSocket = this.sockets.get(sessionId);
607
+ const socketUrl = sessionSocket?.url;
608
+ const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
609
+ event.sessionId = sessionId;
610
+ this.sockets.delete(sessionId);
611
+
612
+ if (sessionSocket) {
613
+ sessionSocket.removeAllListeners();
614
+ sessionSocket = null;
615
+ this._emit(`offline${suffix}`, event);
616
+ }
453
617
 
454
- this.socket.removeAllListeners();
455
- this.unset('socket');
456
- this.connected = false;
457
- this._emit('offline', event);
618
+ // Update overall connected status
619
+ this.connecting = this.hasConnectingSockets();
620
+ this.connected = this.hasConnectedSockets();
621
+
622
+ if (!this.connected) {
623
+ this.webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus(false);
624
+ }
458
625
 
459
626
  switch (event.code) {
460
627
  case 1003:
461
628
  // metric: disconnect
462
629
  this.logger.info(
463
- `${this.namespace}: Mercury service rejected last message; will not reconnect: ${event.reason}`
630
+ `${this.namespace}: Mercury service rejected last message for ${sessionId}; will not reconnect: ${event.reason}`
464
631
  );
465
- this._emit('offline.permanent', event);
632
+ this._emit(`offline.permanent${suffix}`, event);
466
633
  break;
467
634
  case 4000:
468
635
  // metric: disconnect
469
- this.logger.info(`${this.namespace}: socket replaced; will not reconnect`);
470
- this._emit('offline.replaced', event);
636
+ this.logger.info(`${this.namespace}: socket ${sessionId} replaced; will not reconnect`);
637
+ this._emit(`offline.replaced${suffix}`, event);
471
638
  break;
472
639
  case 1001:
473
640
  case 1005:
474
641
  case 1006:
475
642
  case 1011:
476
- this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
477
- this._emit('offline.transient', event);
478
- this._reconnect(socketUrl);
643
+ this.logger.info(`${this.namespace}: socket ${sessionId} disconnected; reconnecting`);
644
+ this._emit(`offline.transient${suffix}`, event);
645
+ this._reconnect(socketUrl, sessionId);
479
646
  // metric: disconnect
480
647
  // if (code == 1011 && reason !== ping error) metric: unexpected disconnect
481
648
  break;
482
649
  case 1000:
483
650
  case 3050: // 3050 indicates logout form of closure, default to old behavior, use config reason defined by consumer to proceed with the permanent block
484
651
  if (normalReconnectReasons.includes(reason)) {
485
- this.logger.info(`${this.namespace}: socket disconnected; reconnecting`);
486
- this._emit('offline.transient', event);
487
- this._reconnect(socketUrl);
652
+ this.logger.info(`${this.namespace}: socket ${sessionId} disconnected; reconnecting`);
653
+ this._emit(`offline.transient${suffix}`, event);
654
+ this._reconnect(socketUrl, sessionId);
488
655
  // metric: disconnect
489
656
  // if (reason === done forced) metric: force closure
490
657
  } else {
491
658
  this.logger.info(
492
- `${this.namespace}: socket disconnected; will not reconnect: ${event.reason}`
659
+ `${this.namespace}: socket ${sessionId} disconnected; will not reconnect: ${event.reason}`
493
660
  );
494
- this._emit('offline.permanent', event);
661
+ this._emit(`offline.permanent${suffix}`, event);
495
662
  }
496
663
  break;
497
664
  default:
498
665
  this.logger.info(
499
- `${this.namespace}: socket disconnected unexpectedly; will not reconnect`
666
+ `${this.namespace}: socket ${sessionId} disconnected unexpectedly; will not reconnect`
500
667
  );
501
668
  // unexpected disconnect
502
- this._emit('offline.permanent', event);
669
+ this._emit(`offline.permanent${suffix}`, event);
503
670
  }
504
671
  } catch (error) {
505
- this.logger.error(`${this.namespace}: error occurred in close handler`, error);
672
+ this.logger.error(
673
+ `${this.namespace}: error occurred in close handler for ${sessionId}`,
674
+ error
675
+ );
506
676
  }
507
677
  },
508
678
 
509
- _onmessage(event) {
510
- this._setTimeOffset(event);
679
+ _onmessage(sessionId, event) {
680
+ this._setTimeOffset(sessionId, event);
511
681
  const envelope = event.data;
512
682
 
513
683
  if (process.env.ENABLE_MERCURY_LOGGING) {
514
- this.logger.debug(`${this.namespace}: message envelope: `, envelope);
684
+ this.logger.debug(`${this.namespace}: message envelope from ${sessionId}: `, envelope);
515
685
  }
516
686
 
687
+ envelope.sessionId = sessionId;
517
688
  const {data} = envelope;
518
689
 
519
690
  this._applyOverrides(data);
@@ -528,7 +699,7 @@ const Mercury = WebexPlugin.extend({
528
699
  resolve((this.webex[namespace] || this.webex.internal[namespace])[name](data))
529
700
  ).catch((reason) =>
530
701
  this.logger.error(
531
- `${this.namespace}: error occurred in autowired event handler for ${data.eventType}`,
702
+ `${this.namespace}: error occurred in autowired event handler for ${data.eventType} from ${sessionId}`,
532
703
  reason
533
704
  )
534
705
  );
@@ -536,32 +707,37 @@ const Mercury = WebexPlugin.extend({
536
707
  Promise.resolve()
537
708
  )
538
709
  .then(() => {
539
- this._emit('event', event.data);
710
+ const suffix = sessionId === this.defaultSessionId ? '' : `:${sessionId}`;
711
+
712
+ this._emit(`event${suffix}`, envelope);
540
713
  const [namespace] = data.eventType.split('.');
541
714
 
542
715
  if (namespace === data.eventType) {
543
- this._emit(`event:${namespace}`, envelope);
716
+ this._emit(`event:${namespace}${suffix}`, envelope);
544
717
  } else {
545
- this._emit(`event:${namespace}`, envelope);
546
- this._emit(`event:${data.eventType}`, envelope);
718
+ this._emit(`event:${namespace}${suffix}`, envelope);
719
+ this._emit(`event:${data.eventType}${suffix}`, envelope);
547
720
  }
548
721
  })
549
722
  .catch((reason) => {
550
- this.logger.error(`${this.namespace}: error occurred processing socket message`, reason);
723
+ this.logger.error(
724
+ `${this.namespace}: error occurred processing socket message from ${sessionId}`,
725
+ reason
726
+ );
551
727
  });
552
728
  },
553
729
 
554
- _setTimeOffset(event) {
730
+ _setTimeOffset(sessionId, event) {
555
731
  const {wsWriteTimestamp} = event.data;
556
732
  if (typeof wsWriteTimestamp === 'number' && wsWriteTimestamp > 0) {
557
733
  this.mercuryTimeOffset = Date.now() - wsWriteTimestamp;
558
734
  }
559
735
  },
560
736
 
561
- _reconnect(webSocketUrl) {
562
- this.logger.info(`${this.namespace}: reconnecting`);
737
+ _reconnect(webSocketUrl, sessionId = this.defaultSessionId) {
738
+ this.logger.info(`${this.namespace}: reconnecting ${sessionId}`);
563
739
 
564
- return this.connect(webSocketUrl);
740
+ return this.connect(webSocketUrl, sessionId);
565
741
  },
566
742
  });
567
743