diff --git a/package.json b/package.json index 2e20e66..0a2cbc0 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "react-router-dom": "^6.23.1", "react-transition-group": "^4.4.5", "react-virtuoso": "^4.7.11", - "swr": "^1.3.0", + "sockette": "^2.0.6", + "swr": "^2.2.5", "tar": "^6.2.1", "types-pac": "^1.0.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95d27aa..58c68d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,9 +100,12 @@ importers: react-virtuoso: specifier: ^4.7.11 version: 4.7.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sockette: + specifier: ^2.0.6 + version: 2.0.6 swr: - specifier: ^1.3.0 - version: 1.3.0(react@18.3.1) + specifier: ^2.2.5 + version: 2.2.5(react@18.3.1) tar: specifier: ^6.2.1 version: 6.2.1 @@ -4032,6 +4035,12 @@ packages: integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==, } + sockette@2.0.6: + resolution: + { + integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==, + } + source-map-js@1.2.0: resolution: { @@ -4110,10 +4119,10 @@ packages: integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==, } - swr@1.3.0: + swr@2.2.5: resolution: { - integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==, + integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==, } peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 @@ -4303,6 +4312,14 @@ packages: peerDependencies: browserslist: ">= 4.21.0" + use-sync-external-store@1.2.2: + resolution: + { + integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==, + } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + vfile-message@4.0.2: resolution: { @@ -7096,6 +7113,8 @@ snapshots: dot-case: 3.0.4 tslib: 2.6.3 + sockette@2.0.6: {} + source-map-js@1.2.0: {} source-map-support@0.5.21: @@ -7130,9 +7149,11 @@ snapshots: svg-parser@2.0.4: {} - swr@1.3.0(react@18.3.1): + swr@2.2.5(react@18.3.1): dependencies: + client-only: 0.0.1 react: 18.3.1 + use-sync-external-store: 1.2.2(react@18.3.1) systemjs@6.15.1: {} @@ -7237,6 +7258,10 @@ snapshots: escalade: 3.1.2 picocolors: 1.0.1 + use-sync-external-store@1.2.2(react@18.3.1): + dependencies: + react: 18.3.1 + vfile-message@4.0.2: dependencies: "@types/unist": 3.0.2 diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx index fce7967..6b8dd32 100644 --- a/src/components/layout/layout-traffic.tsx +++ b/src/components/layout/layout-traffic.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useRef } from "react"; import { Box, Typography } from "@mui/material"; import { ArrowDownward, @@ -10,8 +10,14 @@ import { useVerge } from "@/hooks/use-verge"; import { TrafficGraph, type TrafficRef } from "./traffic-graph"; import { useLogSetup } from "./use-log-setup"; import { useVisibility } from "@/hooks/use-visibility"; -import { useWebsocket } from "@/hooks/use-websocket"; import parseTraffic from "@/utils/parse-traffic"; +import useSWRSubscription from "swr/subscription"; +import Sockette from "sockette"; + +interface MemoryUsage { + inuse: number; + oslimit?: number; +} // setup the traffic export const LayoutTraffic = () => { @@ -22,51 +28,92 @@ export const LayoutTraffic = () => { const trafficGraph = verge?.traffic_graph ?? true; const trafficRef = useRef(null); - const [traffic, setTraffic] = useState({ up: 0, down: 0 }); - const [memory, setMemory] = useState({ inuse: 0 }); const pageVisible = useVisibility(); // setup log ws during layout useLogSetup(); - const trafficWs = useWebsocket( - (event) => { - const data = JSON.parse(event.data) as ITrafficItem; - trafficRef.current?.appendData(data); - setTraffic(data); + const { data: traffic = { up: 0, down: 0 } } = useSWRSubscription< + ITrafficItem, + any, + "getRealtimeTraffic" | null + >( + clashInfo && pageVisible ? "getRealtimeTraffic" : null, + (_key, { next }) => { + const { server = "", secret = "" } = clashInfo!; + + let errorCount = 10; + + const s = new Sockette( + `ws://${server}/traffic?token=${encodeURIComponent(secret)}`, + { + onmessage(event) { + errorCount = 0; // reset counter + const data = JSON.parse(event.data) as ITrafficItem; + trafficRef.current?.appendData(data); + next(null, data); + }, + onerror(event) { + errorCount -= 1; + + if (errorCount <= 0) { + this.close(); + next(event, { up: 0, down: 0 }); + } + }, + } + ); + + return () => { + s.close(); + }; }, - { onError: () => setTraffic({ up: 0, down: 0 }), errorCount: 10 } + { + fallbackData: { up: 0, down: 0 }, + keepPreviousData: true, + } ); - useEffect(() => { - if (!clashInfo || !pageVisible) return; - - const { server = "", secret = "" } = clashInfo; - trafficWs.connect( - `ws://${server}/traffic?token=${encodeURIComponent(secret)}` - ); - return () => trafficWs.disconnect(); - }, [clashInfo, pageVisible]); - /* --------- meta memory information --------- */ const isMetaCore = verge?.clash_core?.includes("clash-meta"); const displayMemory = isMetaCore && (verge?.enable_memory_usage ?? true); - const memoryWs = useWebsocket( - (event) => { - setMemory(JSON.parse(event.data)); - }, - { onError: () => setMemory({ inuse: 0 }), errorCount: 10 } - ); + const { data: memory = { inuse: 0 } } = useSWRSubscription< + MemoryUsage, + any, + "getRealtimeMemory" | null + >( + clashInfo && pageVisible && displayMemory ? "getRealtimeMemory" : null, + (_key, { next }) => { + const { server = "", secret = "" } = clashInfo!; + const ws = new WebSocket( + `ws://${server}/memory?token=${encodeURIComponent(secret)}` + ); - useEffect(() => { - if (!clashInfo || !pageVisible || !displayMemory) return; - const { server = "", secret = "" } = clashInfo; - memoryWs.connect( - `ws://${server}/memory?token=${encodeURIComponent(secret)}` - ); - return () => memoryWs.disconnect(); - }, [clashInfo, pageVisible, displayMemory]); + let errorCount = 10; + + ws.addEventListener("message", (event) => { + errorCount = 0; // reset counter + next(null, JSON.parse(event.data)); + }); + ws.addEventListener("error", (event) => { + errorCount -= 1; + + if (errorCount <= 0) { + ws.close(); + next(event, { inuse: 0 }); + } + }); + + return () => { + ws.close(); + }; + }, + { + fallbackData: { inuse: 0 }, + keepPreviousData: true, + } + ); const [up, upUnit] = parseTraffic(traffic.up); const [down, downUnit] = parseTraffic(traffic.down);