diff --git a/UPDATELOG.md b/UPDATELOG.md index a8ee02f5..28ac07e7 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -8,6 +8,7 @@ - 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题 - 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步 - 修复导入订阅时非 http 协议链接被错误尝试导入 +- 修复切换节点后页面长时间 loading 及缓存过期导致的数据不同步问题 ### ✨ 新增功能 @@ -20,6 +21,10 @@ - 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死 - 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死 - 改进主页订阅卡流量已使用比例计算精度 +- 优化后端缓存刷新机制,支持毫秒级 TTL(默认 3000ms),减少重复请求并提升性能,切换节点时强制刷新后端数据,前端 UI 实时更新,操作更流畅 +- 解耦前端数据拉取与后端缓存刷新,提升节点切换速度和一致性 + +### 🐞 修复问题 ### 🗑️ 移除内容 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 65f5da01..bd213685 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1070,6 +1070,7 @@ dependencies = [ "base64 0.22.1", "boa_engine", "chrono", + "dashmap 6.1.0", "deelevate", "delay_timer", "dirs 6.0.0", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b66203d3..15b18212 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -82,6 +82,7 @@ sha2 = "0.10.9" hex = "0.4.3" scopeguard = "1.2.0" tauri-plugin-notification = "2.3.0" +dashmap = "6.1.0" [target.'cfg(windows)'.dependencies] runas = "=1.2.0" diff --git a/src-tauri/src/cmd/proxy.rs b/src-tauri/src/cmd/proxy.rs index 48be7c2c..1ba6eec4 100644 --- a/src-tauri/src/cmd/proxy.rs +++ b/src-tauri/src/cmd/proxy.rs @@ -1,99 +1,43 @@ use super::CmdResult; -use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState}; -use std::{ - sync::Mutex, - time::{Duration, Instant}, -}; -use tauri::Manager; +use crate::module::mihomo::MihomoManager; +use std::time::Duration; -const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3); -const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1); +use crate::state::proxy::ProxyRequestCache; + +const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60); +const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60); #[tauri::command] pub async fn get_proxies() -> CmdResult { let manager = MihomoManager::global(); - - let app_handle = handle::Handle::global().app_handle().unwrap(); - let cmd_proxy_state = app_handle.state::>(); - - let should_refresh = { - let mut state = cmd_proxy_state.lock().unwrap(); - let now = Instant::now(); - if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL { - state.need_refresh = true; - state.last_refresh_time = now; - } - state.need_refresh - }; - - if should_refresh { - let proxies = manager.get_refresh_proxies().await?; - { - let mut state = cmd_proxy_state.lock().unwrap(); - state.proxies = Box::new(proxies); - state.need_refresh = false; - } - log::debug!(target: "app", "proxies刷新成功"); - } - - let proxies = { - let state = cmd_proxy_state.lock().unwrap(); - state.proxies.clone() - }; - Ok(*proxies) + let cache = ProxyRequestCache::global(); + let key = ProxyRequestCache::make_key("proxies", "default"); + let value = cache + .get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async { + manager.get_refresh_proxies().await.expect("fetch failed") + }) + .await; + Ok((*value).clone()) } /// 强制刷新代理缓存用于profile切换 #[tauri::command] pub async fn force_refresh_proxies() -> CmdResult { - let manager = MihomoManager::global(); - let app_handle = handle::Handle::global().app_handle().unwrap(); - let cmd_proxy_state = app_handle.state::>(); - - log::debug!(target: "app", "强制刷新代理缓存"); - - let proxies = manager.get_refresh_proxies().await?; - - { - let mut state = cmd_proxy_state.lock().unwrap(); - state.proxies = Box::new(proxies.clone()); - state.need_refresh = false; - state.last_refresh_time = Instant::now(); - } - - log::debug!(target: "app", "强制刷新代理缓存完成"); - Ok(proxies) + let cache = ProxyRequestCache::global(); + let key = ProxyRequestCache::make_key("proxies", "default"); + cache.map.remove(&key); + get_proxies().await } #[tauri::command] pub async fn get_providers_proxies() -> CmdResult { - let app_handle = handle::Handle::global().app_handle().unwrap(); - let cmd_proxy_state = app_handle.state::>(); - - let should_refresh = { - let mut state = cmd_proxy_state.lock().unwrap(); - let now = Instant::now(); - if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL { - state.need_refresh = true; - state.last_refresh_time = now; - } - state.need_refresh - }; - - if should_refresh { - let manager = MihomoManager::global(); - let providers = manager.get_providers_proxies().await?; - { - let mut state = cmd_proxy_state.lock().unwrap(); - state.providers_proxies = Box::new(providers); - state.need_refresh = false; - } - log::debug!(target: "app", "providers_proxies刷新成功"); - } - - let providers_proxies = { - let state = cmd_proxy_state.lock().unwrap(); - state.providers_proxies.clone() - }; - Ok(*providers_proxies) + let manager = MihomoManager::global(); + let cache = ProxyRequestCache::global(); + let key = ProxyRequestCache::make_key("providers", "default"); + let value = cache + .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async { + manager.get_providers_proxies().await.expect("fetch failed") + }) + .await; + Ok((*value).clone()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 712e852d..461c8cc3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -213,7 +213,6 @@ pub fn run() { logging!(error, Type::Setup, true, "初始化资源失败: {}", e); } - app.manage(Mutex::new(state::proxy::CmdProxyState::default())); app.manage(Mutex::new(state::lightweight::LightWeightState::default())); logging!(info, Type::Setup, true, "初始化完成,继续执行"); diff --git a/src-tauri/src/state/proxy.rs b/src-tauri/src/state/proxy.rs index c3df7d8d..be7d153b 100644 --- a/src-tauri/src/state/proxy.rs +++ b/src-tauri/src/state/proxy.rs @@ -1,19 +1,65 @@ +use std::time::{Duration, Instant}; +pub struct CacheEntry { + pub value: Arc, + pub expires_at: Instant, +} +use dashmap::DashMap; use serde_json::Value; +use std::sync::Arc; +use tokio::sync::OnceCell; -pub struct CmdProxyState { - pub last_refresh_time: std::time::Instant, - pub need_refresh: bool, - pub proxies: Box, - pub providers_proxies: Box, +pub struct ProxyRequestCache { + pub map: DashMap>>, } -impl Default for CmdProxyState { - fn default() -> Self { - Self { - last_refresh_time: std::time::Instant::now(), - need_refresh: true, - proxies: Box::new(Value::Null), - providers_proxies: Box::new(Value::Null), +impl ProxyRequestCache { + pub fn global() -> &'static Self { + static INSTANCE: once_cell::sync::OnceCell = + once_cell::sync::OnceCell::new(); + INSTANCE.get_or_init(|| ProxyRequestCache { + map: DashMap::new(), + }) + } + + pub fn make_key(prefix: &str, id: &str) -> String { + format!("{}:{}", prefix, id) + } + + pub async fn get_or_fetch(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc + where + F: Fn() -> Fut, + Fut: std::future::Future, + { + let now = Instant::now(); + let key_cloned = key.clone(); + let cell = self + .map + .entry(key) + .or_insert_with(|| Arc::new(OnceCell::new())) + .clone(); + + if let Some(entry) = cell.get() { + if entry.expires_at > now { + return Arc::clone(&entry.value); + } } + + if let Some(entry) = cell.get() { + if entry.expires_at <= now { + self.map + .remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell)); + let new_cell = Arc::new(OnceCell::new()); + self.map.insert(key_cloned.clone(), new_cell.clone()); + return Box::pin(self.get_or_fetch(key_cloned, ttl, fetch_fn)).await; + } + } + + let value = fetch_fn().await; + let entry = CacheEntry { + value: Arc::new(value), + expires_at: Instant::now() + ttl, + }; + let _ = cell.set(entry); + Arc::clone(&cell.get().unwrap().value) } } diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index fdfe4d95..fc2279d0 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -8,6 +8,7 @@ import { deleteConnection, getGroupProxyDelays, } from "@/services/api"; +import { forceRefreshProxies } from "@/services/cmds"; import { useProfiles } from "@/hooks/use-profiles"; import { useVerge } from "@/hooks/use-verge"; import { BaseEmpty } from "../base"; @@ -341,6 +342,9 @@ export const ProxyGroups = (props: Props) => { const { name, now } = group; await updateProxy(name, proxy.name); + + await forceRefreshProxies(); + onProxies(); // 断开连接 diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts index 836ddb44..c8f2cea0 100644 --- a/src/hooks/use-profiles.ts +++ b/src/hooks/use-profiles.ts @@ -3,6 +3,7 @@ import { getProfiles, patchProfile, patchProfilesConfig, + forceRefreshProxies, } from "@/services/cmds"; import { getProxies, updateProxy } from "@/services/api"; @@ -128,6 +129,9 @@ export const useProfiles = () => { await patchProfile(profileData.current!, { selected: newSelected }); console.log("[ActivateSelected] 代理选择配置保存成功"); + // 切换节点后强制刷新后端缓存 + await forceRefreshProxies(); + setTimeout(() => { mutate("getProxies", getProxies()); }, 100); diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 9e7ab3af..cbaebc98 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -101,33 +101,19 @@ export const AppDataProvider = ({ lastProfileId = newProfileId; lastUpdateTime = now; - setTimeout(async () => { - try { - console.log("[AppDataProvider] 强制刷新代理缓存"); - - const refreshPromise = Promise.race([ - forceRefreshProxies(), - new Promise((_, reject) => - setTimeout( - () => reject(new Error("forceRefreshProxies timeout")), - 8000, - ), - ), - ]); - - await refreshPromise; - - console.log("[AppDataProvider] 刷新前端代理数据"); - await refreshProxy(); - - console.log("[AppDataProvider] Profile切换的代理数据刷新完成"); - } catch (error) { - console.error("[AppDataProvider] 强制刷新代理缓存失败:", error); - - refreshProxy().catch((e) => - console.warn("[AppDataProvider] 普通刷新也失败:", e), - ); - } + setTimeout(() => { + // 先执行 forceRefreshProxies,完成后稍延迟再刷新前端数据,避免页面一直 loading + forceRefreshProxies() + .catch((e) => + console.warn("[AppDataProvider] forceRefreshProxies 失败:", e), + ) + .finally(() => { + setTimeout(() => { + refreshProxy().catch((e) => + console.warn("[AppDataProvider] 普通刷新也失败:", e), + ); + }, 200); // 200ms 延迟,保证后端缓存已清理 + }); }, 0); });