fix: resolve frontend data race conditions in hooks

- use-system-state: convert module-level `disablingTunMode` to useRef
  to isolate state per hook instance, fix no-op clearTimeout, add
  proper effect cleanup
- use-profiles: convert forEach to for..of so selectNodeForGroup is
  properly awaited, remove fire-and-forget setTimeout around mutate
- use-clash: add useLockFn to patchInfo for concurrency safety
This commit is contained in:
Tunglies 2026-03-31 20:26:34 +08:00
parent b62d89e163
commit 9bcb79465c
No known key found for this signature in database
GPG Key ID: B9B01B389469B3E8
3 changed files with 35 additions and 21 deletions

View File

@ -87,7 +87,7 @@ export const useClashInfo = () => {
getClashInfo, getClashInfo,
) )
const patchInfo = async (patch: ClashInfoPatch) => { const patchInfo = useLockFn(async (patch: ClashInfoPatch) => {
if (!hasClashInfoPayload(patch)) return if (!hasClashInfoPayload(patch)) return
validatePorts(patch) validatePorts(patch)
@ -95,7 +95,7 @@ export const useClashInfo = () => {
await patchClashConfig(patch) await patchClashConfig(patch)
mutateInfo() mutateInfo()
mutate('getClashConfig') mutate('getClashConfig')
} })
return { return {
clashInfo, clashInfo,

View File

@ -120,9 +120,9 @@ export const useProfiles = () => {
]) ])
// 处理所有代理组 // 处理所有代理组
;[global, ...groups].forEach((group) => { for (const group of [global, ...groups]) {
if (!group) { if (!group) {
return continue
} }
const { type, name, now } = group const { type, name, now } = group
@ -134,14 +134,14 @@ export const useProfiles = () => {
const preferredProxy = now ? now : savedProxy const preferredProxy = now ? now : savedProxy
newSelected.push({ name, now: preferredProxy }) newSelected.push({ name, now: preferredProxy })
} }
return continue
} }
if (savedProxy == null) { if (savedProxy == null) {
if (now != null) { if (now != null) {
newSelected.push({ name, now }) newSelected.push({ name, now })
} }
return continue
} }
const existsInGroup = availableProxies.some((proxy) => { const existsInGroup = availableProxies.some((proxy) => {
@ -158,7 +158,7 @@ export const useProfiles = () => {
) )
hasChange = true hasChange = true
newSelected.push({ name, now: now ?? savedProxy }) newSelected.push({ name, now: now ?? savedProxy })
return continue
} }
if (savedProxy !== now) { if (savedProxy !== now) {
@ -166,11 +166,18 @@ export const useProfiles = () => {
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${savedProxy}`, `[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${savedProxy}`,
) )
hasChange = true hasChange = true
selectNodeForGroup(name, savedProxy) try {
await selectNodeForGroup(name, savedProxy)
} catch (error: any) {
console.warn(
`[ActivateSelected] 切换代理组 ${name} 失败:`,
error.message,
)
}
} }
newSelected.push({ name, now: savedProxy }) newSelected.push({ name, now: savedProxy })
}) }
if (!hasChange) { if (!hasChange) {
debugLog('[ActivateSelected] 所有代理选择已经是目标状态,无需更新') debugLog('[ActivateSelected] 所有代理选择已经是目标状态,无需更新')
@ -183,9 +190,7 @@ export const useProfiles = () => {
await patchProfile(profileData.current!, { selected: newSelected }) await patchProfile(profileData.current!, { selected: newSelected })
debugLog('[ActivateSelected] 代理选择配置保存成功') debugLog('[ActivateSelected] 代理选择配置保存成功')
setTimeout(() => { await mutate('getProxies', calcuProxies())
mutate('getProxies', calcuProxies())
}, 100)
} catch (error: any) { } catch (error: any) {
console.error('[ActivateSelected] 保存代理选择配置失败:', error.message) console.error('[ActivateSelected] 保存代理选择配置失败:', error.message)
} }

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react' import { useEffect, useRef } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { getRunningMode, isAdmin, isServiceAvailable } from '@/services/cmds' import { getRunningMode, isAdmin, isServiceAvailable } from '@/services/cmds'
@ -18,14 +18,13 @@ const defaultSystemState = {
isServiceOk: false, isServiceOk: false,
} as SystemState } as SystemState
let disablingTunMode = false
/** /**
* hook * hook
* *
*/ */
export function useSystemState() { export function useSystemState() {
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
const disablingTunRef = useRef(false)
const { const {
data: systemState, data: systemState,
@ -53,16 +52,18 @@ export function useSystemState() {
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk
const enable_tun_mode = verge?.enable_tun_mode const enable_tun_mode = verge?.enable_tun_mode
const cooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => { useEffect(() => {
if (enable_tun_mode === undefined) return if (enable_tun_mode === undefined) return
if ( if (
!disablingTunMode && !disablingTunRef.current &&
enable_tun_mode && enable_tun_mode &&
!isTunModeAvailable && !isTunModeAvailable &&
!isLoading !isLoading
) { ) {
disablingTunMode = true disablingTunRef.current = true
patchVerge({ enable_tun_mode: false }) patchVerge({ enable_tun_mode: false })
.then(() => { .then(() => {
showNotice.info( showNotice.info(
@ -76,13 +77,21 @@ export function useSystemState() {
) )
}) })
.finally(() => { .finally(() => {
const tid = setTimeout(() => {
// 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式 // 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式
disablingTunMode = false cooldownTimerRef.current = setTimeout(() => {
clearTimeout(tid) disablingTunRef.current = false
cooldownTimerRef.current = null
}, 1000) }, 1000)
}) })
} }
return () => {
if (cooldownTimerRef.current != null) {
clearTimeout(cooldownTimerRef.current)
cooldownTimerRef.current = null
disablingTunRef.current = false
}
}
}, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading]) }, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading])
return { return {