From 1beeb3f8047e43bb02608764829dd398d398d59f Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sat, 25 Oct 2025 10:35:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor=EF=BC=9Areplace=20state=20machine=20wi?= =?UTF-8?q?th=20reducer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/profiles.tsx | 325 +++++++++++++++++++++++------------------ 1 file changed, 179 insertions(+), 146 deletions(-) diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index b129239c..f43f4e14 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -31,7 +31,14 @@ 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 { + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from "react"; import type { Dispatch, SetStateAction } from "react"; import { useTranslation } from "react-i18next"; import { useLocation } from "react-router"; @@ -77,12 +84,81 @@ type SwitchRequest = { notifySuccess: boolean; }; -interface ProfileSwitchMachineOptions { +interface SwitchState { + switching: SwitchRequest | null; + queued: SwitchRequest | null; + status: "idle" | "running"; + lastError: string | null; +} + +type SwitchAction = + | { type: "REQUEST"; payload: SwitchRequest } + | { type: "RUN_FINISHED"; payload: { error?: string | null } }; + +const initialSwitchState: SwitchState = { + switching: null, + queued: null, + status: "idle", + lastError: null, +}; + +const switchReducer = ( + state: SwitchState, + action: SwitchAction, +): SwitchState => { + switch (action.type) { + case "REQUEST": { + const payload = action.payload; + if (state.switching) { + if (state.switching.profile === payload.profile) { + const notifySuccess = + state.switching.notifySuccess || payload.notifySuccess; + return { + ...state, + switching: { profile: payload.profile, notifySuccess }, + lastError: null, + }; + } + return { + ...state, + queued: payload, + lastError: null, + }; + } + + return { + switching: payload, + queued: null, + status: "running", + lastError: null, + }; + } + case "RUN_FINISHED": { + const nextError = action.payload.error ?? null; + if (state.queued) { + return { + switching: state.queued, + queued: null, + status: "running", + lastError: nextError, + }; + } + + return { + switching: null, + queued: null, + status: "idle", + lastError: nextError, + }; + } + default: + return state; + } +}; + +interface ProfileSwitchControllerOptions { getCurrentProfileId: () => string | undefined; - patchProfiles: ( - value: Partial, - signal: AbortSignal, - ) => Promise; + patchProfiles: (value: Partial) => Promise; mutateLogs: () => Promise; closeAllConnections: () => Promise; activateSelected: () => Promise; @@ -90,7 +166,7 @@ interface ProfileSwitchMachineOptions { t: TFunction; } -const useProfileSwitchMachine = ({ +const useProfileSwitchController = ({ getCurrentProfileId, patchProfiles, mutateLogs, @@ -98,142 +174,96 @@ const useProfileSwitchMachine = ({ 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]); +}: ProfileSwitchControllerOptions) => { + const [state, dispatch] = useReducer(switchReducer, initialSwitchState); + const runningProfileRef = useRef(null); const requestSwitch = useCallback( (profile: string, notifySuccess: boolean) => { - const currentProfileId = getCurrentProfileId(); - if (currentProfileId === profile && !notifySuccess) { + const currentId = getCurrentProfileId(); + if (!notifySuccess && currentId === profile && state.status === "idle") { 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(); + dispatch({ type: "REQUEST", payload: { profile, notifySuccess } }); }, - [getCurrentProfileId, processSwitchQueue], + [getCurrentProfileId, state.status], ); + useEffect(() => { + if (state.status !== "running" || !state.switching) return; + const { profile, notifySuccess } = state.switching; + + if (runningProfileRef.current === profile) return; + runningProfileRef.current = profile; + + let cancelled = false; + let delayTimer: number | null = null; + setActivatings((prev) => { + if (prev.includes(profile)) return prev; + return [...prev, profile]; + }); + + (async () => { + let errorMessage: string | null = null; + try { + await patchProfiles({ current: profile }); + if (cancelled) return; + + await mutateLogs(); + await closeAllConnections(); + + if (!cancelled && notifySuccess) { + showNotice("success", t("Profile Switched"), 1000); + } + + if (!cancelled) { + await new Promise((resolve) => { + delayTimer = window.setTimeout(() => { + delayTimer = null; + resolve(); + }, 50); + }); + await activateSelected(); + } + } catch (error: any) { + if (!cancelled) { + errorMessage = error?.message || String(error); + showNotice("error", errorMessage, 4000); + } + } finally { + setActivatings((prev) => prev.filter((id) => id !== profile)); + runningProfileRef.current = null; + if (!cancelled) { + dispatch({ type: "RUN_FINISHED", payload: { error: errorMessage } }); + } + } + })(); + + return () => { + cancelled = true; + if (delayTimer !== null) { + clearTimeout(delayTimer); + delayTimer = null; + } + }; + }, [ + state.status, + state.switching, + patchProfiles, + mutateLogs, + closeAllConnections, + activateSelected, + setActivatings, + t, + ]); + return { requestSwitch, - switchingProfileRef, + switchingProfile: state.switching?.profile ?? null, + status: state.status, + lastError: state.lastError, }; }; @@ -255,7 +285,7 @@ const normalizeProfileUrl = (value?: string) => { } catch { const schemeNormalized = trimmed.replace( /^([a-z]+):\/\//i, - (match, scheme: string) => `${scheme.toLowerCase()}://`, + (_match, scheme: string) => `${scheme.toLowerCase()}://`, ); return schemeNormalized.replace(/\/+$/, ""); } @@ -553,15 +583,16 @@ const ProfilePage = () => { await closeAllConnections(); }, []); - const { requestSwitch, switchingProfileRef } = useProfileSwitchMachine({ - getCurrentProfileId, - patchProfiles, - mutateLogs: memoizedMutateLogs, - closeAllConnections: closeConnections, - activateSelected, - setActivatings, - t, - }); + const { requestSwitch, switchingProfile: switchingProfileId } = + useProfileSwitchController({ + getCurrentProfileId, + patchProfiles: (payload) => patchProfiles(payload), + mutateLogs: memoizedMutateLogs, + closeAllConnections: closeConnections, + activateSelected, + setActivatings, + t, + }); // 强化的刷新策略 const performRobustRefresh = async ( @@ -651,7 +682,7 @@ const ProfilePage = () => { const onSelect = (current: string, force: boolean) => { // 阻止重复点击或已激活的profile - if (switchingProfileRef.current === current) { + if (switchingProfileId === current) { debugProfileSwitch("DUPLICATE_CLICK_IGNORED", current); return; } @@ -672,9 +703,9 @@ const ProfilePage = () => { }, [current, mutateProfiles, requestSwitch]); const onEnhance = useLockFn(async (notifySuccess: boolean) => { - if (switchingProfileRef.current) { + if (switchingProfileId) { console.log( - `[Profile] 有profile正在切换中(${switchingProfileRef.current}),跳过enhance操作`, + `[Profile] 有profile正在切换中(${switchingProfileId}),跳过enhance操作`, ); return; } @@ -693,7 +724,9 @@ const ProfilePage = () => { } finally { // 保留正在切换的profile,清除其他状态 setActivatings((prev) => - prev.filter((id) => id === switchingProfileRef.current), + switchingProfileId + ? prev.filter((id) => id === switchingProfileId) + : [], ); } }); @@ -834,7 +867,7 @@ const ProfilePage = () => { }); const mode = useThemeMode(); - const islight = mode === "light" ? true : false; + const islight = mode === "light"; const dividercolor = islight ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)";