mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
feat: enhance proxy management with caching and refresh logic
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题
|
- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题
|
||||||
- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步
|
- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步
|
||||||
- 修复导入订阅时非 http 协议链接被错误尝试导入
|
- 修复导入订阅时非 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",
|
"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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, "初始化完成,继续执行");
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
// 断开连接
|
// 断开连接
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user