telegram-files/web/src/hooks/use-websocket.tsx

168 lines
4.5 KiB
TypeScript

"use client";
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import useWebSocket, { ReadyState } from "react-use-websocket";
import {
type TelegramError,
type WebSocketMessage,
WebSocketMessageType,
} from "@/lib/websocket-types";
import { useToast } from "./use-toast";
import { useDebounce } from "use-debounce";
import { getWsUrl } from "@/lib/api";
import { useParams } from "next/navigation";
const WS_URL = `${getWsUrl()}`;
interface WebsocketContextType {
sendMessage: (message: WebSocketMessage) => void;
lastJsonMessage: WebSocketMessage | null;
connectionStatus: string;
isReady: boolean;
accountDownloadSpeed: number;
}
const WebSocketContext = createContext<WebsocketContextType | undefined>(
undefined,
);
interface WebSocketProviderProps {
children: ReactNode;
}
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({
children,
}) => {
const params = useParams();
const [isReady, setIsReady] = useState(false);
const [accountDownloadSpeed, setAccountDownloadSpeed] = useState({
speed: 0,
lastDownloadedSize: 0,
lastTimestamp: 0,
});
const { toast } = useToast();
const [debounceSpeed] = useDebounce(accountDownloadSpeed.speed, 300, {
leading: true,
maxWait: 1000,
});
const { sendMessage, lastJsonMessage, readyState } =
useWebSocket<WebSocketMessage>(
`${WS_URL}?telegramId=${(params.accountId as string) ?? ""}`,
{
shouldReconnect: (closeEvent) => true,
reconnectAttempts: 3,
reconnectInterval: 3000,
},
);
const connectionStatus = {
[ReadyState.CONNECTING]: "Connecting",
[ReadyState.OPEN]: "Open",
[ReadyState.CLOSING]: "Closing",
[ReadyState.CLOSED]: "Closed",
[ReadyState.UNINSTANTIATED]: "Uninstantiated",
}[readyState];
useEffect(() => {
setIsReady(readyState === ReadyState.OPEN);
}, [readyState]);
useEffect(() => {
if (lastJsonMessage !== null) {
// console.log(
// `🐄: ${JSON.stringify(lastJsonMessage, null, 2)}`,
// )
try {
const payload: WebSocketMessage = lastJsonMessage;
const timestamp = payload.timestamp;
switch (payload.type) {
case WebSocketMessageType.ERROR:
toast({
title: "Error",
description: (payload.data as TelegramError).message,
variant: "destructive",
});
break;
case WebSocketMessageType.FILE_DOWNLOAD:
const { downloadedSize, totalCount } = payload.data as {
totalSize: number;
totalCount: number;
downloadedSize: number;
};
if (totalCount === 0) {
setAccountDownloadSpeed({
speed: 0,
lastDownloadedSize: 0,
lastTimestamp: 0,
});
return;
}
setAccountDownloadSpeed((prev) => {
const state = {
lastTimestamp: timestamp,
lastDownloadedSize: downloadedSize,
};
const timeDiff = (timestamp - prev.lastTimestamp) / 1000;
if (prev.lastTimestamp === 0 || timeDiff <= 0 || downloadedSize <= prev.lastDownloadedSize) {
return {
...state,
speed: prev.speed,
};
}
const speed =
(downloadedSize - prev.lastDownloadedSize) / timeDiff;
return {
speed,
lastDownloadedSize: downloadedSize,
lastTimestamp: timestamp,
};
});
}
} catch (error) {
console.error("Failed to parse WebSocket message:", error);
}
}
}, [lastJsonMessage, toast]);
const sendWebSocketMessage = useCallback(
(message: WebSocketMessage) => {
if (isReady) {
sendMessage(JSON.stringify(message));
}
},
[isReady, sendMessage],
);
return (
<WebSocketContext.Provider
value={{
sendMessage: sendWebSocketMessage,
lastJsonMessage,
connectionStatus,
isReady,
accountDownloadSpeed: debounceSpeed,
}}
>
{children}
</WebSocketContext.Provider>
);
};
export function useWebsocket() {
const context = useContext(WebSocketContext);
if (context === undefined) {
throw new Error(
"useTelegramWebSocket must be used within a WebSocketProvider",
);
}
return context;
}