@webex/internal-plugin-mercury 3.11.0 → 3.12.0

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,7 +1168,7 @@ 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
  });
@@ -1121,6 +1187,7 @@ describe('plugin-mercury', () => {
1121
1187
  removeAllListeners: sinon.stub(),
1122
1188
  };
1123
1189
  mercury.socket = mockSocket;
1190
+ mercury.sockets.set(mercury.defaultSessionId, mockSocket);
1124
1191
  mercury.connected = true;
1125
1192
  sinon.stub(mercury, '_emit');
1126
1193
  sinon.stub(mercury, '_reconnect');
@@ -1139,9 +1206,9 @@ describe('plugin-mercury', () => {
1139
1206
  reason: 'replaced during shutdown',
1140
1207
  };
1141
1208
 
1142
- mercury._onclose(closeEvent, mockSocket);
1209
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1143
1210
 
1144
- assert.calledWith(mercury._emit, 'offline.permanent', closeEvent);
1211
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.permanent', closeEvent);
1145
1212
  assert.notCalled(mercury._reconnect); // No reconnect for 4001 on active socket
1146
1213
  assert.isFalse(mercury.connected);
1147
1214
  });
@@ -1152,9 +1219,9 @@ describe('plugin-mercury', () => {
1152
1219
  reason: 'replaced during shutdown',
1153
1220
  };
1154
1221
 
1155
- mercury._onclose(closeEvent, anotherSocket);
1222
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1156
1223
 
1157
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1224
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
1158
1225
  assert.notCalled(mercury._reconnect);
1159
1226
  assert.isTrue(mercury.connected); // Should remain connected
1160
1227
  assert.notCalled(mercury.unset);
@@ -1167,15 +1234,16 @@ describe('plugin-mercury', () => {
1167
1234
  };
1168
1235
 
1169
1236
  // Test non-active socket
1170
- mercury._onclose(closeEvent, anotherSocket);
1171
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1237
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1238
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
1172
1239
 
1173
1240
  // Reset the spy call history
1174
1241
  mercury._emit.resetHistory();
1175
1242
 
1176
1243
  // Test active socket
1177
- mercury._onclose(closeEvent, mockSocket);
1178
- assert.calledWith(mercury._emit, 'offline.permanent', closeEvent);
1244
+ mercury.sockets.set(mercury.defaultSessionId, mockSocket);
1245
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1246
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.permanent', closeEvent);
1179
1247
  });
1180
1248
 
