mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 10:42:09 +08:00
refactor(hooks): drop the write-only error channel and key-rotating refresh
The mihomo ws subscription's next callback accepted an error half that baseNext silently discarded while traffic, memory and log consumers kept constructing fallback payloads for it. Refresh minted a fresh SWR key per call via a localStorage timestamp, abandoning old keys in the cache forever. Reduce next to its data half and reset the snapshot under a stable key instead.
This commit is contained in:
@@ -54,8 +54,7 @@ export const useLogData = () => {
|
||||
const hasLoadedInitialLogsRef = useRef(false)
|
||||
|
||||
const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({
|
||||
storageKey: 'mihomo_logs_date',
|
||||
buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null),
|
||||
subscriptionKey: enableLog ? 'getClashLog' : null,
|
||||
fallbackData: [],
|
||||
connect: () => MihomoWebSocket.connect_logs(logLevel),
|
||||
setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
|
||||
@@ -77,14 +76,13 @@ export const useLogData = () => {
|
||||
}
|
||||
const pendingLogs = buffer.splice(0, buffer.length)
|
||||
flushTimeStr = null
|
||||
next(null, (current) => appendLogs(current, pendingLogs))
|
||||
next((current) => appendLogs(current, pendingLogs))
|
||||
flushTimer = null
|
||||
}
|
||||
|
||||
return {
|
||||
handleMessage: (data) => {
|
||||
if (data.startsWith('Websocket error')) {
|
||||
next(data)
|
||||
void scheduleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -108,9 +106,7 @@ export const useLogData = () => {
|
||||
if (!flushTimer) {
|
||||
flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
|
||||
}
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
async onConnected() {
|
||||
if (hasLoadedInitialLogsRef.current) {
|
||||
@@ -119,7 +115,7 @@ export const useLogData = () => {
|
||||
const logs = await getClashLogs()
|
||||
hasLoadedInitialLogsRef.current = true
|
||||
if (isMounted()) {
|
||||
next(null, (current) => {
|
||||
next((current) => {
|
||||
if (!current || current.length === 0) {
|
||||
return clampLogs(filterLogsByLevel(logs, allowedTypes))
|
||||
}
|
||||
|
||||
@@ -33,15 +33,13 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
|
||||
const enabled = options?.enabled ?? true
|
||||
|
||||
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
|
||||
storageKey: 'mihomo_memory_date',
|
||||
buildSubscriptKey: (date) => (enabled ? `getClashMemory-${date}` : null),
|
||||
subscriptionKey: enabled ? 'getClashMemory' : null,
|
||||
fallbackData: FALLBACK_MEMORY_USAGE,
|
||||
connect: () => MihomoWebSocket.connect_memory(),
|
||||
throttleMs: 500,
|
||||
setupHandlers: ({ next, scheduleReconnect }) => ({
|
||||
handleMessage: (data) => {
|
||||
if (data.startsWith('Websocket error')) {
|
||||
next(data, FALLBACK_MEMORY_USAGE)
|
||||
void scheduleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -51,10 +49,8 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
|
||||
if (shouldSkipDuplicateMemory(parsed)) {
|
||||
return
|
||||
}
|
||||
next(null, parsed)
|
||||
} catch (error) {
|
||||
next(error, FALLBACK_MEMORY_USAGE)
|
||||
}
|
||||
next(parsed)
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { useLocalStorage } from 'foxact/use-local-storage'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import {
|
||||
removeCacheData,
|
||||
setCacheData,
|
||||
useQuery,
|
||||
} from '@/services/query-client'
|
||||
import { setCacheData, useQuery } from '@/services/query-client'
|
||||
|
||||
const RECONNECT_DELAY_MS = 1000
|
||||
|
||||
@@ -31,7 +26,6 @@ interface SharedSubscriptionEntry {
|
||||
|
||||
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
|
||||
const subscriptionSnapshots = new Map<string, unknown>()
|
||||
const initialSubscriptionDate = Date.now()
|
||||
|
||||
const getSubscriptionSnapshot = <T>(key: string) =>
|
||||
subscriptionSnapshots.get(key) as T | undefined
|
||||
@@ -136,15 +130,13 @@ const createSharedSubscriptionEntry = (
|
||||
return entry
|
||||
}
|
||||
|
||||
type Updater<T> = (current?: T) => T
|
||||
|
||||
/**
|
||||
* Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a
|
||||
* functional updater `(current?: T) => T`. The functional form is resolved
|
||||
* against the current subscription snapshot before updating SWR.
|
||||
* Pass either a plain value or a functional updater; the updater is resolved
|
||||
* against the current subscription snapshot before the cache is written.
|
||||
*/
|
||||
type NextFn<T> = (
|
||||
error?: any,
|
||||
data?: T | ((current?: T) => T | undefined),
|
||||
) => void
|
||||
type NextFn<T> = (data: T | Updater<T>) => void
|
||||
|
||||
interface HandlerContext<T> {
|
||||
next: NextFn<T>
|
||||
@@ -159,8 +151,7 @@ interface HandlerResult {
|
||||
}
|
||||
|
||||
interface UseMihomoWsSubscriptionOptions<T> {
|
||||
storageKey: string
|
||||
buildSubscriptKey: (date: number) => string | null
|
||||
subscriptionKey: string | null
|
||||
fallbackData: T
|
||||
connect: () => Promise<MihomoWebSocket>
|
||||
/**
|
||||
@@ -178,18 +169,12 @@ interface UseMihomoWsSubscriptionOptions<T> {
|
||||
export const useMihomoWsSubscription = <T>(
|
||||
options: UseMihomoWsSubscriptionOptions<T>,
|
||||
) => {
|
||||
const {
|
||||
storageKey,
|
||||
buildSubscriptKey,
|
||||
fallbackData,
|
||||
connect,
|
||||
throttleMs,
|
||||
setupHandlers,
|
||||
} = options
|
||||
const { subscriptionKey, fallbackData, connect, throttleMs, setupHandlers } =
|
||||
options
|
||||
|
||||
const [date, setDate] = useLocalStorage(storageKey, initialSubscriptionDate)
|
||||
const subscriptKey = buildSubscriptKey(date)
|
||||
const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null
|
||||
const subscriptionCacheKey = subscriptionKey
|
||||
? `$sub$${subscriptionKey}`
|
||||
: null
|
||||
const lastSubscriptionCacheKeyRef = useRef<string | null>(null)
|
||||
if (subscriptionCacheKey) {
|
||||
lastSubscriptionCacheKeyRef.current = subscriptionCacheKey
|
||||
@@ -197,20 +182,9 @@ export const useMihomoWsSubscription = <T>(
|
||||
const responseCacheKey =
|
||||
subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current
|
||||
|
||||
const resolveNextData = useCallback(
|
||||
(
|
||||
data: T | ((current?: T) => T | undefined) | undefined,
|
||||
cacheKey: string,
|
||||
): T => {
|
||||
if (typeof data === 'function') {
|
||||
const updater = data as (current?: T) => T | undefined
|
||||
const current = getSubscriptionSnapshot<T>(cacheKey)
|
||||
return updater(current) ?? fallbackData
|
||||
}
|
||||
return data ?? fallbackData
|
||||
},
|
||||
[fallbackData],
|
||||
)
|
||||
// Bumping the epoch re-runs the subscription effect, rebuilding the shared
|
||||
// entry (and its captured `connect`/handler closures) under the same key.
|
||||
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
||||
|
||||
const response = useQuery<T>({
|
||||
queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'],
|
||||
@@ -238,44 +212,35 @@ export const useMihomoWsSubscription = <T>(
|
||||
let throttleCleanup: (() => void) | undefined
|
||||
let wrappedNext: NextFn<T>
|
||||
|
||||
const baseNext: NextFn<T> = (error, data) => {
|
||||
if (error !== undefined && error !== null) {
|
||||
return
|
||||
}
|
||||
if (data === undefined) return
|
||||
const resolved = resolveNextData(data, subscriptionCacheKey)
|
||||
const baseNext: NextFn<T> = (data) => {
|
||||
const resolved =
|
||||
typeof data === 'function'
|
||||
? (data as Updater<T>)(
|
||||
getSubscriptionSnapshot<T>(subscriptionCacheKey),
|
||||
)
|
||||
: (data as T)
|
||||
writeSubscriptionSnapshot(subscriptionCacheKey, resolved)
|
||||
}
|
||||
|
||||
if (throttleMs && throttleMs > 0) {
|
||||
let pendingData: T | ((current?: T) => T | undefined) | undefined
|
||||
let hasPending = false
|
||||
let pendingData: T | Updater<T> | undefined
|
||||
let timerId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const flush = () => {
|
||||
timerId = null
|
||||
if (hasPending) {
|
||||
if (pendingData !== undefined) {
|
||||
const data = pendingData
|
||||
pendingData = undefined
|
||||
hasPending = false
|
||||
baseNext(undefined, data)
|
||||
baseNext(data)
|
||||
}
|
||||
}
|
||||
|
||||
wrappedNext = (
|
||||
error?: any,
|
||||
data?: T | ((current?: T) => T | undefined),
|
||||
) => {
|
||||
if (error !== undefined && error !== null) {
|
||||
baseNext(error, data)
|
||||
return
|
||||
}
|
||||
wrappedNext = (data) => {
|
||||
if (!timerId) {
|
||||
baseNext(undefined, data)
|
||||
baseNext(data)
|
||||
timerId = setTimeout(flush, throttleMs)
|
||||
} else {
|
||||
pendingData = data
|
||||
hasPending = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,15 +306,14 @@ export const useMihomoWsSubscription = <T>(
|
||||
}
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
|
||||
}, [subscriptionCacheKey])
|
||||
}, [subscriptionCacheKey, refreshEpoch])
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (subscriptionCacheKey) {
|
||||
subscriptionSnapshots.delete(subscriptionCacheKey)
|
||||
void removeCacheData([subscriptionCacheKey])
|
||||
writeSubscriptionSnapshot(subscriptionCacheKey, fallbackData)
|
||||
}
|
||||
setDate(Date.now())
|
||||
}, [subscriptionCacheKey, setDate])
|
||||
setRefreshEpoch((epoch) => epoch + 1)
|
||||
}, [subscriptionCacheKey, fallbackData])
|
||||
|
||||
const setData = useCallback(
|
||||
(data: T) => {
|
||||
|
||||
@@ -32,15 +32,13 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
||||
graphData: { appendData },
|
||||
} = useTrafficMonitorEnhanced({ subscribe: false, enabled })
|
||||
const { response, refresh } = useMihomoWsSubscription<ITrafficItem>({
|
||||
storageKey: 'mihomo_traffic_date',
|
||||
buildSubscriptKey: (date) => (enabled ? `getClashTraffic-${date}` : null),
|
||||
subscriptionKey: enabled ? 'getClashTraffic' : null,
|
||||
fallbackData: FALLBACK_TRAFFIC,
|
||||
connect: () => MihomoWebSocket.connect_traffic(),
|
||||
throttleMs: 200,
|
||||
setupHandlers: ({ next, scheduleReconnect }) => ({
|
||||
handleMessage: (data) => {
|
||||
if (data.startsWith('Websocket error')) {
|
||||
next(data, FALLBACK_TRAFFIC)
|
||||
void scheduleReconnect()
|
||||
return
|
||||
}
|
||||
@@ -51,10 +49,8 @@ export const useTrafficData = (options?: { enabled?: boolean }) => {
|
||||
return
|
||||
}
|
||||
appendData(parsed)
|
||||
next(null, parsed)
|
||||
} catch (error) {
|
||||
next(error, FALLBACK_TRAFFIC)
|
||||
}
|
||||
next(parsed)
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user