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
+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())
}
}, [])