@webex/internal-plugin-mercury 3.11.0 → 3.12.0-mobius-socket.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.
@@ -100,9 +100,32 @@ describe('plugin-mercury', () => {
100
100
  });
101
101
 
102
102
  mercury = webex.internal.mercury;
103
+ mercury.defaultSessionId = 'mercury-default-session';
103
104
  });
104
105
 
105
- afterEach(() => {
106
+ afterEach(async () => {
107
+ // Clean up Mercury connections and internal state
108
+ if (mercury) {
109
+ try {
110
+ await mercury.disconnectAll();
111
+ } catch (e) {
112
+ // Ignore cleanup errors
113
+ }
114
+ // Clear any remaining connection promises
115
+ if (mercury._connectPromises) {
116
+ mercury._connectPromises.clear();
117
+ }
118
+ }
119
+
120
+ // Ensure mock socket is properly closed
121
+ if (mockWebSocket && typeof mockWebSocket.close === 'function') {
122
+ try {
123
+ mockWebSocket.close();
124
+ } catch (e) {
125
+ // Ignore cleanup errors
126
+ }
127
+ }
128
+
106
129
  if (socketOpenStub) {
107
130
  socketOpenStub.restore();
108
131
  }
@@ -110,6 +133,9 @@ describe('plugin-mercury', () => {
110
133
  if (Socket.getWebSocketConstructor.restore) {
111
134
  Socket.getWebSocketConstructor.restore();
112
135
  }
136
+
137
+ // Small delay to ensure all async operations complete
138
+ await new Promise(resolve => setTimeout(resolve, 10));
113
139
  });
114
140
 
115
141
  describe('#listen()', () => {
@@ -499,9 +525,13 @@ describe('plugin-mercury', () => {
499
525
 
500
526
  // skipping due to apparent bug with lolex in all browsers but Chrome.
501
527
  skipInBrowser(it)('does not continue attempting to connect', () => {
502
- mercury.connect();
528
+ const promise = mercury.connect();
503
529
 
504
- return promiseTick(2)
530
+ // Wait for the connection to be established before proceeding
531
+ mockWebSocket.open();
532
+
533
+ return promise.then(() =>
534
+ promiseTick(2)
505
535
  .then(() => {
506
536
  clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
507
537
 
@@ -509,7 +539,8 @@ describe('plugin-mercury', () => {
509
539
  })
510
540
  .then(() => {
511
541
  assert.calledOnce(Socket.prototype.open);
512
- });
542
+ })
543
+ );
513
544
  });
514
545
  });
515
546
 
@@ -584,11 +615,11 @@ describe('plugin-mercury', () => {
584
615
  });
585
616
 
586
617
  describe('#logout()', () => {
587
- it('calls disconnect and logs', () => {
618
+ it('calls disconnectAll and logs', () => {
588
619
  sinon.stub(mercury.logger, 'info');
589
- sinon.stub(mercury, 'disconnect');
620
+ sinon.stub(mercury, 'disconnectAll');
590
621
  mercury.logout();
591
- assert.called(mercury.disconnect);
622
+ assert.called(mercury.disconnectAll);
592
623
  assert.calledTwice(mercury.logger.info);
593
624
 
594
625
  assert.calledWith(mercury.logger.info.getCall(0), 'Mercury: logout() called');
@@ -600,24 +631,24 @@ describe('plugin-mercury', () => {
600
631
  });
601
632
 
602
633
  it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 3050 for logout', () => {
603
- sinon.stub(mercury, 'disconnect');
634
+ sinon.stub(mercury, 'disconnectAll');
604
635
  mercury.config.beforeLogoutOptionsCloseReason = 'done (permanent)';
605
636
  mercury.logout();
606
- assert.calledWith(mercury.disconnect, {code: 3050, reason: 'done (permanent)'});
637
+ assert.calledWith(mercury.disconnectAll, {code: 3050, reason: 'done (permanent)'});
607
638
  });
608
639
 
609
640
  it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 3050 for logout if the reason is different than standard', () => {
610
- sinon.stub(mercury, 'disconnect');
641
+ sinon.stub(mercury, 'disconnectAll');
611
642
  mercury.config.beforeLogoutOptionsCloseReason = 'test';
612
643
  mercury.logout();
613
- assert.calledWith(mercury.disconnect, {code: 3050, reason: 'test'});
644
+ assert.calledWith(mercury.disconnectAll, {code: 3050, reason: 'test'});
614
645
  });
615
646
 
616
647
  it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send undefined for logout if the reason is same as standard', () => {
617
- sinon.stub(mercury, 'disconnect');
648
+ sinon.stub(mercury, 'disconnectAll');
618
649
  mercury.config.beforeLogoutOptionsCloseReason = 'done (forced)';
619
650
  mercury.logout();
620
- assert.calledWith(mercury.disconnect, undefined);
651
+ assert.calledWith(mercury.disconnectAll, undefined);
621
652
  });
622
653
  });
623
654
 
@@ -723,12 +754,12 @@ describe('plugin-mercury', () => {
723
754
  return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
724
755
  // By this time backoffCall and mercury socket should be defined by the
725
756
  // 'connect' call
726
- assert.isDefined(mercury.backoffCall, 'Mercury backoffCall is not defined');
757
+ assert.isDefined(mercury.backoffCalls.get('mercury-default-session'), 'Mercury backoffCall is not defined');
727
758
  assert.isDefined(mercury.socket, 'Mercury socket is not defined');
728
759
  // Calling disconnect will abort the backoffCall, close the socket, and
729
760
  // reject the connect
730
761
  mercury.disconnect();
731
- assert.isUndefined(mercury.backoffCall, 'Mercury backoffCall is still defined');
762
+ assert.isUndefined(mercury.backoffCalls.get('mercury-default-session'), 'Mercury backoffCall is still defined');
732
763
  // The socket will never be unset (which seems bad)
733
764
  assert.isDefined(mercury.socket, 'Mercury socket is not defined');
734
765
 
@@ -746,16 +777,24 @@ describe('plugin-mercury', () => {
746
777
 
747
778
  let reason;
748
779
 
749
- mercury.backoffCall = undefined;
750
- mercury._attemptConnection('ws://example.com', (_reason) => {
751
- reason = _reason;
752
- });
780
+ mercury.backoffCalls.clear();
781
+
782
+ const promise = mercury._attemptConnection(
783
+ 'ws://example.com',
784
+ 'mercury-default-session',
785
+ (_reason) => {
786
+ reason = _reason;
787
+ }
788
+ );
753
789
 
754
790
  return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
755
791
  assert.equal(
756
792
  reason.message,
757
- 'Mercury: prevent socket open when backoffCall no longer defined'
793
+ `Mercury: prevent socket open when backoffCall no longer defined for ${mercury.defaultSessionId}`
758
794
  );
795
+
796
+ // Ensure the promise was actually rejected (short-circuited)
797
+ return assert.isRejected(promise);
759
798
  });
760
799
  });
761
800
 
@@ -776,7 +815,7 @@ describe('plugin-mercury', () => {
776
815
  return assert.isRejected(promise).then((error) => {
777
816
  const lastError = mercury.getLastError();
778
817
 
779
- assert.equal(error.message, 'Mercury Connection Aborted');
818
+ assert.equal(error.message, `Mercury Connection Aborted for ${mercury.defaultSessionId}`);
780
819
  assert.isDefined(lastError);
781
820
  assert.equal(lastError, realError);
782
821
  });
@@ -870,7 +909,7 @@ describe('plugin-mercury', () => {
870
909
  },
871
910
  };
872
911
  assert.isUndefined(mercury.mercuryTimeOffset);
873
- mercury._setTimeOffset(event);
912
+ mercury._setTimeOffset('mercury-default-session', event);
874
913
  assert.isDefined(mercury.mercuryTimeOffset);
875
914
  assert.isTrue(mercury.mercuryTimeOffset > 0);
876
915
  });
@@ -880,7 +919,7 @@ describe('plugin-mercury', () => {
880
919
  wsWriteTimestamp: Date.now() + 60000,
881
920
  },
882
921
  };
883
- mercury._setTimeOffset(event);
922
+ mercury._setTimeOffset('mercury-default-session', event);
884
923
  assert.isTrue(mercury.mercuryTimeOffset < 0);
885
924
  });
886
925
  it('handles invalid wsWriteTimestamp', () => {
@@ -891,7 +930,7 @@ describe('plugin-mercury', () => {
891
930
  wsWriteTimestamp: invalidTimestamp,
892
931
  },
893
932
  };
894
- mercury._setTimeOffset(event);
933
+ mercury._setTimeOffset('mercury-default-session', event);
895
934
  assert.isUndefined(mercury.mercuryTimeOffset);
896
935
  });
897
936
  });
