fix: recover the websocket when a close goes unreported

This commit is contained in:
Carlos Valente
2026-09-15 18:20:35 +02:00
parent daeca0050d
commit 1d402385ac
2 changed files with 29 additions and 7 deletions
@@ -40,6 +40,10 @@ class MockWebSocket {
this.onclose?.(); this.onclose?.();
} }
/** Simulates a lost connection without an onclose event. */
closeSilently() {
this.readyState = MockWebSocket.CLOSED;
}
open() { open() {
this.readyState = MockWebSocket.OPEN; this.readyState = MockWebSocket.OPEN;
this.onopen?.(); this.onopen?.();
@@ -146,6 +150,16 @@ describe('socket connection watchdog', () => {
expect(MockWebSocket.instances).toHaveLength(2); expect(MockWebSocket.instances).toHaveLength(2);
}); });
it('recovers when a close goes unreported', () => {
const socket = openConnection();
// The watchdog is the only recovery path when no close event arrives.
socket.closeSilently();
vi.advanceTimersByTime(watchdogInterval + socketConfig.reconnectBaseInterval * 2);
expect(MockWebSocket.instances).toHaveLength(2);
});
it('ignores events from a socket which has been replaced', () => { it('ignores events from a socket which has been replaced', () => {
const stale = openConnection(); const stale = openConnection();
+14 -6
View File
@@ -320,13 +320,9 @@ function registerConnectionAttempt() {
* while paused, so extended silence is evidence of a dropped connection. * while paused, so extended silence is evidence of a dropped connection.
*/ */
function checkConnection() { function checkConnection() {
if (!websocket) {
return;
}
const silentFor = Date.now() - lastContact; const silentFor = Date.now() - lastContact;
if (websocket.readyState === WebSocket.CONNECTING) { if (websocket?.readyState === WebSocket.CONNECTING) {
// A connection attempt can otherwise hang indefinitely. // A connection attempt can otherwise hang indefinitely.
if (silentFor > socketConfig.connectTimeout) { if (silentFor > socketConfig.connectTimeout) {
reconnectNow('WebSocket: connection attempt timed out'); reconnectNow('WebSocket: connection attempt timed out');
@@ -334,9 +330,21 @@ function checkConnection() {
return; return;
} }
if (websocket.readyState === WebSocket.OPEN && silentFor > socketConfig.silenceTimeout) { if (websocket?.readyState === WebSocket.OPEN) {
if (silentFor > socketConfig.silenceTimeout) {
reconnectNow('WebSocket: no data from server, reconnecting'); reconnectNow('WebSocket: no data from server, reconnecting');
} }
return;
}
/**
* The socket is closing, closed or was never created.
* Closing schedules its own reconnect, this is what covers us if that did not happen,
* so that there is no state the client can settle in without a way out.
*/
if (!reconnectTimeout) {
scheduleReconnect();
}
} }
function startWatchdog() { function startWatchdog() {