1181
1249
  it('should handle missing sourceSocket parameter (treats as non-active)', () => {
@@ -1184,10 +1252,10 @@ describe('plugin-mercury', () => {
1184
1252
  reason: 'replaced during shutdown',
1185
1253
  };
1186
1254
 
1187
- mercury._onclose(closeEvent); // No sourceSocket parameter
1255
+ mercury._onclose(mercury.defaultSessionId, closeEvent); // No sourceSocket parameter
1188
1256
 
1189
1257
  // With simplified logic, undefined !== this.socket, so isActiveSocket = false
1190
- assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
1258
+ assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
1191
1259
  assert.notCalled(mercury._reconnect);
1192
1260
  });
1193
1261
 
@@ -1198,7 +1266,7 @@ describe('plugin-mercury', () => {
1198
1266
  };
1199
1267
 
1200
1268
  // Close non-active socket (not the active one)
1201
- mercury._onclose(closeEvent, anotherSocket);
1269
+ mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
1202
1270
 
1203
1271
  // Verify listeners were removed from the old socket
1204
1272
  // The _onclose method checks if sourceSocket !== this.socket (non-active)
@@ -1213,7 +1281,7 @@ describe('plugin-mercury', () => {
1213
1281
  };
1214
1282
 
1215
1283
  // Close active socket
1216
- mercury._onclose(closeEvent, mockSocket);
1284
+ mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
1217
1285
 
1218
1286
  // Verify listeners were removed from active socket
1219
1287
  assert.calledOnce(mockSocket.removeAllListeners);
@@ -1222,13 +1290,15 @@ describe('plugin-mercury', () => {
1222
1290
 
1223
1291
  describe('shutdown switchover with retry logic', () => {
1224
1292
  let connectWithBackoffStub;
1293
+ const sessionId = 'mercury-default-session';
1225
1294
 
1226
1295
  beforeEach(() => {
1227
1296
  mercury.connected = true;
1228
- mercury.socket = {
1297
+ mercury.sockets.set(sessionId, {
1229
1298
  url: 'ws://old-socket.com',
1230
1299
  removeAllListeners: sinon.stub(),
1231
- };
1300
+ });
1301
+ mercury.socket = mercury.sockets.get(sessionId);
1232
1302
  connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
1233
1303
  sinon.stub(mercury, '_emit');
1234
1304
  });
@@ -1236,39 +1306,46 @@ describe('plugin-mercury', () => {
1236
1306
  afterEach(() => {
1237
1307
  connectWithBackoffStub.restore();
1238
1308
  mercury._emit.restore();
1309
+ mercury.sockets.clear();
1239
1310
  });
1240
1311
 
1241
1312
  it('should call _connectWithBackoff with shutdown switchover context', (done) => {
1242
1313
  connectWithBackoffStub.returns(Promise.resolve());
1243
1314
 
1244
- mercury._handleImminentShutdown();
1315
+ mercury._handleImminentShutdown(sessionId);
1245
1316
 
1246
- // Give it a tick for the async call to happen
1247
1317
  process.nextTick(() => {
1248
1318
  assert.calledOnce(connectWithBackoffStub);
1249
1319
  const callArgs = connectWithBackoffStub.firstCall.args;
1250
1320
 
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);
1321
+ assert.isUndefined(callArgs[0]); // webSocketUrl
1322
+ assert.equal(callArgs[1], sessionId);
1323
+ assert.isObject(callArgs[2]);
1324
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1325
+ assert.isObject(callArgs[2].attemptOptions);
1326
+ assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
1256
1327
  done();
1257
1328
  });
1258
1329
  });
1259
1330
 
1260
1331
  it('should set _shutdownSwitchoverInProgress flag during switchover', () => {
1261
- connectWithBackoffStub.returns(new Promise(() => {})); // Never resolves
1332
+ // With the new behavior, "in progress" is represented by the presence
1333
+ // of an entry in _shutdownSwitchoverBackoffCalls.
1334
+ // Since _connectWithBackoff is stubbed in this suite, simulate its side-effect
1335
+ // of seeding the backoff-call map entry.
1336
+ connectWithBackoffStub.callsFake(() => {
1337
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
1338
+ return new Promise(() => {}); // Never resolves
1339
+ });
1262
1340
 
1263
- mercury._handleImminentShutdown();
1341
+ mercury._handleImminentShutdown(sessionId);
1264
1342
 
1265
- assert.isTrue(mercury._shutdownSwitchoverInProgress);
1343
+ const switchoverBackoffCall = mercury._shutdownSwitchoverBackoffCalls.get(sessionId);
1344
+ assert.isOk(switchoverBackoffCall);
1266
1345
  });
1267
1346
 
1268
1347
  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
1348
+ connectWithBackoffStub.callsFake((url, sid, context) => {
1272
1349
  if (context && context.attemptOptions && context.attemptOptions.onSuccess) {
1273
1350
  const mockSocket = {url: 'ws://new-socket.com'};
1274
1351
  context.attemptOptions.onSuccess(mockSocket, 'ws://new-socket.com');
@@ -1276,14 +1353,15 @@ describe('plugin-mercury', () => {
1276
1353
  return Promise.resolve();
1277
1354
  });
1278
1355
 
1279
- mercury._handleImminentShutdown();
1356
+ mercury._handleImminentShutdown(sessionId);
1280
1357
 
1281
- // Wait for async operations
1282
1358
  await promiseTick(50);
1283
1359
 
1284
1360
  const emitCalls = mercury._emit.getCalls();
1285
1361
  const hasCompleteEvent = emitCalls.some(
1286
- (call) => call.args[0] === 'event:mercury_shutdown_switchover_complete'
1362
+ (call) =>
1363
+ call.args[0] === sessionId &&
1364
+ call.args[1] === 'event:mercury_shutdown_switchover_complete'
1287
1365
  );
1288
1366
 
1289
1367
  assert.isTrue(hasCompleteEvent, 'Should emit switchover complete event');
@@ -1294,16 +1372,16 @@ describe('plugin-mercury', () => {
1294
1372
 
1295
1373
  connectWithBackoffStub.returns(Promise.reject(testError));
1296
1374
 
1297
- mercury._handleImminentShutdown();
1375
+ mercury._handleImminentShutdown(sessionId);
1298
1376
  await promiseTick(50);
1299
1377
 
1300
- // Check if failure event was emitted
1301
1378
  const emitCalls = mercury._emit.getCalls();
1302
1379
  const hasFailureEvent = emitCalls.some(
1303
1380
  (call) =>
1304
- call.args[0] === 'event:mercury_shutdown_switchover_failed' &&
1305
- call.args[1] &&
1306
- call.args[1].reason === testError
1381
+ call.args[0] === sessionId &&
1382
+ call.args[1] === 'event:mercury_shutdown_switchover_failed' &&
1383
+ call.args[2] &&
1384
+ call.args[2].reason === testError
1307
1385
  );
1308
1386
 
1309
1387
  assert.isTrue(hasFailureEvent, 'Should emit switchover failed event');
@@ -1312,10 +1390,9 @@ describe('plugin-mercury', () => {
1312
1390
  it('should allow old socket to be closed by server after switchover failure', async () => {
1313
1391
  connectWithBackoffStub.returns(Promise.reject(new Error('Failed')));
1314
1392
 
1315
- mercury._handleImminentShutdown();
1393
+ mercury._handleImminentShutdown(sessionId);
1316
1394
  await promiseTick(50);
1317
1395
 
1318
- // Old socket should not be closed immediately - server will close it
1319
1396
  assert.equal(mercury.socket.removeAllListeners.callCount, 0);
1320
1397
  });
1321
1398
  });
@@ -1412,18 +1489,16 @@ describe('plugin-mercury', () => {
1412
1489
  });
1413
1490
 
1414
1491
  describe('#_attemptConnection() with shutdown switchover', () => {
1415
- let mockSocket, prepareAndOpenSocketStub, callback;
1492
+ let prepareAndOpenSocketStub, callback;
1493
+ const sessionId = 'mercury-default-session';
1416
1494
 
1417
1495
  beforeEach(() => {
1418
- mockSocket = {
1419
- url: 'ws://test.com',
1420
- };
1421
1496
  prepareAndOpenSocketStub = sinon
1422
1497
  .stub(mercury, '_prepareAndOpenSocket')
1423
1498
  .returns(Promise.resolve('ws://new-socket.com'));
1424
1499
  callback = sinon.stub();
1425
- mercury._shutdownSwitchoverBackoffCall = {}; // Mock backoff call
1426
- mercury.socket = mockSocket;
1500
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: sinon.stub()});
1501
+ mercury.socket = {url: 'ws://test.com'};
1427
1502
  mercury.connected = true;
1428
1503
  sinon.stub(mercury, '_emit');
1429
1504
  sinon.stub(mercury, '_attachSocketEventListeners');
@@ -1433,28 +1508,26 @@ describe('plugin-mercury', () => {
1433
1508
  prepareAndOpenSocketStub.restore();
1434
1509
  mercury._emit.restore();
1435
1510
  mercury._attachSocketEventListeners.restore();
1511
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1436
1512
  });
1437
1513
 
1438
1514
  it('should not set socket reference before opening for shutdown switchover', async () => {
1439
1515
  const originalSocket = mercury.socket;
1440
1516
 
1441
- await mercury._attemptConnection('ws://test.com', callback, {
1517
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1442
1518
  isShutdownSwitchover: true,
1443
1519
  onSuccess: (newSocket, url) => {
1444
- // During onSuccess, verify original socket is still set
1445
- // (socket swap happens inside onSuccess callback in _handleImminentShutdown)
1446
1520
  assert.equal(mercury.socket, originalSocket);
1447
1521
  },
1448
1522
  });
1449
1523
 
1450
- // After onSuccess, socket should still be original since we only swap in _handleImminentShutdown
1451
1524
  assert.equal(mercury.socket, originalSocket);
1452
1525
  });
1453
1526
 
1454
1527
  it('should call onSuccess callback with new socket and URL for shutdown', async () => {
1455
1528
  const onSuccessStub = sinon.stub();
1456
1529
 
1457
- await mercury._attemptConnection('ws://test.com', callback, {
1530
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1458
1531
  isShutdownSwitchover: true,
1459
1532
  onSuccess: onSuccessStub,
1460
1533
  });
@@ -1464,20 +1537,22 @@ describe('plugin-mercury', () => {
1464
1537
  });
1465
1538
 
1466
1539
  it('should emit shutdown switchover complete event', async () => {
1467
- const oldSocket = mercury.socket;
1468
-
1469
- await mercury._attemptConnection('ws://test.com', callback, {
1540
+ await mercury._attemptConnection('ws://test.com', sessionId, callback, {
1470
1541
  isShutdownSwitchover: true,
1471
1542
  onSuccess: (newSocket, url) => {
1472
- // Simulate the onSuccess callback behavior
1473
1543
  mercury.socket = newSocket;
1474
1544
  mercury.connected = true;
1475
- mercury._emit('event:mercury_shutdown_switchover_complete', {url});
1545
+ mercury._emit(
1546
+ sessionId,
1547
+ 'event:mercury_shutdown_switchover_complete',
1548
+ {url}
1549
+ );
1476
1550
  },
1477
1551
  });
1478
1552
 
1479
1553
  assert.calledWith(
1480
1554
  mercury._emit,
1555
+ sessionId,
1481
1556
  'event:mercury_shutdown_switchover_complete',
1482
1557
  sinon.match.has('url', 'ws://new-socket.com')
1483
1558
  );
@@ -1486,25 +1561,25 @@ describe('plugin-mercury', () => {
1486
1561
  it('should use simpler error handling for shutdown switchover failures', async () => {
1487
1562
  prepareAndOpenSocketStub.returns(Promise.reject(new Error('Connection failed')));
1488
1563
 
1489
- try {
1490
- await mercury._attemptConnection('ws://test.com', callback, {
1564
+ await mercury
1565
+ ._attemptConnection('ws://test.com', sessionId, callback, {
1491
1566
  isShutdownSwitchover: true,
1492
- });
1493
- } catch (err) {
1494
- // Error should be caught and passed to callback
1495
- }
1567
+ })
1568
+ .catch(() => {});
1496
1569
 
1497
- // Should call callback with error for retry
1498
1570
  assert.calledOnce(callback);
1499
1571
  assert.instanceOf(callback.firstCall.args[0], Error);
1500
1572
  });
1501
1573
 
1502
1574
  it('should check _shutdownSwitchoverBackoffCall for shutdown connections', () => {
1503
- mercury._shutdownSwitchoverBackoffCall = undefined;
1575
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1504
1576
 
1505
- const result = mercury._attemptConnection('ws://test.com', callback, {
1506
- isShutdownSwitchover: true,
1507
- });
1577
+ const result = mercury._attemptConnection(
1578
+ 'ws://test.com',
1579
+ sessionId,
1580
+ callback,
1581
+ {isShutdownSwitchover: true}
1582
+ );
1508
1583
 
1509
1584
  return result.catch((err) => {
1510
1585
  assert.instanceOf(err, Error);
@@ -1514,35 +1589,28 @@ describe('plugin-mercury', () => {
1514
1589
  });
1515
1590
 
1516
1591
  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.
1592
+ const sessionId = 'mercury-default-session';
1519
1593
 
1520
1594
  it('should use shutdown-specific parameters when called', () => {
1521
- // Stub _connectWithBackoff to prevent real execution
1522
1595
  const connectWithBackoffStub = sinon
1523
1596
  .stub(mercury, '_connectWithBackoff')
1524
1597
  .returns(Promise.resolve());
1525
1598
 
1526
- mercury._handleImminentShutdown();
1599
+ mercury._handleImminentShutdown(sessionId);
1527
1600
 
1528
- // Verify it was called with shutdown context
1529
1601
  assert.calledOnce(connectWithBackoffStub);
1530
1602
  const callArgs = connectWithBackoffStub.firstCall.args;
1531
- assert.isObject(callArgs[1]); // context
1532
- assert.isTrue(callArgs[1].isShutdownSwitchover);
1603
+ assert.equal(callArgs[1], sessionId);
1604
+ assert.isObject(callArgs[2]);
1605
+ assert.isTrue(callArgs[2].isShutdownSwitchover);
1533
1606
 
1534
1607
  connectWithBackoffStub.restore();
1535
1608
  });
1536
1609
 
1537
1610
  it('should pass shutdown switchover options to _attemptConnection', () => {
1538
- // Stub _attemptConnection to verify it receives correct options
1539
1611
  const attemptStub = sinon.stub(mercury, '_attemptConnection');
1540
- attemptStub.callsFake((url, callback) => {
1541
- // Immediately succeed
1542
- callback();
1543
- });
1612
+ attemptStub.callsFake((url, sid, cb) => cb());
1544
1613
 
1545
- // Call _connectWithBackoff with shutdown context
1546
1614
  const context = {
1547
1615
  isShutdownSwitchover: true,
1548
1616
  attemptOptions: {
@@ -1551,34 +1619,30 @@ describe('plugin-mercury', () => {
1551
1619
  },
1552
1620
  };
1553
1621
 
1554
- // Start the backoff
1555
- const promise = mercury._connectWithBackoff(undefined, context);
1622
+ const promise = mercury._connectWithBackoff(undefined, sessionId, context);
1556
1623
 
1557
- // Check that _attemptConnection was called with shutdown options
1558
1624
  return promise.then(() => {
1559
1625
  assert.calledOnce(attemptStub);
1560
1626
  const callArgs = attemptStub.firstCall.args;
1561
- assert.isObject(callArgs[2]); // options parameter
1562
- assert.isTrue(callArgs[2].isShutdownSwitchover);
1563
-
1627
+ assert.equal(callArgs[1], sessionId);
1628
+ assert.isObject(callArgs[3]);
1629
+ assert.isTrue(callArgs[3].isShutdownSwitchover);
1564
1630
  attemptStub.restore();
1565
1631
  });
1566
1632
  });
1567
1633
 
1568
1634
  it('should set and clear state flags appropriately', () => {
1569
- // Stub to prevent actual connection
1570
- sinon.stub(mercury, '_attemptConnection').callsFake((url, callback) => callback());
1635
+ sinon.stub(mercury, '_attemptConnection').callsFake((url, sid, cb) => cb());
1571
1636
 
1572
- mercury._shutdownSwitchoverInProgress = true;
1637
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
1573
1638
 
1574
- const promise = mercury._connectWithBackoff(undefined, {
1639
+ const promise = mercury._connectWithBackoff(undefined, sessionId, {
1575
1640
  isShutdownSwitchover: true,
1576
1641
  attemptOptions: {isShutdownSwitchover: true, onSuccess: () => {}},
1577
1642
  });
1578
1643
 
1579
1644
  return promise.then(() => {
1580
- // Should be cleared after completion
1581
- assert.isFalse(mercury._shutdownSwitchoverInProgress);
1645
+ assert.isUndefined(mercury._shutdownSwitchoverBackoffCalls.get(sessionId));
1582
1646
  mercury._attemptConnection.restore();
1583
1647
  });
1584
1648
  });
@@ -1586,32 +1650,30 @@ describe('plugin-mercury', () => {
1586
1650
 
1587
1651
  describe('#disconnect() with shutdown switchover in progress', () => {
1588
1652
  let abortStub;
1653
+ const sessionId = 'mercury-default-session';
1589
1654
 
1590
1655
  beforeEach(() => {
1591
- mercury.socket = {
1656
+ mercury.sockets.clear();
1657
+ mercury.sockets.set(sessionId, {
1592
1658
  close: sinon.stub().returns(Promise.resolve()),
1593
1659
  removeAllListeners: sinon.stub(),
1594
- };
1660
+ });
1595
1661
  abortStub = sinon.stub();
1596
- mercury._shutdownSwitchoverBackoffCall = {
1597
- abort: abortStub,
1598
- };
1662
+ mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: abortStub});
1599
1663
  });
1600
1664
 
1601
1665
  it('should abort shutdown switchover backoff call on disconnect', async () => {
1602
- await mercury.disconnect();
1666
+ await mercury.disconnect(undefined, sessionId);
1603
1667
 
1604
1668
  assert.calledOnce(abortStub);
1605
1669
  });
1606
1670
 
1607
1671
  it('should handle disconnect when no switchover is in progress', async () => {
1608
- mercury._shutdownSwitchoverBackoffCall = undefined;
1672
+ mercury._shutdownSwitchoverBackoffCalls.clear();
1609
1673
 
1610
- // Should not throw
1611
- await mercury.disconnect();
1674
+ await mercury.disconnect(undefined, sessionId);
1612
1675
 
1613
- // Should still close the socket
1614
- assert.calledOnce(mercury.socket.close);
1676
+ assert.calledOnce(mercury.sockets.get(sessionId).close);
1615
1677
  });
1616
1678
  });
1617
1679
  });