@webex/internal-plugin-mercury 3.12.0-next.5 → 3.12.0-next.50

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
@@ -21,6 +21,7 @@ import {
21
21
  } from './errors';
22
22
 
23
23
  const normalReconnectReasons = ['idle', 'done (forced)', 'pong not received', 'pong mismatch'];
24
+ const MERCURY_CLOSE_4000_METRIC = 'JS_SDK_MERCURY_CLOSE_4000';
24
25
 
25
26
  const Mercury = WebexPlugin.extend({
26
27
  namespace: 'Mercury',
@@ -44,6 +45,10 @@ const Mercury = WebexPlugin.extend({
44
45
  default: () => new Map(),
45
46
  type: 'object',
46
47
  },
48
+ sessionWebSocketUrls: {
49
+ default: () => new Map(),
50
+ type: 'object',
51
+ },
47
52
  backoffCalls: {
48
53
  default: () => new Map(),
49
54
  type: 'object',
@@ -377,6 +382,7 @@ const Mercury = WebexPlugin.extend({
377
382
  return Promise.all(disconnectPromises).then(() => {
378
383
  this.connected = false;
379
384
  this.sockets.clear();
385
+ this.sessionWebSocketUrls.clear();
380
386
  this.backoffCalls.clear();
381
387
  // Clear connection promises to prevent stale promises
382
388
  if (this._connectPromises) {
@@ -610,12 +616,24 @@ const Mercury = WebexPlugin.extend({
610
616
  return this.webex.internal.feature
611
617
  .getFeature('developer', 'web-high-availability')
612
618
  .then((haMessagingEnabled) => {
619
+ const wsUrl = newWSUrl || reason.webSocketUrl;
620
+
613
621
  if (haMessagingEnabled) {
614
622
  this.logger.info(
615
- `${this.namespace}: received a generic connection error for ${sessionId}, will try to connect to another datacenter. failed, action: 'failed', url: ${newWSUrl} error: ${reason.message}`
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}`
616
624
  );
617
625
 
618
- return this.webex.internal.services.markFailedUrl(newWSUrl);
626
+ if (wsUrl) {
627
+ this.logger.info(
628
+ `${this.namespace}: marking ${wsUrl} as failed for ${sessionId} due to connection error`
629
+ );
630
+
631
+ return this.webex.internal.services.markFailedUrl(wsUrl);
632
+ }
633
+
634
+ this.logger.info(
635
+ `${this.namespace}: no socket url available to mark as failed for ${sessionId} due to connection error`
636
+ );
619
637
  }
620
638
 
621
639
  return null;
@@ -664,7 +682,22 @@ const Mercury = WebexPlugin.extend({
664
682
 
665
683
  this.logger.info(`${this.namespace} ${logPrefix} url for ${sessionId}: ${webSocketUrl}`);
666
684
 
667
- return socket.open(webSocketUrl, options).then(() => webSocketUrl);
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);
692
+
693
+ return socket
694
+ .open(webSocketUrl, options)
695
+ .then(() => webSocketUrl)
696
+ .catch((err) => {
697
+ err.webSocketUrl = webSocketUrl;
698
+
699
+ return Promise.reject(err);
700
+ });
668
701
  }
669
702
  );
670
703
  },
@@ -850,6 +883,29 @@ const Mercury = WebexPlugin.extend({
850
883
  return handlers;
851
884
  },
852
885
 
886
+ _submitMercuryClose4000Metric(sessionId, event, options) {
887
+ const {action, isActiveSocket, messageType} = options;
888
+
889
+ try {
890
+ this.webex.internal.metrics.submitClientMetrics(MERCURY_CLOSE_4000_METRIC, {
891
+ fields: {
892
+ action,
893
+ close_code: event.code,
894
+ close_reason: event.reason || '',
895
+ is_active_socket: isActiveSocket,
896
+ message_type: messageType,
897
+ session_id: sessionId,
898
+ },
899
+ tags: {
900
+ action,
901
+ message_type: messageType,
902
+ },
903
+ });
904
+ } catch (error) {
905
+ this.logger.warn(`${this.namespace}: failed to submit Mercury 4000 close metric`, error);
906
+ }
907
+ },
908
+
853
909
  _onclose(sessionId, event, sourceSocket) {
854
910
  // I don't see any way to avoid the complexity or statement count in here.
855
911
  /* eslint complexity: [0] */
@@ -857,13 +913,18 @@ const Mercury = WebexPlugin.extend({
857
913
  try {
858
914
  const reason = event.reason && event.reason.toLowerCase();
859
915
  const sessionSocket = this.sockets.get(sessionId);
860
- let socketUrl;
861
916
  event.sessionId = sessionId;
862
917
 
863
918
  const isActiveSocket = sourceSocket === sessionSocket;
864
- if (sourceSocket) {
865
- socketUrl = sourceSocket.url;
866
- }
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
+
867
928
  this.sockets.delete(sessionId);
868
929
 
869
930
  if (isActiveSocket) {
@@ -901,9 +962,28 @@ const Mercury = WebexPlugin.extend({
901
962
  break;
902
963
  case 4000:
903
964
  // metric: disconnect
904
- this.logger.info(`${this.namespace}: socket ${sessionId} replaced; will not reconnect`);
905
- if (isActiveSocket) this._emit(sessionId, 'offline.replaced', event);
906
- // If not active, nothing to do
965
+ if (reason === 'replaced') {
966
+ this._submitMercuryClose4000Metric(sessionId, event, {
967
+ action: 'no_action',
968
+ isActiveSocket,
969
+ messageType: 'replaced',
970
+ });
971
+ this.logger.info(`${this.namespace}: socket ${sessionId} replaced; will not reconnect`);
972
+ if (isActiveSocket) this._emit(sessionId, 'offline.replaced', event);
973
+ } else {
974
+ this._submitMercuryClose4000Metric(sessionId, event, {
975
+ action: isActiveSocket ? 'reconnect' : 'ignore_non_active',
976
+ isActiveSocket,
977
+ messageType: 'other',
978
+ });
979
+ this.logger.info(
980
+ `${this.namespace}: socket ${sessionId} disconnected with 4000: ${event.reason}; reconnecting`
981
+ );
982
+ if (isActiveSocket) {
983
+ this._emit(sessionId, 'offline.transient', event);
984
+ this._reconnect(socketUrl, sessionId);
985
+ }
986
+ }
907
987
  break;
908
988
  case 4001:
909
989
  // replaced during shutdown
@@ -267,8 +267,14 @@ describe('plugin-mercury', () => {
267
267
  },
268
268
  {
269
269
  code: 4000,
270
+ reason: 'Replaced',
270
271
  action: 'replace',
271
272
  },
273
+ {
274
+ code: 4000,
275
+ reason: 'Unexpected close',
276
+ action: 'reconnect',
277
+ },
272
278
  {
273
279
  action: 'close',
274
280
  },
@@ -292,6 +298,8 @@ describe('plugin-mercury', () => {
292
298
  mercury._reconnect.restore();
293
299
  }
294
300
 
301
+ let expectedReconnectUrl;
302
+
295
303
  sinon.spy(mercury, 'connect');
296
304
 
297
305
  const offlineSpy = sinon.spy();
@@ -313,6 +321,12 @@ describe('plugin-mercury', () => {
313
321
  // Make sure mercury.connect has a call count of zero
314
322
  mercury.connect.resetHistory();
315
323
 
324
+ // Reconnection re-derives from the resolved URL
325
+ // captured in _prepareAndOpenSocket, not the socket's actual
326
+ // (possibly changed by interceptors) url.
327
+ expectedReconnectUrl =
328
+ mercury.sessionWebSocketUrls.get('mercury-default-session');
329
+
316
330
  mockWebSocket.emit('close', {code, reason});
317
331
 
318
332
  return promiseTick(1);
@@ -342,7 +356,7 @@ describe('plugin-mercury', () => {
342
356
  assert.isFalse(mercury.connected, 'Mercury is not connected');
343
357
  if (action === 'reconnect') {
344
358
  assert.called(mercury.connect);
345
- assert.calledWith(mercury.connect, mockWebSocket.url);
359
+ assert.calledWith(mercury.connect, expectedReconnectUrl);
346
360
  assert.isTrue(mercury.connecting, 'Mercury is connecting');
347
361
 
348
362
  // Block until reconnect completes so logs don't overlap