diff --git a/src/hooks/use-websocket.ts b/src/hooks/use-websocket.ts deleted file mode 100644 index a27792bd..00000000 --- a/src/hooks/use-websocket.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useRef } from "react"; - -export type WsMsgFn = (event: MessageEvent) => void; - -export interface WsOptions { - errorCount?: number; // default is 5 - retryInterval?: number; // default is 2500 - onError?: (event: Event) => void; - onClose?: (event: CloseEvent) => void; -} - -export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => { - const wsRef = useRef(null); - const timerRef = useRef(null); - - const disconnect = () => { - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - if (timerRef.current) { - clearTimeout(timerRef.current); - } - }; - - const connect = (url: string) => { - let errorCount = options?.errorCount ?? 5; - - if (!url) return; - - const connectHelper = () => { - disconnect(); - - const ws = new WebSocket(url); - wsRef.current = ws; - - ws.addEventListener("message", (event) => { - errorCount = 0; // reset counter - onMessage(event); - }); - ws.addEventListener("error", (event) => { - errorCount -= 1; - - if (errorCount >= 0) { - timerRef.current = setTimeout(connectHelper, 2500); - } else { - disconnect(); - options?.onError?.(event); - } - }); - ws.addEventListener("close", (event) => { - options?.onClose?.(event); - }); - }; - - connectHelper(); - }; - - return { connect, disconnect }; -}; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index ab3b5c74..d69d3f2d 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { useLockFn } from "ahooks"; import { Box, Button, IconButton, MenuItem } from "@mui/material"; import { Virtuoso } from "react-virtuoso"; @@ -8,7 +8,6 @@ import { closeAllConnections } from "@/services/api"; import { useConnectionSetting } from "@/services/states"; import { useClashInfo } from "@/hooks/use-clash"; import { BaseEmpty, BasePage } from "@/components/base"; -import { useWebsocket } from "@/hooks/use-websocket"; import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionTable } from "@/components/connection/connection-table"; import { @@ -19,6 +18,8 @@ import parseTraffic from "@/utils/parse-traffic"; import { useCustomTheme } from "@/components/layout/use-custom-theme"; import { BaseSearchBox } from "@/components/base/base-search-box"; import { BaseStyledSelect } from "@/components/base/base-styled-select"; +import useSWRSubscription from "swr/subscription"; +import { createSockette } from "@/utils/websocket"; const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; @@ -31,7 +32,6 @@ const ConnectionsPage = () => { const isDark = theme.palette.mode === "dark"; const [match, setMatch] = useState(() => (_: string) => true); const [curOrderOpt, setOrderOpt] = useState("Default"); - const [connData, setConnData] = useState(initConn); const [setting, setSetting] = useConnectionSetting(); @@ -49,6 +49,56 @@ const ConnectionsPage = () => { list.sort((a, b) => b.curDownload! - a.curDownload!), }; + const { data: connData = initConn } = useSWRSubscription< + IConnections, + any, + "getClashConnections" | null + >(clashInfo ? "getClashConnections" : null, (_key, { next }) => { + const { server = "", secret = "" } = clashInfo!; + + const s = createSockette( + `ws://${server}/connections?token=${encodeURIComponent(secret)}`, + { + onmessage(event) { + // meta v1.15.0 出现 data.connections 为 null 的情况 + const data = JSON.parse(event.data) as IConnections; + // 尽量与前一次 connections 的展示顺序保持一致 + next(null, (old = initConn) => { + const oldConn = old.connections; + const maxLen = data.connections?.length; + + const connections: IConnectionsItem[] = []; + + const rest = (data.connections || []).filter((each) => { + const index = oldConn.findIndex((o) => o.id === each.id); + + if (index >= 0 && index < maxLen) { + const old = oldConn[index]; + each.curUpload = each.upload - old.upload; + each.curDownload = each.download - old.download; + + connections[index] = each; + return false; + } + return true; + }); + + for (let i = 0; i < maxLen; ++i) { + if (!connections[i] && rest.length > 0) { + connections[i] = rest.shift()!; + connections[i].curUpload = 0; + connections[i].curDownload = 0; + } + } + + return { ...data, connections }; + }); + }, + }, + 3 + ); + }); + const [filterConn, download, upload] = useMemo(() => { const orderFunc = orderOpts[curOrderOpt]; let connections = connData.connections.filter((conn) => @@ -65,55 +115,6 @@ const ConnectionsPage = () => { return [connections, download, upload]; }, [connData, match, curOrderOpt]); - const { connect, disconnect } = useWebsocket( - (event) => { - // meta v1.15.0 出现data.connections为null的情况 - const data = JSON.parse(event.data) as IConnections; - // 尽量与前一次connections的展示顺序保持一致 - setConnData((old) => { - const oldConn = old.connections; - const maxLen = data.connections?.length; - - const connections: typeof oldConn = []; - - const rest = (data.connections || []).filter((each) => { - const index = oldConn.findIndex((o) => o.id === each.id); - - if (index >= 0 && index < maxLen) { - const old = oldConn[index]; - each.curUpload = each.upload - old.upload; - each.curDownload = each.download - old.download; - - connections[index] = each; - return false; - } - return true; - }); - - for (let i = 0; i < maxLen; ++i) { - if (!connections[i] && rest.length > 0) { - connections[i] = rest.shift()!; - connections[i].curUpload = 0; - connections[i].curDownload = 0; - } - } - - return { ...data, connections }; - }); - }, - { errorCount: 3, retryInterval: 1000 } - ); - - useEffect(() => { - if (!clashInfo) return; - const { server = "", secret = "" } = clashInfo; - connect(`ws://${server}/connections?token=${encodeURIComponent(secret)}`); - - return () => { - disconnect(); - }; - }, [clashInfo]); - const onCloseAll = useLockFn(closeAllConnections); const detailRef = useRef(null!);