From 598235279294d759b0b5ff9dd9da03de21d8dd63 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 8 Aug 2023 17:35:12 +0200 Subject: [PATCH] Fix websocket backoff mechanisms --- .../app/[lang]/{ => (layout)}/Body.module.css | 0 .../src/app/[lang]/{ => (layout)}/Body.tsx | 0 .../src/app/[lang]/(page)/RootMain.module.css | 26 ++++++++++ todoblue/src/app/[lang]/(page)/RootMain.tsx | 4 +- .../app/[lang]/board/[board]/BoardMain.tsx | 4 +- .../src/app/[lang]/board/[board]/useBoard.tsx | 4 +- .../app/[lang]/board/[board]/useBoardWs.ts | 9 ++-- todoblue/src/app/[lang]/layout.tsx | 2 +- todoblue/src/app/[lang]/page.module.css | 8 --- todoblue/src/app/[lang]/useWs.ts | 50 ++++++++++--------- 10 files changed, 64 insertions(+), 43 deletions(-) rename todoblue/src/app/[lang]/{ => (layout)}/Body.module.css (100%) rename todoblue/src/app/[lang]/{ => (layout)}/Body.tsx (100%) create mode 100644 todoblue/src/app/[lang]/(page)/RootMain.module.css diff --git a/todoblue/src/app/[lang]/Body.module.css b/todoblue/src/app/[lang]/(layout)/Body.module.css similarity index 100% rename from todoblue/src/app/[lang]/Body.module.css rename to todoblue/src/app/[lang]/(layout)/Body.module.css diff --git a/todoblue/src/app/[lang]/Body.tsx b/todoblue/src/app/[lang]/(layout)/Body.tsx similarity index 100% rename from todoblue/src/app/[lang]/Body.tsx rename to todoblue/src/app/[lang]/(layout)/Body.tsx diff --git a/todoblue/src/app/[lang]/(page)/RootMain.module.css b/todoblue/src/app/[lang]/(page)/RootMain.module.css new file mode 100644 index 0000000..e9058b3 --- /dev/null +++ b/todoblue/src/app/[lang]/(page)/RootMain.module.css @@ -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; +} diff --git a/todoblue/src/app/[lang]/(page)/RootMain.tsx b/todoblue/src/app/[lang]/(page)/RootMain.tsx index be7845e..2a95a09 100644 --- a/todoblue/src/app/[lang]/(page)/RootMain.tsx +++ b/todoblue/src/app/[lang]/(page)/RootMain.tsx @@ -1,12 +1,12 @@ import {CreateBoardChapter} from "@/app/[lang]/(page)/CreateBoardChapter" 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" export async function RootMain({lng}: {lng: string}) { return ( -
+
diff --git a/todoblue/src/app/[lang]/board/[board]/BoardMain.tsx b/todoblue/src/app/[lang]/board/[board]/BoardMain.tsx index f2a6a9e..0f93381 100644 --- a/todoblue/src/app/[lang]/board/[board]/BoardMain.tsx +++ b/todoblue/src/app/[lang]/board/[board]/BoardMain.tsx @@ -6,7 +6,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" export function BoardMain({className}: {className?: string}) { - const {webSocketState} = useManagedBoard() + const {webSocketState, webSocketBackoffMs} = useManagedBoard() switch(webSocketState) { case undefined: @@ -18,6 +18,6 @@ export function BoardMain({className}: {className?: string}) { case WebSocket.CLOSING: return } text={"Disconnessione..."} className={className}/> case WebSocket.CLOSED: - return } text={"Disconnesso"} className={className}/> + return } text={`Disconnesso, riconnessione tra ${webSocketBackoffMs}ms`} className={className}/> } } diff --git a/todoblue/src/app/[lang]/board/[board]/useBoard.tsx b/todoblue/src/app/[lang]/board/[board]/useBoard.tsx index 480b93e..12c4751 100644 --- a/todoblue/src/app/[lang]/board/[board]/useBoard.tsx +++ b/todoblue/src/app/[lang]/board/[board]/useBoard.tsx @@ -16,6 +16,7 @@ export interface UseBoardReturns { tasksById: {[id: string]: Task}, taskGroups: TaskGroup[], webSocketState: number | undefined, + webSocketBackoffMs: number, isEditingTitle: boolean, stopEditingTitle: () => void, startEditingTitle: () => void, @@ -38,7 +39,7 @@ export interface 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: taskSorter, move: moveSorter, next: nextSorter, previous: previousSorter} = useCycleState(TASK_SORTERS); @@ -56,6 +57,7 @@ export function useBoard(name: string): UseBoardReturns { tasksById, taskGroups, webSocketState, + webSocketBackoffMs, isEditingTitle, stopEditingTitle, startEditingTitle, diff --git a/todoblue/src/app/[lang]/board/[board]/useBoardWs.ts b/todoblue/src/app/[lang]/board/[board]/useBoardWs.ts index 0cc59a9..128e16e 100644 --- a/todoblue/src/app/[lang]/board/[board]/useBoardWs.ts +++ b/todoblue/src/app/[lang]/board/[board]/useBoardWs.ts @@ -13,7 +13,7 @@ export function useBoardWs(name: string) { const {state, act} = useBoardState(); - const {webSocket, webSocketState} = useWs(wsFullURL, { + const {webSocket, webSocketState, webSocketBackoffMs} = useWs(wsFullURL, { onopen: useCallback(({}) => { console.debug("[useBoardWs] Connected to board:", name); act(null); @@ -27,9 +27,8 @@ export function useBoardWs(name: string) { console.error("[useBoardWs] Encountered a WebSocket error, closing current connection:", event); closeWebSocket() }, []), - onclose: useCallback(({event, openWebSocket}: WebSocketHandlerParams) => { - console.debug("[useBoardWs] WebSocket was closed, trying to reconnect:", event); - openWebSocket() + onclose: useCallback(({event}: WebSocketHandlerParams) => { + console.debug("[useBoardWs] WebSocket was closed:", event); }, []) }); @@ -42,5 +41,5 @@ export function useBoardWs(name: string) { webSocket.send(JSON.stringify(data)); }, [webSocket, webSocketState]) - return {state, sendAction, webSocketState} + return {state, sendAction, webSocketState, webSocketBackoffMs} } diff --git a/todoblue/src/app/[lang]/layout.tsx b/todoblue/src/app/[lang]/layout.tsx index 30d0bc6..6d2ef67 100644 --- a/todoblue/src/app/[lang]/layout.tsx +++ b/todoblue/src/app/[lang]/layout.tsx @@ -1,7 +1,7 @@ // noinspection JSUnusedGlobalSymbols import "./layout.css"; -import {Body} from "@/app/[lang]/Body" +import {Body} from "@/app/[lang]/(layout)/Body" import {StarredManager} from "@/app/[lang]/StarContext" import type {Metadata as NextMetadata} from "next" import {default as React, ReactNode} from "react" diff --git a/todoblue/src/app/[lang]/page.module.css b/todoblue/src/app/[lang]/page.module.css index d95d4e4..4394803 100644 --- a/todoblue/src/app/[lang]/page.module.css +++ b/todoblue/src/app/[lang]/page.module.css @@ -9,11 +9,3 @@ padding: 8px; } - -.pageMain h2 { - margin-top: 0; -} - -.pageMain div { - max-width: 960px; -} diff --git a/todoblue/src/app/[lang]/useWs.ts b/todoblue/src/app/[lang]/useWs.ts index 8942f7b..fc7dc06 100644 --- a/todoblue/src/app/[lang]/useWs.ts +++ b/todoblue/src/app/[lang]/useWs.ts @@ -4,7 +4,7 @@ import {useState, useCallback, useEffect} from "react" export interface WebSocketHandlerParams { event: E, - openWebSocket: () => void, + openWebSocketAfterBackoff: () => void, closeWebSocket: () => void, } @@ -18,6 +18,7 @@ export interface WebSocketHandlers { export function useWs(url: string | undefined, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) { const [webSocket, setWebSocket] = useState(undefined) const [webSocketState, setWebSocketState] = useState(undefined); + const [isBackingOff, setBackingOff] = useState(false); const [webSocketBackoffMs, setWebSocketBackoffMs] = useState(1); const closeWebSocket = useCallback(() => { @@ -36,56 +37,57 @@ export function useWs(url: string | undefined, {onclose, onerror, onmessage, ono setWebSocketState(WebSocket.CLOSED); }, [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); sock.onopen = (event) => { console.debug("[useWebSocket] Opened connection:", event) setWebSocket(sock) setWebSocketState(sock.readyState) - setWebSocketBackoffMs(1) - onopen?.({event, openWebSocket, closeWebSocket}); + onopen?.({event, openWebSocketAfterBackoff, closeWebSocket}); } sock.onclose = (event) => { console.debug("[useWebSocket] Closed connection:", event) setWebSocket(undefined) setWebSocketState(sock.readyState); - onclose?.({event, openWebSocket, closeWebSocket}); + onclose?.({event, openWebSocketAfterBackoff, closeWebSocket}); } sock.onerror = (event) => { console.error("[useWebSocket] Error in connection:", event) setWebSocketState(sock.readyState) - setWebSocketBackoffMs(prev => prev * 2) - onerror?.({event, openWebSocket, closeWebSocket}); + onerror?.({event, openWebSocketAfterBackoff, closeWebSocket}); } sock.onmessage = (event) => { console.debug("[useWebSocket] Received message:", event) - onmessage?.({event, openWebSocket, closeWebSocket}); + onmessage?.({event, openWebSocketAfterBackoff, closeWebSocket}); } - }, [url, onopen, onclose, onerror, onmessage]) - - // @ts-ignore - const openWebSocketAfterBackoff = useCallback(() => { - console.debug("[useWebSocket] Backing off for:", webSocketBackoffMs, "ms") - return setTimeout(openWebSocket, webSocketBackoffMs) - }, [openWebSocket, webSocketBackoffMs]) + }, [url, onopen, onclose, onerror, onmessage, backOff, closeWebSocket]) useEffect(() => { - if(!url) { - return; - } + if(!url) return; + if(webSocket !== undefined) return; + if(isBackingOff) return; console.debug("[useWebSocket] Hook mounted, opening connection as soon as possible...") - // @ts-ignore - const handle = openWebSocketAfterBackoff() - return () => { - clearTimeout(handle) - } - }, [url, openWebSocketAfterBackoff]) + const openWebSocketAfterBackoff = backOff(openWebSocket); + // noinspection JSIgnoredPromiseFromCall + openWebSocketAfterBackoff() + }, [url, isBackingOff, webSocketState]) - return {webSocket, webSocketState, openWebSocket, closeWebSocket} + return {webSocket, webSocketState, webSocketBackoffMs} }