feat: enhance proxy management with caching and refresh logic

This commit is contained in:
Tunglies
2025-06-30 20:14:04 +08:00
parent 4435a5aee4
commit 18ef7f0272
9 changed files with 113 additions and 123 deletions

View File

@@ -8,6 +8,7 @@
- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题 - 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题
- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步 - 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步
- 修复导入订阅时非 http 协议链接被错误尝试导入 - 修复导入订阅时非 http 协议链接被错误尝试导入
- 修复切换节点后页面长时间 loading 及缓存过期导致的数据不同步问题
### ✨ 新增功能 ### ✨ 新增功能
@@ -20,6 +21,10 @@
- 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死 - 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死
- 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死 - 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死
- 改进主页订阅卡流量已使用比例计算精度 - 改进主页订阅卡流量已使用比例计算精度
- 优化后端缓存刷新机制,支持毫秒级 TTL默认 3000ms减少重复请求并提升性能切换节点时强制刷新后端数据前端 UI 实时更新,操作更流畅
- 解耦前端数据拉取与后端缓存刷新,提升节点切换速度和一致性
### 🐞 修复问题
### 🗑️ 移除内容 ### 🗑️ 移除内容

1
src-tauri/Cargo.lock generated
View File

@@ -1070,6 +1070,7 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"boa_engine", "boa_engine",
"chrono", "chrono",
"dashmap 6.1.0",
"deelevate", "deelevate",
"delay_timer", "delay_timer",
"dirs 6.0.0", "dirs 6.0.0",

View File

@@ -82,6 +82,7 @@ sha2 = "0.10.9"
hex = "0.4.3" hex = "0.4.3"
scopeguard = "1.2.0" scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.0" tauri-plugin-notification = "2.3.0"
dashmap = "6.1.0"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"

View File

@@ -1,99 +1,43 @@
use super::CmdResult; use super::CmdResult;
use crate::{core::handle, module::mihomo::MihomoManager, state::proxy::CmdProxyState}; use crate::module::mihomo::MihomoManager;
use std::{ use std::time::Duration;
sync::Mutex,
time::{Duration, Instant},
};
use tauri::Manager;
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(3); use crate::state::proxy::ProxyRequestCache;
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[tauri::command] #[tauri::command]
pub async fn get_proxies() -> CmdResult<serde_json::Value> { pub async fn get_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global(); let manager = MihomoManager::global();
let cache = ProxyRequestCache::global();
let app_handle = handle::Handle::global().app_handle().unwrap(); let key = ProxyRequestCache::make_key("proxies", "default");
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>(); let value = cache
.get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async {
let should_refresh = { manager.get_refresh_proxies().await.expect("fetch failed")
let mut state = cmd_proxy_state.lock().unwrap(); })
let now = Instant::now(); .await;
if now.duration_since(state.last_refresh_time) > PROXIES_REFRESH_INTERVAL { Ok((*value).clone())
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)
} }
/// 强制刷新代理缓存用于profile切换 /// 强制刷新代理缓存用于profile切换
#[tauri::command] #[tauri::command]
pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> { pub async fn force_refresh_proxies() -> CmdResult<serde_json::Value> {
let manager = MihomoManager::global(); let cache = ProxyRequestCache::global();
let app_handle = handle::Handle::global().app_handle().unwrap(); let key = ProxyRequestCache::make_key("proxies", "default");
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>(); cache.map.remove(&key);
get_proxies().await
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)
} }
#[tauri::command] #[tauri::command]
pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> { pub async fn get_providers_proxies() -> CmdResult<serde_json::Value> {
let app_handle = handle::Handle::global().app_handle().unwrap(); let manager = MihomoManager::global();
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>(); let cache = ProxyRequestCache::global();
let key = ProxyRequestCache::make_key("providers", "default");
let should_refresh = { let value = cache
let mut state = cmd_proxy_state.lock().unwrap(); .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async {
let now = Instant::now(); manager.get_providers_proxies().await.expect("fetch failed")
if now.duration_since(state.last_refresh_time) > PROVIDERS_REFRESH_INTERVAL { })
state.need_refresh = true; .await;
state.last_refresh_time = now; Ok((*value).clone())
}
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)
} }

View File

@@ -213,7 +213,6 @@ pub fn run() {
logging!(error, Type::Setup, true, "初始化资源失败: {}", e); logging!(error, Type::Setup, true, "初始化资源失败: {}", e);
} }
app.manage(Mutex::new(state::proxy::CmdProxyState::default()));
app.manage(Mutex::new(state::lightweight::LightWeightState::default())); app.manage(Mutex::new(state::lightweight::LightWeightState::default()));
logging!(info, Type::Setup, true, "初始化完成,继续执行"); logging!(info, Type::Setup, true, "初始化完成,继续执行");

