feat: enhance proxy management with caching and refresh logic
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题
|
||||
- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步
|
||||
- 修复导入订阅时非 http 协议链接被错误尝试导入
|
||||
- 修复切换节点后页面长时间 loading 及缓存过期导致的数据不同步问题
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
@@ -20,6 +21,10 @@
|
||||
- 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死
|
||||
- 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死
|
||||
- 改进主页订阅卡流量已使用比例计算精度
|
||||
- 优化后端缓存刷新机制,支持毫秒级 TTL(默认 3000ms),减少重复请求并提升性能,切换节点时强制刷新后端数据,前端 UI 实时更新,操作更流畅
|
||||
- 解耦前端数据拉取与后端缓存刷新,提升节点切换速度和一致性
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
### 🗑️ 移除内容
|
||||
|
||||
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -1070,6 +1070,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"boa_engine",
|
||||
"chrono",
|
||||
"dashmap 6.1.0",
|
||||
"deelevate",
|
||||
"delay_timer",
|
||||
"dirs 6.0.0",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
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<serde_json::Value> {
|
||||
let manager = MihomoManager::global();
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
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<serde_json::Value> {
|
||||
let app_handle = handle::Handle::global().app_handle().unwrap();
|
||||
let cmd_proxy_state = app_handle.state::<Mutex<CmdProxyState>>();
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -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, "初始化完成,继续执行");
|
||||
|
||||
@@ -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 std::sync::Arc;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
pub struct CmdProxyState {
|
||||
pub last_refresh_time: std::time::Instant,
|
||||
pub need_refresh: bool,
|
||||
pub proxies: Box<Value>,
|
||||
pub providers_proxies: Box<Value>,
|
||||
pub struct ProxyRequestCache {
|
||||
pub map: DashMap<String, Arc<OnceCell<CacheEntry>>>,
|
||||
}
|
||||
|
||||
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<ProxyRequestCache> =
|
||||
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<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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
// 断开连接
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user