diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 597ec4ec..b129239c 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -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, + signal: AbortSignal, + ) => Promise; + mutateLogs: () => Promise; + closeAllConnections: () => Promise; + activateSelected: () => Promise; + setActivatings: Dispatch>; + t: TFunction; +} + +const useProfileSwitchMachine = ({ + getCurrentProfileId, + patchProfiles, + mutateLogs, + closeAllConnections, + activateSelected, + setActivatings, + t, +}: ProfileSwitchMachineOptions) => { + const switchingProfileRef = useRef(null); + const pendingProfileRef = useRef(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(null); - - // 支持中断当前切换操作 - const abortControllerRef = useRef(null); - - // 只处理最新的切换请求 - const requestSequenceRef = useRef(0); - - // 待处理请求跟踪,取消排队的请求 - const pendingRequestRef = useRef | 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 (