mirror of
https://github.com/Steffo99/todocolors.git
synced 2024-11-25 17:54:18 +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" />
|
||||
<node-interpreter value="project" />
|
||||
<envs>
|
||||
<env name="TODOBLUE_SITE_NAME" value="Tododev" />
|
||||
<env name="TODORED_BASE_URL" value="ws://192.168.1.135:8080" />
|
||||
<env name="NEXT_PUBLIC_TODOBLUE_OVERRIDE_BASE_URL" value="http://localhost:8080" />
|
||||
<env name="TODOBLUE_SITE_DESCRIPTION" value="Development version of Todoblue" />
|
||||
<env name="TODOBLUE_SITE_NAME" value="Tododeve" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
|
|
|
@ -10,7 +10,7 @@ export function BoardColumn({taskGroup}: {taskGroup: TaskGroup}) {
|
|||
{taskGroup.name}
|
||||
</h3>
|
||||
<div>
|
||||
{taskGroup.tasks.map(task => <TaskDisplay task={task}/>)}
|
||||
{taskGroup.tasks.map(task => <TaskDisplay task={task} key={task.id}/>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -36,7 +36,11 @@ function TitleArea({children}: {children: ReactNode}) {
|
|||
}
|
||||
|
||||
function TitleInput() {
|
||||
const {editTitle, setEditTitle, stopEditingTitle} = useManagedBoard()
|
||||
const {editTitle, setEditTitle, stopEditingTitle, webSocketState} = useManagedBoard()
|
||||
|
||||
if(webSocketState !== WebSocket.OPEN) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={stopEditingTitle}>
|
||||
|
@ -52,7 +56,11 @@ function TitleInput() {
|
|||
}
|
||||
|
||||
function TitleDisplay() {
|
||||
const {title} = useManagedBoard()
|
||||
const {title, webSocketState} = useManagedBoard()
|
||||
|
||||
if(webSocketState !== WebSocket.OPEN) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -85,9 +93,9 @@ function HomeButton() {
|
|||
}
|
||||
|
||||
function EditTitleButton() {
|
||||
const {websocketState, toggleEditingTitle} = useManagedBoard()
|
||||
const {webSocketState, toggleEditingTitle} = useManagedBoard()
|
||||
|
||||
if(websocketState != WebSocket.OPEN) {
|
||||
if(webSocketState != WebSocket.OPEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -107,9 +115,9 @@ function RightButtonsArea({children}: {children: ReactNode}) {
|
|||
}
|
||||
|
||||
function CycleGroupButton() {
|
||||
const {websocketState, nextGrouper} = useManagedBoard()
|
||||
const {webSocketState, nextGrouper} = useManagedBoard()
|
||||
|
||||
if(websocketState != WebSocket.OPEN) {
|
||||
if(webSocketState != WebSocket.OPEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -121,9 +129,9 @@ function CycleGroupButton() {
|
|||
}
|
||||
|
||||
function CycleSortButton() {
|
||||
const {websocketState, nextSorter} = useManagedBoard()
|
||||
const {webSocketState, nextSorter} = useManagedBoard()
|
||||
|
||||
if(websocketState != WebSocket.OPEN) {
|
||||
if(webSocketState != WebSocket.OPEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import {BoardMainIcon} from "@/app/board/[board]/BoardMainIcon"
|
||||
import {BoardMainTaskGroups} from "@/app/board/[board]/BoardMainTaskGroups"
|
||||
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 cn from "classnames"
|
||||
|
||||
|
||||
export function BoardMain({className}: {className?: string}) {
|
||||
const {websocketState} = useManagedBoard()
|
||||
const {webSocketState} = useManagedBoard()
|
||||
|
||||
switch(websocketState) {
|
||||
switch(webSocketState) {
|
||||
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:
|
||||
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:
|
||||
return <BoardMainTaskGroups className={className}/>
|
||||
case WebSocket.CLOSING:
|
||||
return <BoardMainIcon icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash} beatFade/>} text={"Disconnessione..."} className={className}/>
|
||||
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 (
|
||||
<main className={cn(style.boardMainTaskGroups, className)}>
|
||||
{taskGroups.map((tg) => <BoardColumn taskGroup={tg}/>)}
|
||||
{taskGroups.map((tg) => <BoardColumn taskGroup={tg} key={tg.key}/>)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import {faAdd} from "@fortawesome/free-solid-svg-icons"
|
|||
|
||||
|
||||
export function BoardTaskEditor({className}: {className?: string}) {
|
||||
const {editedTask, send, setEditedTaskText, websocketState} = useManagedBoard()
|
||||
const {editedTask, send, setEditedTaskText, webSocketState} = useManagedBoard()
|
||||
|
||||
const submitTask = useCallback((e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
@ -15,7 +15,7 @@ export function BoardTaskEditor({className}: {className?: string}) {
|
|||
setEditedTaskText("")
|
||||
}, [send, editedTask])
|
||||
|
||||
if(websocketState != WebSocket.OPEN) {
|
||||
if(webSocketState != WebSocket.OPEN) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,8 @@ export function TaskDisplay({task}: {task: TaskWithId}) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={cn({
|
||||
<div
|
||||
className={cn({
|
||||
"panel": true,
|
||||
[style.taskDiv]: true,
|
||||
[style.taskDivFront]: !isDisplayingActions,
|
||||
|
@ -73,7 +74,9 @@ export function TaskDisplay({task}: {task: TaskWithId}) {
|
|||
[style.taskStatusUnfinished]: task.status === "Unfinished",
|
||||
[style.taskStatusInProgress]: task.status === "InProgress",
|
||||
[style.taskStatusComplete]: task.status === "Complete",
|
||||
})} onClick={() => setDisplayingActions(!isDisplayingActions)}>
|
||||
})}
|
||||
onClick={() => setDisplayingActions(!isDisplayingActions)}
|
||||
>
|
||||
{contents}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ import {TASK_GROUPERS} from "@/app/board/[board]/doTaskGrouping"
|
|||
import {TASK_SORTERS} from "@/app/board/[board]/doTaskSorting"
|
||||
import {BoardAction, Task} from "@/app/board/[board]/Types"
|
||||
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 {useBoardTitleEditor} from "@/app/board/[board]/useBoardTitleEditor"
|
||||
import {useCycleState} from "@/app/useCycleState"
|
||||
|
@ -14,7 +14,7 @@ export interface UseBoardReturns {
|
|||
title: string,
|
||||
tasksById: {[id: string]: Task},
|
||||
taskGroups: TaskGroup[],
|
||||
websocketState: number,
|
||||
webSocketState: number | undefined,
|
||||
isEditingTitle: boolean,
|
||||
stopEditingTitle: () => void,
|
||||
startEditingTitle: () => void,
|
||||
|
@ -35,13 +35,13 @@ export interface 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: taskSorter, move: moveSorter, next: nextSorter, previous: previousSorter} = useCycleState(TASK_SORTERS);
|
||||
|
||||
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()
|
||||
|
||||
|
@ -49,7 +49,7 @@ export function useBoard(name: string): UseBoardReturns {
|
|||
title,
|
||||
tasksById,
|
||||
taskGroups,
|
||||
websocketState,
|
||||
webSocketState,
|
||||
isEditingTitle,
|
||||
stopEditingTitle,
|
||||
startEditingTitle,
|
||||
|
@ -62,7 +62,7 @@ export function useBoard(name: string): UseBoardReturns {
|
|||
previousSorter,
|
||||
editTitle,
|
||||
setEditTitle,
|
||||
send,
|
||||
send: sendAction,
|
||||
editedTaskText,
|
||||
setEditedTaskText,
|
||||
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