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

Fix websocket backoff mechanisms

This commit is contained in:
Steffo 2023-08-08 17:35:12 +02:00
parent 550b2e6c51
commit 5982352792
Signed by: steffo
GPG key ID: 2A24051445686895
10 changed files with 64 additions and 43 deletions

View file

@ -0,0 +1,26 @@
.rootMain {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.rootMain h2 {
margin-top: 0;
}
.rootMain > :global(.chapter-2) {
max-width: 976px;
}
.rootMain small {
font-style: italic;
}
.rootMain :global(.panel) {
max-width: 484px;
padding-left: 12px;
padding-right: 12px;
}

View file

@ -1,12 +1,12 @@
import {CreateBoardChapter} from "@/app/[lang]/(page)/CreateBoardChapter" import {CreateBoardChapter} from "@/app/[lang]/(page)/CreateBoardChapter"
import {ExistingBoardChapter} from "@/app/[lang]/(page)/ExistingBoardChapter" import {ExistingBoardChapter} from "@/app/[lang]/(page)/ExistingBoardChapter"
import style from "@/app/[lang]/page.module.css" import style from "./RootMain.module.css"
import {default as React} from "react" import {default as React} from "react"
export async function RootMain({lng}: {lng: string}) { export async function RootMain({lng}: {lng: string}) {
return ( return (
<main className={style.pageMain}> <main className={style.rootMain}>
<CreateBoardChapter lng={lng}/> <CreateBoardChapter lng={lng}/>
<ExistingBoardChapter lng={lng}/> <ExistingBoardChapter lng={lng}/>
</main> </main>

View file

@ -6,7 +6,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
export function BoardMain({className}: {className?: string}) { export function BoardMain({className}: {className?: string}) {
const {webSocketState} = useManagedBoard() const {webSocketState, webSocketBackoffMs} = useManagedBoard()
switch(webSocketState) { switch(webSocketState) {
case undefined: case undefined:
@ -18,6 +18,6 @@ export function BoardMain({className}: {className?: string}) {
case WebSocket.CLOSING: case WebSocket.CLOSING:
return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash} beatFade/>} text={"Disconnessione..."} className={className}/> return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash} beatFade/>} text={"Disconnessione..."} className={className}/>
case WebSocket.CLOSED: case WebSocket.CLOSED:
return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash}/>} text={"Disconnesso"} className={className}/> return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash}/>} text={`Disconnesso, riconnessione tra ${webSocketBackoffMs}ms`} className={className}/>
} }
} }

View file