View File

@@ -1,19 +1,65 @@
use std::time::{Duration, Instant};
pub struct CacheEntry {
pub value: Arc<Value>,
pub expires_at: Instant,
}
use dashmap::DashMap;
use serde_json::Value; use serde_json::Value;
use std::sync::Arc;
use tokio::sync::OnceCell;
pub struct CmdProxyState { pub struct ProxyRequestCache {
pub last_refresh_time: std::time::Instant, pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
pub need_refresh: bool,
pub proxies: Box<Value>,
pub providers_proxies: Box<Value>,
} }
impl Default for CmdProxyState { impl ProxyRequestCache {
fn default() -> Self { pub fn global() -> &'static Self {
Self { static INSTANCE: once_cell::sync::OnceCell<ProxyRequestCache> =
last_refresh_time: std::time::Instant::now(), once_cell::sync::OnceCell::new();
need_refresh: true, INSTANCE.get_or_init(|| ProxyRequestCache {
proxies: Box::new(Value::Null), map: DashMap::new(),
providers_proxies: Box::new(Value::Null), })
}
pub fn make_key(prefix: &str, id: &str) -> String {
format!("{}:{}", prefix, id)
}
pub async fn get_or_fetch<F, Fut>(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc<Value>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Value>,
{
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)
} }
} }

View File

@@ -8,6 +8,7 @@ import {
deleteConnection, deleteConnection,
getGroupProxyDelays, getGroupProxyDelays,
} from "@/services/api"; } from "@/services/api";
import { forceRefreshProxies } from "@/services/cmds";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { BaseEmpty } from "../base"; import { BaseEmpty } from "../base";
@@ -341,6 +342,9 @@ export const ProxyGroups = (props: Props) => {
const { name, now } = group; const { name, now } = group;
await updateProxy(name, proxy.name); await updateProxy(name, proxy.name);
await forceRefreshProxies();
onProxies(); onProxies();
// 断开连接 // 断开连接

View File

@@ -3,6 +3,7 @@ import {
getProfiles, getProfiles,
patchProfile, patchProfile,
patchProfilesConfig, patchProfilesConfig,
forceRefreshProxies,
} from "@/services/cmds"; } from "@/services/cmds";
import { getProxies, updateProxy } from "@/services/api"; import { getProxies, updateProxy } from "@/services/api";
@@ -128,6 +129,9 @@ export const useProfiles = () => {
await patchProfile(profileData.current!, { selected: newSelected }); await patchProfile(profileData.current!, { selected: newSelected });
console.log("[ActivateSelected] 代理选择配置保存成功"); console.log("[ActivateSelected] 代理选择配置保存成功");
// 切换节点后强制刷新后端缓存
await forceRefreshProxies();
setTimeout(() => { setTimeout(() => {
mutate("getProxies", getProxies()); mutate("getProxies", getProxies());
}, 100); }, 100);

View File

@@ -101,33 +101,19 @@ export const AppDataProvider = ({
lastProfileId = newProfileId; lastProfileId = newProfileId;
lastUpdateTime = now; lastUpdateTime = now;
setTimeout(async () => { setTimeout(() => {
try { // 先执行 forceRefreshProxies完成后稍延迟再刷新前端数据避免页面一直 loading
console.log("[AppDataProvider] 强制刷新代理缓存"); forceRefreshProxies()
.catch((e) =>
const refreshPromise = Promise.race([ console.warn("[AppDataProvider] forceRefreshProxies 失败:", e),
forceRefreshProxies(), )
new Promise((_, reject) => .finally(() => {
setTimeout( setTimeout(() => {
() => reject(new Error("forceRefreshProxies timeout")), refreshProxy().catch((e) =>
8000, console.warn("[AppDataProvider] 普通刷新也失败:", e),
), );
), }, 200); // 200ms 延迟,保证后端缓存已清理
]); });
await refreshPromise;
console.log("[AppDataProvider] 刷新前端代理数据");
await refreshProxy();
console.log("[AppDataProvider] Profile切换的代理数据刷新完成");
} catch (error) {
console.error("[AppDataProvider] 强制刷新代理缓存失败:", error);
refreshProxy().catch((e) =>
console.warn("[AppDataProvider] 普通刷新也失败:", e),
);
}
}, 0); }, 0);
}); });