refactor: add debounce to optimize config updates and provider refresh handling

This commit is contained in:
wonfen
2025-05-25 21:34:48 +08:00
Unverified
parent d3dbc11b1b
commit af1689ee07
7 changed files with 207 additions and 21 deletions

View File

@@ -10,7 +10,6 @@ use crate::{
/// 获取配置文件列表
#[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> {
let _ = Tray::global().update_menu();
Ok(Config::profiles().data().clone())
}
@@ -154,11 +153,25 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
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) = &current_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<bool> {
// 静默恢复,不触发验证
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, "成功恢复到之前的配置");
}

View File

@@ -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<Mutex<Option<Instant>>> = Lazy::new(|| Mutex::new(None));
static LAST_EVENT_TIME: Lazy<Mutex<Option<Instant>>> = 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<serde_json::Value> {
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<serde_json::Value> {
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)
}

View File

@@ -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() {

View File

@@ -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<RwLock<Option<broadcast::Sender<()>>>>,
is_subscribed: Arc<RwLock<bool>>,
pub rate_cache: Arc<Mutex<Option<Rate>>>,
last_menu_update: Mutex<Option<Instant>>,
menu_updating: AtomicBool,
}
#[cfg(not(target_os = "macos"))]
pub struct Tray {}
pub struct Tray {
last_menu_update: Mutex<Option<Instant>>,
menu_updating: AtomicBool,
}
impl TrayState {
pub fn get_common_tray_icon() -> (bool, Vec<u8>) {
@@ -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 => {

View File

@@ -170,6 +170,8 @@ const Layout = () => {
// 设置监听器
useEffect(() => {
let providersDebounceTimer: ReturnType<typeof setTimeout> | 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());

View File

@@ -297,17 +297,28 @@ const ProfilePage = () => {
// 监听后端配置变更
useEffect(() => {
let unlistenPromise: Promise<() => void> | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const setupListener = async () => {
unlistenPromise = listen<string>('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]);

View File

@@ -198,6 +198,11 @@ export const getProxyProviders = async () => {
const response = await invoke<{
providers: Record<string, IProxyProviderItem>;
}>("get_providers_proxies");
if (!response || !response.providers) {
console.warn("getProxyProviders: Invalid response structure, returning empty object");
return {};
}
const providers = response.providers as Record<string, IProxyProviderItem>;
return Object.fromEntries(