perf: optimize profile switching logic with interrupt support to prevent freeze
This commit is contained in:
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
github: clash-verge-rev
|
||||
github: clash-verge-rev
|
||||
|
||||
@@ -6,12 +6,31 @@ use crate::{
|
||||
utils::{dirs, help, logging::Type},
|
||||
wrap_err,
|
||||
};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
// 添加全局互斥锁防止并发配置更新
|
||||
// 全局互斥锁防止并发配置更新
|
||||
static PROFILE_UPDATE_MUTEX: Mutex<()> = Mutex::const_new(());
|
||||
|
||||
// 全局请求序列号跟踪,用于避免队列化执行
|
||||
static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
static CURRENT_PROCESSING_PROFILE: RwLock<Option<String>> = RwLock::const_new(None);
|
||||
|
||||
/// 清理配置处理状态
|
||||
async fn cleanup_processing_state(sequence: u64, reason: &str) {
|
||||
*CURRENT_PROCESSING_PROFILE.write().await = None;
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"{},清理状态,序列号: {}",
|
||||
reason,
|
||||
sequence
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取配置文件避免锁竞争
|
||||
#[tauri::command]
|
||||
pub async fn get_profiles() -> CmdResult<IProfiles> {
|
||||
@@ -151,10 +170,60 @@ pub async fn delete_profile(index: String) -> CmdResult {
|
||||
/// 修改profiles的配置
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 获取互斥锁,防止并发执行
|
||||
let _guard = PROFILE_UPDATE_MUTEX.lock().await;
|
||||
// 为当前请求分配序列号
|
||||
let current_sequence = CURRENT_REQUEST_SEQUENCE.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
let target_profile = profiles.current.clone();
|
||||
|
||||
logging!(info, Type::Cmd, true, "开始修改配置文件");
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"开始修改配置文件,请求序列号: {}, 目标profile: {:?}",
|
||||
current_sequence,
|
||||
target_profile
|
||||
);
|
||||
|
||||
let mutex_result =
|
||||
tokio::time::timeout(Duration::from_millis(100), PROFILE_UPDATE_MUTEX.lock()).await;
|
||||
|
||||
let _guard = match mutex_result {
|
||||
Ok(guard) => guard,
|
||||
Err(_) => {
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"检测到更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"强制获取锁以处理最新请求: {}",
|
||||
current_sequence
|
||||
);
|
||||
PROFILE_UPDATE_MUTEX.lock().await
|
||||
}
|
||||
};
|
||||
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"获取锁后发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 保存当前配置,以便在验证失败时恢复
|
||||
let current_profile = Config::profiles().latest().current.clone();
|
||||
@@ -269,14 +338,68 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"在核心操作前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(ref profile) = target_profile {
|
||||
*CURRENT_PROCESSING_PROFILE.write().await = Some(profile.clone());
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"设置当前处理profile: {}, 序列号: {}",
|
||||
profile,
|
||||
current_sequence
|
||||
);
|
||||
}
|
||||
|
||||
// 更新profiles配置
|
||||
logging!(info, Type::Cmd, true, "正在更新配置草稿");
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"正在更新配置草稿,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
|
||||
let current_value = profiles.current.clone();
|
||||
|
||||
let _ = Config::profiles().draft().patch_config(profiles);
|
||||
|
||||
// 在调用内核前再次验证请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"在内核交互前发现更新的请求 (序列号: {} < {}),放弃当前请求",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 为配置更新添加超时保护
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"开始内核配置更新,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
let update_result = tokio::time::timeout(
|
||||
Duration::from_secs(30), // 30秒超时
|
||||
CoreManager::global().update_config(),
|
||||
@@ -286,7 +409,28 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
// 更新配置并进行验证
|
||||
match update_result {
|
||||
Ok(Ok((true, _))) => {
|
||||
logging!(info, Type::Cmd, true, "配置更新成功");
|
||||
// 内核操作完成后再次检查请求有效性
|
||||
let latest_sequence = CURRENT_REQUEST_SEQUENCE.load(Ordering::SeqCst);
|
||||
if current_sequence < latest_sequence {
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"内核操作后发现更新的请求 (序列号: {} < {}),忽略当前结果",
|
||||
current_sequence,
|
||||
latest_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"配置更新成功,序列号: {}",
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().apply();
|
||||
handle::Handle::refresh_clash();
|
||||
|
||||
@@ -314,10 +458,19 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
// 立即通知前端配置变更
|
||||
if let Some(current) = ¤t_value {
|
||||
logging!(info, Type::Cmd, true, "向前端发送配置变更事件: {}", current);
|
||||
logging!(
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"向前端发送配置变更事件: {}, 序列号: {}",
|
||||
current,
|
||||
current_sequence
|
||||
);
|
||||
handle::Handle::notify_profile_changed(current.clone());
|
||||
}
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置切换完成").await;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
Ok(Ok((false, error_msg))) => {
|
||||
@@ -351,18 +504,38 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
|
||||
// 发送验证错误通知
|
||||
handle::Handle::notice_message("config_validate::error", &error_msg);
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置验证失败").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
logging!(warn, Type::Cmd, true, "更新过程发生错误: {}", e);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"更新过程发生错误: {}, 序列号: {}",
|
||||
e,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
handle::Handle::notice_message("config_validate::boot_error", e.to_string());
|
||||
|
||||
cleanup_processing_state(current_sequence, "更新过程错误").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
Err(_) => {
|
||||
// 超时处理
|
||||
let timeout_msg = "配置更新超时(30秒),可能是配置验证或核心通信阻塞";
|
||||
logging!(error, Type::Cmd, true, "{}", timeout_msg);
|
||||
logging!(
|
||||
error,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"{}, 序列号: {}",
|
||||
timeout_msg,
|
||||
current_sequence
|
||||
);
|
||||
Config::profiles().discard();
|
||||
|
||||
if let Some(prev_profile) = current_profile {
|
||||
@@ -370,8 +543,9 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
info,
|
||||
Type::Cmd,
|
||||
true,
|
||||
"超时后尝试恢复到之前的配置: {}",
|
||||
prev_profile
|
||||
"超时后尝试恢复到之前的配置: {}, 序列号: {}",
|
||||
prev_profile,
|
||||
current_sequence
|
||||
);
|
||||
let restore_profiles = IProfiles {
|
||||
current: Some(prev_profile),
|
||||
@@ -382,6 +556,9 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<bool> {
|
||||
}
|
||||
|
||||
handle::Handle::notice_message("config_validate::timeout", timeout_msg);
|
||||
|
||||
cleanup_processing_state(current_sequence, "配置更新超时").await;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +463,15 @@ export const ProfileItem = (props: Props) => {
|
||||
>
|
||||
<ProfileBox
|
||||
aria-selected={selected}
|
||||
onClick={() => onSelect(false)}
|
||||
onClick={(e) => {
|
||||
// 如果正在激活中,阻止重复点击
|
||||
if (activating) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onSelect(false);
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
@@ -484,9 +492,16 @@ export const ProfileItem = (props: Props) => {
|
||||
bottom: 2,
|
||||
zIndex: 10,
|
||||
backdropFilter: "blur(2px)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
||||
}}
|
||||
>
|
||||
<CircularProgress color="inherit" size={20} />
|
||||
<CircularProgress
|
||||
color="inherit"
|
||||
size={20}
|
||||
sx={{
|
||||
animation: "pulse 1.5s ease-in-out infinite",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box position="relative">
|
||||
@@ -535,6 +550,10 @@ export const ProfileItem = (props: Props) => {
|
||||
disabled={loading}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// 如果正在激活或加载中,阻止更新操作
|
||||
if (activating || loading) {
|
||||
return;
|
||||
}
|
||||
onUpdate(1);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -19,21 +19,29 @@ export const useProfiles = () => {
|
||||
},
|
||||
);
|
||||
|
||||
const patchProfiles = async (value: Partial<IProfilesConfig>) => {
|
||||
// 立即更新本地状态
|
||||
if (value.current && profiles) {
|
||||
const optimisticUpdate = {
|
||||
...profiles,
|
||||
current: value.current,
|
||||
};
|
||||
mutateProfiles(optimisticUpdate, false); // 不重新验证
|
||||
}
|
||||
|
||||
const patchProfiles = async (
|
||||
value: Partial<IProfilesConfig>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
try {
|
||||
await patchProfilesConfig(value);
|
||||
mutateProfiles();
|
||||
if (signal?.aborted) {
|
||||
throw new DOMException("Operation was aborted", "AbortError");
|
||||
}
|
||||
const success = await patchProfilesConfig(value);
|
||||
|
||||
if (signal?.aborted) {
|
||||
throw new DOMException("Operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
await mutateProfiles();
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
mutateProfiles();
|
||||
if (error instanceof DOMException && error.name === "AbortError") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await mutateProfiles();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -392,6 +392,7 @@
|
||||
"Profile Imported Successfully": "Profile Imported Successfully",
|
||||
"Profile Switched": "Profile Switched",
|
||||
"Profile Reactivated": "Profile Reactivated",
|
||||
"Profile switch interrupted by new selection": "Profile switch interrupted by new selection",
|
||||
"Only YAML Files Supported": "Only YAML Files Supported",
|
||||
"Settings Applied": "Settings Applied",
|
||||
"Installing Service...": "Installing Service...",
|
||||
|
||||
@@ -392,6 +392,7 @@
|
||||
"Profile Imported Successfully": "导入订阅成功",
|
||||
"Profile Switched": "订阅已切换",
|
||||
"Profile Reactivated": "订阅已激活",
|
||||
"Profile switch interrupted by new selection": "配置切换被新选择中断",
|
||||
"Only YAML Files Supported": "仅支持 YAML 文件",
|
||||
"Settings Applied": "设置已应用",
|
||||
"Installing Service...": "安装服务中...",
|
||||
|
||||
@@ -55,6 +55,44 @@ import { listen } from "@tauri-apps/api/event";
|
||||
import { TauriEvent } from "@tauri-apps/api/event";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
// 记录profile切换状态
|
||||
const debugProfileSwitch = (action: string, profile: string, extra?: any) => {
|
||||
const timestamp = new Date().toISOString().substring(11, 23);
|
||||
console.log(
|
||||
`[Profile-Debug][${timestamp}] ${action}: ${profile}`,
|
||||
extra || "",
|
||||
);
|
||||
};
|
||||
|
||||
// 检查请求是否已过期
|
||||
const isRequestOutdated = (
|
||||
currentSequence: number,
|
||||
requestSequenceRef: any,
|
||||
profile: string,
|
||||
) => {
|
||||
if (currentSequence !== requestSequenceRef.current) {
|
||||
debugProfileSwitch(
|
||||
"REQUEST_OUTDATED",
|
||||
profile,
|
||||
`当前序列号: ${currentSequence}, 最新序列号: ${requestSequenceRef.current}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 检查是否被中断
|
||||
const isOperationAborted = (
|
||||
abortController: AbortController,
|
||||
profile: string,
|
||||
) => {
|
||||
if (abortController.signal.aborted) {
|
||||
debugProfileSwitch("OPERATION_ABORTED", profile);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
@@ -63,6 +101,55 @@ const ProfilePage = () => {
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [activatings, setActivatings] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 防止重复切换
|
||||
const switchingProfileRef = useRef<string | null>(null);
|
||||
|
||||
// 支持中断当前切换操作
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
// 只处理最新的切换请求
|
||||
const requestSequenceRef = useRef<number>(0);
|
||||
|
||||
// 待处理请求跟踪,取消排队的请求
|
||||
const pendingRequestRef = useRef<Promise<any> | null>(null);
|
||||
|
||||
// 处理profile切换中断
|
||||
const handleProfileInterrupt = (
|
||||
previousSwitching: string,
|
||||
newProfile: string,
|
||||
) => {
|
||||
debugProfileSwitch(
|
||||
"INTERRUPT_PREVIOUS",
|
||||
previousSwitching,
|
||||
`被 ${newProfile} 中断`,
|
||||
);
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
debugProfileSwitch("ABORT_CONTROLLER_TRIGGERED", previousSwitching);
|
||||
}
|
||||
|
||||
if (pendingRequestRef.current) {
|
||||
debugProfileSwitch("CANCEL_PENDING_REQUEST", previousSwitching);
|
||||
}
|
||||
|
||||
setActivatings((prev) => prev.filter((id) => id !== previousSwitching));
|
||||
showNotice(
|
||||
"info",
|
||||
`${t("Profile switch interrupted by new selection")}: ${previousSwitching} → ${newProfile}`,
|
||||
3000,
|
||||
);
|
||||
};
|
||||
|
||||
// 清理切换状态
|
||||
const cleanupSwitchState = (profile: string, sequence: number) => {
|
||||
setActivatings((prev) => prev.filter((id) => id !== profile));
|
||||
switchingProfileRef.current = null;
|
||||
abortControllerRef.current = null;
|
||||
pendingRequestRef.current = null;
|
||||
debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`);
|
||||
};
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
@@ -190,57 +277,165 @@ const ProfilePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const activateProfile = useLockFn(
|
||||
async (profile: string, notifySuccess: boolean) => {
|
||||
if (profiles.current === profile && !notifySuccess) {
|
||||
console.log(
|
||||
`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`,
|
||||
const executeBackgroundTasks = async (
|
||||
profile: string,
|
||||
sequence: number,
|
||||
abortController: AbortController,
|
||||
) => {
|
||||
try {
|
||||
if (
|
||||
sequence === requestSequenceRef.current &&
|
||||
switchingProfileRef.current === profile &&
|
||||
!abortController.signal.aborted
|
||||
) {
|
||||
await activateSelected();
|
||||
console.log(`[Profile] 后台处理完成,序列号: ${sequence}`);
|
||||
} else {
|
||||
debugProfileSwitch(
|
||||
"BACKGROUND_TASK_SKIPPED",
|
||||
profile,
|
||||
`序列号过期或被中断: ${sequence} vs ${requestSequenceRef.current}`,
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.warn("Failed to activate selected proxies:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const activateProfile = async (profile: string, notifySuccess: boolean) => {
|
||||
if (profiles.current === profile && !notifySuccess) {
|
||||
console.log(`[Profile] 目标profile ${profile} 已经是当前配置,跳过切换`);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSequence = ++requestSequenceRef.current;
|
||||
debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`);
|
||||
|
||||
// 处理中断逻辑
|
||||
const previousSwitching = switchingProfileRef.current;
|
||||
if (previousSwitching && previousSwitching !== profile) {
|
||||
handleProfileInterrupt(previousSwitching, profile);
|
||||
}
|
||||
|
||||
// 防止重复切换同一个profile
|
||||
if (switchingProfileRef.current === profile) {
|
||||
debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile);
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化切换状态
|
||||
switchingProfileRef.current = profile;
|
||||
debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`);
|
||||
|
||||
const currentAbortController = new AbortController();
|
||||
abortControllerRef.current = currentAbortController;
|
||||
|
||||
setActivatings((prev) => {
|
||||
if (prev.includes(profile)) return prev;
|
||||
return [...prev, profile];
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`[Profile] 开始切换到: ${profile},序列号: ${currentSequence}`,
|
||||
);
|
||||
|
||||
// 检查请求有效性
|
||||
if (
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
|
||||
isOperationAborted(currentAbortController, profile)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 避免大多数情况下loading态闪烁
|
||||
const reset = setTimeout(() => {
|
||||
setActivatings((prev) => [...prev, profile]);
|
||||
}, 100);
|
||||
// 执行切换请求
|
||||
const requestPromise = patchProfiles(
|
||||
{ current: profile },
|
||||
currentAbortController.signal,
|
||||
);
|
||||
pendingRequestRef.current = requestPromise;
|
||||
|
||||
try {
|
||||
console.log(`[Profile] 开始切换到: ${profile}`);
|
||||
const success = await requestPromise;
|
||||
|
||||
const success = await patchProfiles({ current: profile });
|
||||
await mutateLogs();
|
||||
closeAllConnections();
|
||||
|
||||
if (notifySuccess && success) {
|
||||
showNotice("success", t("Profile Switched"), 1000);
|
||||
}
|
||||
|
||||
// 立即清除loading状态
|
||||
clearTimeout(reset);
|
||||
setActivatings([]);
|
||||
|
||||
console.log(`[Profile] 切换到 ${profile} 完成,开始后台处理`);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await activateSelected();
|
||||
console.log(`[Profile] 后台处理完成`);
|
||||
} catch (err: any) {
|
||||
console.warn("Failed to activate selected proxies:", err);
|
||||
}
|
||||
}, 50);
|
||||
} catch (err: any) {
|
||||
console.error(`[Profile] 切换失败:`, err);
|
||||
showNotice("error", err?.message || err.toString(), 4000);
|
||||
clearTimeout(reset);
|
||||
setActivatings([]);
|
||||
if (pendingRequestRef.current === requestPromise) {
|
||||
pendingRequestRef.current = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
const onSelect = useLockFn(async (current: string, force: boolean) => {
|
||||
if (!force && current === profiles.current) return;
|
||||
|
||||
// 再次检查有效性
|
||||
if (
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile) ||
|
||||
isOperationAborted(currentAbortController, profile)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 完成切换
|
||||
await mutateLogs();
|
||||
closeAllConnections();
|
||||
|
||||
if (notifySuccess && success) {
|
||||
showNotice("success", t("Profile Switched"), 1000);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Profile] 切换到 ${profile} 完成,序列号: ${currentSequence},开始后台处理`,
|
||||
);
|
||||
|
||||
// 延迟执行后台任务
|
||||
setTimeout(
|
||||
() =>
|
||||
executeBackgroundTasks(
|
||||
profile,
|
||||
currentSequence,
|
||||
currentAbortController,
|
||||
),
|
||||
50,
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (pendingRequestRef.current) {
|
||||
pendingRequestRef.current = null;
|
||||
}
|
||||
|
||||
// 检查是否因为中断或过期而出错
|
||||
if (
|
||||
isOperationAborted(currentAbortController, profile) ||
|
||||
isRequestOutdated(currentSequence, requestSequenceRef, profile)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`[Profile] 切换失败:`, err);
|
||||
showNotice("error", err?.message || err.toString(), 4000);
|
||||
} finally {
|
||||
// 只有当前profile仍然是正在切换的profile且序列号匹配时才清理状态
|
||||
if (
|
||||
switchingProfileRef.current === profile &&
|
||||
currentSequence === requestSequenceRef.current
|
||||
) {
|
||||
cleanupSwitchState(profile, currentSequence);
|
||||
} else {
|
||||
debugProfileSwitch(
|
||||
"CLEANUP_SKIPPED",
|
||||
profile,
|
||||
`序列号不匹配或已被接管: ${currentSequence} vs ${requestSequenceRef.current}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onSelect = async (current: string, force: boolean) => {
|
||||
// 阻止重复点击或已激活的profile
|
||||
if (switchingProfileRef.current === current) {
|
||||
debugProfileSwitch("DUPLICATE_CLICK_IGNORED", current);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && current === profiles.current) {
|
||||
debugProfileSwitch("ALREADY_CURRENT_IGNORED", current);
|
||||
return;
|
||||
}
|
||||
|
||||
await activateProfile(current, true);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -252,7 +447,16 @@ const ProfilePage = () => {
|
||||
}, current);
|
||||
|
||||
const onEnhance = useLockFn(async (notifySuccess: boolean) => {
|
||||
setActivatings(currentActivatings());
|
||||
if (switchingProfileRef.current) {
|
||||
console.log(
|
||||
`[Profile] 有profile正在切换中(${switchingProfileRef.current}),跳过enhance操作`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentProfiles = currentActivatings();
|
||||
setActivatings((prev) => [...new Set([...prev, ...currentProfiles])]);
|
||||
|
||||
try {
|
||||
await enhanceProfiles();
|
||||
mutateLogs();
|
||||
@@ -262,7 +466,10 @@ const ProfilePage = () => {
|
||||
} catch (err: any) {
|
||||
showNotice("error", err.message || err.toString(), 3000);
|
||||
} finally {
|
||||
setActivatings([]);
|
||||
// 保留正在切换的profile,清除其他状态
|
||||
setActivatings((prev) =>
|
||||
prev.filter((id) => id === switchingProfileRef.current),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -366,6 +573,16 @@ const ProfilePage = () => {
|
||||
};
|
||||
}, [mutateProfiles]);
|
||||
|
||||
// 组件卸载时清理中断控制器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
debugProfileSwitch("COMPONENT_UNMOUNT_CLEANUP", "all");
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
full
|
||||
|
||||
Reference in New Issue
Block a user