Skip to content

Commit 98e5998

Browse files
committed
Reconnect the WebSocket if we disconnect uncleanly
1 parent 6ab9526 commit 98e5998

File tree

3 files changed

+72
-40
lines changed

3 files changed

+72
-40
lines changed

ui/frontend/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ module.exports = {
6060
},
6161
overrides: [
6262
{
63-
files: ['.eslintrc.js'],
63+
files: ['.eslintrc.js', 'websocketMiddleware.ts'],
6464
extends: ['prettier'],
6565
},
6666
],

ui/frontend/.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ node_modules
1111

1212
# Slowly migrate files that we've touched
1313
!.eslintrc.js
14+
!websocketMiddleware.ts

ui/frontend/websocketMiddleware.ts

Lines changed: 70 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const reportWebSocketError = async (error: string) => {
2424
} catch (reportError) {
2525
console.log('Unable to report WebSocket error', error, reportError);
2626
}
27-
}
27+
};
2828

2929
const openWebSocket = (currentLocation: Location) => {
3030
try {
@@ -34,55 +34,86 @@ const openWebSocket = (currentLocation: Location) => {
3434
} catch (e) {
3535
// WebSocket URL error or WebSocket is not supported by browser.
3636
// Assume it's the second case since URL error is easy to notice.
37-
const detail = (e instanceof Error) ? e.toString() : 'An unknown error occurred';
38-
reportWebSocketError(`Could not create the WebSocket: ${detail}`)
37+
const detail = e instanceof Error ? e.toString() : 'An unknown error occurred';
38+
reportWebSocketError(`Could not create the WebSocket: ${detail}`);
3939

4040
return null;
4141
}
42-
}
42+
};
4343

44-
export const websocketMiddleware = (window: Window): Middleware => store => {
45-
const socket = openWebSocket(window.location);
44+
// https://exponentialbackoffcalculator.com
45+
const backoffMs = (n: number) => Math.min(100 * Math.pow(2, n), 10000);
4646

47-
if (socket) {
48-
socket.addEventListener('open', () => {
49-
store.dispatch(websocketConnected());
50-
});
47+
export const websocketMiddleware =
48+
(window: Window): Middleware =>
49+
(store) => {
50+
let socket: WebSocket | null = null;
51+
let wasConnected = false;
52+
let reconnectAttempt = 0;
5153

52-
socket.addEventListener('close', () => {
53-
store.dispatch(websocketDisconnected());
54-
});
54+
const connect = () => {
55+
socket = openWebSocket(window.location);
5556

56-
socket.addEventListener('error', () => {
57-
// We cannot get detailed information about the failure
58-
// https://stackoverflow.com/a/31003057/155423
59-
const error = 'Generic WebSocket Error';
60-
store.dispatch(websocketError(error));
61-
reportWebSocketError(error);
62-
});
57+
if (socket) {
58+
socket.addEventListener('open', () => {
59+
store.dispatch(websocketConnected());
60+
61+
wasConnected = true;
62+
});
6363

64-
// TODO: reconnect on error? (if ever connected? if < n failures?)
64+
socket.addEventListener('close', (event) => {
65+
store.dispatch(websocketDisconnected());
6566

66-
socket.addEventListener('message', (event) => {
67-
try {
68-
const rawMessage = JSON.parse(event.data);
69-
const message = WSMessageResponse.parse(rawMessage);
70-
store.dispatch(message);
71-
} catch (e) {
72-
console.log('Unable to parse WebSocket message', event.data, e);
67+
// Reconnect if we've previously connected
68+
if (wasConnected && !event.wasClean) {
69+
wasConnected = false;
70+
reconnectAttempt = 0;
71+
reconnect();
72+
}
73+
});
74+
75+
socket.addEventListener('error', () => {
76+
// We cannot get detailed information about the failure
77+
// https://stackoverflow.com/a/31003057/155423
78+
const error = 'Generic WebSocket Error';
79+
store.dispatch(websocketError(error));
80+
reportWebSocketError(error);
81+
});
82+
83+
socket.addEventListener('message', (event) => {
84+
try {
85+
const rawMessage = JSON.parse(event.data);
86+
const message = WSMessageResponse.parse(rawMessage);
87+
store.dispatch(message);
88+
} catch (e) {
89+
console.log('Unable to parse WebSocket message', event.data, e);
90+
}
91+
});
7392
}
74-
});
75-
}
93+
};
94+
95+
const reconnect = () => {
96+
if (socket && socket.readyState == socket.OPEN) {
97+
return;
98+
}
99+
100+
connect();
76101

77-
return next => action => {
78-
if (socket && socket.readyState == socket.OPEN && sendActionOnWebsocket(action)) {
79-
const message = JSON.stringify(action);
80-
socket.send(message);
81-
}
102+
const delay = backoffMs(reconnectAttempt);
103+
reconnectAttempt += 1;
104+
setTimeout(reconnect, delay);
105+
};
106+
107+
connect();
108+
109+
return (next) => (action) => {
110+
if (socket && socket.readyState == socket.OPEN && sendActionOnWebsocket(action)) {
111+
const message = JSON.stringify(action);
112+
socket.send(message);
113+
}
82114

83-
next(action);
115+
next(action);
116+
};
84117
};
85-
}
86118

87-
const sendActionOnWebsocket = (action: any): boolean =>
88-
action.type === ActionType.WSExecuteRequest;
119+
const sendActionOnWebsocket = (action: any): boolean => action.type === ActionType.WSExecuteRequest;

0 commit comments

Comments
 (0)