1
Fork 0
mirror of https://github.com/Steffo99/todocolors.git synced 2024-11-25 17:54:18 +00:00

Fix useWs opening multiple connections at once

This commit is contained in:
Steffo 2023-08-16 16:42:22 +02:00
parent 98261cea79
commit f11dde412c
Signed by: steffo
GPG key ID: 2A24051445686895

View file

@ -1,10 +1,9 @@
'use client';
import {useState, useCallback, useEffect} from "react"
import {useState, useCallback, useEffect, useReducer, Reducer} from "react"
export interface WebSocketHandlerParams<E extends Event> {
event: E,
openWebSocketAfterBackoff: () => void,
closeWebSocket: () => void,
}
@ -15,11 +14,45 @@ export interface WebSocketHandlers {
onopen?: (params: WebSocketHandlerParams<Event>) => void,
}
interface WebSocketState {
webSocket: WebSocket | undefined,
webSocketState: number | undefined,
webSocketBackoffMs: number | undefined,
nextBackOffMs: number
}
type WebSocketAction = { event: "connect", webSocket: WebSocket } | { event: "disconnect" } | { event: "backoffExpire" } | { event: "stateChange" };
function wsReducer(prevState: WebSocketState, action: WebSocketAction) {
switch(action.event) {
case "connect":
return {
webSocket: action.webSocket,
webSocketState: action.webSocket.readyState,
webSocketBackoffMs: prevState.nextBackOffMs,
nextBackOffMs: prevState.nextBackOffMs * 2,
}
case "disconnect":
return {
...prevState,
webSocket: undefined,
webSocketState: prevState.webSocket?.readyState,
}
case "backoffExpire":
return {
...prevState,
webSocketBackoffMs: undefined,
}
case "stateChange":
return {
...prevState,
webSocketState: prevState.webSocket?.readyState
}
}
}
export function useWs(url: string | undefined, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
const [webSocket, setWebSocket] = useState<WebSocket | undefined>(undefined)
const [webSocketState, setWebSocketState] = useState<number | undefined>(undefined);
const [isBackingOff, setBackingOff] = useState<boolean>(false);
const [webSocketBackoffMs, setWebSocketBackoffMs] = useState<number>(1);
const [{webSocket, webSocketState, webSocketBackoffMs, nextBackOffMs}, dispatch] = useReducer<Reducer<WebSocketState, WebSocketAction>>(wsReducer, {webSocket: undefined, webSocketState: undefined, webSocketBackoffMs: undefined, nextBackOffMs: 1000})
const closeWebSocket = useCallback(() => {
console.debug("[useWebSocket] Closing WebSocket:", webSocket);
@ -33,61 +66,52 @@ export function useWs(url: string | undefined, {onclose, onerror, onmessage, ono
catch(closeErr) {
console.debug("[useWebSocket] Failed to close the websocket (it might be already closed):", closeErr)
}
setWebSocket(undefined);
setWebSocketState(WebSocket.CLOSED);
dispatch({event: "disconnect"})
}, [webSocket])
const backOff = useCallback((func: () => void) => async () => {
// This WILL cause no-ops, but they're going to be pretty infrequent, so idc
console.debug("[useWebSocket] Backing off for:", webSocketBackoffMs, "ms")
setBackingOff(true)
await new Promise(resolve => setTimeout(resolve, webSocketBackoffMs))
setBackingOff(false)
func()
}, [webSocketBackoffMs])
const openWebSocket = useCallback(() => {
console.debug("[useWebSocket] Opening WebSocket:", url);
if(url === undefined) {
console.warn("[useWebSocket] Trying to open WebSocket, but no URL has been given; ignoring request...")
return;
}
setWebSocketBackoffMs(prev => prev * 2); // Workaround for connections that get closed immediately by the server
setWebSocketState(WebSocket.CONNECTING); // Workaround for constructor blocking and giving no feedback to the user
const sock = new WebSocket(url);
const openWebSocketAfterBackoff = backOff(openWebSocket);
dispatch({event: "connect", webSocket: sock})
sock.onopen = (event) => {
console.debug("[useWebSocket] Opened connection:", event)
setWebSocket(sock)
setWebSocketState(sock.readyState)
onopen?.({event, openWebSocketAfterBackoff, closeWebSocket});
dispatch({event: "stateChange"})
onopen?.({event, closeWebSocket});
}
sock.onclose = (event) => {
console.debug("[useWebSocket] Closed connection:", event)
setWebSocket(undefined)
setWebSocketState(sock.readyState);
onclose?.({event, openWebSocketAfterBackoff, closeWebSocket});
dispatch({event: "stateChange"})
onclose?.({event, closeWebSocket});
}
sock.onerror = (event) => {
console.error("[useWebSocket] Error in connection:", event)
setWebSocketState(sock.readyState)
onerror?.({event, openWebSocketAfterBackoff, closeWebSocket});
dispatch({event: "stateChange"})
onerror?.({event, closeWebSocket});
}
sock.onmessage = (event) => {
console.debug("[useWebSocket] Received message:", event)
onmessage?.({event, openWebSocketAfterBackoff, closeWebSocket});
onmessage?.({event, closeWebSocket});
}
}, [url, onopen, onclose, onerror, onmessage, backOff, closeWebSocket])
}, [url, onopen, onclose, onerror, onmessage, closeWebSocket])
useEffect(() => {
if(webSocketBackoffMs === undefined) return;
console.debug("[useWebSocket] Backing off for:", webSocketBackoffMs, "ms")
new Promise(resolve => setTimeout(resolve, webSocketBackoffMs)).then(() => {
dispatch({event: "backoffExpire"})
})
}, [webSocketBackoffMs])
useEffect(() => {
if(!url) return;
if(webSocket !== undefined) return;
if(isBackingOff) return;
console.debug("[useWebSocket] Hook mounted, opening connection as soon as possible...")
const openWebSocketAfterBackoff = backOff(openWebSocket);
// noinspection JSIgnoredPromiseFromCall
openWebSocketAfterBackoff()
}, [url, webSocket, isBackingOff, webSocketState])
if(webSocketBackoffMs !== undefined) return;
console.debug("[useWebSocket] Back off expired, opening websocket...")
openWebSocket()
}, [webSocket, webSocketBackoffMs, openWebSocket])
return {webSocket, webSocketState, webSocketBackoffMs}
}