From 62666b5e27ef97d816c4bf19dd87e5536729ff6f Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 11 Nov 2026 05:47:22 +0800 Subject: [PATCH] fix(proxies): derive mounted delays from shared snapshots Remove mirrored item delay state so profile cache invalidation cannot leave reused rows blank until remount. Read current node history when the shared cache is empty and preserve snapshot identity for React subscriptions. --- src/hooks/use-proxy-delay-state.ts | 70 ++++++++++-------------------- src/services/delay.test.ts | 3 ++ src/services/delay.ts | 3 +- 3 files changed, 29 insertions(+), 47 deletions(-) diff --git a/src/hooks/use-proxy-delay-state.ts b/src/hooks/use-proxy-delay-state.ts index 66913f281..0e37be0f5 100644 --- a/src/hooks/use-proxy-delay-state.ts +++ b/src/hooks/use-proxy-delay-state.ts @@ -1,5 +1,5 @@ import { useLockFn } from 'ahooks' -import { useCallback, useEffect, useReducer } from 'react' +import { useCallback, useSyncExternalStore } from 'react' import { useVerge } from '@/hooks/use-verge' import delayManager, { type DelayUpdate } from '@/services/delay' @@ -17,13 +17,6 @@ const PRESET_PROXY_NAMES = [ 'COMPATIBLE', ] -const identity = (previous: DelayUpdate, next: DelayUpdate): DelayUpdate => - previous.delay === next.delay && - previous.elapsed === next.elapsed && - previous.updatedAt === next.updatedAt - ? previous - : next - const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 } export interface UseProxyDelayState { @@ -42,48 +35,33 @@ export function useProxyDelayState( const details = memberDetails(member) const unresolved = member.kind === 'unresolved' const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name) - const [delayState, setDelayState] = useReducer(identity, INITIAL_DELAY) const { verge } = useVerge() const timeout = verge?.default_latency_timeout || 10000 - useEffect(() => { - if (isPreset) return - return delayManager.addListener(name, groupName, setDelayState) - }, [name, groupName, isPreset]) + const subscribe = useCallback( + (onChange: () => void) => + isPreset ? () => {} : delayManager.addListener(name, groupName, onChange), + [name, groupName, isPreset], + ) + const read = useCallback( + () => + unresolved ? undefined : delayManager.getDelayUpdate(name, groupName), + [name, groupName, unresolved], + ) + const cachedUpdate = useSyncExternalStore(subscribe, read, read) - const updateDelay = useCallback(() => { - if (unresolved) { - setDelayState(INITIAL_DELAY) - return - } - const cachedUpdate = delayManager.getDelayUpdate(name, groupName) - if (cachedUpdate) { - setDelayState({ ...cachedUpdate }) - return - } - - const fallbackDelay = delayManager.getDelayFix(member, groupName) - if (fallbackDelay === -1) { - setDelayState({ delay: -1, updatedAt: 0 }) - return - } - - let updatedAt = 0 - const history = details?.history - if (history && history.length > 0) { - const lastRecord = history[history.length - 1] - const parsed = Date.parse(lastRecord.time) - if (!Number.isNaN(parsed)) { - updatedAt = parsed - } - } - - setDelayState({ delay: fallbackDelay, updatedAt }) - }, [details?.history, groupName, member, name, unresolved]) - - useEffect(() => { - updateDelay() - }, [updateDelay]) + const fallbackDelay = delayManager.getDelayFix(member, groupName) + const history = details?.history + const lastRecord = history?.[history.length - 1] + const parsed = lastRecord ? Date.parse(lastRecord.time) : NaN + const delayState = + cachedUpdate ?? + (fallbackDelay === -1 + ? INITIAL_DELAY + : { + delay: fallbackDelay, + updatedAt: Number.isNaN(parsed) ? 0 : parsed, + }) const onDelay = useLockFn(async () => { if (!isInteractableMember(member)) return diff --git a/src/services/delay.test.ts b/src/services/delay.test.ts index e7ea32bf0..ebdf8414c 100644 --- a/src/services/delay.test.ts +++ b/src/services/delay.test.ts @@ -74,9 +74,12 @@ describe('profile delay isolation', () => { const stopSecond = delayManager.addListener('a', 'g', second) try { delayManager.setDelay('a', 'g', -2) + const pending = delayManager.getDelayUpdate('a', 'g') + expect(delayManager.getDelayUpdate('a', 'g')).toBe(pending) await flush() stopFirst() delayManager.setDelay('a', 'g', 120) + expect(delayManager.getDelayUpdate('a', 'g')).not.toBe(pending) await flush() expect(first.mock.calls.map(([update]) => update.delay)).toEqual([-2]) expect(second.mock.calls.map(([update]) => update.delay)).toEqual([ diff --git a/src/services/delay.ts b/src/services/delay.ts index 48b5f6e39..67db25cb8 100644 --- a/src/services/delay.ts +++ b/src/services/delay.ts @@ -274,7 +274,8 @@ class DelayManager { return undefined } - return { ...entry } + // React's external-store reads must retain identity until the value changes. + return entry } getDelay(name: string, group: string) {