From afee21dae4578c294bc966ddb3e989858ac6c61a Mon Sep 17 00:00:00 2001 From: Sline Date: Thu, 4 Dec 2025 14:58:03 +0800 Subject: [PATCH] refactor: unify Mihomo WS subscription with shared hook (#5719) * refactor: unify Mihomo WS subscription with shared hook * refactor: relocate clash log hook and streamline services * docs: Changelog.md --- Changelog.md | 1 + src/components/setting/setting-clash.tsx | 2 +- src/hooks/use-clash-log.ts | 14 ++ src/hooks/use-connection-data.ts | 209 +++++++++------------ src/hooks/use-connection-setting.ts | 13 ++ src/hooks/use-log-data.ts | 220 ++++++++++------------- src/hooks/use-memory-data.ts | 100 +++-------- src/hooks/use-mihomo-ws-subscription.ts | 156 ++++++++++++++++ src/hooks/use-traffic-data.ts | 106 +++-------- src/pages/connections.tsx | 2 +- src/pages/logs.tsx | 2 +- src/services/states.ts | 41 ----- src/types/types.d.ts | 15 ++ 13 files changed, 434 insertions(+), 447 deletions(-) create mode 100644 src/hooks/use-clash-log.ts create mode 100644 src/hooks/use-connection-setting.ts create mode 100644 src/hooks/use-mihomo-ws-subscription.ts diff --git a/Changelog.md b/Changelog.md index 21a29ebd1..f8a3f6d30 100644 --- a/Changelog.md +++ b/Changelog.md @@ -56,6 +56,7 @@ - 优化前端数据刷新 - 优化流量采样和数据处理 - 优化应用重启/退出时的资源清理性能, 大幅缩短执行时间 +- 优化 WebSocket 连接机制 diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 931a17c6c..530ae86d9 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -9,10 +9,10 @@ import { updateGeo } from "tauri-plugin-mihomo-api"; import { DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useClash } from "@/hooks/use-clash"; +import { useClashLog } from "@/hooks/use-clash-log"; import { useVerge } from "@/hooks/use-verge"; import { invoke_uwp_tool } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; -import { useClashLog } from "@/services/states"; import getSystem from "@/utils/get-system"; import { ClashCoreViewer } from "./mods/clash-core-viewer"; diff --git a/src/hooks/use-clash-log.ts b/src/hooks/use-clash-log.ts new file mode 100644 index 000000000..66579f798 --- /dev/null +++ b/src/hooks/use-clash-log.ts @@ -0,0 +1,14 @@ +import { useLocalStorage } from "foxact/use-local-storage"; + +const defaultClashLog: IClashLog = { + enable: true, + logLevel: "info", + logFilter: "all", + logOrder: "asc", +}; + +export const useClashLog = () => + useLocalStorage("clash-log", defaultClashLog, { + serializer: JSON.stringify, + deserializer: JSON.parse, + }); diff --git a/src/hooks/use-connection-data.ts b/src/hooks/use-connection-data.ts index a4a103a00..15b41d25f 100644 --- a/src/hooks/use-connection-data.ts +++ b/src/hooks/use-connection-data.ts @@ -1,9 +1,10 @@ -import { useLocalStorage } from "foxact/use-local-storage"; -import { useEffect, useRef } from "react"; import { mutate } from "swr"; -import useSWRSubscription from "swr/subscription"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; +import { useMihomoWsSubscription } from "./use-mihomo-ws-subscription"; + +const MAX_CLOSED_CONNS_NUM = 500; + export const initConnData: ConnectionMonitorData = { uploadTotal: 0, downloadTotal: 0, @@ -18,129 +19,91 @@ export interface ConnectionMonitorData { closedConnections: IConnectionsItem[]; } -const MAX_CLOSED_CONNS_NUM = 500; +const trimClosedConnections = ( + closedConnections: IConnectionsItem[], +): IConnectionsItem[] => + closedConnections.length > MAX_CLOSED_CONNS_NUM + ? closedConnections.slice(-MAX_CLOSED_CONNS_NUM) + : closedConnections; + +const mergeConnectionSnapshot = ( + payload: IConnections, + previous: ConnectionMonitorData = initConnData, +): ConnectionMonitorData => { + const nextConnections = payload.connections ?? []; + const previousActive = previous.activeConnections ?? []; + const nextById = new Map(nextConnections.map((conn) => [conn.id, conn])); + const newIds = new Set(nextConnections.map((conn) => conn.id)); + + // Keep surviving connections in their previous relative order to reduce row reshuffle, + // but constrain the array to the incoming snapshot length. + const carried = previousActive + .map((prev) => { + const next = nextById.get(prev.id); + if (!next) return null; + + nextById.delete(prev.id); + return { + ...next, + curUpload: next.upload - prev.upload, + curDownload: next.download - prev.download, + } as IConnectionsItem; + }) + .filter(Boolean) as IConnectionsItem[]; + + const newcomers = nextConnections + .filter((conn) => nextById.has(conn.id)) + .map((conn) => ({ + ...conn, + curUpload: 0, + curDownload: 0, + })); + + const activeConnections = [...carried, ...newcomers]; + + const closedConnections = trimClosedConnections([ + ...(previous.closedConnections ?? []), + ...previousActive.filter((conn) => !newIds.has(conn.id)), + ]); + + return { + uploadTotal: payload.uploadTotal ?? 0, + downloadTotal: payload.downloadTotal ?? 0, + activeConnections, + closedConnections, + }; +}; export const useConnectionData = () => { - const [date, setDate] = useLocalStorage("mihomo_connection_date", Date.now()); - const subscriptKey = `getClashConnection-${date}`; - - const ws = useRef(null); - const wsFirstConnection = useRef(true); - const timeoutRef = useRef>(null); - - const response = useSWRSubscription< - ConnectionMonitorData, - any, - string | null - >( - subscriptKey, - (_key, { next }) => { - const reconnect = async () => { - await ws.current?.close(); - ws.current = null; - timeoutRef.current = setTimeout(async () => await connect(), 500); - }; - - const connect = () => - MihomoWebSocket.connect_connections() - .then((ws_) => { - ws.current = ws_; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - ws_.addListener(async (msg) => { - if (msg.type === "Text") { - if (msg.data.startsWith("Websocket error")) { - next(msg.data); - await reconnect(); - } else { - const data = JSON.parse(msg.data) as IConnections; - next(null, (old = initConnData) => { - const oldConn = old.activeConnections; - const maxLen = data.connections?.length; - const activeConns: 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; - activeConns[index] = each; - return false; - } - return true; - }); - for (let i = 0; i < maxLen; ++i) { - if (!activeConns[i] && rest.length > 0) { - activeConns[i] = rest.shift()!; - activeConns[i].curUpload = 0; - activeConns[i].curDownload = 0; - } - } - const currentClosedConns = oldConn.filter((each) => { - const index = activeConns.findIndex( - (o) => o.id === each.id, - ); - return index < 0; - }); - let closedConns = - old.closedConnections.concat(currentClosedConns); - if (closedConns.length > 500) { - closedConns = closedConns.slice(-MAX_CLOSED_CONNS_NUM); - } - return { - uploadTotal: data.uploadTotal, - downloadTotal: data.downloadTotal, - activeConnections: activeConns, - closedConnections: closedConns, - }; - }); - } - } - }); - }) - .catch((_) => { - if (!ws.current) { - timeoutRef.current = setTimeout(async () => await connect(), 500); - } - }); - - if ( - wsFirstConnection.current || - (ws.current && !wsFirstConnection.current) - ) { - wsFirstConnection.current = false; - if (ws.current) { - ws.current.close(); - ws.current = null; - } - connect(); - } - - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - ws.current?.close(); - ws.current = null; - }; - }, - { + const { response, refresh, subscriptionCacheKey } = + useMihomoWsSubscription({ + storageKey: "mihomo_connection_date", + buildSubscriptKey: (date) => `getClashConnection-${date}`, fallbackData: initConnData, - keepPreviousData: true, - }, - ); + connect: () => MihomoWebSocket.connect_connections(), + setupHandlers: ({ next, scheduleReconnect }) => ({ + handleMessage: (data) => { + if (data.startsWith("Websocket error")) { + next(data); + void scheduleReconnect(); + return; + } - useEffect(() => { - mutate(`$sub$${subscriptKey}`); - }, [date, subscriptKey]); - - const refreshGetClashConnection = () => { - setDate(Date.now()); - }; + try { + const parsed = JSON.parse(data) as IConnections; + next(null, (old = initConnData) => + mergeConnectionSnapshot(parsed, old), + ); + } catch (error) { + next(error); + } + }, + }), + }); const clearClosedConnections = () => { - mutate(`$sub$${subscriptKey}`, { + if (!subscriptionCacheKey) return; + mutate(subscriptionCacheKey, { uploadTotal: response.data?.uploadTotal ?? 0, downloadTotal: response.data?.downloadTotal ?? 0, activeConnections: response.data?.activeConnections ?? [], @@ -148,5 +111,9 @@ export const useConnectionData = () => { }); }; - return { response, refreshGetClashConnection, clearClosedConnections }; + return { + response, + refreshGetClashConnection: refresh, + clearClosedConnections, + }; }; diff --git a/src/hooks/use-connection-setting.ts b/src/hooks/use-connection-setting.ts new file mode 100644 index 000000000..4b88ccab2 --- /dev/null +++ b/src/hooks/use-connection-setting.ts @@ -0,0 +1,13 @@ +import { useLocalStorage } from "foxact/use-local-storage"; + +const defaultConnectionSetting: IConnectionSetting = { layout: "table" }; + +export const useConnectionSetting = () => + useLocalStorage( + "connections-setting", + defaultConnectionSetting, + { + serializer: JSON.stringify, + deserializer: JSON.parse, + }, + ); diff --git a/src/hooks/use-log-data.ts b/src/hooks/use-log-data.ts index 67f4ac190..7fc26a685 100644 --- a/src/hooks/use-log-data.ts +++ b/src/hooks/use-log-data.ts @@ -1,142 +1,113 @@ import dayjs from "dayjs"; -import { useLocalStorage } from "foxact/use-local-storage"; import { useEffect, useRef } from "react"; import { mutate } from "swr"; -import useSWRSubscription from "swr/subscription"; -import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; +import { MihomoWebSocket, type LogLevel } from "tauri-plugin-mihomo-api"; import { getClashLogs } from "@/services/cmds"; -import { useClashLog } from "@/services/states"; + +import { useClashLog } from "./use-clash-log"; +import { useMihomoWsSubscription } from "./use-mihomo-ws-subscription"; const MAX_LOG_NUM = 1000; +const FLUSH_DELAY_MS = 50; +type LogType = ILogItem["type"]; + +const DEFAULT_LOG_TYPES: LogType[] = ["debug", "info", "warning", "error"]; +const LOG_LEVEL_FILTERS: Record = { + debug: DEFAULT_LOG_TYPES, + info: ["info", "warning", "error"], + warning: ["warning", "error"], + error: ["error"], + silent: [], +}; + +const clampLogs = (logs: ILogItem[]): ILogItem[] => + logs.length > MAX_LOG_NUM ? logs.slice(-MAX_LOG_NUM) : logs; + +const filterLogsByLevel = ( + logs: ILogItem[], + allowedTypes: LogType[], +): ILogItem[] => { + if (allowedTypes.length === 0) return []; + if (allowedTypes.length === DEFAULT_LOG_TYPES.length) return logs; + return logs.filter((log) => allowedTypes.includes(log.type)); +}; + +const appendLogs = ( + current: ILogItem[] | undefined, + incoming: ILogItem[], +): ILogItem[] => clampLogs([...(current ?? []), ...incoming]); export const useLogData = () => { const [clashLog] = useClashLog(); const enableLog = clashLog.enable; const logLevel = clashLog.logLevel; + const allowedTypes = LOG_LEVEL_FILTERS[logLevel] ?? DEFAULT_LOG_TYPES; - const [date, setDate] = useLocalStorage("mihomo_logs_date", Date.now()); - const subscriptKey = enableLog ? `getClashLog-${date}` : null; + const { response, refresh, subscriptionCacheKey } = useMihomoWsSubscription< + ILogItem[] + >({ + storageKey: "mihomo_logs_date", + buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null), + fallbackData: [], + keepPreviousData: true, + connect: () => MihomoWebSocket.connect_logs(logLevel), + setupHandlers: ({ next, scheduleReconnect, isMounted }) => { + let flushTimer: ReturnType | null = null; + const buffer: ILogItem[] = []; - const ws = useRef(null); - const wsFirstConnection = useRef(true); - const timeoutRef = useRef>(null); - - const response = useSWRSubscription( - subscriptKey, - (_key, { next }) => { - const reconnect = async () => { - await ws.current?.close(); - ws.current = null; - timeoutRef.current = setTimeout(async () => await connect(), 500); + const clearFlushTimer = () => { + if (flushTimer) { + clearTimeout(flushTimer); + flushTimer = null; + } }; - const connect = () => - MihomoWebSocket.connect_logs(logLevel) - .then(async (ws_) => { - ws.current = ws_; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - const logs = await getClashLogs(); - let filterLogs: ILogItem[] = []; - switch (logLevel) { - case "debug": - filterLogs = logs.filter((i) => - ["debug", "info", "warning", "error"].includes(i.type), - ); - break; - case "info": - filterLogs = logs.filter((i) => - ["info", "warning", "error"].includes(i.type), - ); - break; - case "warning": - filterLogs = logs.filter((i) => - ["warning", "error"].includes(i.type), - ); - break; - case "error": - filterLogs = logs.filter((i) => i.type === "error"); - break; - case "silent": - filterLogs = []; - break; - default: - filterLogs = logs; - break; - } - next(null, filterLogs); - - const buffer: ILogItem[] = []; - let flushTimer: ReturnType | null = null; - const flush = () => { - if (buffer.length > 0) { - next(null, (l) => { - let newList = [...(l ?? []), ...buffer.splice(0)]; - if (newList.length > MAX_LOG_NUM) { - newList = newList.slice( - -Math.min(MAX_LOG_NUM, newList.length), - ); - } - return newList; - }); - } - flushTimer = null; - }; - ws_.addListener(async (msg) => { - if (msg.type === "Text") { - if (msg.data.startsWith("Websocket error")) { - next(msg.data); - await reconnect(); - } else { - const data = JSON.parse(msg.data) as ILogItem; - data.time = dayjs().format("MM-DD HH:mm:ss"); - buffer.push(data); - - // flush data - if (!flushTimer) { - flushTimer = setTimeout(flush, 50); - } - } - } - }); - }) - .catch((_) => { - if (!ws.current) { - timeoutRef.current = setTimeout(async () => await connect(), 500); - } - }); - - if ( - wsFirstConnection.current || - (ws.current && !wsFirstConnection.current) - ) { - wsFirstConnection.current = false; - if (ws.current) { - ws.current.close(); - ws.current = null; + const flush = () => { + if (!buffer.length || !isMounted()) { + flushTimer = null; + return; } - connect(); - } + const pendingLogs = buffer.splice(0, buffer.length); + next(null, (current) => appendLogs(current, pendingLogs)); + flushTimer = null; + }; - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; - } - ws.current?.close(); - ws.current = null; + return { + handleMessage: (data) => { + if (data.startsWith("Websocket error")) { + next(data); + void scheduleReconnect(); + return; + } + + try { + const parsed = JSON.parse(data) as ILogItem; + if ( + allowedTypes.length > 0 && + !allowedTypes.includes(parsed.type) + ) { + return; + } + parsed.time = dayjs().format("MM-DD HH:mm:ss"); + buffer.push(parsed); + if (!flushTimer) { + flushTimer = setTimeout(flush, FLUSH_DELAY_MS); + } + } catch (error) { + next(error); + } + }, + async onConnected() { + const logs = await getClashLogs(); + if (isMounted()) { + next(null, clampLogs(filterLogsByLevel(logs, allowedTypes))); + } + }, + cleanup: clearFlushTimer, }; }, - { - fallbackData: [], - keepPreviousData: true, - }, - ); - - useEffect(() => { - mutate(`$sub$${subscriptKey}`); - }, [date, subscriptKey]); + }); const previousLogLevel = useRef(undefined); @@ -151,15 +122,16 @@ export const useLogData = () => { } previousLogLevel.current = logLevel; - ws.current?.close(); - setDate(Date.now()); - }, [logLevel, setDate]); + refresh(); + }, [logLevel, refresh]); const refreshGetClashLog = (clear = false) => { if (clear) { - mutate(`$sub$${subscriptKey}`, []); + if (subscriptionCacheKey) { + mutate(subscriptionCacheKey, []); + } } else { - setDate(Date.now()); + refresh(); } }; diff --git a/src/hooks/use-memory-data.ts b/src/hooks/use-memory-data.ts index 12b25d340..2a7d78da3 100644 --- a/src/hooks/use-memory-data.ts +++ b/src/hooks/use-memory-data.ts @@ -1,89 +1,37 @@ -import { useLocalStorage } from "foxact/use-local-storage"; -import { useEffect, useRef } from "react"; -import { mutate } from "swr"; -import useSWRSubscription from "swr/subscription"; import { MihomoWebSocket } from "tauri-plugin-mihomo-api"; +import { useMihomoWsSubscription } from "./use-mihomo-ws-subscription"; + export interface IMemoryUsageItem { inuse: number; oslimit?: number; } +const FALLBACK_MEMORY_USAGE: IMemoryUsageItem = { inuse: 0 }; + export const useMemoryData = () => { - const [date, setDate] = useLocalStorage("mihomo_memory_date", Date.now()); - const subscriptKey = `getClashMemory-${date}`; - - const ws = useRef(null); - const wsFirstConnection = useRef(true); - const timeoutRef = useRef>(null); - - const response = useSWRSubscription( - subscriptKey, - (_key, { next }) => { - const reconnect = async () => { - await ws.current?.close(); - ws.current = null; - timeoutRef.current = setTimeout(async () => await connect(), 500); - }; - - const connect = () => - MihomoWebSocket.connect_memory() - .then((ws_) => { - ws.current = ws_; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - ws_.addListener(async (msg) => { - if (msg.type === "Text") { - if (msg.data.startsWith("Websocket error")) { - next(msg.data, { inuse: 0 }); - await reconnect(); - } else { - const data = JSON.parse(msg.data) as IMemoryUsageItem; - next(null, data); - } - } - }); - }) - .catch((_) => { - if (!ws.current) { - timeoutRef.current = setTimeout(async () => await connect(), 500); - } - }); - - if ( - wsFirstConnection.current || - (ws.current && !wsFirstConnection.current) - ) { - wsFirstConnection.current = false; - if (ws.current) { - ws.current.close(); - ws.current = null; + const { response, refresh } = useMihomoWsSubscription({ + storageKey: "mihomo_memory_date", + buildSubscriptKey: (date) => `getClashMemory-${date}`, + fallbackData: FALLBACK_MEMORY_USAGE, + connect: () => MihomoWebSocket.connect_memory(), + setupHandlers: ({ next, scheduleReconnect }) => ({ + handleMessage: (data) => { + if (data.startsWith("Websocket error")) { + next(data, FALLBACK_MEMORY_USAGE); + void scheduleReconnect(); + return; } - connect(); - } - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; + try { + const parsed = JSON.parse(data) as IMemoryUsageItem; + next(null, parsed); + } catch (error) { + next(error, FALLBACK_MEMORY_USAGE); } - ws.current?.close(); - ws.current = null; - }; - }, - { - fallbackData: { inuse: 0 }, - keepPreviousData: true, - }, - ); + }, + }), + }); - useEffect(() => { - mutate(`$sub$${subscriptKey}`); - }, [date, subscriptKey]); - - const refreshGetClashMemory = () => { - setDate(Date.now()); - }; - - return { response, refreshGetClashMemory }; + return { response, refreshGetClashMemory: refresh }; }; diff --git a/src/hooks/use-mihomo-ws-subscription.ts b/src/hooks/use-mihomo-ws-subscription.ts new file mode 100644 index 000000000..f2cdad104 --- /dev/null +++ b/src/hooks/use-mihomo-ws-subscription.ts @@ -0,0 +1,156 @@ +import { useLocalStorage } from "foxact/use-local-storage"; +import { useCallback, useEffect, useRef } from "react"; +import { mutate, type MutatorCallback } from "swr"; +import useSWRSubscription from "swr/subscription"; +import { type Message, type MihomoWebSocket } from "tauri-plugin-mihomo-api"; + +export const RECONNECT_DELAY_MS = 500; + +type NextFn = (error?: any, data?: T | MutatorCallback) => void; + +interface HandlerContext { + next: NextFn; + scheduleReconnect: () => Promise; + isMounted: () => boolean; +} + +interface HandlerResult { + handleMessage: (data: string) => void; + onConnected?: (ws: MihomoWebSocket) => Promise | void; + cleanup?: () => void; +} + +interface UseMihomoWsSubscriptionOptions { + storageKey: string; + buildSubscriptKey: (date: number) => string | null; + fallbackData: T; + connect: () => Promise; + keepPreviousData?: boolean; + setupHandlers: (ctx: HandlerContext) => HandlerResult; +} + +export const useMihomoWsSubscription = ( + options: UseMihomoWsSubscriptionOptions, +) => { + const { + storageKey, + buildSubscriptKey, + fallbackData, + connect, + keepPreviousData = true, + setupHandlers, + } = options; + + const [date, setDate] = useLocalStorage(storageKey, Date.now()); + const subscriptKey = buildSubscriptKey(date); + const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null; + + const wsRef = useRef(null); + const wsFirstConnection = useRef(true); + const timeoutRef = useRef | null>(null); + + const response = useSWRSubscription( + subscriptKey, + (_key, { next }) => { + let isMounted = true; + + const clearReconnectTimer = () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + + const closeSocket = async () => { + if (wsRef.current) { + await wsRef.current.close(); + wsRef.current = null; + } + }; + + const scheduleReconnect = async () => { + if (!isMounted) return; + clearReconnectTimer(); + await closeSocket(); + if (!isMounted) return; + timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS); + }; + + const { + handleMessage: handleTextMessage, + onConnected, + cleanup, + } = setupHandlers({ + next, + scheduleReconnect, + isMounted: () => isMounted, + }); + + const cleanupAll = () => { + clearReconnectTimer(); + cleanup?.(); + void closeSocket(); + }; + + const handleMessage = (msg: Message) => { + if (msg.type !== "Text") return; + handleTextMessage(msg.data); + }; + + async function connectWs() { + try { + const ws_ = await connect(); + if (!isMounted) { + await ws_.close(); + return; + } + + wsRef.current = ws_; + clearReconnectTimer(); + + if (onConnected) { + await onConnected(ws_); + if (!isMounted) { + await ws_.close(); + return; + } + } + + ws_.addListener(handleMessage); + } catch (ignoreError) { + if (!wsRef.current && isMounted) { + timeoutRef.current = setTimeout(connectWs, RECONNECT_DELAY_MS); + } + } + } + + if (wsFirstConnection.current || !wsRef.current) { + wsFirstConnection.current = false; + cleanupAll(); + void connectWs(); + } + + return () => { + isMounted = false; + wsFirstConnection.current = true; + cleanupAll(); + }; + }, + { + fallbackData, + keepPreviousData, + }, + ); + + useEffect(() => { + if (subscriptionCacheKey) { + mutate(subscriptionCacheKey); + } + }, [subscriptionCacheKey]); + + const refresh = useCallback(() => { + setDate(Date.now()); + }, [setDate]); + + return { response, refresh, subscriptionCacheKey, wsRef }; +}; diff --git a/src/hooks/use-traffic-data.ts b/src/hooks/use-traffic-data.ts index eb8ca4111..4739927b1 100644 --- a/src/hooks/use-traffic-data.ts +++ b/src/hooks/use-traffic-data.ts @@ -1,95 +1,37 @@ -import { useLocalStorage } from "foxact/use-local-storage"; -import { useEffect, useRef } from "react"; -import { mutate } from "swr"; -import useSWRSubscription from "swr/subscription"; import { MihomoWebSocket, Traffic } from "tauri-plugin-mihomo-api"; -import { TrafficRef } from "@/components/layout/traffic-graph"; - +import { useMihomoWsSubscription } from "./use-mihomo-ws-subscription"; import { useTrafficMonitorEnhanced } from "./use-traffic-monitor"; -export const useTrafficData = () => { - const [date, setDate] = useLocalStorage("mihomo_traffic_date", Date.now()); - const subscriptKey = `getClashTraffic-${date}`; +const FALLBACK_TRAFFIC: Traffic = { up: 0, down: 0 }; - const trafficRef = useRef(null); +export const useTrafficData = () => { const { graphData: { appendData }, } = useTrafficMonitorEnhanced({ subscribe: false }); - const ws = useRef(null); - const wsFirstConnection = useRef(true); - const timeoutRef = useRef>(null); - - const response = useSWRSubscription( - subscriptKey, - (_key, { next }) => { - const reconnect = async () => { - await ws.current?.close(); - ws.current = null; - timeoutRef.current = setTimeout(async () => await connect(), 500); - }; - - const connect = async () => { - MihomoWebSocket.connect_traffic() - .then(async (ws_) => { - ws.current = ws_; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - - ws_.addListener(async (msg) => { - if (msg.type === "Text") { - if (msg.data.startsWith("Websocket error")) { - next(msg.data, { up: 0, down: 0 }); - await reconnect(); - } else { - const data = JSON.parse(msg.data) as Traffic; - trafficRef.current?.appendData(data); - appendData(data); - next(null, data); - } - } - }); - }) - .catch((_) => { - if (!ws.current) { - timeoutRef.current = setTimeout(async () => await connect(), 500); - } - }); - }; - - if ( - wsFirstConnection.current || - (ws.current && !wsFirstConnection.current) - ) { - wsFirstConnection.current = false; - if (ws.current) { - ws.current.close(); - ws.current = null; + const { response, refresh } = useMihomoWsSubscription({ + storageKey: "mihomo_traffic_date", + buildSubscriptKey: (date) => `getClashTraffic-${date}`, + fallbackData: FALLBACK_TRAFFIC, + connect: () => MihomoWebSocket.connect_traffic(), + setupHandlers: ({ next, scheduleReconnect }) => ({ + handleMessage: (data) => { + if (data.startsWith("Websocket error")) { + next(data, FALLBACK_TRAFFIC); + void scheduleReconnect(); + return; } - connect(); - } - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - timeoutRef.current = null; + try { + const parsed = JSON.parse(data) as Traffic; + appendData(parsed); + next(null, parsed); + } catch (error) { + next(error, FALLBACK_TRAFFIC); } - ws.current?.close(); - ws.current = null; - }; - }, - { - fallbackData: { up: 0, down: 0 }, - keepPreviousData: true, - }, - ); + }, + }), + }); - useEffect(() => { - mutate(`$sub$${subscriptKey}`); - }, [date, subscriptKey]); - - const refreshGetClashTraffic = () => { - setDate(Date.now()); - }; - - return { response, refreshGetClashTraffic }; + return { response, refreshGetClashTraffic: refresh }; }; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index 52475813e..614a5e239 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -28,7 +28,7 @@ import { import { ConnectionItem } from "@/components/connection/connection-item"; import { ConnectionTable } from "@/components/connection/connection-table"; import { useConnectionData } from "@/hooks/use-connection-data"; -import { useConnectionSetting } from "@/services/states"; +import { useConnectionSetting } from "@/hooks/use-connection-setting"; import parseTraffic from "@/utils/parse-traffic"; type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]; diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx index c3dbb9e31..53d012cb7 100644 --- a/src/pages/logs.tsx +++ b/src/pages/logs.tsx @@ -13,8 +13,8 @@ import { BaseSearchBox } from "@/components/base/base-search-box"; import { SearchState } from "@/components/base/base-search-box"; import { BaseStyledSelect } from "@/components/base/base-styled-select"; import LogItem from "@/components/log/log-item"; +import { useClashLog } from "@/hooks/use-clash-log"; import { useLogData } from "@/hooks/use-log-data"; -import { LogFilter, useClashLog } from "@/services/states"; const LogPage = () => { const { t } = useTranslation(); diff --git a/src/services/states.ts b/src/services/states.ts index e8a308fd2..11e28faba 100644 --- a/src/services/states.ts +++ b/src/services/states.ts @@ -1,50 +1,9 @@ import { createContextState } from "foxact/create-context-state"; -import { useLocalStorage } from "foxact/use-local-storage"; -import { LogLevel } from "tauri-plugin-mihomo-api"; const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState< "light" | "dark" >(); -export type LogFilter = "all" | "debug" | "info" | "warn" | "err"; -export type LogOrder = "asc" | "desc"; - -interface IClashLog { - enable: boolean; - logLevel: LogLevel; - logFilter: LogFilter; - logOrder: LogOrder; -} -const defaultClashLog: IClashLog = { - enable: true, - logLevel: "info", - logFilter: "all", - logOrder: "asc", -}; -export const useClashLog = () => - useLocalStorage("clash-log", defaultClashLog, { - serializer: JSON.stringify, - deserializer: JSON.parse, - }); - -// export const useEnableLog = () => useLocalStorage("enable-log", false); - -interface IConnectionSetting { - layout: "table" | "list"; -} - -const defaultConnectionSetting: IConnectionSetting = { layout: "table" }; - -export const useConnectionSetting = () => - useLocalStorage( - "connections-setting", - defaultConnectionSetting, - { - serializer: JSON.stringify, - deserializer: JSON.parse, - }, - ); - // save the state of each profile item loading const [LoadingCacheProvider, useLoadingCache, useSetLoadingCache] = createContextState>({}); diff --git a/src/types/types.d.ts b/src/types/types.d.ts index fd57faa66..0ff7ea5d0 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -197,6 +197,17 @@ interface ILogItem { payload: string; } +type LogLevel = import("tauri-plugin-mihomo-api").LogLevel; +type LogFilter = "all" | "debug" | "info" | "warn" | "err"; +type LogOrder = "asc" | "desc"; + +interface IClashLog { + enable: boolean; + logLevel: LogLevel; + logFilter: LogFilter; + logOrder: LogOrder; +} + interface IConnectionsItem { id: string; metadata: { @@ -227,6 +238,10 @@ interface IConnections { connections: IConnectionsItem[]; } +interface IConnectionSetting { + layout: "table" | "list"; +} + /** * Some interface for command */