mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-10 12:22:07 +08:00
fix(proxies): retain independent delay listeners
Notify every mounted occurrence sharing a delay key and scope cleanup to its registered listener. Cover pending results, profile reset, and either occurrence unmounting without restoring direct unfenced updates.
This commit is contained in:
@@ -48,10 +48,7 @@ export function useProxyDelayState(
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreset) return
|
||||
delayManager.setListener(name, groupName, setDelayState)
|
||||
return () => {
|
||||
delayManager.removeListener(name, groupName)
|
||||
}
|
||||
return delayManager.addListener(name, groupName, setDelayState)
|
||||
}, [name, groupName, isPreset])
|
||||
|
||||
const updateDelay = useCallback(() => {
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('group delay completion', () => {
|
||||
describe('profile delay isolation', () => {
|
||||
test('drops cached and queued results on switch, exposing current history', async () => {
|
||||
const listener = vi.fn()
|
||||
delayManager.setListener('a', 'g', listener)
|
||||
const stop = delayManager.addListener('a', 'g', listener)
|
||||
try {
|
||||
delayManager.setDelay('a', 'g', 120)
|
||||
const previousSnapshot = delayManager.groupDelays('g')
|
||||
@@ -99,10 +99,57 @@ describe('profile delay isolation', () => {
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(settles).toBe(1)
|
||||
} finally {
|
||||
delayManager.removeListener('a', 'g')
|
||||
stop()
|
||||
}
|
||||
})
|
||||
|
||||
test.each([0, 1])(
|
||||
'keeps shared-key subscribers independent when occurrence %i unmounts',
|
||||
async (removed) => {
|
||||
const listeners = [vi.fn(), vi.fn()]
|
||||
const stops = listeners.map((listener) =>
|
||||
delayManager.addListener('a', 'g', listener),
|
||||
)
|
||||
try {
|
||||
const checking = delayManager.checkDelay(node('a'), 'g', 5000)
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
for (const listener of listeners) {
|
||||
expect(listener).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ delay: -2 }),
|
||||
)
|
||||
}
|
||||
|
||||
await vi.runAllTimersAsync()
|
||||
await checking
|
||||
for (const listener of listeners) {
|
||||
expect(listener.mock.calls.map(([update]) => update.delay)).toEqual([
|
||||
-2, 120,
|
||||
])
|
||||
}
|
||||
|
||||
delayManager.setProfile('next')
|
||||
for (const listener of listeners) {
|
||||
expect(listener).toHaveBeenLastCalledWith({
|
||||
delay: -1,
|
||||
updatedAt: 0,
|
||||
})
|
||||
listener.mockClear()
|
||||
}
|
||||
|
||||
const nextCheck = delayManager.checkDelay(node('a'), 'g', 5000)
|
||||
stops[removed]()
|
||||
await vi.runAllTimersAsync()
|
||||
await nextCheck
|
||||
expect(listeners[removed]).not.toHaveBeenCalled()
|
||||
expect(
|
||||
listeners[1 - removed].mock.calls.map(([update]) => update.delay),
|
||||
).toEqual([-2, 120])
|
||||
} finally {
|
||||
for (const stop of stops) stop()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
test.each(['success', 'failure'] as const)(
|
||||
'ignores an old-profile single test completing with %s',
|
||||
async (outcome) => {
|
||||
|
||||
+33
-25
@@ -34,7 +34,7 @@ class DelayManager {
|
||||
private cache = new Map<string, DelayUpdate>()
|
||||
private urlMap = new Map<string, string>()
|
||||
|
||||
private listenerMap = new Map<string, (update: DelayUpdate) => void>()
|
||||
private listenerMap = new Map<string, Set<(update: DelayUpdate) => void>>()
|
||||
|
||||
private groupListenerMap = new Map<string, Set<() => void>>()
|
||||
// Consumers compare snapshot identity; replace it only when the group settles.
|
||||
@@ -63,14 +63,16 @@ class DelayManager {
|
||||
this.groupSetSnapshots.clear()
|
||||
|
||||
// Reset before a refreshed proxy snapshot can publish newer history.
|
||||
for (const [key, listener] of this.listenerMap) {
|
||||
try {
|
||||
listener({ delay: -1, updatedAt: 0 })
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] Failed to reset delay listener: ${key}`,
|
||||
error,
|
||||
)
|
||||
for (const [key, listeners] of this.listenerMap) {
|
||||
for (const listener of [...listeners]) {
|
||||
try {
|
||||
listener({ delay: -1, updatedAt: 0 })
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] Failed to reset delay listener: ${key}`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const group of this.groupListenerMap.keys()) {
|
||||
@@ -103,17 +105,19 @@ class DelayManager {
|
||||
this.pendingItemUpdates = new Map()
|
||||
|
||||
updates.forEach((queue, key) => {
|
||||
const listener = this.listenerMap.get(key)
|
||||
if (!listener) return
|
||||
const listeners = this.listenerMap.get(key)
|
||||
if (!listeners) return
|
||||
|
||||
queue.forEach((update) => {
|
||||
try {
|
||||
listener(update)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] 通知节点延迟监听器失败: ${key}`,
|
||||
error,
|
||||
)
|
||||
for (const listener of [...listeners]) {
|
||||
try {
|
||||
listener(update)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] 通知节点延迟监听器失败: ${key}`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -199,18 +203,22 @@ class DelayManager {
|
||||
return url || 'http://cp.cloudflare.com/generate_204'
|
||||
}
|
||||
|
||||
setListener(
|
||||
addListener(
|
||||
name: string,
|
||||
group: string,
|
||||
listener: (update: DelayUpdate) => void,
|
||||
) {
|
||||
): () => void {
|
||||
const key = hashKey(name, group)
|
||||
this.listenerMap.set(key, listener)
|
||||
}
|
||||
const listeners = this.listenerMap.get(key) ?? new Set()
|
||||
listeners.add(listener)
|
||||
this.listenerMap.set(key, listeners)
|
||||
|
||||
removeListener(name: string, group: string) {
|
||||
const key = hashKey(name, group)
|
||||
this.listenerMap.delete(key)
|
||||
return () => {
|
||||
const current = this.listenerMap.get(key)
|
||||
if (!current) return
|
||||
current.delete(listener)
|
||||
if (current.size === 0) this.listenerMap.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
/** Multiple views may subscribe independently; notifications occur only after settle. */
|
||||
|
||||
Reference in New Issue
Block a user