refactor: fix startup init chain — resolve_done semantics, dedupe events, cleanup

Backend:
- Move resolve_done() from sync setup() to async task after futures::join!
  so Timer waits for actual init completion instead of firing immediately
- Replace std::thread::sleep(50ms) with tokio::time::sleep in async context
- Remove duplicate refresh_tray_menu in tray_init (keep post-join call only)
- Delete dead code reset_resolve_done (process restarts, static is destroyed)
- Rename create_window(is_show) → create_window(should_create) for clarity

Frontend:
- Remove duplicate verge://refresh-clash-config listener from AppDataProvider
  (useLayoutEvents handles it via invalidateQueries — single consumer path)
- Stabilize useEffect deps with useRef for TQ refetch references
- Simplify AppDataProvider event listener setup (profile-changed + proxy only)
This commit is contained in:
Tunglies
2026-04-06 12:20:16 +08:00
parent 437fef1c30
commit 3aa39bff94
5 changed files with 29 additions and 128 deletions
+22 -111
View File
@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import { listen } from '@tauri-apps/api/event'
import React, { useCallback, useEffect, useMemo } from 'react'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import {
getBaseConfig,
getRuleProviders,
@@ -79,106 +79,45 @@ export const AppDataProvider = ({
...TQ_MIHOMO,
})
const refreshProxyRef = useRef(refreshProxy)
const refreshRulesRef = useRef(refreshRules)
const refreshRuleProvidersRef = useRef(refreshRuleProviders)
useEffect(() => {
refreshProxyRef.current = refreshProxy
}, [refreshProxy])
useEffect(() => {
refreshRulesRef.current = refreshRules
}, [refreshRules])
useEffect(() => {
refreshRuleProvidersRef.current = refreshRuleProviders
}, [refreshRuleProviders])
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)
refreshRulesRef.current().catch(() => {})
refreshRuleProvidersRef.current().catch(() => {})
}
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)
refreshProxyRef.current().catch(() => {})
}
const initializeListeners = async () => {
@@ -187,62 +126,34 @@ export const AppDataProvider = ({
'profile-changed',
handleProfileChanged,
)
registerCleanup(unlistenProfile)
cleanupFns.push(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()
})
cleanupFns.push(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) => {
cleanupFns.forEach((fn) => {
try {
fn()
} catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)))
console.error('[DataProvider] Cleanup error:', 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'],