refactor(profiles): introduce a state machine

This commit is contained in:
Slinetrac
2025-10-24 23:37:01 +08:00
Unverified
parent f79c2bfdff
commit 0867c713f7

View File

@@ -29,8 +29,10 @@ import { listen, TauriEvent } from "@tauri-apps/api/event";
import { readText } from "@tauri-apps/plugin-clipboard-manager";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { useLockFn } from "ahooks";
import type { TFunction } from "i18next";
import { throttle } from "lodash-es";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import useSWR, { mutate } from "swr";
@@ -70,33 +72,169 @@ const debugProfileSwitch = (action: string, profile: string, extra?: any) => {
);
};
// 检查请求是否已过期
const isRequestOutdated = (
currentSequence: number,
requestSequenceRef: any,
profile: string,
) => {
if (currentSequence !== requestSequenceRef.current) {
debugProfileSwitch(
"REQUEST_OUTDATED",
profile,
`当前序列号: ${currentSequence}, 最新序列号: ${requestSequenceRef.current}`,
);
return true;
}
return false;
type SwitchRequest = {
profile: string;
notifySuccess: boolean;
};
// 检查是否被中断
const isOperationAborted = (
abortController: AbortController,
profile: string,
) => {
if (abortController.signal.aborted) {
debugProfileSwitch("OPERATION_ABORTED", profile);
return true;
}
return false;
interface ProfileSwitchMachineOptions {
getCurrentProfileId: () => string | undefined;
patchProfiles: (
value: Partial<IProfilesConfig>,
signal: AbortSignal,
) => Promise<boolean>;
mutateLogs: () => Promise<void>;
closeAllConnections: () => Promise<void>;
activateSelected: () => Promise<void>;
setActivatings: Dispatch<SetStateAction<string[]>>;
t: TFunction;
}
const useProfileSwitchMachine = ({
getCurrentProfileId,
patchProfiles,
mutateLogs,
closeAllConnections,
activateSelected,
setActivatings,
t,
}: ProfileSwitchMachineOptions) => {
const switchingProfileRef = useRef<string | null>(null);
const pendingProfileRef = useRef<SwitchRequest | null>(null);
const processingQueueRef = useRef(false);
const requestSequenceRef = useRef(0);
const cleanupSwitchState = useCallback(
(profile: string, sequence: number) => {
setActivatings((prev) => prev.filter((id) => id !== profile));
if (switchingProfileRef.current === profile) {
switchingProfileRef.current = null;
}
debugProfileSwitch("SWITCH_END", profile, `序列号: ${sequence}`);
},
[setActivatings],
);
const runProfileSwitch = useCallback(
async (request: SwitchRequest) => {
const { profile, notifySuccess } = request;
const currentSequence = ++requestSequenceRef.current;
debugProfileSwitch("NEW_REQUEST", profile, `序列号: ${currentSequence}`);
switchingProfileRef.current = profile;
debugProfileSwitch("SWITCH_START", profile, `序列号: ${currentSequence}`);
setActivatings((prev) => {
if (prev.includes(profile)) return prev;
return [...prev, profile];
});
const isOutdated = () => currentSequence !== requestSequenceRef.current;
const controller = new AbortController();
try {
if (isOutdated()) {
return;
}
const requestPromise = patchProfiles(
{ current: profile },
controller.signal,
);
const success = await requestPromise;
if (isOutdated()) {
return;
}
await mutateLogs();
await closeAllConnections();
if (notifySuccess && success) {
showNotice("success", t("Profile Switched"), 1000);
}
setTimeout(() => {
if (isOutdated()) return;
activateSelected().catch((err) =>
console.warn("Failed to activate selected proxies:", err),
);
}, 50);
} catch (error: any) {
console.error(`[Profile] 切换失败:`, error);
showNotice("error", error?.message || error.toString(), 4000);
} finally {
cleanupSwitchState(profile, currentSequence);
}
},
[
activateSelected,
cleanupSwitchState,
closeAllConnections,
mutateLogs,
patchProfiles,
setActivatings,
t,
],
);
const processSwitchQueue = useCallback(() => {
if (processingQueueRef.current) return;
const run = async () => {
processingQueueRef.current = true;
try {
while (true) {
const next = pendingProfileRef.current;
if (!next) break;
pendingProfileRef.current = null;
await runProfileSwitch(next);
}
} finally {
processingQueueRef.current = false;
}
};
run().catch((error) => {
console.error("[Profile] 处理切换队列失败:", error);
});
}, [runProfileSwitch]);
const requestSwitch = useCallback(
(profile: string, notifySuccess: boolean) => {
const currentProfileId = getCurrentProfileId();
if (currentProfileId === profile && !notifySuccess) {
debugProfileSwitch("ALREADY_CURRENT_IGNORED", profile);
return;
}
const currentSwitching = switchingProfileRef.current;
if (currentSwitching === profile) {
debugProfileSwitch("DUPLICATE_SWITCH_BLOCKED", profile);
return;
}
if (currentSwitching && currentSwitching !== profile) {
pendingProfileRef.current = { profile, notifySuccess };
return;
}
if (pendingProfileRef.current?.profile === profile) {
pendingProfileRef.current = { profile, notifySuccess };
return;
}
pendingProfileRef.current = { profile, notifySuccess };
processSwitchQueue();
},
[getCurrentProfileId, processSwitchQueue],
);
return {
requestSwitch,
switchingProfileRef,
};
};
const normalizeProfileUrl = (value?: string) => {
@@ -220,57 +358,6 @@ const ProfilePage = () => {
() => new Set(),
);
// 防止重复切换
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 = useCallback(
(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,
);
},
[t],
);
// 清理切换状态
const cleanupSwitchState = useCallback(
(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, {
@@ -453,6 +540,29 @@ const ProfilePage = () => {
}
};
const getCurrentProfileId = useCallback(
() => profiles.current ?? undefined,
[profiles],
);
const memoizedMutateLogs = useCallback(async () => {
await mutateLogs();
}, [mutateLogs]);
const closeConnections = useCallback(async () => {
await closeAllConnections();
}, []);
const { requestSwitch, switchingProfileRef } = useProfileSwitchMachine({
getCurrentProfileId,
patchProfiles,
mutateLogs: memoizedMutateLogs,
closeAllConnections: closeConnections,
activateSelected,
setActivatings,
t,
});
// 强化的刷新策略
const performRobustRefresh = async (
importVerifier: ImportLandingVerifier,
@@ -539,168 +649,7 @@ const ProfilePage = () => {
}
};
const executeBackgroundTasks = useCallback(
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);
}
},
[activateSelected],
);
const activateProfile = useCallback(
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;
}
// 执行切换请求
const requestPromise = patchProfiles(
{ current: profile },
currentAbortController.signal,
);
pendingRequestRef.current = requestPromise;
const success = await requestPromise;
if (pendingRequestRef.current === requestPromise) {
pendingRequestRef.current = null;
}
// 再次检查有效性
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}`,
);
}
}
},
[
profiles,
patchProfiles,
mutateLogs,
t,
executeBackgroundTasks,
handleProfileInterrupt,
cleanupSwitchState,
],
);
const onSelect = async (current: string, force: boolean) => {
const onSelect = (current: string, force: boolean) => {
// 阻止重复点击或已激活的profile
if (switchingProfileRef.current === current) {
debugProfileSwitch("DUPLICATE_CLICK_IGNORED", current);
@@ -712,17 +661,15 @@ const ProfilePage = () => {
return;
}
await activateProfile(current, true);
requestSwitch(current, true);
};
useEffect(() => {
(async () => {
if (current) {
mutateProfiles();
await activateProfile(current, false);
}
})();
}, [current, activateProfile, mutateProfiles]);
if (current) {
mutateProfiles();
requestSwitch(current, false);
}
}, [current, mutateProfiles, requestSwitch]);
const onEnhance = useLockFn(async (notifySuccess: boolean) => {
if (switchingProfileRef.current) {
@@ -945,16 +892,6 @@ const ProfilePage = () => {
};
}, [mutateProfiles]);
// 组件卸载时清理中断控制器
useEffect(() => {
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
debugProfileSwitch("COMPONENT_UNMOUNT_CLEANUP", "all");
}
};
}, []);
return (
<BasePage
full