fix: stop idle mihomo websocket subscriptions

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