Compare commits

...

3 Commits

Author SHA1 Message Date
renovate[bot]
acf7ddf896
chore(deps): lock file maintenance 2026-04-06 17:14:32 +00:00
Tunglies
1005baabe6
feat: add babel-plugin-react-compiler and configure Vite for optimized chunking 2026-04-07 01:11:46 +08:00
Tunglies
3aa39bff94
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:🧵: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)
2026-04-06 12:20:16 +08:00
8 changed files with 408 additions and 570 deletions

View File

@ -92,8 +92,9 @@
"@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.0.0",
"@vitejs/plugin-react": "^6.0.1",
"axios": "^1.13.6",
"adm-zip": "^0.5.16",
"axios": "^1.13.6",
"babel-plugin-react-compiler": "^1.0.0",
"cli-color": "^2.0.4",
"commander": "^14.0.3",
"cross-env": "^10.1.0",

785
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ use crate::{
core::{CoreManager, handle, tray},
feat::clean_async,
process::AsyncHandler,
utils::{self, resolve::reset_resolve_done},
utils,
};
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::{Mapping, Value};
@ -42,7 +42,6 @@ pub async fn restart_app() {
if cleanup_result { 0 } else { 1 }
);
reset_resolve_done();
let app_handle = handle::Handle::app_handle();
app_handle.restart();
}

View File

@ -251,7 +251,6 @@ pub fn run() {
resolve::resolve_setup_async();
resolve::resolve_setup_sync();
resolve::init_signal();
resolve::resolve_done();
logging!(info, Type::Setup, "初始化已启动");
Ok(())

View File

@ -62,14 +62,9 @@ pub fn resolve_setup_async() {
init_system_proxy_guard().await;
});
let tray_init = async {
init_tray().await;
refresh_tray_menu().await;
};
let _ = futures::join!(
core_init,
tray_init,
init_tray(),
init_timer(),
init_hotkey(),
init_auto_lightweight_boot(),
@ -79,6 +74,7 @@ pub fn resolve_setup_async() {
Handle::refresh_clash();
refresh_tray_menu().await;
resolve_done();
});
}
@ -220,7 +216,3 @@ pub fn resolve_done() {
pub fn is_resolve_done() -> bool {
RESOLVE_DONE.load(Ordering::Acquire)
}
pub fn reset_resolve_done() {
RESOLVE_DONE.store(false, Ordering::Release);
}

View File

@ -129,7 +129,7 @@ impl WindowManager {
logging!(info, Type::Window, "窗口不存在,创建新窗口");
if Self::create_window(true).await {
logging!(info, Type::Window, "窗口创建成功");
std::thread::sleep(std::time::Duration::from_millis(50));
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
WindowOperationResult::Created
} else {
logging!(warn, Type::Window, "窗口创建失败");
@ -286,11 +286,11 @@ impl WindowManager {
/// 创建新窗口,防抖避免重复调用
/// 窗口创建后保持隐藏,由前端 index.html 在 overlay 渲染后调用 show避免主题闪烁
pub fn create_window(is_show: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
pub fn create_window(should_create: bool) -> Pin<Box<dyn Future<Output = bool> + Send>> {
Box::pin(async move {
logging!(info, Type::Window, "开始创建/显示主窗口, is_show={}", is_show);
logging!(info, Type::Window, "开始创建主窗口, should_create={}", should_create);
if !is_show {
if !should_create {
return false;
}

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'],

View File

@ -10,7 +10,11 @@ export default defineConfig({
server: { port: 3000 },
plugins: [
svgr(),
react(),
react({
babel: {
plugins: ['babel-plugin-react-compiler'],
},
} as any),
legacy({
modernTargets: ['edge>=109', 'safari>=14'],
renderLegacyChunks: false,
@ -26,6 +30,24 @@ export default defineConfig({
outDir: '../dist',
emptyOutDir: true,
chunkSizeWarningLimit: 4000,
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('monaco-yaml')) return 'monaco-yaml'
if (
id.includes('node_modules/react/') ||
id.includes('node_modules/react-dom/')
)
return 'react'
if (id.includes('node_modules/react-router')) return 'router'
if (
id.includes('node_modules/i18next') ||
id.includes('node_modules/react-i18next')
)
return 'i18n'
},
},
},
},
resolve: {
alias: {
@ -36,4 +58,13 @@ export default defineConfig({
define: {
OS_PLATFORM: `"${process.platform}"`,
},
optimizeDeps: {
include: [
'react',
'react-dom',
'react-router-dom',
'i18next',
'react-i18next',
],
},
})