mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-13 05:20:28 +08:00
- Change TQ_MIHOMO retryDelay from fixed 2000ms to exponential backoff (200ms → 400ms → 800ms, cap 3s) so core-dependent queries retry faster - Expose isCoreDataPending from AppDataProvider to distinguish between data still loading vs actual errors - ClashModeCard: show placeholder instead of "communication error" while core data is pending - CurrentProxyCard: show empty space instead of "no active node" while core data is pending
369 lines
9.3 KiB
TypeScript
369 lines
9.3 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
||
import { listen } from '@tauri-apps/api/event'
|
||
import React, { useCallback, useEffect, useMemo } from 'react'
|
||
import {
|
||
getBaseConfig,
|
||
getRuleProviders,
|
||
getRules,
|
||
} from 'tauri-plugin-mihomo-api'
|
||
|
||
import { useVerge } from '@/hooks/use-verge'
|
||
import {
|
||
calcuProxies,
|
||
calcuProxyProviders,
|
||
getAppUptime,
|
||
getRunningMode,
|
||
getSystemProxy,
|
||
} from '@/services/cmds'
|
||
|
||
import { AppDataContext, AppDataContextType } from './app-data-context'
|
||
|
||
const TQ_MIHOMO = {
|
||
refetchOnWindowFocus: false,
|
||
refetchOnReconnect: false,
|
||
staleTime: 1500,
|
||
retry: 3,
|
||
retryDelay: (attempt: number) => Math.min(200 * 2 ** attempt, 3000),
|
||
} as const
|
||
|
||
const TQ_DEFAULTS = {
|
||
refetchOnWindowFocus: false,
|
||
refetchOnReconnect: false,
|
||
staleTime: 5000,
|
||
retry: 2,
|
||
} as const
|
||
|
||
// 全局数据提供者组件
|
||
export const AppDataProvider = ({
|
||
children,
|
||
}: {
|
||
children: React.ReactNode
|
||
}) => {
|
||
const { verge } = useVerge()
|
||
|
||
const {
|
||
data: proxiesData,
|
||
isPending: isProxiesPending,
|
||
refetch: refreshProxy,
|
||
} = useQuery({
|
||
queryKey: ['getProxies'],
|
||
queryFn: calcuProxies,
|
||
...TQ_MIHOMO,
|
||
})
|
||
|
||
const {
|
||
data: clashConfig,
|
||
isPending: isClashConfigPending,
|
||
refetch: refreshClashConfig,
|
||
} = useQuery({
|
||
queryKey: ['getClashConfig'],
|
||
queryFn: getBaseConfig,
|
||
...TQ_MIHOMO,
|
||
})
|
||
|
||
const { data: proxyProviders, refetch: refreshProxyProviders } = useQuery({
|
||
queryKey: ['getProxyProviders'],
|
||
queryFn: calcuProxyProviders,
|
||
...TQ_MIHOMO,
|
||
})
|
||
|
||
const { data: ruleProviders, refetch: refreshRuleProviders } = useQuery({
|
||
queryKey: ['getRuleProviders'],
|
||
queryFn: getRuleProviders,
|
||
...TQ_MIHOMO,
|
||
})
|
||
|
||
const { data: rulesData, refetch: refreshRules } = useQuery({
|
||
queryKey: ['getRules'],
|
||
queryFn: getRules,
|
||
...TQ_MIHOMO,
|
||
})
|
||
|
||
useEffect(() => {
|
||
let lastProfileId: string | null = null
|
||
let lastUpdateTime = 0
|
||
const refreshThrottle = 800
|
||
|
||
let isUnmounted = false
|
||
const scheduledTimeouts = new Set<number>()
|
||
const cleanupFns: Array<() => void> = []
|
||
|
||
const registerCleanup = (fn: () => void) => {
|
||
if (isUnmounted) {
|
||
try {
|
||
fn()
|
||
} catch (error) {
|
||
console.error('[DataProvider] Immediate cleanup failed:', error)
|
||
}
|
||
} else {
|
||
cleanupFns.push(fn)
|
||
}
|
||
}
|
||
|
||
const addWindowListener = (eventName: string, handler: EventListener) => {
|
||
// eslint-disable-next-line @eslint-react/web-api-no-leaked-event-listener
|
||
window.addEventListener(eventName, handler)
|
||
return () => window.removeEventListener(eventName, handler)
|
||
}
|
||
|
||
const scheduleTimeout = (
|
||
callback: () => void | Promise<void>,
|
||
delay: number,
|
||
) => {
|
||
if (isUnmounted) return -1
|
||
|
||
const timeoutId = window.setTimeout(() => {
|
||
scheduledTimeouts.delete(timeoutId)
|
||
if (!isUnmounted) {
|
||
void callback()
|
||
}
|
||
}, delay)
|
||
|
||
scheduledTimeouts.add(timeoutId)
|
||
return timeoutId
|
||
}
|
||
|
||
const clearAllTimeouts = () => {
|
||
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId))
|
||
scheduledTimeouts.clear()
|
||
}
|
||
|
||
const handleProfileChanged = (event: { payload: string }) => {
|
||
const newProfileId = event.payload
|
||
const now = Date.now()
|
||
|
||
if (
|
||
lastProfileId === newProfileId &&
|
||
now - lastUpdateTime < refreshThrottle
|
||
) {
|
||
return
|
||
}
|
||
|
||
lastProfileId = newProfileId
|
||
lastUpdateTime = now
|
||
|
||
scheduleTimeout(() => {
|
||
refreshRules().catch((error) =>
|
||
console.warn('[DataProvider] Rules refresh failed:', error),
|
||
)
|
||
refreshRuleProviders().catch((error) =>
|
||
console.warn('[DataProvider] Rule providers refresh failed:', error),
|
||
)
|
||
}, 200)
|
||
}
|
||
|
||
const handleRefreshClash = () => {
|
||
const now = Date.now()
|
||
if (now - lastUpdateTime <= refreshThrottle) return
|
||
|
||
lastUpdateTime = now
|
||
scheduleTimeout(async () => {
|
||
await Promise.all([
|
||
refreshProxy().catch((error) =>
|
||
console.error('[DataProvider] Proxy refresh failed:', error),
|
||
),
|
||
refreshClashConfig().catch((error) =>
|
||
console.error('[DataProvider] Clash config refresh failed:', error),
|
||
),
|
||
])
|
||
}, 200)
|
||
}
|
||
|
||
const handleRefreshProxy = () => {
|
||
const now = Date.now()
|
||
if (now - lastUpdateTime <= refreshThrottle) return
|
||
|
||
lastUpdateTime = now
|
||
scheduleTimeout(() => {
|
||
refreshProxy().catch((error) =>
|
||
console.warn('[DataProvider] Proxy refresh failed:', error),
|
||
)
|
||
}, 200)
|
||
}
|
||
|
||
const initializeListeners = async () => {
|
||
try {
|
||
const unlistenProfile = await listen<string>(
|
||
'profile-changed',
|
||
handleProfileChanged,
|
||
)
|
||
registerCleanup(unlistenProfile)
|
||
} catch (error) {
|
||
console.error('[AppDataProvider] 监听 Profile 事件失败:', error)
|
||
}
|
||
|
||
try {
|
||
const unlistenClash = await listen(
|
||
'verge://refresh-clash-config',
|
||
handleRefreshClash,
|
||
)
|
||
const unlistenProxy = await listen(
|
||
'verge://refresh-proxy-config',
|
||
handleRefreshProxy,
|
||
)
|
||
|
||
registerCleanup(() => {
|
||
unlistenClash()
|
||
unlistenProxy()
|
||
})
|
||
} catch (error) {
|
||
console.warn('[AppDataProvider] 设置 Tauri 事件监听器失败:', error)
|
||
|
||
const fallbackHandlers: Array<[string, EventListener]> = [
|
||
['verge://refresh-clash-config', handleRefreshClash],
|
||
['verge://refresh-proxy-config', handleRefreshProxy],
|
||
]
|
||
|
||
fallbackHandlers.forEach(([eventName, handler]) => {
|
||
registerCleanup(addWindowListener(eventName, handler))
|
||
})
|
||
}
|
||
}
|
||
|
||
void initializeListeners()
|
||
|
||
return () => {
|
||
isUnmounted = true
|
||
clearAllTimeouts()
|
||
|
||
const errors: Error[] = []
|
||
cleanupFns.splice(0).forEach((fn) => {
|
||
try {
|
||
fn()
|
||
} catch (error) {
|
||
errors.push(error instanceof Error ? error : new Error(String(error)))
|
||
}
|
||
})
|
||
|
||
if (errors.length > 0) {
|
||
console.error(
|
||
`[DataProvider] ${errors.length} errors during cleanup:`,
|
||
errors,
|
||
)
|
||
}
|
||
}
|
||
}, [refreshProxy, refreshClashConfig, refreshRules, refreshRuleProviders])
|
||
|
||
const { data: sysproxy, refetch: refreshSysproxy } = useQuery({
|
||
queryKey: ['getSystemProxy'],
|
||
queryFn: getSystemProxy,
|
||
...TQ_DEFAULTS,
|
||
})
|
||
|
||
const { data: runningMode } = useQuery({
|
||
queryKey: ['getRunningMode'],
|
||
queryFn: getRunningMode,
|
||
...TQ_DEFAULTS,
|
||
})
|
||
|
||
const { data: uptimeData } = useQuery({
|
||
queryKey: ['appUptime'],
|
||
queryFn: getAppUptime,
|
||
...TQ_DEFAULTS,
|
||
refetchInterval: 3000,
|
||
retry: 1,
|
||
})
|
||
|
||
// 提供统一的刷新方法
|
||
const refreshAll = useCallback(async () => {
|
||
await Promise.all([
|
||
refreshProxy(),
|
||
refreshClashConfig(),
|
||
refreshRules(),
|
||
refreshSysproxy(),
|
||
refreshProxyProviders(),
|
||
refreshRuleProviders(),
|
||
])
|
||
}, [
|
||
refreshProxy,
|
||
refreshClashConfig,
|
||
refreshRules,
|
||
refreshSysproxy,
|
||
refreshProxyProviders,
|
||
refreshRuleProviders,
|
||
])
|
||
|
||
// 聚合所有数据
|
||
const value = useMemo(() => {
|
||
// 计算系统代理地址
|
||
const calculateSystemProxyAddress = () => {
|
||
if (!verge || !clashConfig) return '-'
|
||
|
||
const isPacMode = verge.proxy_auto_config ?? false
|
||
|
||
if (isPacMode) {
|
||
// PAC模式:显示我们期望设置的代理地址
|
||
const proxyHost = verge.proxy_host || '127.0.0.1'
|
||
const proxyPort =
|
||
verge.verge_mixed_port || clashConfig.mixedPort || 7897
|
||
return `${proxyHost}:${proxyPort}`
|
||
} else {
|
||
// HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址
|
||
const systemServer = sysproxy?.server
|
||
if (
|
||
systemServer &&
|
||
systemServer !== '-' &&
|
||
!systemServer.startsWith(':')
|
||
) {
|
||
return systemServer
|
||
} else {
|
||
// 系统地址无效,返回期望的代理地址
|
||
const proxyHost = verge.proxy_host || '127.0.0.1'
|
||
const proxyPort =
|
||
verge.verge_mixed_port || clashConfig.mixedPort || 7897
|
||
return `${proxyHost}:${proxyPort}`
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
// 数据
|
||
proxies: proxiesData,
|
||
clashConfig,
|
||
rules: rulesData?.rules ?? [],
|
||
sysproxy,
|
||
runningMode,
|
||
uptime: uptimeData || 0,
|
||
|
||
// 提供者数据
|
||
proxyProviders: proxyProviders || {},
|
||
ruleProviders: ruleProviders?.providers || {},
|
||
|
||
systemProxyAddress: calculateSystemProxyAddress(),
|
||
|
||
// core 数据加载状态
|
||
isCoreDataPending: isProxiesPending || isClashConfigPending,
|
||
|
||
// 刷新方法
|
||
refreshProxy,
|
||
refreshClashConfig,
|
||
refreshRules,
|
||
refreshSysproxy,
|
||
refreshProxyProviders,
|
||
refreshRuleProviders,
|
||
refreshAll,
|
||
} as AppDataContextType
|
||
}, [
|
||
proxiesData,
|
||
clashConfig,
|
||
isProxiesPending,
|
||
isClashConfigPending,
|
||
rulesData,
|
||
sysproxy,
|
||
runningMode,
|
||
uptimeData,
|
||
proxyProviders,
|
||
ruleProviders,
|
||
verge,
|
||
refreshProxy,
|
||
refreshClashConfig,
|
||
refreshRules,
|
||
refreshSysproxy,
|
||
refreshProxyProviders,
|
||
refreshRuleProviders,
|
||
refreshAll,
|
||
])
|
||
|
||
return <AppDataContext value={value}>{children}</AppDataContext>
|
||
}
|