refactor(profile-switch): serialize switches with async queue and enrich frontend events
This commit is contained in:
@@ -23,9 +23,14 @@ use std::{
|
||||
sync::atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{
|
||||
Mutex,
|
||||
mpsc::{self, error::TrySendError},
|
||||
};
|
||||
|
||||
static SWITCH_MUTEX: OnceCell<Mutex<()>> = OnceCell::new();
|
||||
static SWITCH_QUEUE: OnceCell<mpsc::Sender<(String, bool)>> = OnceCell::new();
|
||||
const SWITCH_QUEUE_CAPACITY: usize = 32;
|
||||
// 全局请求序列号跟踪,用于避免队列化执行
|
||||
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
|
||||
@@ -45,6 +50,113 @@ impl Drop for SwitchScope {
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_queue_sender() -> &'static mpsc::Sender<(String, bool)> {
|
||||
SWITCH_QUEUE.get_or_init(|| {
|
||||
let (tx, mut rx) = mpsc::channel::<(String, bool)>(SWITCH_QUEUE_CAPACITY);
|
||||
tokio::spawn(async move {
|
||||
let mutex = SWITCH_MUTEX.get_or_init(|| Mutex::new(()));
|
||||
while let Some((profile, notify)) = rx.recv().await {
|
||||
let _guard = mutex.lock().await;
|
||||
if let Err(err) = process_switch_task(profile.clone(), notify).await {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Failed to process profile switch task ({}): {}",
|
||||
profile,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
tx
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_switch_task(profile_index: String, notify_success: bool) -> CmdResult<()> {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"Processing queued profile switch: {} (notify={})",
|
||||
profile_index,
|
||||
notify_success
|
||||
);
|
||||
|
||||
let switch_result = AssertUnwindSafe(patch_profiles_config_internal(IProfiles {
|
||||
current: Some(profile_index.clone()),
|
||||
items: None,
|
||||
}))
|
||||
.catch_unwind()
|
||||
.await;
|
||||
|
||||
let switch_result = match switch_result {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Panic occurred during profile switch: {}",
|
||||
profile_index
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::panic",
|
||||
format!("profile switch panic: {}", profile_index),
|
||||
);
|
||||
handle::Handle::notify_profile_switch_finished(
|
||||
profile_index.clone(),
|
||||
false,
|
||||
notify_success,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let success = match switch_result {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Profile switch failed ({}): {}",
|
||||
profile_index,
|
||||
err
|
||||
);
|
||||
handle::Handle::notice_message("config_validate::error", err.clone());
|
||||
handle::Handle::notify_profile_switch_finished(
|
||||
profile_index.clone(),
|
||||
false,
|
||||
notify_success,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
handle::Handle::notify_profile_switch_finished(profile_index.clone(), success, notify_success);
|
||||
|
||||
if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"Failed to close connections after profile switch ({}): {}",
|
||||
profile_index,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
if notify_success && success {
|
||||
handle::Handle::notice_message("info", "Profile Switched");
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"Profile switch task finished: {} (success={})",
|
||||
profile_index,
|
||||
success
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
// 策略1: 尝试快速获取latest数据
|
||||
@@ -593,51 +705,46 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn switch_profile(profile_index: String, notify_success: bool) -> CmdResult<bool> {
|
||||
logging!(info, Type::Cmd, "请求切换配置到: {}", &profile_index);
|
||||
|
||||
let mutex = SWITCH_MUTEX.get_or_init(|| Mutex::new(()));
|
||||
let _guard = mutex.lock().await;
|
||||
|
||||
let switch_result = AssertUnwindSafe(patch_profiles_config_internal(IProfiles {
|
||||
current: Some(profile_index.clone()),
|
||||
items: None,
|
||||
}))
|
||||
.catch_unwind()
|
||||
.await;
|
||||
|
||||
let result = match switch_result {
|
||||
Ok(inner) => inner?,
|
||||
Err(_) => {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
"Queue profile switch to: {}",
|
||||
&profile_index
|
||||
);
|
||||
let sender = switch_queue_sender();
|
||||
match sender.try_send((profile_index.clone(), notify_success)) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(TrySendError::Full(task)) => {
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
"Profile switch queue is full, waiting for space: {}",
|
||||
&profile_index
|
||||
);
|
||||
match sender.send(task).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(err) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"Profile switch queue closed while waiting ({}): {}",
|
||||
&profile_index,
|
||||
err
|
||||
);
|
||||
Err("switch profile queue unavailable".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(TrySendError::Closed(_)) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
"切换配置过程中发生panic,目标: {}",
|
||||
profile_index
|
||||
"Profile switch queue is closed, cannot enqueue: {}",
|
||||
&profile_index
|
||||
);
|
||||
handle::Handle::notice_message(
|
||||
"config_validate::panic",
|
||||
format!("profile switch panic: {}", profile_index),
|
||||
);
|
||||
handle::Handle::notify_profile_switch_finished(profile_index.clone(), false);
|
||||
return Ok(false);
|
||||
Err("switch profile queue unavailable".into())
|
||||
}
|
||||
};
|
||||
|
||||
if result {
|
||||
handle::Handle::notify_profile_switch_finished(profile_index.clone(), true);
|
||||
} else {
|
||||
handle::Handle::notify_profile_switch_finished(profile_index.clone(), false);
|
||||
}
|
||||
|
||||
if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await {
|
||||
logging!(warn, Type::Cmd, "切换配置后关闭连接失败: {}", err);
|
||||
}
|
||||
|
||||
if notify_success && result {
|
||||
handle::Handle::notice_message("info", "Profile Switched");
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 修改某个profile item的
|
||||
|
||||
@@ -100,10 +100,11 @@ impl Handle {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn notify_profile_switch_finished(profile_id: String, success: bool) {
|
||||
pub fn notify_profile_switch_finished(profile_id: String, success: bool, notify: bool) {
|
||||
Self::send_event(FrontendEvent::ProfileSwitchFinished {
|
||||
profile_id,
|
||||
success,
|
||||
notify,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,14 +20,34 @@ pub enum FrontendEvent {
|
||||
RefreshClash,
|
||||
RefreshVerge,
|
||||
RefreshProxy,
|
||||
ProxiesUpdated { payload: serde_json::Value },
|
||||
NoticeMessage { status: String, message: String },
|
||||
ProfileChanged { current_profile_id: String },
|
||||
ProfileSwitchFinished { profile_id: String, success: bool },
|
||||
TimerUpdated { profile_index: String },
|
||||
ProfileUpdateStarted { uid: String },
|
||||
ProfileUpdateCompleted { uid: String },
|
||||
RustPanic { message: String, location: String },
|
||||
ProxiesUpdated {
|
||||
payload: serde_json::Value,
|
||||
},
|
||||
NoticeMessage {
|
||||
status: String,
|
||||
message: String,
|
||||
},
|
||||
ProfileChanged {
|
||||
current_profile_id: String,
|
||||
},
|
||||
ProfileSwitchFinished {
|
||||
profile_id: String,
|
||||
success: bool,
|
||||
notify: bool,
|
||||
},
|
||||
TimerUpdated {
|
||||
profile_index: String,
|
||||
},
|
||||
ProfileUpdateStarted {
|
||||
uid: String,
|
||||
},
|
||||
ProfileUpdateCompleted {
|
||||
uid: String,
|
||||
},
|
||||
RustPanic {
|
||||
message: String,
|
||||
location: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -172,9 +192,10 @@ impl NotificationSystem {
|
||||
FrontendEvent::ProfileSwitchFinished {
|
||||
profile_id,
|
||||
success,
|
||||
notify,
|
||||
} => (
|
||||
"profile-switch-finished",
|
||||
Ok(json!({ "profileId": profile_id, "success": success })),
|
||||
Ok(json!({ "profileId": profile_id, "success": success, "notify": notify })),
|
||||
),
|
||||
FrontendEvent::TimerUpdated { profile_index } => {
|
||||
("verge://timer-updated", Ok(json!(profile_index)))
|
||||
|
||||
@@ -61,7 +61,7 @@ import {
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { useSetLoadingCache, useThemeMode } from "@/services/states";
|
||||
|
||||
// 记录profile切换状态
|
||||
// Record profile switch state
|
||||
const debugProfileSwitch = (action: string, profile: string, extra?: any) => {
|
||||
const timestamp = new Date().toISOString().substring(11, 23);
|
||||
console.log(
|
||||
@@ -73,6 +73,7 @@ const debugProfileSwitch = (action: string, profile: string, extra?: any) => {
|
||||
type ProfileSwitchFinishedPayload = {
|
||||
profileId: string;
|
||||
success: boolean;
|
||||
notify: boolean;
|
||||
};
|
||||
|
||||
type RustPanicPayload = {
|
||||
@@ -80,6 +81,11 @@ type RustPanicPayload = {
|
||||
location: string;
|
||||
};
|
||||
|
||||
type SwitchRequest = {
|
||||
profileId: string;
|
||||
notifySuccess: boolean;
|
||||
};
|
||||
|
||||
const normalizeProfileUrl = (value?: string) => {
|
||||
if (!value) return "";
|
||||
const trimmed = value.trim();
|
||||
@@ -140,7 +146,7 @@ const createImportLandingVerifier = (
|
||||
|
||||
if (currentCount > baselineCount) {
|
||||
console.log(
|
||||
`[导入验证] 配置数量已增加: ${baselineCount} -> ${currentCount}`,
|
||||
`[Import Verify] Configuration count increased: ${baselineCount} -> ${currentCount}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -158,7 +164,9 @@ const createImportLandingVerifier = (
|
||||
}
|
||||
|
||||
if (!hadBaselineProfile) {
|
||||
console.log("[导入验证] 检测到新的订阅记录,判定为导入成功");
|
||||
console.log(
|
||||
"[Import Verify] Detected new profile record; treating as success",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -167,13 +175,15 @@ const createImportLandingVerifier = (
|
||||
|
||||
if (currentUpdated > baselineUpdated) {
|
||||
console.log(
|
||||
`[导入验证] 订阅更新时间已更新 ${baselineUpdated} -> ${currentUpdated}`,
|
||||
`[Import Verify] Profile timestamp updated ${baselineUpdated} -> ${currentUpdated}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentSignature !== baselineSignature) {
|
||||
console.log("[导入验证] 订阅详情发生变化,判定为导入成功");
|
||||
console.log(
|
||||
"[Import Verify] Profile details changed; treating as success",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -256,28 +266,28 @@ const ProfilePage = () => {
|
||||
};
|
||||
}, [addListener, mutateProfiles, t]);
|
||||
|
||||
// 添加紧急恢复功能
|
||||
// Add emergency recovery capability
|
||||
const onEmergencyRefresh = useLockFn(async () => {
|
||||
console.log("[紧急刷新] 开始强制刷新所有数据");
|
||||
console.log("[Emergency Refresh] Starting forced refresh of all data");
|
||||
|
||||
try {
|
||||
// 清除所有SWR缓存
|
||||
// Clear all SWR caches
|
||||
await mutate(() => true, undefined, { revalidate: false });
|
||||
|
||||
// 强制重新获取配置数据
|
||||
// Force fetching profile data
|
||||
await mutateProfiles(undefined, {
|
||||
revalidate: true,
|
||||
rollbackOnError: false,
|
||||
});
|
||||
|
||||
// 等待状态稳定后增强配置
|
||||
// Wait for state to stabilize before enhancing the profile
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await onEnhance(false);
|
||||
|
||||
showNotice("success", "数据已强制刷新", 2000);
|
||||
showNotice("success", "Data forcibly refreshed", 2000);
|
||||
} catch (error: any) {
|
||||
console.error("[紧急刷新] 失败:", error);
|
||||
showNotice("error", `紧急刷新失败: ${error.message}`, 4000);
|
||||
console.error("[Emergency Refresh] Failed:", error);
|
||||
showNotice("error", `Emergency refresh failed: ${error.message}`, 4000);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -304,7 +314,7 @@ const ProfilePage = () => {
|
||||
|
||||
const onImport = async () => {
|
||||
if (!url) return;
|
||||
// 校验url是否为http/https
|
||||
// Validate that the URL uses http/https
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
showNotice("error", t("Invalid Profile URL"));
|
||||
return;
|
||||
@@ -334,7 +344,10 @@ const ProfilePage = () => {
|
||||
);
|
||||
}
|
||||
} catch (verifyErr) {
|
||||
console.warn("[导入验证] 获取配置状态失败:", verifyErr);
|
||||
console.warn(
|
||||
"[Import Verify] Failed to fetch profile state:",
|
||||
verifyErr,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -343,33 +356,33 @@ const ProfilePage = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
// 尝试正常导入
|
||||
// Attempt standard import
|
||||
await importProfile(url);
|
||||
await handleImportSuccess("Profile Imported Successfully");
|
||||
return;
|
||||
} catch (initialErr) {
|
||||
console.warn("[订阅导入] 首次导入失败:", initialErr);
|
||||
console.warn("[Profile Import] Initial import failed:", initialErr);
|
||||
|
||||
const alreadyImported = await waitForImportLanding();
|
||||
if (alreadyImported) {
|
||||
console.warn(
|
||||
"[订阅导入] 接口返回失败,但检测到订阅已导入,跳过回退导入流程",
|
||||
"[Profile Import] API reported failure, but profile already imported; skipping rollback",
|
||||
);
|
||||
await handleImportSuccess("Profile Imported Successfully");
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次导入失败且未检测到数据变更,尝试使用自身代理
|
||||
// Initial import failed without data change; try built-in proxy
|
||||
showNotice("info", t("Import failed, retrying with Clash proxy..."));
|
||||
try {
|
||||
// 使用自身代理尝试导入
|
||||
// Attempt import using built-in proxy
|
||||
await importProfile(url, {
|
||||
with_proxy: false,
|
||||
self_proxy: true,
|
||||
});
|
||||
await handleImportSuccess("Profile Imported with Clash proxy");
|
||||
} catch (retryErr: any) {
|
||||
// 回退导入也失败
|
||||
// Rollback import also failed
|
||||
const retryErrmsg = retryErr?.message || retryErr.toString();
|
||||
showNotice(
|
||||
"error",
|
||||
@@ -384,7 +397,7 @@ const ProfilePage = () => {
|
||||
|
||||
const currentProfileId = profiles.current ?? null;
|
||||
|
||||
// 强化的刷新策略
|
||||
// Enhanced refresh strategy
|
||||
const performRobustRefresh = async (
|
||||
importVerifier: ImportLandingVerifier,
|
||||
) => {
|
||||
@@ -395,43 +408,50 @@ const ProfilePage = () => {
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
console.log(`[导入刷新] 第${retryCount + 1}次尝试刷新配置数据`);
|
||||
console.log(
|
||||
`[Import Refresh] Attempt ${retryCount + 1} to refresh profile data`,
|
||||
);
|
||||
|
||||
// 强制刷新,绕过所有缓存
|
||||
// Force refresh and bypass caches
|
||||
await mutateProfiles(undefined, {
|
||||
revalidate: true,
|
||||
rollbackOnError: false,
|
||||
});
|
||||
|
||||
// 等待状态稳定
|
||||
// Wait for state to stabilize
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, baseDelay * (retryCount + 1)),
|
||||
);
|
||||
|
||||
// 验证刷新是否成功
|
||||
// Verify whether refresh succeeded
|
||||
const currentProfiles = await getProfiles();
|
||||
const currentCount = currentProfiles?.items?.length || 0;
|
||||
|
||||
if (currentCount > baselineCount) {
|
||||
console.log(
|
||||
`[导入刷新] 配置刷新成功,配置数量 ${baselineCount} -> ${currentCount}`,
|
||||
`[Import Refresh] Profile refresh succeeded; count ${baselineCount} -> ${currentCount}`,
|
||||
);
|
||||
await onEnhance(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasLanding(currentProfiles)) {
|
||||
console.log("[导入刷新] 检测到订阅内容更新,判定刷新成功");
|
||||
console.log(
|
||||
"[Import Refresh] Detected profile update; treating as success",
|
||||
);
|
||||
await onEnhance(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`[导入刷新] 配置数量未增加 (${currentCount}), 继续重试...`,
|
||||
`[Import Refresh] Profile count unchanged (${currentCount}), retrying...`,
|
||||
);
|
||||
retryCount++;
|
||||
} catch (error) {
|
||||
console.error(`[导入刷新] 第${retryCount + 1}次刷新失败:`, error);
|
||||
console.error(
|
||||
`[Import Refresh] Attempt ${retryCount + 1} failed:`,
|
||||
error,
|
||||
);
|
||||
retryCount++;
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, baseDelay * retryCount),
|
||||
@@ -439,10 +459,12 @@ const ProfilePage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 所有重试失败后的最后尝试
|
||||
console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`);
|
||||
// Final attempt after all retries fail
|
||||
console.warn(
|
||||
`[Import Refresh] Regular refresh failed; clearing cache and retrying`,
|
||||
);
|
||||
try {
|
||||
// 清除SWR缓存并重新获取
|
||||
// Clear SWR cache and refetch
|
||||
await mutate("getProfiles", getProfiles(), { revalidate: true });
|
||||
await onEnhance(false);
|
||||
showNotice(
|
||||
@@ -451,7 +473,10 @@ const ProfilePage = () => {
|
||||
3000,
|
||||
);
|
||||
} catch (finalError) {
|
||||
console.error(`[导入刷新] 最终刷新尝试失败:`, finalError);
|
||||
console.error(
|
||||
`[Import Refresh] Final refresh attempt failed:`,
|
||||
finalError,
|
||||
);
|
||||
showNotice(
|
||||
"error",
|
||||
t("Profile imported successfully, please restart if not visible"),
|
||||
@@ -471,25 +496,108 @@ const ProfilePage = () => {
|
||||
const [switchingProfileId, setSwitchingProfileId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const pendingProfileRef = useRef<string | null>(null);
|
||||
const isSwitching = switchingProfileId != null;
|
||||
const activeSwitchRef = useRef<SwitchRequest | null>(null);
|
||||
const pendingSwitchRef = useRef<SwitchRequest | null>(null);
|
||||
|
||||
const executeSwitch = useCallback(
|
||||
async (targetProfile: string, notifySuccess: boolean) => {
|
||||
const currentRequest = activeSwitchRef.current;
|
||||
if (currentRequest?.profileId === targetProfile) {
|
||||
if (currentRequest.notifySuccess !== notifySuccess) {
|
||||
activeSwitchRef.current = { profileId: targetProfile, notifySuccess };
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
activeSwitchRef.current = { profileId: targetProfile, notifySuccess };
|
||||
setSwitchingProfileId(targetProfile);
|
||||
setActivatings((prev) =>
|
||||
prev.includes(targetProfile) ? prev : [...prev, targetProfile],
|
||||
);
|
||||
|
||||
try {
|
||||
const accepted = await switchProfileCommand(
|
||||
targetProfile,
|
||||
notifySuccess,
|
||||
);
|
||||
if (!accepted) {
|
||||
throw new Error(t("Profile switch failed"));
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (activeSwitchRef.current?.profileId === targetProfile) {
|
||||
activeSwitchRef.current = null;
|
||||
}
|
||||
setSwitchingProfileId((prev) => (prev === targetProfile ? null : prev));
|
||||
setActivatings((prev) => prev.filter((id) => id !== targetProfile));
|
||||
showNotice(
|
||||
"error",
|
||||
error?.message || error?.toString?.() || String(error),
|
||||
);
|
||||
await mutateProfiles();
|
||||
const next = pendingSwitchRef.current;
|
||||
pendingSwitchRef.current = null;
|
||||
if (next) {
|
||||
executeSwitch(next.profileId, next.notifySuccess);
|
||||
}
|
||||
}
|
||||
},
|
||||
[mutateProfiles, setActivatings, t],
|
||||
);
|
||||
|
||||
const enqueueSwitch = useCallback(
|
||||
(targetProfile: string, notifySuccess: boolean) => {
|
||||
if (activeSwitchRef.current) {
|
||||
pendingSwitchRef.current = { profileId: targetProfile, notifySuccess };
|
||||
return;
|
||||
}
|
||||
executeSwitch(targetProfile, notifySuccess);
|
||||
},
|
||||
[executeSwitch],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const unlistenPromise = listen<ProfileSwitchFinishedPayload>(
|
||||
"profile-switch-finished",
|
||||
(event) => {
|
||||
async (event) => {
|
||||
if (!mounted) return;
|
||||
const payload = event.payload;
|
||||
if (!payload) return;
|
||||
|
||||
setActivatings((prev) => prev.filter((id) => id !== payload.profileId));
|
||||
setSwitchingProfileId((prev) =>
|
||||
prev === payload.profileId ? null : prev,
|
||||
);
|
||||
const { profileId, success, notify } = payload;
|
||||
|
||||
if (!payload.success) {
|
||||
showNotice("error", t("Profile switch failed"));
|
||||
setActivatings((prev) => prev.filter((id) => id !== profileId));
|
||||
|
||||
const isActive = activeSwitchRef.current?.profileId === profileId;
|
||||
if (isActive) {
|
||||
activeSwitchRef.current = null;
|
||||
setSwitchingProfileId((prev) => (prev === profileId ? null : prev));
|
||||
|
||||
if (success) {
|
||||
await mutateLogs();
|
||||
await activateSelected();
|
||||
if (notify) {
|
||||
showNotice("success", t("Profile Switched"), 1000);
|
||||
}
|
||||
} else {
|
||||
showNotice("error", t("Profile switch failed"));
|
||||
}
|
||||
|
||||
await mutateProfiles();
|
||||
|
||||
const next = pendingSwitchRef.current;
|
||||
pendingSwitchRef.current = null;
|
||||
if (next && next.profileId !== profileId) {
|
||||
executeSwitch(next.profileId, next.notifySuccess);
|
||||
}
|
||||
} else {
|
||||
if (!success) {
|
||||
showNotice("error", t("Profile switch failed"));
|
||||
}
|
||||
if (pendingSwitchRef.current?.profileId === profileId) {
|
||||
pendingSwitchRef.current = null;
|
||||
}
|
||||
await mutateProfiles();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -498,73 +606,39 @@ const ProfilePage = () => {
|
||||
mounted = false;
|
||||
unlistenPromise.then((unlisten) => unlisten()).catch(() => {});
|
||||
};
|
||||
}, [setActivatings, t]);
|
||||
|
||||
const executeSwitch = useCallback(
|
||||
async function run(targetProfile: string, notifySuccess: boolean) {
|
||||
setSwitchingProfileId(targetProfile);
|
||||
setActivatings((prev) => {
|
||||
if (prev.includes(targetProfile)) return prev;
|
||||
return [...prev, targetProfile];
|
||||
});
|
||||
|
||||
try {
|
||||
const success = await switchProfileCommand(
|
||||
targetProfile,
|
||||
notifySuccess,
|
||||
);
|
||||
if (!success) {
|
||||
showNotice("error", t("Profile switch failed"));
|
||||
return;
|
||||
}
|
||||
await mutateLogs();
|
||||
await activateSelected();
|
||||
if (notifySuccess) {
|
||||
showNotice("success", t("Profile Switched"), 1000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
showNotice(
|
||||
"error",
|
||||
error?.message || error?.toString?.() || String(error),
|
||||
);
|
||||
} finally {
|
||||
setActivatings((prev) => prev.filter((id) => id !== targetProfile));
|
||||
setSwitchingProfileId(null);
|
||||
await mutateProfiles();
|
||||
const next = pendingProfileRef.current;
|
||||
pendingProfileRef.current = null;
|
||||
if (next && next !== targetProfile) {
|
||||
await run(next, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
[activateSelected, mutateLogs, mutateProfiles, setActivatings, t],
|
||||
);
|
||||
}, [
|
||||
activateSelected,
|
||||
enqueueSwitch,
|
||||
executeSwitch,
|
||||
mutateLogs,
|
||||
mutateProfiles,
|
||||
setActivatings,
|
||||
t,
|
||||
]);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(targetProfile: string, force: boolean) => {
|
||||
if (isSwitching) {
|
||||
pendingProfileRef.current = targetProfile;
|
||||
if (activeSwitchRef.current?.profileId === targetProfile) {
|
||||
return;
|
||||
}
|
||||
if (!force && targetProfile === currentProfileId) {
|
||||
debugProfileSwitch("ALREADY_CURRENT_IGNORED", targetProfile);
|
||||
return;
|
||||
}
|
||||
executeSwitch(targetProfile, true);
|
||||
enqueueSwitch(targetProfile, true);
|
||||
},
|
||||
[currentProfileId, executeSwitch, isSwitching],
|
||||
[currentProfileId, enqueueSwitch],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!current) return;
|
||||
if (isSwitching) {
|
||||
pendingProfileRef.current = current;
|
||||
if (activeSwitchRef.current) {
|
||||
pendingSwitchRef.current = { profileId: current, notifySuccess: false };
|
||||
return;
|
||||
}
|
||||
if (current === currentProfileId) return;
|
||||
executeSwitch(current, false);
|
||||
}, [current, currentProfileId, executeSwitch, isSwitching]);
|
||||
enqueueSwitch(current, false);
|
||||
}, [current, currentProfileId, enqueueSwitch]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
@@ -587,7 +661,7 @@ const ProfilePage = () => {
|
||||
const onEnhance = useLockFn(async (notifySuccess: boolean) => {
|
||||
if (switchingProfileId) {
|
||||
console.log(
|
||||
`[Profile] 有profile正在切换中(${switchingProfileId}),跳过enhance操作`,
|
||||
`[Profile] A profile is currently switching (${switchingProfileId}); skipping enhance operation`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -604,7 +678,7 @@ const ProfilePage = () => {
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString(), 3000);
|
||||
} finally {
|
||||
// 保留正在切换的profile,清除其他状态
|
||||
// Keep the switching profile active and clear other states
|
||||
setActivatings((prev) =>
|
||||
switchingProfileId
|
||||
? prev.filter((id) => id === switchingProfileId)
|
||||
@@ -630,7 +704,7 @@ const ProfilePage = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 更新所有订阅
|
||||
// Update all profiles
|
||||
const setLoadingCache = useSetLoadingCache();
|
||||
const onUpdateAll = useLockFn(async () => {
|
||||
const throttleMutate = throttle(mutateProfiles, 2000, {
|
||||
@@ -641,7 +715,7 @@ const ProfilePage = () => {
|
||||
await updateProfile(uid);
|
||||
throttleMutate();
|
||||
} catch (err: any) {
|
||||
console.error(`更新订阅 ${uid} 失败:`, err);
|
||||
console.error(`Failed to update profile ${uid}:`, err);
|
||||
} finally {
|
||||
setLoadingCache((cache) => ({ ...cache, [uid]: false }));
|
||||
}
|
||||
@@ -649,7 +723,7 @@ const ProfilePage = () => {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setLoadingCache((cache) => {
|
||||
// 获取没有正在更新的订阅
|
||||
// Gather profiles that are not updating
|
||||
const items = profileItems.filter(
|
||||
(e) => e.type === "remote" && !cache[e.uid],
|
||||
);
|
||||
@@ -703,11 +777,11 @@ const ProfilePage = () => {
|
||||
|
||||
const getSelectionState = () => {
|
||||
if (selectedProfiles.size === 0) {
|
||||
return "none"; // 无选择
|
||||
return "none"; // no selection
|
||||
} else if (selectedProfiles.size === profileItems.length) {
|
||||
return "all"; // 全选
|
||||
return "all"; // all selected
|
||||
} else {
|
||||
return "partial"; // 部分选择
|
||||
return "partial"; // partially selected
|
||||
}
|
||||
};
|
||||
|
||||
@@ -754,7 +828,7 @@ const ProfilePage = () => {
|
||||
? "rgba(0, 0, 0, 0.06)"
|
||||
: "rgba(255, 255, 255, 0.06)";
|
||||
|
||||
// 监听后端配置变更
|
||||
// Observe configuration changes from backend
|
||||
useEffect(() => {
|
||||
let unlistenPromise: Promise<() => void> | undefined;
|
||||
let lastProfileId: string | null = null;
|
||||
@@ -768,29 +842,29 @@ const ProfilePage = () => {
|
||||
const newProfileId = event.payload;
|
||||
const now = Date.now();
|
||||
|
||||
console.log(`[Profile] 收到配置变更事件: ${newProfileId}`);
|
||||
console.log(`[Profile] Received profile-change event: ${newProfileId}`);
|
||||
|
||||
if (
|
||||
lastProfileId === newProfileId &&
|
||||
now - lastUpdateTime < debounceDelay
|
||||
) {
|
||||
console.log(`[Profile] 重复事件被防抖,跳过`);
|
||||
console.log(`[Profile] Duplicate event throttled; skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
lastProfileId = newProfileId;
|
||||
lastUpdateTime = now;
|
||||
|
||||
console.log(`[Profile] 执行配置数据刷新`);
|
||||
console.log(`[Profile] Performing profile data refresh`);
|
||||
|
||||
if (refreshTimer !== null) {
|
||||
window.clearTimeout(refreshTimer);
|
||||
}
|
||||
|
||||
// 使用异步调度避免阻塞事件处理
|
||||
// Use async scheduling to avoid blocking event handling
|
||||
refreshTimer = window.setTimeout(() => {
|
||||
mutateProfiles().catch((error) => {
|
||||
console.error("[Profile] 配置数据刷新失败:", error);
|
||||
console.error("[Profile] Profile data refresh failed:", error);
|
||||
});
|
||||
refreshTimer = null;
|
||||
}, 0);
|
||||
@@ -853,12 +927,12 @@ const ProfilePage = () => {
|
||||
<LocalFireDepartmentRounded />
|
||||
</IconButton>
|
||||
|
||||
{/* 故障检测和紧急恢复按钮 */}
|
||||
{/* Fault detection and emergency recovery button */}
|
||||
{(error || isStale) && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="warning"
|
||||
title="数据异常,点击强制刷新"
|
||||
title="Data issue detected, click to force refresh"
|
||||
onClick={onEmergencyRefresh}
|
||||
sx={{
|
||||
animation: "pulse 2s infinite",
|
||||
@@ -1076,7 +1150,7 @@ const ProfilePage = () => {
|
||||
ref={viewerRef}
|
||||
onChange={async (isActivating) => {
|
||||
mutateProfiles();
|
||||
// 只有更改当前激活的配置时才触发全局重新加载
|
||||
// Only trigger global reload when the active profile changes
|
||||
if (isActivating) {
|
||||
await onEnhance(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user