mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 05:40:47 +08:00
fix: stop idle mihomo websocket subscriptions
This commit is contained in:
@@ -147,18 +147,19 @@ export const EnhancedTrafficStats = () => {
|
|||||||
|
|
||||||
// 是否显示流量图表
|
// 是否显示流量图表
|
||||||
const trafficGraph = verge?.traffic_graph ?? true
|
const trafficGraph = verge?.traffic_graph ?? true
|
||||||
|
const displayMemory = verge?.enable_memory_usage ?? true
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: traffic },
|
response: { data: traffic },
|
||||||
} = useTrafficData({ enabled: trafficGraph && pageVisible })
|
} = useTrafficData({ enabled: pageVisible })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: memory },
|
response: { data: memory },
|
||||||
} = useMemoryData()
|
} = useMemoryData({ enabled: displayMemory && pageVisible })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: connectionSummary },
|
response: { data: connectionSummary },
|
||||||
} = useConnectionSummaryData()
|
} = useConnectionSummaryData({ enabled: pageVisible })
|
||||||
|
|
||||||
// Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点
|
// Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点
|
||||||
|
|
||||||
@@ -211,8 +212,8 @@ export const EnhancedTrafficStats = () => {
|
|||||||
}, [trafficGraph, pageVisible, theme.palette.divider])
|
}, [trafficGraph, pageVisible, theme.palette.divider])
|
||||||
|
|
||||||
// 使用useMemo计算统计卡片配置
|
// 使用useMemo计算统计卡片配置
|
||||||
const statCards = useMemo(
|
const statCards = useMemo(() => {
|
||||||
() => [
|
const cards: StatCardProps[] = [
|
||||||
{
|
{
|
||||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||||
title: t('home.components.traffic.metrics.uploadSpeed'),
|
title: t('home.components.traffic.metrics.uploadSpeed'),
|
||||||
@@ -248,17 +249,20 @@ export const EnhancedTrafficStats = () => {
|
|||||||
unit: parsedData.downloadTotalUnit,
|
unit: parsedData.downloadTotalUnit,
|
||||||
color: 'primary' as const,
|
color: 'primary' as const,
|
||||||
},
|
},
|
||||||
{
|
]
|
||||||
|
|
||||||
|
if (displayMemory) {
|
||||||
|
cards.push({
|
||||||
icon: <MemoryRounded fontSize="small" />,
|
icon: <MemoryRounded fontSize="small" />,
|
||||||
title: t('home.components.traffic.metrics.memoryUsage'),
|
title: t('home.components.traffic.metrics.memoryUsage'),
|
||||||
value: parsedData.inuse,
|
value: parsedData.inuse,
|
||||||
unit: parsedData.inuseUnit,
|
unit: parsedData.inuseUnit,
|
||||||
color: 'error' as const,
|
color: 'error' as const,
|
||||||
onClick: undefined,
|
})
|
||||||
},
|
}
|
||||||
],
|
|
||||||
[t, parsedData],
|
return cards
|
||||||
)
|
}, [t, parsedData, displayMemory])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrafficErrorBoundary
|
<TrafficErrorBoundary
|
||||||
@@ -276,7 +280,7 @@ export const EnhancedTrafficStats = () => {
|
|||||||
{/* 统计卡片区域 */}
|
{/* 统计卡片区域 */}
|
||||||
{statCards.map((card) => (
|
{statCards.map((card) => (
|
||||||
<Grid key={card.title} size={4}>
|
<Grid key={card.title} size={4}>
|
||||||
<CompactStatCard {...(card as StatCardProps)} />
|
<CompactStatCard {...card} />
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -24,16 +24,17 @@ export const LayoutTraffic = () => {
|
|||||||
|
|
||||||
// whether hide traffic graph
|
// whether hide traffic graph
|
||||||
const trafficGraph = verge?.traffic_graph ?? true
|
const trafficGraph = verge?.traffic_graph ?? true
|
||||||
|
const displayMemory = verge?.enable_memory_usage ?? true
|
||||||
|
|
||||||
const trafficRef = useRef<TrafficRef>(null)
|
const trafficRef = useRef<TrafficRef>(null)
|
||||||
const pageVisible = useVisibility()
|
const pageVisible = useVisibility()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: traffic },
|
response: { data: traffic },
|
||||||
} = useTrafficData({ enabled: trafficGraph && pageVisible })
|
} = useTrafficData({ enabled: pageVisible })
|
||||||
const {
|
const {
|
||||||
response: { data: memory },
|
response: { data: memory },
|
||||||
} = useMemoryData()
|
} = useMemoryData({ enabled: displayMemory && pageVisible })
|
||||||
|
|
||||||
// 监听数据变化,为图表添加数据点
|
// 监听数据变化,为图表添加数据点
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,9 +48,6 @@ export const LayoutTraffic = () => {
|
|||||||
}
|
}
|
||||||
}, [traffic])
|
}, [traffic])
|
||||||
|
|
||||||
// 显示内存使用情况的设置
|
|
||||||
const displayMemory = verge?.enable_memory_usage ?? true
|
|
||||||
|
|
||||||
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
|
// 使用parseTraffic统一处理转换,保持与首页一致的显示格式
|
||||||
const [up, upUnit] = parseTraffic(traffic?.up || 0)
|
const [up, upUnit] = parseTraffic(traffic?.up || 0)
|
||||||
const [down, downUnit] = parseTraffic(traffic?.down || 0)
|
const [down, downUnit] = parseTraffic(traffic?.down || 0)
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export const initConnSummaryData: ConnectionSummaryData = {
|
|||||||
let connectionData: ConnectionMonitorData = initConnData
|
let connectionData: ConnectionMonitorData = initConnData
|
||||||
let connectionSummary: ConnectionSummaryData = initConnSummaryData
|
let connectionSummary: ConnectionSummaryData = initConnSummaryData
|
||||||
let connectionSocket: MihomoWebSocket | null = null
|
let connectionSocket: MihomoWebSocket | null = null
|
||||||
let connectionStarted = false
|
|
||||||
let connectionConnecting = false
|
let connectionConnecting = false
|
||||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let flushTimer: ReturnType<typeof setTimeout> | null = null
|
let flushTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -53,6 +52,9 @@ const notifySummaryListeners = () => {
|
|||||||
summaryListeners.forEach((listener) => listener())
|
summaryListeners.forEach((listener) => listener())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasConnectionSubscribers = () =>
|
||||||
|
connectionListeners.size > 0 || summaryListeners.size > 0
|
||||||
|
|
||||||
const sameMetadata = (left: ConnectionMetadata, right: ConnectionMetadata) =>
|
const sameMetadata = (left: ConnectionMetadata, right: ConnectionMetadata) =>
|
||||||
metadataValue(left.network) === metadataValue(right.network) &&
|
metadataValue(left.network) === metadataValue(right.network) &&
|
||||||
metadataValue(left.type) === metadataValue(right.type) &&
|
metadataValue(left.type) === metadataValue(right.type) &&
|
||||||
@@ -215,7 +217,7 @@ const flushPendingMessage = () => {
|
|||||||
flushTimer = null
|
flushTimer = null
|
||||||
const messageData = pendingMessageData
|
const messageData = pendingMessageData
|
||||||
pendingMessageData = null
|
pendingMessageData = null
|
||||||
if (!messageData) return
|
if (!messageData || !hasConnectionSubscribers()) return
|
||||||
|
|
||||||
let payload: IConnections
|
let payload: IConnections
|
||||||
try {
|
try {
|
||||||
@@ -270,6 +272,7 @@ const closeConnectionSocket = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scheduleReconnect = () => {
|
const scheduleReconnect = () => {
|
||||||
|
if (!hasConnectionSubscribers()) return
|
||||||
if (reconnectTimer) return
|
if (reconnectTimer) return
|
||||||
reconnectTimer = window.setTimeout(() => {
|
reconnectTimer = window.setTimeout(() => {
|
||||||
reconnectTimer = null
|
reconnectTimer = null
|
||||||
@@ -278,20 +281,27 @@ const scheduleReconnect = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function reconnectConnectionSocket() {
|
async function reconnectConnectionSocket() {
|
||||||
|
if (!hasConnectionSubscribers()) return
|
||||||
await closeConnectionSocket()
|
await closeConnectionSocket()
|
||||||
scheduleReconnect()
|
scheduleReconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connectConnectionSocket() {
|
async function connectConnectionSocket() {
|
||||||
if (connectionSocket || connectionConnecting) return
|
if (connectionSocket || connectionConnecting) return
|
||||||
|
if (!hasConnectionSubscribers()) return
|
||||||
|
|
||||||
clearReconnectTimer()
|
clearReconnectTimer()
|
||||||
connectionConnecting = true
|
connectionConnecting = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const socket = await MihomoWebSocket.connect_connections()
|
const socket = await MihomoWebSocket.connect_connections()
|
||||||
|
if (!hasConnectionSubscribers()) {
|
||||||
|
await socket.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
connectionSocket = socket
|
connectionSocket = socket
|
||||||
socket.addListener((message) => {
|
socket.addListener((message) => {
|
||||||
|
if (connectionSocket !== socket) return
|
||||||
if (message.type !== 'Text') return
|
if (message.type !== 'Text') return
|
||||||
if (message.data.startsWith('Websocket error')) {
|
if (message.data.startsWith('Websocket error')) {
|
||||||
void reconnectConnectionSocket()
|
void reconnectConnectionSocket()
|
||||||
@@ -308,27 +318,39 @@ async function connectConnectionSocket() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startConnectionMonitor = () => {
|
const startConnectionMonitor = () => {
|
||||||
if (connectionStarted) return
|
|
||||||
connectionStarted = true
|
|
||||||
void connectConnectionSocket()
|
void connectConnectionSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopConnectionMonitorIfIdle = () => {
|
||||||
|
if (hasConnectionSubscribers()) return
|
||||||
|
|
||||||
|
clearReconnectTimer()
|
||||||
|
pendingMessageData = null
|
||||||
|
if (flushTimer) {
|
||||||
|
window.clearTimeout(flushTimer)
|
||||||
|
flushTimer = null
|
||||||
|
}
|
||||||
|
void closeConnectionSocket()
|
||||||
|
}
|
||||||
|
|
||||||
const getConnectionSnapshot = () => connectionData
|
const getConnectionSnapshot = () => connectionData
|
||||||
const getConnectionSummarySnapshot = () => connectionSummary
|
const getConnectionSummarySnapshot = () => connectionSummary
|
||||||
|
|
||||||
const subscribeConnectionData = (listener: ConnectionListener) => {
|
const subscribeConnectionData = (listener: ConnectionListener) => {
|
||||||
startConnectionMonitor()
|
|
||||||
connectionListeners.add(listener)
|
connectionListeners.add(listener)
|
||||||
|
startConnectionMonitor()
|
||||||
return () => {
|
return () => {
|
||||||
connectionListeners.delete(listener)
|
connectionListeners.delete(listener)
|
||||||
|
stopConnectionMonitorIfIdle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscribeConnectionSummary = (listener: ConnectionListener) => {
|
const subscribeConnectionSummary = (listener: ConnectionListener) => {
|
||||||
startConnectionMonitor()
|
|
||||||
summaryListeners.add(listener)
|
summaryListeners.add(listener)
|
||||||
|
startConnectionMonitor()
|
||||||
return () => {
|
return () => {
|
||||||
summaryListeners.delete(listener)
|
summaryListeners.delete(listener)
|
||||||
|
stopConnectionMonitorIfIdle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,9 +373,15 @@ const clearClosedConnectionData = () => {
|
|||||||
notifyConnectionListeners()
|
notifyConnectionListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useConnectionData = () => {
|
export const useConnectionData = (options?: { enabled?: boolean }) => {
|
||||||
|
const enabled = options?.enabled ?? true
|
||||||
|
const subscribe = useCallback(
|
||||||
|
(listener: ConnectionListener) =>
|
||||||
|
enabled ? subscribeConnectionData(listener) : () => {},
|
||||||
|
[enabled],
|
||||||
|
)
|
||||||
const data = useSyncExternalStore(
|
const data = useSyncExternalStore(
|
||||||
subscribeConnectionData,
|
subscribe,
|
||||||
getConnectionSnapshot,
|
getConnectionSnapshot,
|
||||||
getConnectionSnapshot,
|
getConnectionSnapshot,
|
||||||
)
|
)
|
||||||
@@ -372,9 +400,15 @@ export const useConnectionData = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useConnectionSummaryData = () => {
|
export const useConnectionSummaryData = (options?: { enabled?: boolean }) => {
|
||||||
|
const enabled = options?.enabled ?? true
|
||||||
|
const subscribe = useCallback(
|
||||||
|
(listener: ConnectionListener) =>
|
||||||
|
enabled ? subscribeConnectionSummary(listener) : () => {},
|
||||||
|
[enabled],
|
||||||
|
)
|
||||||
const data = useSyncExternalStore(
|
const data = useSyncExternalStore(
|
||||||
subscribeConnectionSummary,
|
subscribe,
|
||||||
getConnectionSummarySnapshot,
|
getConnectionSummarySnapshot,
|
||||||
getConnectionSummarySnapshot,
|
getConnectionSummarySnapshot,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ const shouldSkipDuplicateMemory = (memory: IMemoryUsageItem) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useMemoryData = () => {
|
export const useMemoryData = (options?: { enabled?: boolean }) => {
|
||||||
|
const enabled = options?.enabled ?? true
|
||||||
|
|
||||||
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
|
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
|
||||||
storageKey: 'mihomo_memory_date',
|
storageKey: 'mihomo_memory_date',
|
||||||
buildSubscriptKey: (date) => `getClashMemory-${date}`,
|
buildSubscriptKey: (date) => (enabled ? `getClashMemory-${date}` : null),
|
||||||
fallbackData: FALLBACK_MEMORY_USAGE,
|
fallbackData: FALLBACK_MEMORY_USAGE,
|
||||||
connect: () => MihomoWebSocket.connect_memory(),
|
connect: () => MihomoWebSocket.connect_memory(),
|
||||||
throttleMs: 500,
|
throttleMs: 500,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
|||||||
} = useTrafficMonitorEnhanced({ subscribe: false, enabled })
|
} = useTrafficMonitorEnhanced({ subscribe: false, enabled })
|
||||||
const { response, refresh } = useMihomoWsSubscription<ITrafficItem>({
|
const { response, refresh } = useMihomoWsSubscription<ITrafficItem>({
|
||||||
storageKey: 'mihomo_traffic_date',
|
storageKey: 'mihomo_traffic_date',
|
||||||
buildSubscriptKey: (date) => `getClashTraffic-${date}`,
|
buildSubscriptKey: (date) => (enabled ? `getClashTraffic-${date}` : null),
|
||||||
fallbackData: FALLBACK_TRAFFIC,
|
fallbackData: FALLBACK_TRAFFIC,
|
||||||
connect: () => MihomoWebSocket.connect_traffic(),
|
connect: () => MihomoWebSocket.connect_traffic(),
|
||||||
throttleMs: 200,
|
throttleMs: 200,
|
||||||
|
|||||||
+52
-12
@@ -1,28 +1,68 @@
|
|||||||
|
import { TauriEvent } from '@tauri-apps/api/event'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const isDocumentVisible = () =>
|
||||||
|
typeof document === 'undefined' || document.visibilityState === 'visible'
|
||||||
|
|
||||||
export const useVisibility = () => {
|
export const useVisibility = () => {
|
||||||
const [visible, setVisible] = useState(() =>
|
const [visible, setVisible] = useState(isDocumentVisible)
|
||||||
typeof document === 'undefined'
|
|
||||||
? true
|
|
||||||
: document.visibilityState === 'visible',
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleVisibilityChange = () => {
|
let mounted = true
|
||||||
setVisible(document.visibilityState === 'visible')
|
let visibilityTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const appWindow = getCurrentWindow()
|
||||||
|
|
||||||
|
const updateVisible = async () => {
|
||||||
|
const windowVisible = await appWindow.isVisible().catch(() => true)
|
||||||
|
if (mounted) {
|
||||||
|
setVisible(isDocumentVisible() && windowVisible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateVisibleSoon = () => {
|
||||||
|
if (visibilityTimer) {
|
||||||
|
window.clearTimeout(visibilityTimer)
|
||||||
|
}
|
||||||
|
visibilityTimer = window.setTimeout(() => {
|
||||||
|
visibilityTimer = null
|
||||||
|
void updateVisible()
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVisibleEvent = () => {
|
||||||
|
void updateVisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFocus = () => setVisible(true)
|
|
||||||
const handlePointerDown = () => setVisible(true)
|
const handlePointerDown = () => setVisible(true)
|
||||||
|
|
||||||
document.addEventListener('focus', handleFocus)
|
document.addEventListener('focus', handleVisibleEvent)
|
||||||
document.addEventListener('pointerdown', handlePointerDown)
|
document.addEventListener('pointerdown', handlePointerDown)
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
document.addEventListener('visibilitychange', handleVisibleEvent)
|
||||||
|
window.addEventListener('focus', handleVisibleEvent)
|
||||||
|
|
||||||
|
const unlistenFocusChanged = appWindow.onFocusChanged(updateVisibleSoon)
|
||||||
|
const unlistenCloseRequested = appWindow.listen(
|
||||||
|
TauriEvent.WINDOW_CLOSE_REQUESTED,
|
||||||
|
() => {
|
||||||
|
setVisible(false)
|
||||||
|
updateVisibleSoon()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
void updateVisible()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('focus', handleFocus)
|
mounted = false
|
||||||
|
if (visibilityTimer) {
|
||||||
|
window.clearTimeout(visibilityTimer)
|
||||||
|
visibilityTimer = null
|
||||||
|
}
|
||||||
|
document.removeEventListener('focus', handleVisibleEvent)
|
||||||
document.removeEventListener('pointerdown', handlePointerDown)
|
document.removeEventListener('pointerdown', handlePointerDown)
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
document.removeEventListener('visibilitychange', handleVisibleEvent)
|
||||||
|
window.removeEventListener('focus', handleVisibleEvent)
|
||||||
|
void unlistenFocusChanged.then((unlisten) => unlisten())
|
||||||
|
void unlistenCloseRequested.then((unlisten) => unlisten())
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { ConnectionTable } from '@/components/connection/connection-table'
|
|||||||
import { useConnectionData } from '@/hooks/use-connection-data'
|
import { useConnectionData } from '@/hooks/use-connection-data'
|
||||||
import { useConnectionSetting } from '@/hooks/use-connection-setting'
|
import { useConnectionSetting } from '@/hooks/use-connection-setting'
|
||||||
import { useTrafficData } from '@/hooks/use-traffic-data'
|
import { useTrafficData } from '@/hooks/use-traffic-data'
|
||||||
|
import { useVisibility } from '@/hooks/use-visibility'
|
||||||
import parseTraffic from '@/utils/parse-traffic'
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
|
|
||||||
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]
|
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[]
|
||||||
@@ -80,6 +81,7 @@ const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
|
|||||||
const EMPTY_CONNECTIONS: IConnectionsItem[] = []
|
const EMPTY_CONNECTIONS: IConnectionsItem[] = []
|
||||||
const ConnectionsPage = () => {
|
const ConnectionsPage = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const pageVisible = useVisibility()
|
||||||
const [match, setMatch] = useState<(input: string) => boolean>(
|
const [match, setMatch] = useState<(input: string) => boolean>(
|
||||||
() => () => true,
|
() => () => true,
|
||||||
)
|
)
|
||||||
@@ -92,10 +94,10 @@ const ConnectionsPage = () => {
|
|||||||
const {
|
const {
|
||||||
response: { data: connections },
|
response: { data: connections },
|
||||||
clearClosedConnections,
|
clearClosedConnections,
|
||||||
} = useConnectionData()
|
} = useConnectionData({ enabled: pageVisible })
|
||||||
const {
|
const {
|
||||||
response: { data: traffic },
|
response: { data: traffic },
|
||||||
} = useTrafficData()
|
} = useTrafficData({ enabled: pageVisible })
|
||||||
|
|
||||||
const [setting, setSetting] = useConnectionSetting()
|
const [setting, setSetting] = useConnectionSetting()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user