@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.
- package/README.md +54 -0
- package/dist/mercury.js +388 -198
- package/dist/mercury.js.map +1 -1
- package/dist/socket/constants.js +16 -0
- package/dist/socket/constants.js.map +1 -0
- package/dist/socket/socket-base.js +36 -3
- package/dist/socket/socket-base.js.map +1 -1
- package/package.json +17 -17
- package/src/mercury.js +389 -171
- package/src/socket/constants.js +6 -0
- package/src/socket/socket-base.js +40 -3
- package/test/unit/spec/mercury-events.js +20 -2
- package/test/unit/spec/mercury.js +201 -139
- package/test/unit/spec/socket.js +61 -0
|
@@ -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
|
-
|
|
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
|
|
618
|
+
it('calls disconnectAll and logs', () => {
|
|
588
619
|
sinon.stub(mercury.logger, 'info');
|
|
589
|
-
sinon.stub(mercury, '
|
|
620
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
590
621
|
mercury.logout();
|
|
591
|
-
assert.called(mercury.
|
|
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, '
|
|
634
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
604
635
|
mercury.config.beforeLogoutOptionsCloseReason = 'done (permanent)';
|
|
605
636
|
mercury.logout();
|
|
606
|
-
assert.calledWith(mercury.
|
|
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, '
|
|
641
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
611
642
|
mercury.config.beforeLogoutOptionsCloseReason = 'test';
|
|
612
643
|
mercury.logout();
|
|
613
|
-
assert.calledWith(mercury.
|
|
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, '
|
|
648
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
618
649
|
mercury.config.beforeLogoutOptionsCloseReason = 'done (forced)';
|
|
619
650
|
mercury.logout();
|
|
620
|
-
assert.calledWith(mercury.
|
|
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.
|
|
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.
|
|
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.
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1041
|
-
assert.
|
|
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
|
-
|
|
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(
|
|
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.
|
|
1178
|
-
|
|
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.
|
|
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
|
|
1252
|
-
assert.
|
|
1253
|
-
assert.
|
|
1254
|
-
assert.
|
|
1255
|
-
assert.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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] ===
|
|
1305
|
-
call.args[1] &&
|
|
1306
|
-
call.args[
|
|
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
|
|
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.
|
|
1426
|
-
mercury.socket =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1490
|
-
|
|
1564
|
+
await mercury
|
|
1565
|
+
._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1491
1566
|
isShutdownSwitchover: true,
|
|
1492
|
-
})
|
|
1493
|
-
|
|
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.
|
|
1575
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1504
1576
|
|
|
1505
|
-
const result = mercury._attemptConnection(
|
|
1506
|
-
|
|
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
|
-
|
|
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.
|
|
1532
|
-
assert.
|
|
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,
|
|
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
|
-
|
|
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.
|
|
1562
|
-
assert.
|
|
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
|
-
|
|
1570
|
-
sinon.stub(mercury, '_attemptConnection').callsFake((url, callback) => callback());
|
|
1635
|
+
sinon.stub(mercury, '_attemptConnection').callsFake((url, sid, cb) => cb());
|
|
1571
1636
|
|
|
1572
|
-
mercury.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
1672
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1609
1673
|
|
|
1610
|
-
|
|
1611
|
-
await mercury.disconnect();
|
|
1674
|
+
await mercury.disconnect(undefined, sessionId);
|
|
1612
1675
|
|
|
1613
|
-
|
|
1614
|
-
assert.calledOnce(mercury.socket.close);
|
|
1676
|
+
assert.calledOnce(mercury.sockets.get(sessionId).close);
|
|
1615
1677
|
});
|
|
1616
1678
|
});
|
|
1617
1679
|
});
|