From 262b6f8adfd862c42d954383dca33f9a68e8451a Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:43:05 +0800 Subject: [PATCH] fix: unexpected latency when switching nodes #6363 --- src-tauri/src/cmd/proxy.rs | 50 ++++++++++++--- src/hooks/use-proxy-selection.ts | 106 +++++++++++++++++++++++-------- 2 files changed, 122 insertions(+), 34 deletions(-) diff --git a/src-tauri/src/cmd/proxy.rs b/src-tauri/src/cmd/proxy.rs index 0d74667e5..327d7cc6a 100644 --- a/src-tauri/src/cmd/proxy.rs +++ b/src-tauri/src/cmd/proxy.rs @@ -1,20 +1,52 @@ use super::CmdResult; +use crate::core::tray::Tray; +use crate::process::AsyncHandler; use clash_verge_logging::{Type, logging}; +use std::sync::atomic::{AtomicBool, Ordering}; + +static TRAY_SYNC_RUNNING: AtomicBool = AtomicBool::new(false); +static TRAY_SYNC_PENDING: AtomicBool = AtomicBool::new(false); -// TODO: 前端通过 emit 发送更新事件, tray 监听更新事件 /// 同步托盘和GUI的代理选择状态 #[tauri::command] pub async fn sync_tray_proxy_selection() -> CmdResult<()> { - use crate::core::tray::Tray; + if TRAY_SYNC_RUNNING + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + AsyncHandler::spawn(move || async move { + run_tray_sync_loop().await; + }); + } else { + TRAY_SYNC_PENDING.store(true, Ordering::Release); + } - match Tray::global().update_menu().await { - Ok(_) => { - logging!(info, Type::Cmd, "Tray proxy selection synced successfully"); - Ok(()) + Ok(()) +} + +async fn run_tray_sync_loop() { + loop { + match Tray::global().update_menu().await { + Ok(_) => { + logging!(info, Type::Cmd, "Tray proxy selection synced successfully"); + } + Err(e) => { + logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}"); + } } - Err(e) => { - logging!(error, Type::Cmd, "Failed to sync tray proxy selection: {e}"); - Err(e.to_string().into()) + + if !TRAY_SYNC_PENDING.swap(false, Ordering::AcqRel) { + TRAY_SYNC_RUNNING.store(false, Ordering::Release); + + if TRAY_SYNC_PENDING.swap(false, Ordering::AcqRel) + && TRAY_SYNC_RUNNING + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + continue; + } + + break; } } } diff --git a/src/hooks/use-proxy-selection.ts b/src/hooks/use-proxy-selection.ts index f0761c6e4..0b689828e 100644 --- a/src/hooks/use-proxy-selection.ts +++ b/src/hooks/use-proxy-selection.ts @@ -1,5 +1,4 @@ -import { useLockFn } from "ahooks"; -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useRef } from "react"; import { closeConnection, getConnections, @@ -34,10 +33,19 @@ interface ProxySelectionOptions { enableConnectionCleanup?: boolean; } +interface ProxyChangeRequest { + groupName: string; + proxyName: string; + previousProxy?: string; + skipConfigSave: boolean; +} + // 代理选择 Hook export const useProxySelection = (options: ProxySelectionOptions = {}) => { const { current, patchCurrent } = useProfiles(); const { verge } = useVerge(); + const pendingRequestRef = useRef(null); + const isProcessingRef = useRef(false); const { onSuccess, onError, enableConnectionCleanup = true } = options; @@ -51,36 +59,46 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { ); // 切换节点 - const changeProxy = useLockFn( - async ( - groupName: string, - proxyName: string, - previousProxy?: string, - skipConfigSave: boolean = false, - ) => { + const syncTraySelection = useCallback(() => { + syncTrayProxySelection().catch((error) => { + console.error("[ProxySelection] 托盘状态同步失败:", error); + }); + }, []); + + const persistSelection = useCallback( + (groupName: string, proxyName: string, skipConfigSave: boolean) => { + if (!current || skipConfigSave) return; + + const selected = current.selected ? [...current.selected] : []; + const index = selected.findIndex((item) => item.name === groupName); + + if (index < 0) { + selected.push({ name: groupName, now: proxyName }); + } else { + selected[index] = { name: groupName, now: proxyName }; + } + + patchCurrent({ selected }).catch((error) => { + console.error("[ProxySelection] 保存代理选择失败:", error); + }); + }, + [current, patchCurrent], + ); + + const executeChange = useCallback( + async (request: ProxyChangeRequest) => { + const { groupName, proxyName, previousProxy, skipConfigSave } = request; debugLog(`[ProxySelection] 代理切换: ${groupName} -> ${proxyName}`); try { - if (current && !skipConfigSave) { - const selected = current.selected ? [...current.selected] : []; - const index = selected.findIndex((item) => item.name === groupName); - - if (index < 0) { - selected.push({ name: groupName, now: proxyName }); - } else { - selected[index] = { name: groupName, now: proxyName }; - } - await patchCurrent({ selected }); - } - await selectNodeForGroup(groupName, proxyName); - await syncTrayProxySelection(); + onSuccess?.(); + syncTraySelection(); + persistSelection(groupName, proxyName, skipConfigSave); debugLog( `[ProxySelection] 代理和状态同步完成: ${groupName} -> ${proxyName}`, ); - onSuccess?.(); - if ( config.enableConnectionCleanup && config.autoCloseConnection && @@ -96,8 +114,9 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { try { await selectNodeForGroup(groupName, proxyName); - await syncTrayProxySelection(); onSuccess?.(); + syncTraySelection(); + persistSelection(groupName, proxyName, skipConfigSave); debugLog( `[ProxySelection] 代理切换回退成功: ${groupName} -> ${proxyName}`, ); @@ -110,6 +129,43 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => { } } }, + [config, onError, onSuccess, persistSelection, syncTraySelection], + ); + + const flushChangeQueue = useCallback(async () => { + if (isProcessingRef.current) return; + isProcessingRef.current = true; + + try { + while (pendingRequestRef.current) { + const request = pendingRequestRef.current; + pendingRequestRef.current = null; + await executeChange(request); + } + } finally { + isProcessingRef.current = false; + if (pendingRequestRef.current) { + void flushChangeQueue(); + } + } + }, [executeChange]); + + const changeProxy = useCallback( + ( + groupName: string, + proxyName: string, + previousProxy?: string, + skipConfigSave: boolean = false, + ) => { + pendingRequestRef.current = { + groupName, + proxyName, + previousProxy, + skipConfigSave, + }; + void flushChangeQueue(); + }, + [flushChangeQueue], ); const handleSelectChange = useCallback(