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

View File

@ -120,9 +120,9 @@ export const useProfiles = () => {
])
// 处理所有代理组
;[global, ...groups].forEach((group) => {
for (const group of [global, ...groups]) {
if (!group) {
return
continue
}
const { type, name, now } = group
@ -134,14 +134,14 @@ export const useProfiles = () => {
const preferredProxy = now ? now : savedProxy
newSelected.push({ name, now: preferredProxy })
}
return
continue
}
if (savedProxy == null) {
if (now != null) {
newSelected.push({ name, now })
}
return
continue
}
const existsInGroup = availableProxies.some((proxy) => {
@ -158,7 +158,7 @@ export const useProfiles = () => {
)
hasChange = true
newSelected.push({ name, now: now ?? savedProxy })
return
continue
}
if (savedProxy !== now) {
@ -166,11 +166,18 @@ export const useProfiles = () => {
`[ActivateSelected] 需要切换代理组 ${name}: ${now} -> ${savedProxy}`,
)
hasChange = true
selectNodeForGroup(name, savedProxy)
try {
await selectNodeForGroup(name, savedProxy)
} catch (error: any) {
console.warn(
`[ActivateSelected] 切换代理组 ${name} 失败:`,
error.message,
)
}
}
newSelected.push({ name, now: savedProxy })
})
}
if (!hasChange) {
debugLog('[ActivateSelected] 所有代理选择已经是目标状态,无需更新')
@ -183,9 +190,7 @@ export const useProfiles = () => {
await patchProfile(profileData.current!, { selected: newSelected })
debugLog('[ActivateSelected] 代理选择配置保存成功')
setTimeout(() => {
mutate('getProxies', calcuProxies())
}, 100)
await mutate('getProxies', calcuProxies())
} catch (error: any) {
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 { getRunningMode, isAdmin, isServiceAvailable } from '@/services/cmds'
@ -18,14 +18,13 @@ const defaultSystemState = {
isServiceOk: false,
} as SystemState
let disablingTunMode = false
/**
* hook
*
*/
export function useSystemState() {
const { verge, patchVerge } = useVerge()
const disablingTunRef = useRef(false)
const {
data: systemState,
@ -53,16 +52,18 @@ export function useSystemState() {
const isTunModeAvailable = systemState.isAdminMode || systemState.isServiceOk
const enable_tun_mode = verge?.enable_tun_mode
const cooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
if (enable_tun_mode === undefined) return
if (
!disablingTunMode &&
!disablingTunRef.current &&
enable_tun_mode &&
!isTunModeAvailable &&
!isLoading
) {
disablingTunMode = true
disablingTunRef.current = true
patchVerge({ enable_tun_mode: false })
.then(() => {
showNotice.info(
@ -76,13 +77,21 @@ export function useSystemState() {
)
})
.finally(() => {
const tid = setTimeout(() => {
// 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式
disablingTunMode = false
clearTimeout(tid)
// 避免 verge 数据更新不及时导致重复执行关闭 Tun 模式
cooldownTimerRef.current = setTimeout(() => {
disablingTunRef.current = false
cooldownTimerRef.current = null
}, 1000)
})
}
return () => {
if (cooldownTimerRef.current != null) {
clearTimeout(cooldownTimerRef.current)
cooldownTimerRef.current = null
disablingTunRef.current = false
}
}
}, [enable_tun_mode, isTunModeAvailable, patchVerge, isLoading])
return {