mirror of
https://github.com/Steffo99/todocolors.git
synced 2024-11-22 16:24:19 +00:00
Implement reconnection mechanism and other good things
This commit is contained in:
parent
ee80f373ca
commit
b0eac94b18
15 changed files with 223 additions and 142 deletions
|
@ -8,9 +8,9 @@
|
||||||
<arguments value="--port=8081" />
|
<arguments value="--port=8081" />
|
||||||
<node-interpreter value="project" />
|
<node-interpreter value="project" />
|
||||||
<envs>
|
<envs>
|
||||||
<env name="TODOBLUE_SITE_NAME" value="Tododev" />
|
<env name="NEXT_PUBLIC_TODOBLUE_OVERRIDE_BASE_URL" value="http://localhost:8080" />
|
||||||
<env name="TODORED_BASE_URL" value="ws://192.168.1.135:8080" />
|
|
||||||
<env name="TODOBLUE_SITE_DESCRIPTION" value="Development version of Todoblue" />
|
<env name="TODOBLUE_SITE_DESCRIPTION" value="Development version of Todoblue" />
|
||||||
|
<env name="TODOBLUE_SITE_NAME" value="Tododeve" />
|
||||||
</envs>
|
</envs>
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
@ -10,7 +10,7 @@ export function BoardColumn({taskGroup}: {taskGroup: TaskGroup}) {
|
||||||
{taskGroup.name}
|
{taskGroup.name}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
{taskGroup.tasks.map(task => <TaskDisplay task={task}/>)}
|
{taskGroup.tasks.map(task => <TaskDisplay task={task} key={task.id}/>)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,7 +36,11 @@ function TitleArea({children}: {children: ReactNode}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TitleInput() {
|
function TitleInput() {
|
||||||
const {editTitle, setEditTitle, stopEditingTitle} = useManagedBoard()
|
const {editTitle, setEditTitle, stopEditingTitle, webSocketState} = useManagedBoard()
|
||||||
|
|
||||||
|
if(webSocketState !== WebSocket.OPEN) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={stopEditingTitle}>
|
<form onSubmit={stopEditingTitle}>
|
||||||
|
@ -52,7 +56,11 @@ function TitleInput() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function TitleDisplay() {
|
function TitleDisplay() {
|
||||||
const {title} = useManagedBoard()
|
const {title, webSocketState} = useManagedBoard()
|
||||||
|
|
||||||
|
if(webSocketState !== WebSocket.OPEN) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -85,9 +93,9 @@ function HomeButton() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditTitleButton() {
|
function EditTitleButton() {
|
||||||
const {websocketState, toggleEditingTitle} = useManagedBoard()
|
const {webSocketState, toggleEditingTitle} = useManagedBoard()
|
||||||
|
|
||||||
if(websocketState != WebSocket.OPEN) {
|
if(webSocketState != WebSocket.OPEN) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,9 +115,9 @@ function RightButtonsArea({children}: {children: ReactNode}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CycleGroupButton() {
|
function CycleGroupButton() {
|
||||||
const {websocketState, nextGrouper} = useManagedBoard()
|
const {webSocketState, nextGrouper} = useManagedBoard()
|
||||||
|
|
||||||
if(websocketState != WebSocket.OPEN) {
|
if(webSocketState != WebSocket.OPEN) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,9 +129,9 @@ function CycleGroupButton() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function CycleSortButton() {
|
function CycleSortButton() {
|
||||||
const {websocketState, nextSorter} = useManagedBoard()
|
const {webSocketState, nextSorter} = useManagedBoard()
|
||||||
|
|
||||||
if(websocketState != WebSocket.OPEN) {
|
if(webSocketState != WebSocket.OPEN) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import {BoardMainIcon} from "@/app/board/[board]/BoardMainIcon"
|
import {BoardMainIcon} from "@/app/board/[board]/BoardMainIcon"
|
||||||
import {BoardMainTaskGroups} from "@/app/board/[board]/BoardMainTaskGroups"
|
import {BoardMainTaskGroups} from "@/app/board/[board]/BoardMainTaskGroups"
|
||||||
import {useManagedBoard} from "@/app/board/[board]/BoardManager"
|
import {useManagedBoard} from "@/app/board/[board]/BoardManager"
|
||||||
import {faArrowsSpin, faExclamationCircle, faGear} from "@fortawesome/free-solid-svg-icons"
|
import {faLink, faLinkSlash, faGear} from "@fortawesome/free-solid-svg-icons"
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||||
import cn from "classnames"
|
|
||||||
|
|
||||||
|
|
||||||
export function BoardMain({className}: {className?: string}) {
|
export function BoardMain({className}: {className?: string}) {
|
||||||
const {websocketState} = useManagedBoard()
|
const {webSocketState} = useManagedBoard()
|
||||||
|
|
||||||
switch(websocketState) {
|
switch(webSocketState) {
|
||||||
case undefined:
|
case undefined:
|
||||||
return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faGear} spin/>} text={"Caricamento..."} className={className}/>
|
return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faGear} beatFade/>} text={"Caricamento..."} className={className}/>
|
||||||
case WebSocket.CONNECTING:
|
case WebSocket.CONNECTING:
|
||||||
return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faArrowsSpin} spin/>} text={"Connessione..."} className={className}/>
|
return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faLink} beatFade/>} text={"Connessione..."} className={className}/>
|
||||||
case WebSocket.OPEN:
|
case WebSocket.OPEN:
|
||||||
return <BoardMainTaskGroups className={className}/>
|
return <BoardMainTaskGroups className={className}/>
|
||||||
case WebSocket.CLOSING:
|
case WebSocket.CLOSING:
|
||||||
|
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={faExclamationCircle}/>} text={"Errore"} className={cn("red", className)}/>
|
return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash}/>} text={"Disconnesso"} className={className}/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export function BoardMainTaskGroups({className}: {className?: string}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={cn(style.boardMainTaskGroups, className)}>
|
<main className={cn(style.boardMainTaskGroups, className)}>
|
||||||
{taskGroups.map((tg) => <BoardColumn taskGroup={tg}/>)}
|
{taskGroups.map((tg) => <BoardColumn taskGroup={tg} key={tg.key}/>)}
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {faAdd} from "@fortawesome/free-solid-svg-icons"
|
||||||
|
|
||||||
|
|
||||||
export function BoardTaskEditor({className}: {className?: string}) {
|
export function BoardTaskEditor({className}: {className?: string}) {
|
||||||
const {editedTask, send, setEditedTaskText, websocketState} = useManagedBoard()
|
const {editedTask, send, setEditedTaskText, webSocketState} = useManagedBoard()
|
||||||
|
|
||||||
const submitTask = useCallback((e: FormEvent) => {
|
const submitTask = useCallback((e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -15,7 +15,7 @@ export function BoardTaskEditor({className}: {className?: string}) {
|
||||||
setEditedTaskText("")
|
setEditedTaskText("")
|
||||||
}, [send, editedTask])
|
}, [send, editedTask])
|
||||||
|
|
||||||
if(websocketState != WebSocket.OPEN) {
|
if(webSocketState != WebSocket.OPEN) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,25 +55,28 @@ export function TaskDisplay({task}: {task: TaskWithId}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn({
|
<div
|
||||||
"panel": true,
|
className={cn({
|
||||||
[style.taskDiv]: true,
|
"panel": true,
|
||||||
[style.taskDivFront]: !isDisplayingActions,
|
[style.taskDiv]: true,
|
||||||
[style.taskDivBack]: isDisplayingActions,
|
[style.taskDivFront]: !isDisplayingActions,
|
||||||
[style.taskPriorityHighest]: task.priority === "Highest",
|
[style.taskDivBack]: isDisplayingActions,
|
||||||
[style.taskPriorityHigh]: task.priority === "High",
|
[style.taskPriorityHighest]: task.priority === "Highest",
|
||||||
[style.taskPriorityNormal]: task.priority === "Normal",
|
[style.taskPriorityHigh]: task.priority === "High",
|
||||||
[style.taskPriorityLow]: task.priority === "Low",
|
[style.taskPriorityNormal]: task.priority === "Normal",
|
||||||
[style.taskPriorityLowest]: task.priority === "Lowest",
|
[style.taskPriorityLow]: task.priority === "Low",
|
||||||
[style.taskImportanceHighest]: task.importance === "Highest",
|
[style.taskPriorityLowest]: task.priority === "Lowest",
|
||||||
[style.taskImportanceHigh]: task.importance === "High",
|
[style.taskImportanceHighest]: task.importance === "Highest",
|
||||||
[style.taskImportanceNormal]: task.importance === "Normal",
|
[style.taskImportanceHigh]: task.importance === "High",
|
||||||
[style.taskImportanceLow]: task.importance === "Low",
|
[style.taskImportanceNormal]: task.importance === "Normal",
|
||||||
[style.taskImportanceLowest]: task.importance === "Lowest",
|
[style.taskImportanceLow]: task.importance === "Low",
|
||||||
[style.taskStatusUnfinished]: task.status === "Unfinished",
|
[style.taskImportanceLowest]: task.importance === "Lowest",
|
||||||
[style.taskStatusInProgress]: task.status === "InProgress",
|
[style.taskStatusUnfinished]: task.status === "Unfinished",
|
||||||
[style.taskStatusComplete]: task.status === "Complete",
|
[style.taskStatusInProgress]: task.status === "InProgress",
|
||||||
})} onClick={() => setDisplayingActions(!isDisplayingActions)}>
|
[style.taskStatusComplete]: task.status === "Complete",
|
||||||
|
})}
|
||||||
|
onClick={() => setDisplayingActions(!isDisplayingActions)}
|
||||||
|
>
|
||||||
{contents}
|
{contents}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {TASK_GROUPERS} from "@/app/board/[board]/doTaskGrouping"
|
||||||
import {TASK_SORTERS} from "@/app/board/[board]/doTaskSorting"
|
import {TASK_SORTERS} from "@/app/board/[board]/doTaskSorting"
|
||||||
import {BoardAction, Task} from "@/app/board/[board]/Types"
|
import {BoardAction, Task} from "@/app/board/[board]/Types"
|
||||||
import {useBoardTaskEditor} from "@/app/board/[board]/useBoardTaskEditor"
|
import {useBoardTaskEditor} from "@/app/board/[board]/useBoardTaskEditor"
|
||||||
import {useBoardWebSocket} from "@/app/board/[board]/useBoardWebSocket"
|
import {useBoardWs} from "@/app/board/[board]/useBoardWs"
|
||||||
import {TaskGroup, useBoardTaskArranger} from "@/app/board/[board]/useBoardTaskArranger"
|
import {TaskGroup, useBoardTaskArranger} from "@/app/board/[board]/useBoardTaskArranger"
|
||||||
import {useBoardTitleEditor} from "@/app/board/[board]/useBoardTitleEditor"
|
import {useBoardTitleEditor} from "@/app/board/[board]/useBoardTitleEditor"
|
||||||
import {useCycleState} from "@/app/useCycleState"
|
import {useCycleState} from "@/app/useCycleState"
|
||||||
|
@ -14,7 +14,7 @@ export interface UseBoardReturns {
|
||||||
title: string,
|
title: string,
|
||||||
tasksById: {[id: string]: Task},
|
tasksById: {[id: string]: Task},
|
||||||
taskGroups: TaskGroup[],
|
taskGroups: TaskGroup[],
|
||||||
websocketState: number,
|
webSocketState: number | undefined,
|
||||||
isEditingTitle: boolean,
|
isEditingTitle: boolean,
|
||||||
stopEditingTitle: () => void,
|
stopEditingTitle: () => void,
|
||||||
startEditingTitle: () => void,
|
startEditingTitle: () => void,
|
||||||
|
@ -35,13 +35,13 @@ export interface UseBoardReturns {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBoard(name: string): UseBoardReturns {
|
export function useBoard(name: string): UseBoardReturns {
|
||||||
const {state: {title, tasksById}, send, websocketState} = useBoardWebSocket(name);
|
const {state: {title, tasksById}, sendAction, webSocketState} = 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);
|
||||||
|
|
||||||
const {taskGroups} = useBoardTaskArranger(tasksById, taskGrouper, groupSorter, groupNamer, taskSorter);
|
const {taskGroups} = useBoardTaskArranger(tasksById, taskGrouper, groupSorter, groupNamer, taskSorter);
|
||||||
const {isEditingTitle, stopEditingTitle, startEditingTitle, toggleEditingTitle, editTitle, setEditTitle} = useBoardTitleEditor(title, send);
|
const {isEditingTitle, stopEditingTitle, startEditingTitle, toggleEditingTitle, editTitle, setEditTitle} = useBoardTitleEditor(title, sendAction);
|
||||||
|
|
||||||
const {editedTaskText, setEditedTaskText, editedTask, setEditedTask} = useBoardTaskEditor()
|
const {editedTaskText, setEditedTaskText, editedTask, setEditedTask} = useBoardTaskEditor()
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ export function useBoard(name: string): UseBoardReturns {
|
||||||
title,
|
title,
|
||||||
tasksById,
|
tasksById,
|
||||||
taskGroups,
|
taskGroups,
|
||||||
websocketState,
|
webSocketState,
|
||||||
isEditingTitle,
|
isEditingTitle,
|
||||||
stopEditingTitle,
|
stopEditingTitle,
|
||||||
startEditingTitle,
|
startEditingTitle,
|
||||||
|
@ -62,7 +62,7 @@ export function useBoard(name: string): UseBoardReturns {
|
||||||
previousSorter,
|
previousSorter,
|
||||||
editTitle,
|
editTitle,
|
||||||
setEditTitle,
|
setEditTitle,
|
||||||
send,
|
send: sendAction,
|
||||||
editedTaskText,
|
editedTaskText,
|
||||||
setEditedTaskText,
|
setEditedTaskText,
|
||||||
editedTask,
|
editedTask,
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import {BoardAction} from "@/app/board/[board]/Types"
|
|
||||||
import {useBoardState} from "@/app/board/[board]/useBoardState"
|
|
||||||
import {useBoardWebSocketURL} from "@/app/board/[board]/useBoardWebSocketURL"
|
|
||||||
import {useCallback} from "react"
|
|
||||||
import {useWebSocket} from "@/app/useWebSocket"
|
|
||||||
|
|
||||||
|
|
||||||
export function useBoardWebSocket(name: string) {
|
|
||||||
const {webSocketURL} = useBoardWebSocketURL(name)
|
|
||||||
const {state, act} = useBoardState();
|
|
||||||
|
|
||||||
const {websocket, websocketState} = useWebSocket(webSocketURL, {
|
|
||||||
onopen: useCallback((_sock: WebSocket, _event: Event) => {
|
|
||||||
console.debug("[useBoard] Connected to board:", name);
|
|
||||||
act(null);
|
|
||||||
}, []),
|
|
||||||
onmessage: useCallback((_sock: WebSocket, event: MessageEvent) => {
|
|
||||||
const action: BoardAction = JSON.parse(event.data);
|
|
||||||
console.debug("[useBoard] Received:", action);
|
|
||||||
act(action)
|
|
||||||
}, []),
|
|
||||||
});
|
|
||||||
|
|
||||||
const send = useCallback((data: BoardAction) => {
|
|
||||||
if(!websocket || websocketState !== WebSocket.OPEN) {
|
|
||||||
console.warn("[useBoardWebSocket] Webbsocket is not yet ready, cannot send:", data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.debug("[useBoardWebSocket] Sending:", data);
|
|
||||||
websocket.send(JSON.stringify(data));
|
|
||||||
}, [websocket, websocketState])
|
|
||||||
|
|
||||||
return {state, send, websocketState}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
"use client";
|
|
||||||
import {useMemo} from "react"
|
|
||||||
|
|
||||||
|
|
||||||
const HTTP_TO_WS = {
|
|
||||||
"http:": "ws:",
|
|
||||||
"https:": "wss:",
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBoardWebSocketURL(name: string) {
|
|
||||||
// @ts-ignore
|
|
||||||
const protocol = HTTP_TO_WS[window.location.protocol]
|
|
||||||
const host = window.location.host
|
|
||||||
|
|
||||||
const webSocketURL = useMemo(() => `${protocol}//${host}/api/board/${name}/ws`, [name]);
|
|
||||||
return {webSocketURL}
|
|
||||||
}
|
|
46
todoblue/src/app/board/[board]/useBoardWs.ts
Normal file
46
todoblue/src/app/board/[board]/useBoardWs.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import {BoardAction} from "@/app/board/[board]/Types"
|
||||||
|
import {useBoardState} from "@/app/board/[board]/useBoardState"
|
||||||
|
import {useWsBaseURL} from "@/app/useWsBaseURL"
|
||||||
|
import {useCallback, useMemo} from "react"
|
||||||
|
import {useWs, WebSocketHandlerParams} from "@/app/useWs"
|
||||||
|
|
||||||
|
|
||||||
|
export function useBoardWs(name: string) {
|
||||||
|
const wsBaseURL = useWsBaseURL()
|
||||||
|
const wsFullURL = useMemo(() => wsBaseURL ? `${wsBaseURL}/board/${name}/ws` : undefined, [wsBaseURL, name])
|
||||||
|
|
||||||
|
const {state, act} = useBoardState();
|
||||||
|
|
||||||
|
const {webSocket, webSocketState} = useWs(wsFullURL, {
|
||||||
|
onopen: useCallback(({}) => {
|
||||||
|
console.debug("[useBoardWs] Connected to board:", name);
|
||||||
|
act(null);
|
||||||
|
}, [act]),
|
||||||
|
onmessage: useCallback(({event}: WebSocketHandlerParams<MessageEvent>) => {
|
||||||
|
const action: BoardAction = JSON.parse(event.data);
|
||||||
|
console.debug("[useBoardWs] Parsed payload as:", action);
|
||||||
|
act(action)
|
||||||
|
}, [act]),
|
||||||
|
onerror: useCallback(({event, closeWebSocket}: WebSocketHandlerParams<Event>) => {
|
||||||
|
console.error("[useBoardWs] Encountered a WebSocket error, closing current connection:", event);
|
||||||
|
closeWebSocket()
|
||||||
|
}, []),
|
||||||
|
onclose: useCallback(({event, openWebSocket}: WebSocketHandlerParams<Event>) => {
|
||||||
|
console.debug("[useBoardWs] WebSocket was closed, trying to reconnect:", event);
|
||||||
|
openWebSocket()
|
||||||
|
}, [])
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendAction = useCallback((data: BoardAction) => {
|
||||||
|
if(!webSocket || webSocketState !== WebSocket.OPEN) {
|
||||||
|
console.warn("[useBoardWs] WebSocket is not yet ready, cannot send:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.debug("[useBoardWs] Sending:", data);
|
||||||
|
webSocket.send(JSON.stringify(data));
|
||||||
|
}, [webSocket, webSocketState])
|
||||||
|
|
||||||
|
return {state, sendAction, webSocketState}
|
||||||
|
}
|
20
todoblue/src/app/useHttpBaseURL.ts
Normal file
20
todoblue/src/app/useHttpBaseURL.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import {useEffect, useState} from "react"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **Hook** returning the base URL to use in API calls.
|
||||||
|
*
|
||||||
|
* Obtains the base URL from `window.location`, unless the `NEXT_PUBLIC_TODOBLUE_OVERRIDE_BASE_URL` environment variable is set, which takes precedence in that case.
|
||||||
|
*/
|
||||||
|
export function useHttpBaseURL(): string | undefined {
|
||||||
|
const [baseURL, setBaseURL] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let url = process.env.NEXT_PUBLIC_TODOBLUE_OVERRIDE_BASE_URL;
|
||||||
|
if(!url) url = `${window.location.protocol}//${window.location.host}`;
|
||||||
|
console.debug("[useBaseURL] Using base URL:", url);
|
||||||
|
setBaseURL(url);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return baseURL;
|
||||||
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import {useState, useEffect, useCallback} from "react"
|
|
||||||
|
|
||||||
export interface WebSocketHandlers {
|
|
||||||
onclose?: (sock: WebSocket, event: CloseEvent) => void,
|
|
||||||
onerror?: (sock: WebSocket, event: Event) => void,
|
|
||||||
onmessage?: (sock: WebSocket, event: MessageEvent) => void,
|
|
||||||
onopen?: (sock: WebSocket, event: Event) => void,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWebSocket(url: string, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
|
|
||||||
const [websocket, setWebsocket] = useState<WebSocket | null>(null)
|
|
||||||
const [websocketState, setWebsocketState] = useState<number>(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.debug("[useWebSocket] Creating websocket...");
|
|
||||||
const sock = new WebSocket(url);
|
|
||||||
setWebsocket(sock);
|
|
||||||
sock.onopen = (ev) => {
|
|
||||||
setWebsocketState(sock.readyState);
|
|
||||||
onopen?.(sock, ev);
|
|
||||||
}
|
|
||||||
sock.onclose = (ev) => {
|
|
||||||
setWebsocketState(sock.readyState);
|
|
||||||
onclose?.(sock, ev);
|
|
||||||
}
|
|
||||||
sock.onerror = (ev) => {
|
|
||||||
setWebsocketState(sock.readyState);
|
|
||||||
onerror?.(sock, ev);
|
|
||||||
}
|
|
||||||
sock.onmessage = (ev) => {
|
|
||||||
onmessage?.(sock, ev);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
console.debug("[useWebSocket] Closing websocket...");
|
|
||||||
sock.close();
|
|
||||||
setWebsocket(null);
|
|
||||||
}
|
|
||||||
}, [url, onclose, onerror, onmessage, onopen])
|
|
||||||
|
|
||||||
return {websocket, websocketState}
|
|
||||||
}
|
|
91
todoblue/src/app/useWs.ts
Normal file
91
todoblue/src/app/useWs.ts
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import {useState, useCallback, useEffect} from "react"
|
||||||
|
|
||||||
|
export interface WebSocketHandlerParams<E extends Event> {
|
||||||
|
event: E,
|
||||||
|
openWebSocket: () => void,
|
||||||
|
closeWebSocket: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebSocketHandlers {
|
||||||
|
onclose?: (params: WebSocketHandlerParams<CloseEvent>) => void,
|
||||||
|
onerror?: (params: WebSocketHandlerParams<Event>) => void,
|
||||||
|
onmessage?: (params: WebSocketHandlerParams<MessageEvent>) => void,
|
||||||
|
onopen?: (params: WebSocketHandlerParams<Event>) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [webSocketBackoffMs, setWebSocketBackoffMs] = useState<number>(1);
|
||||||
|
|
||||||
|
const closeWebSocket = useCallback(() => {
|
||||||
|
console.debug("[useWebSocket] Closing WebSocket:", webSocket);
|
||||||
|
if(webSocket === undefined) {
|
||||||
|
console.warn("[useWebSocket] Trying to close WebSocket, but no WebSocket is open; ignoring request...")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
webSocket.close();
|
||||||
|
}
|
||||||
|
catch(closeErr) {
|
||||||
|
console.debug("[useWebSocket] Failed to close the websocket (it might be already closed):", closeErr)
|
||||||
|
}
|
||||||
|
setWebSocket(undefined);
|
||||||
|
setWebSocketState(WebSocket.CLOSED);
|
||||||
|
}, [webSocket])
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
setWebSocketState(WebSocket.CONNECTING); // Workaround for constructor blocking and giving no feedback to the user
|
||||||
|
const sock = new WebSocket(url);
|
||||||
|
sock.onopen = (event) => {
|
||||||
|
console.debug("[useWebSocket] Opened connection:", event)
|
||||||
|
setWebSocket(sock)
|
||||||
|
setWebSocketState(sock.readyState)
|
||||||
|
setWebSocketBackoffMs(1)
|
||||||
|
onopen?.({event, openWebSocket, closeWebSocket});
|
||||||
|
}
|
||||||
|
sock.onclose = (event) => {
|
||||||
|
console.debug("[useWebSocket] Closed connection:", event)
|
||||||
|
setWebSocket(undefined)
|
||||||
|
setWebSocketState(sock.readyState);
|
||||||
|
onclose?.({event, openWebSocket, closeWebSocket});
|
||||||
|
}
|
||||||
|
sock.onerror = (event) => {
|
||||||
|
console.error("[useWebSocket] Error in connection:", event)
|
||||||
|
setWebSocketState(sock.readyState)
|
||||||
|
setWebSocketBackoffMs(prev => prev * 2)
|
||||||
|
onerror?.({event, openWebSocket, closeWebSocket});
|
||||||
|
}
|
||||||
|
sock.onmessage = (event) => {
|
||||||
|
console.debug("[useWebSocket] Received message:", event)
|
||||||
|
onmessage?.({event, openWebSocket, 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])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.debug("[useWebSocket] Hook mounted, opening connection as soon as possible...")
|
||||||
|
// @ts-ignore
|
||||||
|
const handle = openWebSocketAfterBackoff()
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handle)
|
||||||
|
}
|
||||||
|
}, [url, openWebSocketAfterBackoff])
|
||||||
|
|
||||||
|
return {webSocket, webSocketState, openWebSocket, closeWebSocket}
|
||||||
|
}
|
9
todoblue/src/app/useWsBaseURL.ts
Normal file
9
todoblue/src/app/useWsBaseURL.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import {useHttpBaseURL} from "@/app/useHttpBaseURL"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **Hook** similar to {@link useHttpBaseURL}, but returning the websocket URL instead.
|
||||||
|
*/
|
||||||
|
export function useWsBaseURL() {
|
||||||
|
return useHttpBaseURL()?.replace(/^http/, "ws")
|
||||||
|
}
|
Loading…
Reference in a new issue