From af1689ee07b0febff200249a1094dc55420cc660 Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 25 May 2025 21:34:48 +0800 Subject: [PATCH] refactor: add debounce to optimize config updates and provider refresh handling --- src-tauri/src/cmd/profile.rs | 29 +++++++++-- src-tauri/src/cmd/proxy.rs | 93 ++++++++++++++++++++++++++++++---- src-tauri/src/core/handle.rs | 16 ++++++ src-tauri/src/core/tray/mod.rs | 54 ++++++++++++++++++-- src/pages/_layout.tsx | 18 +++++++ src/pages/profiles.tsx | 13 ++++- src/services/api.ts | 5 ++ 7 files changed, 207 insertions(+), 21 deletions(-) diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index 165e25c0..93b3001c 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -10,7 +10,6 @@ use crate::{ /// 获取配置文件列表 #[tauri::command] pub fn get_profiles() -> CmdResult { - let _ = Tray::global().update_menu(); Ok(Config::profiles().data().clone()) } @@ -154,11 +153,25 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { match CoreManager::global().update_config().await { Ok((true, _)) => { logging!(info, Type::Cmd, true, "配置更新成功"); - handle::Handle::refresh_clash(); - let _ = Tray::global().update_tooltip(); Config::profiles().apply(); - wrap_err!(Config::profiles().data().save_file())?; + handle::Handle::refresh_clash(); + crate::process::AsyncHandler::spawn(|| async move { + if let Err(e) = Tray::global().update_tooltip() { + log::warn!(target: "app", "异步更新托盘提示失败: {}", e); + } + + if let Err(e) = Tray::global().update_menu() { + log::warn!(target: "app", "异步更新托盘菜单失败: {}", e); + } + + // 保存配置文件 + if let Err(e) = Config::profiles().data().save_file() { + log::warn!(target: "app", "异步保存配置文件失败: {}", e); + } + }); + + // 立即通知前端配置变更 if let Some(current) = ¤t_value { logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current); handle::Handle::notify_profile_changed(current.clone()); @@ -185,7 +198,13 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { // 静默恢复,不触发验证 wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?; Config::profiles().apply(); - wrap_err!(Config::profiles().data().save_file())?; + + crate::process::AsyncHandler::spawn(|| async move { + if let Err(e) = Config::profiles().data().save_file() { + log::warn!(target: "app", "异步保存恢复配置文件失败: {}", e); + } + }); + logging!(info, Type::Cmd, true, "成功恢复到之前的配置"); } diff --git a/src-tauri/src/cmd/proxy.rs b/src-tauri/src/cmd/proxy.rs index cee4c68b..94bc2dff 100644 --- a/src-tauri/src/cmd/proxy.rs +++ b/src-tauri/src/cmd/proxy.rs @@ -1,24 +1,95 @@ use super::CmdResult; -use crate::module::mihomo::MihomoManager; +use crate::{core::handle, module::mihomo::MihomoManager}; +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + +static LAST_REFRESH_TIME: Lazy>> = Lazy::new(|| Mutex::new(None)); +static LAST_EVENT_TIME: Lazy>> = Lazy::new(|| Mutex::new(None)); +static IS_REFRESHING: AtomicBool = AtomicBool::new(false); +const REFRESH_INTERVAL: Duration = Duration::from_secs(3); +const EVENT_INTERVAL: Duration = Duration::from_secs(1); #[tauri::command] pub async fn get_proxies() -> CmdResult { - let mannager = MihomoManager::global(); + let manager = MihomoManager::global(); - mannager + manager .refresh_proxies() .await - .map(|_| mannager.get_proxies()) - .or_else(|_| Ok(mannager.get_proxies())) + .map(|_| manager.get_proxies()) + .or_else(|_| Ok(manager.get_proxies())) } #[tauri::command] pub async fn get_providers_proxies() -> CmdResult { - let mannager = MihomoManager::global(); + let manager = MihomoManager::global(); + let cached_data = manager.get_providers_proxies(); - mannager - .refresh_providers_proxies() - .await - .map(|_| mannager.get_providers_proxies()) - .or_else(|_| Ok(mannager.get_providers_proxies())) + let safe_data = if cached_data.is_null() { + serde_json::json!({ + "providers": {} + }) + } else { + cached_data + }; + + // 检查是否需要刷新 + let should_refresh = { + let last_refresh = LAST_REFRESH_TIME.lock(); + match *last_refresh { + Some(last_time) => last_time.elapsed() > REFRESH_INTERVAL, + None => true, + } + }; + + if should_refresh && !IS_REFRESHING.load(Ordering::Acquire) { + IS_REFRESHING.store(true, Ordering::Release); + + crate::process::AsyncHandler::spawn(|| async move { + let manager = MihomoManager::global(); + match manager.refresh_providers_proxies().await { + Ok(_) => { + log::debug!(target: "app", "providers_proxies后台刷新成功"); + + let should_send_event = { + let mut last_event = LAST_EVENT_TIME.lock(); + match *last_event { + Some(last_time) => { + if last_time.elapsed() > EVENT_INTERVAL { + *last_event = Some(Instant::now()); + true + } else { + false + } + } + None => { + *last_event = Some(Instant::now()); + true + } + } + }; + if should_send_event { + handle::Handle::refresh_providers_proxies(); + log::debug!(target: "app", "已发送providers_proxies刷新事件"); + } else { + log::debug!(target: "app", "跳过providers_proxies事件发送(频率限制)"); + } + } + Err(e) => { + log::warn!(target: "app", "providers_proxies后台刷新失败: {}", e); + } + } + + { + let mut last_refresh = LAST_REFRESH_TIME.lock(); + *last_refresh = Some(Instant::now()); + } + + IS_REFRESHING.store(false, Ordering::Release); + }); + } + + Ok(safe_data) } diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 0ab9faee..5457a070 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -17,6 +17,7 @@ use crate::{logging, utils::logging::Type}; enum FrontendEvent { RefreshClash, RefreshVerge, + RefreshProvidersProxies, NoticeMessage { status: String, message: String }, ProfileChanged { current_profile_id: String }, TimerUpdated { profile_index: String }, @@ -121,6 +122,9 @@ impl NotificationSystem { FrontendEvent::RefreshVerge => { ("verge://refresh-verge-config", Ok(serde_json::json!("yes"))) } + FrontendEvent::RefreshProvidersProxies => { + ("verge://refresh-providers-proxies", Ok(serde_json::json!("yes"))) + } FrontendEvent::NoticeMessage { status, message } => { match serde_json::to_value((status, message)) { Ok(p) => ("verge://notice-message", Ok(p)), @@ -309,6 +313,18 @@ impl Handle { } } + pub fn refresh_providers_proxies() { + let handle = Self::global(); + if handle.is_exiting() { + return; + } + + let system_opt = handle.notification_system.read(); + if let Some(system) = system_opt.as_ref() { + system.send_event(FrontendEvent::RefreshProvidersProxies); + } + } + pub fn notify_profile_changed(profile_id: String) { let handle = Self::global(); if handle.is_exiting() { diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 13ad92f6..8cb946fe 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -18,15 +18,16 @@ use crate::{ use anyhow::Result; #[cfg(target_os = "macos")] use futures::StreamExt; -#[cfg(target_os = "macos")] use parking_lot::Mutex; #[cfg(target_os = "macos")] use parking_lot::RwLock; #[cfg(target_os = "macos")] pub use speed_rate::{SpeedRate, Traffic}; use std::fs; +use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(target_os = "macos")] use std::sync::Arc; +use std::time::{Duration, Instant}; use tauri::{ menu::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}, tray::{MouseButton, MouseButtonState, TrayIconEvent}, @@ -46,10 +47,15 @@ pub struct Tray { shutdown_tx: Arc>>>, is_subscribed: Arc>, pub rate_cache: Arc>>, + last_menu_update: Mutex>, + menu_updating: AtomicBool, } #[cfg(not(target_os = "macos"))] -pub struct Tray {} +pub struct Tray { + last_menu_update: Mutex>, + menu_updating: AtomicBool, +} impl TrayState { pub fn get_common_tray_icon() -> (bool, Vec) { @@ -164,10 +170,15 @@ impl Tray { shutdown_tx: Arc::new(RwLock::new(None)), is_subscribed: Arc::new(RwLock::new(false)), rate_cache: Arc::new(Mutex::new(None)), + last_menu_update: Mutex::new(None), + menu_updating: AtomicBool::new(false), }); #[cfg(not(target_os = "macos"))] - return TRAY.get_or_init(|| Tray {}); + return TRAY.get_or_init(|| Tray { + last_menu_update: Mutex::new(None), + menu_updating: AtomicBool::new(false), + }); } pub fn init(&self) -> Result<()> { @@ -192,8 +203,28 @@ impl Tray { Ok(()) } - /// 更新托盘菜单 + /// 更新托盘菜单(带频率限制) pub fn update_menu(&self) -> Result<()> { + // 检查是否正在更新或距离上次更新太近 + const MIN_UPDATE_INTERVAL: Duration = Duration::from_millis(500); + + // 检查是否已有更新任务在执行 + if self.menu_updating.load(Ordering::Acquire) { + log::debug!(target: "app", "托盘菜单正在更新中,跳过本次更新"); + return Ok(()); + } + + // 检查更新频率 + { + let last_update = self.last_menu_update.lock(); + if let Some(last_time) = *last_update { + if last_time.elapsed() < MIN_UPDATE_INTERVAL { + log::debug!(target: "app", "托盘菜单更新频率过高,跳过本次更新"); + return Ok(()); + } + } + } + let app_handle = match handle::Handle::global().app_handle() { Some(handle) => handle, None => { @@ -202,6 +233,20 @@ impl Tray { } }; + // 设置更新状态 + self.menu_updating.store(true, Ordering::Release); + + let result = self.update_menu_internal(&app_handle); + + { + let mut last_update = self.last_menu_update.lock(); + *last_update = Some(Instant::now()); + } + self.menu_updating.store(false, Ordering::Release); + + result + } + fn update_menu_internal(&self, app_handle: &AppHandle) -> Result<()> { let verge = Config::verge().latest().clone(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -230,6 +275,7 @@ impl Tray { profile_uid_and_name, is_lightweight_mode, )?)); + log::debug!(target: "app", "托盘菜单更新成功"); Ok(()) } None => { diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 3f2171a4..38087794 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -170,6 +170,8 @@ const Layout = () => { // 设置监听器 useEffect(() => { + let providersDebounceTimer: ReturnType | undefined; + const listeners = [ addListener("verge://refresh-clash-config", async () => { await getAxios(true); @@ -185,6 +187,18 @@ const Layout = () => { mutate("getAutotemProxy"); }), + addListener("verge://refresh-providers-proxies", () => { + if (providersDebounceTimer) { + clearTimeout(providersDebounceTimer); + } + + providersDebounceTimer = setTimeout(() => { + console.log('[Layout] Debounced refresh-providers-proxies event'); + mutate("getProxyProviders"); + providersDebounceTimer = undefined; + }, 500); + }), + addListener("verge://notice-message", ({ payload }) => handleNotice(payload as [string, string]), ), @@ -206,6 +220,10 @@ const Layout = () => { const cleanupWindow = setupWindowListeners(); return () => { + if (providersDebounceTimer) { + clearTimeout(providersDebounceTimer); + } + listeners.forEach((listener) => { if (typeof listener.then === "function") { listener.then((unlisten) => unlisten()); diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index ab76cd0a..a3f0af13 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -297,17 +297,28 @@ const ProfilePage = () => { // 监听后端配置变更 useEffect(() => { let unlistenPromise: Promise<() => void> | undefined; + let timeoutId: ReturnType | undefined; const setupListener = async () => { unlistenPromise = listen('profile-changed', (event) => { console.log('Profile changed event received:', event.payload); - mutateProfiles(); + if (timeoutId) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(() => { + mutateProfiles(); + timeoutId = undefined; + }, 300); }); }; setupListener(); return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } unlistenPromise?.then(unlisten => unlisten()); }; }, [mutateProfiles, t]); diff --git a/src/services/api.ts b/src/services/api.ts index 67489d78..ea25d942 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -198,6 +198,11 @@ export const getProxyProviders = async () => { const response = await invoke<{ providers: Record; }>("get_providers_proxies"); + if (!response || !response.providers) { + console.warn("getProxyProviders: Invalid response structure, returning empty object"); + return {}; + } + const providers = response.providers as Record; return Object.fromEntries(