@@ -998,13 +1037,15 @@ describe('plugin-mercury', () => {
998
1037
  describe('shutdown protocol', () => {
999
1038
  describe('#_handleImminentShutdown()', () => {
1000
1039
  let connectWithBackoffStub;
1040
+ const sessionId = 'mercury-default-session';
1001
1041
 
1002
1042
  beforeEach(() => {
1003
1043
  mercury.connected = true;
1004
- mercury.socket = {
1044
+ mercury.sockets.set(sessionId, {
1005
1045
  url: 'ws://old-socket.com',
1006
1046
  removeAllListeners: sinon.stub(),
1007
- };
1047
+ });
1048
+ mercury.socket = mercury.sockets.get(sessionId);
1008
1049
  connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
1009
1050
  connectWithBackoffStub.returns(Promise.resolve());
1010
1051
  sinon.stub(mercury, '_emit');
@@ -1013,32 +1054,47 @@ describe('plugin-mercury', () => {
1013
1054
  afterEach(() => {
1014
1055
  connectWithBackoffStub.restore();
1015
1056
  mercury._emit.restore();
1057
+ mercury.sockets.clear();
1016
1058
  });
1017
1059
 
1018
1060
  it('should be idempotent - no-op if already in progress', () => {
1019
- mercury._shutdownSwitchoverInProgress = true;
1061
+ // Simulate an existing switchover in progress by seeding the backoff map
1062
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
1020
1063
 
1021
- mercury._handleImminentShutdown();
1064
+ mercury._handleImminentShutdown(sessionId);
1022
1065
 
1023
1066
  assert.notCalled(connectWithBackoffStub);
1024
1067
  });
1025
1068
 
1026
1069
  it('should set switchover flags when called', () => {
1027
- mercury._handleImminentShutdown();
1070
+ mercury._handleImminentShutdown(sessionId);
1028
1071
 
1029
- assert.isTrue(mercury._shutdownSwitchoverInProgress);
1072
+ // With _connectWithBackoff stubbed, the backoff map entry may not be created here.
1073
+ // Assert that switchover initiation state was set and a shutdown switchover connect was requested.
1030
1074
  assert.isDefined(mercury._shutdownSwitchoverId);
1075
+
1076
+ assert.calledOnce(connectWithBackoffStub);
1077
+ const callArgs = connectWithBackoffStub.firstCall.args;
1078
+ assert.isUndefined(callArgs[0]); // webSocketUrl
1079
+ assert.equal(callArgs[1], sessionId); // sessionId
1080
+ assert.isObject(callArgs[2]); // context
1081
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1082
+ assert.isObject(callArgs[2].attemptOptions);
1083
+ assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
1031
1084
  });
1032
1085
 
1033
1086
  it('should call _connectWithBackoff with correct parameters', (done) => {
1034
- mercury._handleImminentShutdown();
1087
+ mercury._handleImminentShutdown(sessionId);
1035
1088
 
1036
1089
  process.nextTick(() => {
1037
1090
  assert.calledOnce(connectWithBackoffStub);
1038
1091
  const callArgs = connectWithBackoffStub.firstCall.args;
1039
1092
  assert.isUndefined(callArgs[0]); // webSocketUrl
1040
- assert.isObject(callArgs[1]); // context
1041
- assert.isTrue(callArgs[1].isShutdownSwitchover);
1093
+ assert.equal(callArgs[1], sessionId); // sessionId
1094
+ assert.isObject(callArgs[2]); // context
1095
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1096
+ assert.isObject(callArgs[2].attemptOptions);
1097
+ assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
1042
1098
  done();
1043
1099
  });
1044
1100
  });
@@ -1047,12 +1103,17 @@ describe('plugin-mercury', () => {
1047
1103
  connectWithBackoffStub.restore();
1048
1104
  sinon.stub(mercury, '_connectWithBackoff').throws(new Error('Connection failed'));
1049
1105
 
1050
- mercury._handleImminentShutdown();
1106
+ mercury._handleImminentShutdown(sessionId);
1051
1107
 
1052
- assert.isFalse(mercury._shutdownSwitchoverInProgress);
1108
+ // When an exception happens synchronously, the placeholder entry
1109
+ // should be removed from the map.
1110
+ const switchoverCall = mercury._shutdownSwitchoverBackoffCalls.get(sessionId);
1111
+ assert.isUndefined(switchoverCall);
1112
+ mercury._connectWithBackoff.restore();
1053
1113
  });
1054
1114
  });
1055
1115
 
1116
+
1056
1117
  describe('#_onmessage() with shutdown message', () => {
1057
1118
  beforeEach(() => {
1058
1119
  sinon.stub(mercury, '_handleImminentShutdown');
@@ -1073,10 +1134,15 @@ describe('plugin-mercury', () => {
1073
1134
  },
1074
1135
  };
1075
1136
 
1076
- const result = mercury._onmessage(shutdownEvent);
1137
+ const result = mercury._onmessage(mercury.defaultSessionId, shutdownEvent);
1077
1138
 
1078
1139
  assert.calledOnce(mercury._handleImminentShutdown);
1079
- assert.calledWith(mercury._emit, 'event:mercury_shutdown_imminent', shutdownEvent.data);
1140
+ assert.calledWith(
1141
+ mercury._emit,
1142
+ mercury.defaultSessionId,
1143
+ 'event:mercury_shutdown_imminent',
1144
+ shutdownEvent.data
1145
+ );
1080
1146
  assert.instanceOf(result, Promise);
1081
1147
  });
1082
1148
 
@@ -1087,7 +1153,7 @@ describe('plugin-mercury', () => {
1087
1153
  },
1088
1154
  };
1089
1155
 
1090
- mercury._onmessage(shutdownEvent);
1156
+ mercury._onmessage(mercury.defaultSessionId, shutdownEvent);
1091
1157
 
1092
1158
  assert.calledOnce(mercury._handleImminentShutdown);
1093
1159
  });
@@ -1102,12 +1168,118 @@ describe('plugin-mercury', () => {
1102
1168
  },
1103
1169
  };
1104
1170
 
1105
- mercury._onmessage(regularEvent);
1171
+ mercury._onmessage(mercury.defaultSessionId, regularEvent);
1106
1172
 
1107
1173
  assert.notCalled(mercury._handleImminentShutdown);
1108
1174
  });
1109
1175
  });
1110
1176
 
1177
+ describe('#_onmessage() with missing data or eventType', () => {
1178
+ beforeEach(() => {
1179
+ sinon.stub(mercury, '_emit');
1180
+ sinon.stub(mercury, '_setTimeOffset');
1181
+ sinon.stub(mercury, '_applyOverrides');
1182
+ });
1183
+
1184
+ afterEach(() => {
1185
+ mercury._emit.restore();
1186
+ mercury._setTimeOffset.restore();
1187
+ mercury._applyOverrides.restore();
1188
+ });
1189
+
1190
+ it('should not throw when envelope.data is undefined', () => {
1191
+ const event = {
1192
+ data: {
1193
+ type: 'someType',
1194
+ // no nested data property
1195
+ },
1196
+ };
1197
+
1198
+ const result = mercury._onmessage(mercury.defaultSessionId, event);
1199
+
1200
+ assert.instanceOf(result, Promise);
1201
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
1202
+ });
1203
+
1204
+ it('should not throw when data.eventType is undefined', () => {
1205
+ const event = {
1206
+ data: {
1207
+ type: 'someType',
1208
+ data: {
1209
+ // no eventType property
1210
+ someField: 'value',
1211
+ },
1212
+ },
1213
+ };
1214
+
1215
+ const result = mercury._onmessage(mercury.defaultSessionId, event);
1216
+
1217
+ assert.instanceOf(result, Promise);
1218
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
1219
+ });
1220
+
1221
+ it('should emit generic event for messages without eventType (e.g. subscription responses)', () => {
1222
+ const event = {
1223
+ data: {
1224
+ id: 'msg-123',
1225
+ sequenceNumber: 5,
1226
+ data: {
1227
+ statusCode: 200,
1228
+ },
1229
+ },
1230
+ };
1231
+
1232
+ const result = mercury._onmessage(mercury.defaultSessionId, event);
1233
+
1234
+ assert.instanceOf(result, Promise);
1235
+ assert.calledOnce(mercury._emit);
1236
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
1237
+ });
1238
+
1239
+ it('should still process messages with a valid eventType', async () => {
1240
+ const event = {
1241
+ data: {
1242
+ data: {
1243
+ eventType: 'conversation.activity',
1244
+ },
1245
+ },
1246
+ };
1247
+
1248
+ await mercury._onmessage(mercury.defaultSessionId, event);
1249
+
1250
+ // Normal flow emits namespace-specific events after processing handlers.
1251
+ // The early-return guard only emits 'event', so asserting these proves the normal path was taken.
1252
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event:conversation', event.data);
1253
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event:conversation.activity', event.data);
1254
+ });
1255
+ });
1256
+
1257
+ describe('#_getEventHandlers()', () => {
1258
+ it('should return an empty array when eventType is undefined', () => {
1259
+ const result = mercury._getEventHandlers(undefined);
1260
+
1261
+ assert.deepEqual(result, []);
1262
+ });
1263
+
1264
+ it('should return an empty array when eventType is null', () => {
1265
+ const result = mercury._getEventHandlers(null);
1266
+
1267
+ assert.deepEqual(result, []);
1268
+ });
1269
+
1270
+ it('should return an empty array when eventType is an empty string', () => {
1271
+ const result = mercury._getEventHandlers('');
1272
+
1273
+ assert.deepEqual(result, []);
1274
+ });
1275
+
1276
+ it('should return an empty array when namespace is not registered', () => {
1277
+ const result = mercury._getEventHandlers('unknownNamespace.someEvent');
1278
+
1279
+ assert.deepEqual(result, []);
1280
+ });
1281
+ });
1282
+
1111
1283
  describe('#_onclose() with code 4001 (shutdown replacement)', () => {
1112
1284
  let mockSocket, anotherSocket;
1113
1285
 
@@ -1121,6 +1293,7 @@ describe('plugin-mercury', () => {
1121
1293
  removeAllListeners: sinon.stub(),
1122
1294
  };
1123
1295
  mercury.socket = mockSocket;
1296
+ mercury.sockets.set(mercury.defaultSessionId, mockSocket);
1124
1297
  mercury.connected = true;
1125
1298
  sinon.stub(mercury, '_emit');
1126
1299
  sinon.stub(mercury, '_reconnect');
@@ -1139,9 +1312,9 @@ describe('plugin-mercury', () => {
1139
1312
  reason: 'replaced during shutdown',
1140
1313
  };
1141
1314
 
1142
- mercury._onclose(closeEvent, mockSocket);
1315
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1143
1316
 
1144
- assert.calledWith(mercury._emit, 'offline.permanent', closeEvent);
1317
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.permanent', closeEvent);
1145
1318
  assert.notCalled(mercury._reconnect); // No reconnect for 4001 on active socket
1146
1319
  assert.isFalse(mercury.connected);
1147
1320
  });
@@ -1152,9 +1325,9 @@ describe('plugin-mercury', () => {
1152
1325
  reason: 'replaced during shutdown',
1153
1326
  };
1154
1327
 
1155
- mercury._onclose(closeEvent, anotherSocket);
1328
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1156
1329
 
1157
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1330
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
1158
1331
  assert.notCalled(mercury._reconnect);
1159
1332
  assert.isTrue(mercury.connected); // Should remain connected
1160
1333
  assert.notCalled(mercury.unset);
@@ -1167,15 +1340,16 @@ describe('plugin-mercury', () => {
1167
1340
  };
1168
1341
 
1169
1342
  // Test non-active socket
1170
- mercury._onclose(closeEvent, anotherSocket);
1171
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1343
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1344
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
1172
1345
 
1173
1346
  // Reset the spy call history
1174
1347
  mercury._emit.resetHistory();
1175
1348
 
1176
1349
  // Test active socket
1177
- mercury._onclose(closeEvent, mockSocket);
1178
- assert.calledWith(mercury._emit, 'offline.permanent', closeEvent);
1350
+ mercury.sockets.set(mercury.defaultSessionId, mockSocket);
1351
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1352
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.permanent', closeEvent);
1179
1353
  });
1180
1354
 
1181
1355
  it('should handle missing sourceSocket parameter (treats as non-active)', () => {
@@ -1184,10 +1358,10 @@ describe('plugin-mercury', () => {
1184
1358
  reason: 'replaced during shutdown',
1185
1359
  };
1186
1360
 
1187
- mercury._onclose(closeEvent); // No sourceSocket parameter
1361
+ mercury._onclose(mercury.defaultSessionId, closeEvent); // No sourceSocket parameter
1188
1362
 
1189
1363
  // With simplified logic, undefined !== this.socket, so isActiveSocket = false
1190
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1364
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
1191
1365
  assert.notCalled(mercury._reconnect);
1192
1366
  });
1193
1367
 
@@ -1198,7 +1372,7 @@ describe('plugin-mercury', () => {
1198
1372
  };
1199
1373
 
1200
1374
  // Close non-active socket (not the active one)
1201
- mercury._onclose(closeEvent, anotherSocket);
1375
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1202
1376
 
1203
1377
  // Verify listeners were removed from the old socket
1204
1378
  // The _onclose method checks if sourceSocket !== this.socket (non-active)
@@ -1213,7 +1387,7 @@ describe('plugin-mercury', () => {
1213
1387
  };
1214
1388
 
1215
1389
  // Close active socket
1216
- mercury._onclose(closeEvent, mockSocket);
1390
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1217
1391
 
1218
1392
  // Verify listeners were removed from active socket
1219
1393
  assert.calledOnce(mockSocket.removeAllListeners);
@@ -1222,13 +1396,15 @@ describe('plugin-mercury', () => {
1222
1396
 
1223
1397
  describe('shutdown switchover with retry logic', () => {
1224
1398
  let connectWithBackoffStub;
1399
+ const sessionId = 'mercury-default-session';
1225
1400
 
1226
1401
  beforeEach(() => {
1227
1402
  mercury.connected = true;
1228
- mercury.socket = {
1403
+ mercury.sockets.set(sessionId, {
1229
1404
  url: 'ws://old-socket.com',
1230
1405
  removeAllListeners: sinon.stub(),
1231
- };
1406
+ });
1407
+ mercury.socket = mercury.sockets.get(sessionId);
1232
1408
  connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
1233
1409
  sinon.stub(mercury, '_emit');
1234
1410
  });
@@ -1236,39 +1412,46 @@ describe('plugin-mercury', () => {
1236
1412
  afterEach(() => {
1237
1413
  connectWithBackoffStub.restore();
1238
1414
  mercury._emit.restore();
1415
+ mercury.sockets.clear();
1239
1416
  });
1240
1417
 
1241
1418
  it('should call _connectWithBackoff with shutdown switchover context', (done) => {
1242
1419
  connectWithBackoffStub.returns(Promise.resolve());
1243
1420
 
1244
- mercury._handleImminentShutdown();
1421
+ mercury._handleImminentShutdown(sessionId);
1245
1422
 
1246
- // Give it a tick for the async call to happen
1247
1423
  process.nextTick(() => {
1248
1424
  assert.calledOnce(connectWithBackoffStub);
1249
1425
  const callArgs = connectWithBackoffStub.firstCall.args;
1250
1426
 
1251
- assert.isUndefined(callArgs[0]); // webSocketUrl is undefined
1252
- assert.isObject(callArgs[1]); // context object
1253
- assert.isTrue(callArgs[1].isShutdownSwitchover);
1254
- assert.isObject(callArgs[1].attemptOptions);
1255
- assert.isTrue(callArgs[1].attemptOptions.isShutdownSwitchover);
1427
+ assert.isUndefined(callArgs[0]); // webSocketUrl
1428
+ assert.equal(callArgs[1], sessionId);
1429
+ assert.isObject(callArgs[2]);
1430
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1431
+ assert.isObject(callArgs[2].attemptOptions);
1432
+ assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
1256
1433
  done();
1257
1434
  });
1258
1435
  });
1259
1436
 
1260
1437
  it('should set _shutdownSwitchoverInProgress flag during switchover', () => {
1261
- connectWithBackoffStub.returns(new Promise(() => {})); // Never resolves
1438
+ // With the new behavior, "in progress" is represented by the presence
1439
+ // of an entry in _shutdownSwitchoverBackoffCalls.
1440
+ // Since _connectWithBackoff is stubbed in this suite, simulate its side-effect
1441
+ // of seeding the backoff-call map entry.
1442
+ connectWithBackoffStub.callsFake(() => {
1443
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
1444
+ return new Promise(() => {}); // Never resolves
1445
+ });
1262
1446
 
1263
- mercury._handleImminentShutdown();
1447
+ mercury._handleImminentShutdown(sessionId);
1264
1448
 
1265
- assert.isTrue(mercury._shutdownSwitchoverInProgress);
1449
+ const switchoverBackoffCall = mercury._shutdownSwitchoverBackoffCalls.get(sessionId);
1450
+ assert.isOk(switchoverBackoffCall);
1266
1451
  });
1267
1452
 
1268
1453
  it('should emit success event when switchover completes', async () => {
1269
- // We need to actually call the onSuccess callback to trigger the event
1270
- connectWithBackoffStub.callsFake((url, context) => {
1271
- // Simulate successful connection by calling onSuccess
1454
+ connectWithBackoffStub.callsFake((url, sid, context) => {
1272
1455
  if (context && context.attemptOptions && context.attemptOptions.onSuccess) {
1273
1456
  const mockSocket = {url: 'ws://new-socket.com'};
1274
1457
  context.attemptOptions.onSuccess(mockSocket, 'ws://new-socket.com');
@@ -1276,14 +1459,15 @@ describe('plugin-mercury', () => {
1276
1459
  return Promise.resolve();
1277
1460
  });
1278
1461
 
1279
- mercury._handleImminentShutdown();
1462
+ mercury._handleImminentShutdown(sessionId);
1280
1463
 
1281
- // Wait for async operations
1282
1464
  await promiseTick(50);
1283
1465
 
1284
1466
  const emitCalls = mercury._emit.getCalls();
1285
1467
  const hasCompleteEvent = emitCalls.some(
1286
- (call) => call.args[0] === 'event:mercury_shutdown_switchover_complete'
1468
+ (call) =>
1469
+ call.args[0] === sessionId &&
1470
+ call.args[1] === 'event:mercury_shutdown_switchover_complete'
1287
1471
  );
1288
1472
 
1289
1473
  assert.isTrue(hasCompleteEvent, 'Should emit switchover complete event');
@@ -1294,16 +1478,16 @@ describe('plugin-mercury', () => {
1294
1478
 
1295
1479
  connectWithBackoffStub.returns(Promise.reject(testError));
1296
1480
 
1297
- mercury._handleImminentShutdown();
1481
+ mercury._handleImminentShutdown(sessionId);
1298
1482
  await promiseTick(50);
1299
1483
 
1300
- // Check if failure event was emitted
1301
1484
  const emitCalls = mercury._emit.getCalls();
1302
1485
  const hasFailureEvent = emitCalls.some(
1303
1486
  (call) =>
1304
- call.args[0] === 'event:mercury_shutdown_switchover_failed' &&
1305
- call.args[1] &&
1306
- call.args[1].reason === testError
1487
+ call.args[0] === sessionId &&
1488
+ call.args[1] === 'event:mercury_shutdown_switchover_failed' &&
1489
+ call.args[2] &&
1490
+ call.args[2].reason === testError
1307
1491
  );
1308
1492
 
1309
1493
  assert.isTrue(hasFailureEvent, 'Should emit switchover failed event');
@@ -1312,10 +1496,9 @@ describe('plugin-mercury', () => {
1312
1496
  it('should allow old socket to be closed by server after switchover failure', async () => {
1313
1497
  connectWithBackoffStub.returns(Promise.reject(new Error('Failed')));
1314
1498
 
1315
- mercury._handleImminentShutdown();
1499
+ mercury._handleImminentShutdown(sessionId);
1316
1500
  await promiseTick(50);
1317
1501
 
1318
- // Old socket should not be closed immediately - server will close it
1319
1502
  assert.equal(mercury.socket.removeAllListeners.callCount, 0);
1320
1503
  });
1321
1504
  });
@@ -1412,18 +1595,16 @@ describe('plugin-mercury', () => {
1412
1595
  });
1413
1596
 
1414
1597
  describe('#_attemptConnection() with shutdown switchover', () => {
1415
- let mockSocket, prepareAndOpenSocketStub, callback;
1598
+ let prepareAndOpenSocketStub, callback;
1599
+ const sessionId = 'mercury-default-session';
1416
1600
 
1417
1601
  beforeEach(() => {
1418
- mockSocket = {
1419
- url: 'ws://test.com',
1420
- };
1421
1602
  prepareAndOpenSocketStub = sinon
1422
1603
  .stub(mercury, '_prepareAndOpenSocket')
1423
1604
  .returns(Promise.resolve('ws://new-socket.com'));
1424
1605
  callback = sinon.stub();
1425
- mercury._shutdownSwitchoverBackoffCall = {}; // Mock backoff call
1426
- mercury.socket = mockSocket;
1606
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: sinon.stub()});
1607
+ mercury.socket = {url: 'ws://test.com'};
1427
1608
  mercury.connected = true;
1428
1609
  sinon.stub(mercury, '_emit');
1429
1610
  sinon.stub(mercury, '_attachSocketEventListeners');
@@ -1433,28 +1614,26 @@ describe('plugin-mercury', () => {
1433
1614
  prepareAndOpenSocketStub.restore();
1434
1615
  mercury._emit.restore();
1435
1616
  mercury._attachSocketEventListeners.restore();
1617
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1436
1618
  });
1437
1619
 
1438
1620
  it('should not set socket reference before opening for shutdown switchover', async () => {
1439
1621
  const originalSocket = mercury.socket;
1440
1622
 
1441
- await mercury._attemptConnection('ws://test.com', callback, {
1623
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1442
1624
  isShutdownSwitchover: true,
1443
1625
  onSuccess: (newSocket, url) => {
1444
- // During onSuccess, verify original socket is still set
1445
- // (socket swap happens inside onSuccess callback in _handleImminentShutdown)
1446
1626
  assert.equal(mercury.socket, originalSocket);
1447
1627
  },
1448
1628
  });
1449
1629
 
1450
- // After onSuccess, socket should still be original since we only swap in _handleImminentShutdown
1451
1630
  assert.equal(mercury.socket, originalSocket);
1452
1631
  });
1453
1632
 
1454
1633
  it('should call onSuccess callback with new socket and URL for shutdown', async () => {
1455
1634
  const onSuccessStub = sinon.stub();
1456
1635
 
1457
- await mercury._attemptConnection('ws://test.com', callback, {
1636
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1458
1637
  isShutdownSwitchover: true,
1459
1638
  onSuccess: onSuccessStub,
1460
1639
  });
@@ -1464,20 +1643,22 @@ describe('plugin-mercury', () => {
1464
1643
  });
1465
1644
 
1466
1645
  it('should emit shutdown switchover complete event', async () => {
1467
- const oldSocket = mercury.socket;
1468
-
1469
- await mercury._attemptConnection('ws://test.com', callback, {
1646
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1470
1647
  isShutdownSwitchover: true,
1471
1648
  onSuccess: (newSocket, url) => {
1472
- // Simulate the onSuccess callback behavior
1473
1649
  mercury.socket = newSocket;
1474
1650
  mercury.connected = true;
1475
- mercury._emit('event:mercury_shutdown_switchover_complete', {url});
1651
+ mercury._emit(
1652
+ sessionId,
1653
+ 'event:mercury_shutdown_switchover_complete',
1654
+ {url}
1655
+ );
1476
1656
  },
1477
1657
  });
1478
1658
 
1479
1659
  assert.calledWith(
1480
1660
  mercury._emit,
1661
+ sessionId,
1481
1662
  'event:mercury_shutdown_switchover_complete',
1482
1663
  sinon.match.has('url', 'ws://new-socket.com')
1483
1664
  );
@@ -1486,25 +1667,25 @@ describe('plugin-mercury', () => {
1486
1667
  it('should use simpler error handling for shutdown switchover failures', async () => {
1487
1668
  prepareAndOpenSocketStub.returns(Promise.reject(new Error('Connection failed')));
1488
1669
 
1489
- try {
1490
- await mercury._attemptConnection('ws://test.com', callback, {
1670
+ await mercury
1671
+ ._attemptConnection('ws://test.com', sessionId, callback, {
1491
1672
  isShutdownSwitchover: true,
1492
- });
1493
- } catch (err) {
1494
- // Error should be caught and passed to callback
1495
- }
1673
+ })
1674
+ .catch(() => {});
1496
1675
 
1497
- // Should call callback with error for retry
1498
1676
  assert.calledOnce(callback);
1499
1677
  assert.instanceOf(callback.firstCall.args[0], Error);
1500
1678
  });
1501
1679
 
1502
1680
  it('should check _shutdownSwitchoverBackoffCall for shutdown connections', () => {
1503
- mercury._shutdownSwitchoverBackoffCall = undefined;
1681
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1504
1682
 
1505
- const result = mercury._attemptConnection('ws://test.com', callback, {
1506
- isShutdownSwitchover: true,
1507
- });
1683
+ const result = mercury._attemptConnection(
1684
+ 'ws://test.com',
1685
+ sessionId,
1686
+ callback,
1687
+ {isShutdownSwitchover: true}
1688
+ );
1508
1689
 
1509
1690
  return result.catch((err) => {
1510
1691
  assert.instanceOf(err, Error);
@@ -1514,35 +1695,28 @@ describe('plugin-mercury', () => {
1514
1695
  });
1515
1696
 
1516
1697
  describe('#_connectWithBackoff() with shutdown switchover', () => {
1517
- // Note: These tests verify the parameterization logic without running real backoff timers
1518
- // to avoid test hangs. The backoff mechanism itself is tested in other test suites.
1698
+ const sessionId = 'mercury-default-session';
1519
1699
 
1520
1700
  it('should use shutdown-specific parameters when called', () => {
1521
- // Stub _connectWithBackoff to prevent real execution
1522
1701
  const connectWithBackoffStub = sinon
1523
1702
  .stub(mercury, '_connectWithBackoff')
1524
1703
  .returns(Promise.resolve());
1525
1704
 
1526
- mercury._handleImminentShutdown();
1705
+ mercury._handleImminentShutdown(sessionId);
1527
1706
 
1528
- // Verify it was called with shutdown context
1529
1707
  assert.calledOnce(connectWithBackoffStub);
1530
1708
  const callArgs = connectWithBackoffStub.firstCall.args;
1531
- assert.isObject(callArgs[1]); // context
1532
- assert.isTrue(callArgs[1].isShutdownSwitchover);
1709
+ assert.equal(callArgs[1], sessionId);
1710
+ assert.isObject(callArgs[2]);
1711
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1533
1712
 
1534
1713
  connectWithBackoffStub.restore();
1535
1714
  });
1536
1715
 
1537
1716
  it('should pass shutdown switchover options to _attemptConnection', () => {
1538
- // Stub _attemptConnection to verify it receives correct options
1539
1717
  const attemptStub = sinon.stub(mercury, '_attemptConnection');
1540
- attemptStub.callsFake((url, callback) => {
1541
- // Immediately succeed
1542
- callback();
1543
- });
1718
+ attemptStub.callsFake((url, sid, cb) => cb());
1544
1719
 
1545
- // Call _connectWithBackoff with shutdown context
1546
1720
  const context = {
1547
1721
  isShutdownSwitchover: true,
1548
1722
  attemptOptions: {
@@ -1551,34 +1725,30 @@ describe('plugin-mercury', () => {
1551
1725
  },
1552
1726
  };
1553
1727
 
1554
- // Start the backoff
1555
- const promise = mercury._connectWithBackoff(undefined, context);
1728
+ const promise = mercury._connectWithBackoff(undefined, sessionId, context);
1556
1729
 
1557
- // Check that _attemptConnection was called with shutdown options
1558
1730
  return promise.then(() => {
1559
1731
  assert.calledOnce(attemptStub);
1560
1732
  const callArgs = attemptStub.firstCall.args;
1561
- assert.isObject(callArgs[2]); // options parameter
1562
- assert.isTrue(callArgs[2].isShutdownSwitchover);
1563
-
1733
+ assert.equal(callArgs[1], sessionId);
1734
+ assert.isObject(callArgs[3]);
1735
+ assert.isTrue(callArgs[3].isShutdownSwitchover);
1564
1736
  attemptStub.restore();
1565
1737
  });
1566
1738
  });
1567
1739
 
1568
1740
  it('should set and clear state flags appropriately', () => {
1569
- // Stub to prevent actual connection
1570
- sinon.stub(mercury, '_attemptConnection').callsFake((url, callback) => callback());
1741
+ sinon.stub(mercury, '_attemptConnection').callsFake((url, sid, cb) => cb());
1571
1742
 
1572
- mercury._shutdownSwitchoverInProgress = true;
1743
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
1573
1744
 
1574
- const promise = mercury._connectWithBackoff(undefined, {
1745
+ const promise = mercury._connectWithBackoff(undefined, sessionId, {
1575
1746
  isShutdownSwitchover: true,
1576
1747
  attemptOptions: {isShutdownSwitchover: true, onSuccess: () => {}},
1577
1748
  });
1578
1749
 
1579
1750
  return promise.then(() => {
1580
- // Should be cleared after completion
1581
- assert.isFalse(mercury._shutdownSwitchoverInProgress);
1751
+ assert.isUndefined(mercury._shutdownSwitchoverBackoffCalls.get(sessionId));
1582
1752
  mercury._attemptConnection.restore();
1583
1753
  });
1584
1754
  });
@@ -1586,32 +1756,30 @@ describe('plugin-mercury', () => {
1586
1756
 
1587
1757
  describe('#disconnect() with shutdown switchover in progress', () => {
1588
1758
  let abortStub;
1759
+ const sessionId = 'mercury-default-session';
1589
1760
 
1590
1761
  beforeEach(() => {
1591
- mercury.socket = {
1762
+ mercury.sockets.clear();
1763
+ mercury.sockets.set(sessionId, {
1592
1764
  close: sinon.stub().returns(Promise.resolve()),
1593
1765
  removeAllListeners: sinon.stub(),
1594
- };
1766
+ });
1595
1767
  abortStub = sinon.stub();
1596
- mercury._shutdownSwitchoverBackoffCall = {
1597
- abort: abortStub,
1598
- };
1768
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: abortStub});
1599
1769
  });
1600
1770
 
1601
1771
  it('should abort shutdown switchover backoff call on disconnect', async () => {
1602
- await mercury.disconnect();
1772
+ await mercury.disconnect(undefined, sessionId);
1603
1773
 
1604
1774
  assert.calledOnce(abortStub);
1605
1775
  });
1606
1776
 
1607
1777
  it('should handle disconnect when no switchover is in progress', async () => {
1608
- mercury._shutdownSwitchoverBackoffCall = undefined;
1778
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1609
1779
 
1610
- // Should not throw
1611
- await mercury.disconnect();
1780
+ await mercury.disconnect(undefined, sessionId);
1612
1781
 
1613
- // Should still close the socket
1614
- assert.calledOnce(mercury.socket.close);
1782
+ assert.calledOnce(mercury.sockets.get(sessionId).close);
1615
1783
  });
1616
1784
  });
1617
1785
  });