@ -16,6 +16,7 @@ export interface UseBoardReturns {
tasksById: {[id: string]: Task}, tasksById: {[id: string]: Task},
taskGroups: TaskGroup[], taskGroups: TaskGroup[],
webSocketState: number | undefined, webSocketState: number | undefined,
webSocketBackoffMs: number,
isEditingTitle: boolean, isEditingTitle: boolean,
stopEditingTitle: () => void, stopEditingTitle: () => void,
startEditingTitle: () => void, startEditingTitle: () => void,
@ -38,7 +39,7 @@ export interface UseBoardReturns {
} }
export function useBoard(name: string): UseBoardReturns { export function useBoard(name: string): UseBoardReturns {
const {state: {title, tasksById}, sendAction, webSocketState} = useBoardWs(name); const {state: {title, tasksById}, sendAction, webSocketState, webSocketBackoffMs} = useBoardWs(name);
const {value: [taskGrouper, groupSorter, groupNamer], move: moveGrouper, next: nextGrouper, previous: previousGrouper} = useCycleState(TASK_GROUPERS); const {value: [taskGrouper, groupSorter, groupNamer], move: moveGrouper, next: nextGrouper, previous: previousGrouper} = useCycleState(TASK_GROUPERS);
const {value: taskSorter, move: moveSorter, next: nextSorter, previous: previousSorter} = useCycleState(TASK_SORTERS); const {value: taskSorter, move: moveSorter, next: nextSorter, previous: previousSorter} = useCycleState(TASK_SORTERS);
@ -56,6 +57,7 @@ export function useBoard(name: string): UseBoardReturns {
tasksById, tasksById,
taskGroups, taskGroups,
webSocketState, webSocketState,
webSocketBackoffMs,
isEditingTitle, isEditingTitle,
stopEditingTitle, stopEditingTitle,
startEditingTitle, startEditingTitle,

View file

@ -13,7 +13,7 @@ export function useBoardWs(name: string) {
const {state, act} = useBoardState(); const {state, act} = useBoardState();
const {webSocket, webSocketState} = useWs(wsFullURL, { const {webSocket, webSocketState, webSocketBackoffMs} = useWs(wsFullURL, {
onopen: useCallback(({}) => { onopen: useCallback(({}) => {
console.debug("[useBoardWs] Connected to board:", name); console.debug("[useBoardWs] Connected to board:", name);
act(null); act(null);
@ -27,9 +27,8 @@ export function useBoardWs(name: string) {
console.error("[useBoardWs] Encountered a WebSocket error, closing current connection:", event); console.error("[useBoardWs] Encountered a WebSocket error, closing current connection:", event);
closeWebSocket() closeWebSocket()
}, []), }, []),
onclose: useCallback(({event, openWebSocket}: WebSocketHandlerParams<Event>) => { onclose: useCallback(({event}: WebSocketHandlerParams<Event>) => {
console.debug("[useBoardWs] WebSocket was closed, trying to reconnect:", event); console.debug("[useBoardWs] WebSocket was closed:", event);
openWebSocket()
}, []) }, [])
}); });
@ -42,5 +41,5 @@ export function useBoardWs(name: string) {
webSocket.send(JSON.stringify(data)); webSocket.send(JSON.stringify(data));
}, [webSocket, webSocketState]) }, [webSocket, webSocketState])
return {state, sendAction, webSocketState} return {state, sendAction, webSocketState, webSocketBackoffMs}
} }

View file

@ -1,7 +1,7 @@
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
import "./layout.css"; import "./layout.css";
import {Body} from "@/app/[lang]/Body" import {Body} from "@/app/[lang]/(layout)/Body"
import {StarredManager} from "@/app/[lang]/StarContext" import {StarredManager} from "@/app/[lang]/StarContext"
import type {Metadata as NextMetadata} from "next" import type {Metadata as NextMetadata} from "next"
import {default as React, ReactNode} from "react" import {default as React, ReactNode} from "react"

View file

@ -9,11 +9,3 @@
padding: 8px; padding: 8px;
} }
.pageMain h2 {
margin-top: 0;
}
.pageMain div {
max-width: 960px;
}

View file

