@webex/internal-plugin-mercury 3.8.1-web-workers-keepalive.1 → 3.9.0-multipleLLM.1

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