@ -4,7 +4,7 @@ import {useState, useCallback, useEffect} from "react"
export interface WebSocketHandlerParams<E extends Event> { export interface WebSocketHandlerParams<E extends Event> {
event: E, event: E,
openWebSocket: () => void, openWebSocketAfterBackoff: () => void,
closeWebSocket: () => void, closeWebSocket: () => void,
} }
@ -18,6 +18,7 @@ export interface WebSocketHandlers {
export function useWs(url: string | undefined, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) { export function useWs(url: string | undefined, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
const [webSocket, setWebSocket] = useState<WebSocket | undefined>(undefined) const [webSocket, setWebSocket] = useState<WebSocket | undefined>(undefined)
const [webSocketState, setWebSocketState] = useState<number | undefined>(undefined); const [webSocketState, setWebSocketState] = useState<number | undefined>(undefined);
const [isBackingOff, setBackingOff] = useState<boolean>(false);
const [webSocketBackoffMs, setWebSocketBackoffMs] = useState<number>(1); const [webSocketBackoffMs, setWebSocketBackoffMs] = useState<number>(1);
const closeWebSocket = useCallback(() => { const closeWebSocket = useCallback(() => {
@ -36,56 +37,57 @@ export function useWs(url: string | undefined, {onclose, onerror, onmessage, ono
setWebSocketState(WebSocket.CLOSED); setWebSocketState(WebSocket.CLOSED);
}, [webSocket]) }, [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(() => { const openWebSocket = useCallback(() => {
console.debug("[useWebSocket] Opening WebSocket:", url); console.debug("[useWebSocket] Opening WebSocket:", url);
if(url === undefined) { if(url === undefined) {
console.warn("[useWebSocket] Trying to open WebSocket, but no URL has been given; ignoring request...") console.warn("[useWebSocket] Trying to open WebSocket, but no URL has been given; ignoring request...")
return; 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 setWebSocketState(WebSocket.CONNECTING); // Workaround for constructor blocking and giving no feedback to the user
const sock = new WebSocket(url); const sock = new WebSocket(url);
const openWebSocketAfterBackoff = backOff(openWebSocket);
sock.onopen = (event) => { sock.onopen = (event) => {
console.debug("[useWebSocket] Opened connection:", event) console.debug("[useWebSocket] Opened connection:", event)
setWebSocket(sock) setWebSocket(sock)
setWebSocketState(sock.readyState) setWebSocketState(sock.readyState)
setWebSocketBackoffMs(1) onopen?.({event, openWebSocketAfterBackoff, closeWebSocket});
onopen?.({event, openWebSocket, closeWebSocket});
} }
sock.onclose = (event) => { sock.onclose = (event) => {
console.debug("[useWebSocket] Closed connection:", event) console.debug("[useWebSocket] Closed connection:", event)
setWebSocket(undefined) setWebSocket(undefined)
setWebSocketState(sock.readyState); setWebSocketState(sock.readyState);
onclose?.({event, openWebSocket, closeWebSocket}); onclose?.({event, openWebSocketAfterBackoff, closeWebSocket});
} }
sock.onerror = (event) => { sock.onerror = (event) => {
console.error("[useWebSocket] Error in connection:", event) console.error("[useWebSocket] Error in connection:", event)
setWebSocketState(sock.readyState) setWebSocketState(sock.readyState)
setWebSocketBackoffMs(prev => prev * 2) onerror?.({event, openWebSocketAfterBackoff, closeWebSocket});
onerror?.({event, openWebSocket, closeWebSocket});
} }
sock.onmessage = (event) => { sock.onmessage = (event) => {
console.debug("[useWebSocket] Received message:", event) console.debug("[useWebSocket] Received message:", event)
onmessage?.({event, openWebSocket, closeWebSocket}); onmessage?.({event, openWebSocketAfterBackoff, closeWebSocket});
} }
}, [url, onopen, onclose, onerror, onmessage]) }, [url, onopen, onclose, onerror, onmessage, backOff, closeWebSocket])
// @ts-ignore
const openWebSocketAfterBackoff = useCallback(() => {
console.debug("[useWebSocket] Backing off for:", webSocketBackoffMs, "ms")
return setTimeout(openWebSocket, webSocketBackoffMs)
}, [openWebSocket, webSocketBackoffMs])
useEffect(() => { useEffect(() => {
if(!url) { if(!url) return;
return; if(webSocket !== undefined) return;
} if(isBackingOff) return;
console.debug("[useWebSocket] Hook mounted, opening connection as soon as possible...") console.debug("[useWebSocket] Hook mounted, opening connection as soon as possible...")
// @ts-ignore const openWebSocketAfterBackoff = backOff(openWebSocket);
const handle = openWebSocketAfterBackoff() // noinspection JSIgnoredPromiseFromCall
return () => { openWebSocketAfterBackoff()
clearTimeout(handle) }, [url, isBackingOff, webSocketState])
}
}, [url, openWebSocketAfterBackoff])
return {webSocket, webSocketState, openWebSocket, closeWebSocket} return {webSocket, webSocketState, webSocketBackoffMs}
} }