refactor(frontend): consolidate query state ownership

Consolidate ordinary query snapshots on SWR and make Profiles own its query key, cache mutation, and invalidation policy.

Impact:
- remove the generic synchronous query Map while keeping dedicated Mihomo WebSocket snapshots
- eliminate duplicate Profiles refreshes while preserving retries and the 800 ms event throttle
- preserve preload-backed Verge updates and system proxy rollback behavior
- scope uptime polling to the Home Clash info card instead of the app lifetime
- leave RunState, Connections, Traffic, backend, and IPC contracts unchanged
- reduce frontend code by 72 net lines without adding dependencies
This commit is contained in:
Tunglies
2026-09-06 04:32:22 +08:00
parent 00a8518e7a
commit 4ed474706a
14 changed files with 114 additions and 186 deletions
+2 -2
View File
@@ -5,11 +5,11 @@ import { useTranslation } from 'react-i18next'
import { useClash } from '@/hooks/use-clash' import { useClash } from '@/hooks/use-clash'
import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port' import { useDisplayedMixedPort } from '@/hooks/use-displayed-mixed-port'
import { useAppUptime } from '@/hooks/use-system-state'
import { import {
useClashConfigData, useClashConfigData,
useRulesData, useRulesData,
useSystemData, useSystemData,
useUptimeData,
} from '@/providers/app-data-context' } from '@/providers/app-data-context'
import { EnhancedCard } from './enhanced-card' import { EnhancedCard } from './enhanced-card'
@@ -28,7 +28,7 @@ export const ClashInfoCard = () => {
const { clashConfig } = useClashConfigData() const { clashConfig } = useClashConfigData()
const displayedMixedPort = useDisplayedMixedPort() const displayedMixedPort = useDisplayedMixedPort()
const { rules } = useRulesData() const { rules } = useRulesData()
const { uptime } = useUptimeData() const uptime = useAppUptime()
const { systemProxyAddress } = useSystemData() const { systemProxyAddress } = useSystemData()
// 使用useMemo缓存格式化后的uptime,避免频繁计算 // 使用useMemo缓存格式化后的uptime,避免频繁计算
@@ -15,7 +15,7 @@ import {
type RunState, type RunState,
} from '@/services/cmds' } from '@/services/cmds'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { setCacheDataAsync, useQuery } from '@/services/query-client' import { setCacheData, useQuery } from '@/services/query-client'
export const ServiceMigrationDialog = () => { export const ServiceMigrationDialog = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -50,7 +50,7 @@ export const ServiceMigrationDialog = () => {
const refreshRunState = async () => { const refreshRunState = async () => {
try { try {
const data = await getRuntimeState() const data = await getRuntimeState()
await setCacheDataAsync<RunState>(runStateQueryKey, data) await setCacheData<RunState>(runStateQueryKey, data)
setStateRefreshFailed(false) setStateRefreshFailed(false)
return data return data
} catch (error) { } catch (error) {
+1 -6
View File
@@ -9,7 +9,6 @@ import {
patchClashConfig, patchClashConfig,
} from '@/services/cmds' } from '@/services/cmds'
import { import {
getCacheData,
revalidateQuery, revalidateQuery,
setCacheData, setCacheData,
useQuery, useQuery,
@@ -97,11 +96,7 @@ export const useClash = () => {
if (updater === undefined) { if (updater === undefined) {
return refetch() return refetch()
} }
const next = void setCacheData<IConfigData>(['getRuntimeConfig'], updater)
typeof updater === 'function'
? updater(getCacheData<IConfigData>(['getRuntimeConfig']))
: updater
setCacheData(['getRuntimeConfig'], next)
if (revalidate !== false) { if (revalidate !== false) {
return refetch() return refetch()
} }
+2 -7
View File
@@ -3,7 +3,6 @@ import { useEffect, useRef } from 'react'
import { MihomoWebSocket, type LogLevel } from 'tauri-plugin-mihomo-api' import { MihomoWebSocket, type LogLevel } from 'tauri-plugin-mihomo-api'
import { getClashLogs } from '@/services/cmds' import { getClashLogs } from '@/services/cmds'
import { setCacheData } from '@/services/query-client'
import { useClashLog } from './use-clash-log' import { useClashLog } from './use-clash-log'
import { useMihomoWsSubscription } from './use-mihomo-ws-subscription' import { useMihomoWsSubscription } from './use-mihomo-ws-subscription'
@@ -54,9 +53,7 @@ export const useLogData = () => {
const allowedTypes = LOG_LEVEL_FILTERS[logLevel] ?? DEFAULT_LOG_TYPES const allowedTypes = LOG_LEVEL_FILTERS[logLevel] ?? DEFAULT_LOG_TYPES
const hasLoadedInitialLogsRef = useRef(false) const hasLoadedInitialLogsRef = useRef(false)
const { response, refresh, subscriptionCacheKey } = useMihomoWsSubscription< const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({
ILogItem[]
>({
storageKey: 'mihomo_logs_date', storageKey: 'mihomo_logs_date',
buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null), buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null),
fallbackData: [], fallbackData: [],
@@ -154,9 +151,7 @@ export const useLogData = () => {
const refreshGetClashLog = (clear = false) => { const refreshGetClashLog = (clear = false) => {
if (clear) { if (clear) {
if (subscriptionCacheKey) { setData([])
setCacheData<ILogItem[]>([subscriptionCacheKey], [])
}
} else { } else {
hasLoadedInitialLogsRef.current = false hasLoadedInitialLogsRef.current = false
refresh() refresh()
+27 -8
View File
@@ -3,7 +3,6 @@ import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api' import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
import { import {
getCacheData,
removeCacheData, removeCacheData,
setCacheData, setCacheData,
useQuery, useQuery,
@@ -32,8 +31,17 @@ interface SharedSubscriptionEntry {
} }
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>() const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
const subscriptionSnapshots = new Map<string, unknown>()
const initialSubscriptionDate = Date.now() const initialSubscriptionDate = Date.now()
const getSubscriptionSnapshot = <T>(key: string) =>
subscriptionSnapshots.get(key) as T | undefined
const writeSubscriptionSnapshot = <T>(key: string, data: T) => {
subscriptionSnapshots.set(key, data)
void setCacheData<T>([key], data)
}
const syncSharedWsRefs = (entry: SharedSubscriptionEntry) => { const syncSharedWsRefs = (entry: SharedSubscriptionEntry) => {
entry.refHolders.forEach((ref) => { entry.refHolders.forEach((ref) => {
ref.current = entry.ws ref.current = entry.ws
@@ -141,7 +149,7 @@ const createSharedSubscriptionEntry = (
/** /**
* Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a * Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a
* functional updater `(current?: T) => T`. The functional form is resolved * functional updater `(current?: T) => T`. The functional form is resolved
* against the current cache entry before calling `setCacheData`. * against the current subscription snapshot before updating SWR.
*/ */
type NextFn<T> = ( type NextFn<T> = (
error?: any, error?: any,
@@ -208,7 +216,7 @@ export const useMihomoWsSubscription = <T>(
): T => { ): T => {
if (typeof data === 'function') { if (typeof data === 'function') {
const updater = data as (current?: T) => T | undefined const updater = data as (current?: T) => T | undefined
const current = getCacheData<T>([cacheKey]) const current = getSubscriptionSnapshot<T>(cacheKey)
return updater(current) ?? fallbackData return updater(current) ?? fallbackData
} }
return data ?? fallbackData return data ?? fallbackData
@@ -218,9 +226,10 @@ export const useMihomoWsSubscription = <T>(
const response = useQuery<T>({ const response = useQuery<T>({
queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'], queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'],
queryFn: () => getCacheData<T>([responseCacheKey!]) ?? fallbackData, queryFn: () =>
getSubscriptionSnapshot<T>(responseCacheKey!) ?? fallbackData,
initialData: () => initialData: () =>
getCacheData<T>([responseCacheKey ?? '$sub$__disabled__']) ?? getSubscriptionSnapshot<T>(responseCacheKey ?? '$sub$__disabled__') ??
fallbackData, fallbackData,
staleTime: Infinity, staleTime: Infinity,
enabled: subscriptionCacheKey !== null, enabled: subscriptionCacheKey !== null,
@@ -249,7 +258,7 @@ export const useMihomoWsSubscription = <T>(
} }
if (data === undefined) return if (data === undefined) return
const resolved = resolveNextData(data, subscriptionCacheKey) const resolved = resolveNextData(data, subscriptionCacheKey)
setCacheData<T>([subscriptionCacheKey], resolved) writeSubscriptionSnapshot(subscriptionCacheKey, resolved)
} }
if (throttleMs && throttleMs > 0) { if (throttleMs && throttleMs > 0) {
@@ -352,10 +361,20 @@ export const useMihomoWsSubscription = <T>(
const refresh = useCallback(() => { const refresh = useCallback(() => {
if (subscriptionCacheKey) { if (subscriptionCacheKey) {
removeCacheData([subscriptionCacheKey]) subscriptionSnapshots.delete(subscriptionCacheKey)
void removeCacheData([subscriptionCacheKey])
} }
setDate(Date.now()) setDate(Date.now())
}, [subscriptionCacheKey, setDate]) }, [subscriptionCacheKey, setDate])
return { response, refresh, subscriptionCacheKey: responseCacheKey, wsRef } const setData = useCallback(
(data: T) => {
if (responseCacheKey) {
writeSubscriptionSnapshot(responseCacheKey, data)
}
},
[responseCacheKey],
)
return { response, refresh, setData, wsRef }
} }
+16 -5
View File
@@ -1,9 +1,21 @@
import { useCallback, useRef } from 'react' import { useCallback, useRef } from 'react'
import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds' import { getProfiles, patchProfile, patchProfilesConfig } from '@/services/cmds'
import { setCacheDataAsync, useQuery } from '@/services/query-client' import {
fetchCacheData,
revalidateQuery,
setCacheData,
useQuery,
} from '@/services/query-client'
import { debugLog } from '@/utils/debug' import { debugLog } from '@/utils/debug'
const profilesQueryKey = ['getProfiles'] as const
export const revalidateProfiles = () => revalidateQuery(profilesQueryKey)
export const fetchProfilesIntoCache = () =>
fetchCacheData(profilesQueryKey, getProfiles)
export const useProfiles = () => { export const useProfiles = () => {
const { const {
data: profiles, data: profiles,
@@ -11,7 +23,7 @@ export const useProfiles = () => {
error, error,
isFetching: isValidating, isFetching: isValidating,
} = useQuery({ } = useQuery({
queryKey: ['getProfiles'], queryKey: profilesQueryKey,
queryFn: async () => { queryFn: async () => {
const data = await getProfiles() const data = await getProfiles()
debugLog( debugLog(
@@ -40,9 +52,8 @@ export const useProfiles = () => {
const outcome = await patchProfilesConfig(value) const outcome = await patchProfilesConfig(value)
if (outcome.status === 'valid') { if (outcome.status === 'valid') {
await setCacheDataAsync<IProfilesConfig>( await setCacheData<IProfilesConfig>(profilesQueryKey, (current) =>
['getProfiles'], current ? { ...current, ...value } : current,
(current) => (current ? { ...current, ...value } : current),
) )
} else if (outcome.status !== 'busy') { } else if (outcome.status !== 'busy') {
await mutateProfiles() await mutateProfiles()
+1 -4
View File
@@ -10,7 +10,6 @@ import {
patchVergeConfig, patchVergeConfig,
} from '@/services/cmds' } from '@/services/cmds'
import { import {
getCacheData,
removeCacheData, removeCacheData,
revalidateQueries, revalidateQueries,
useQuery, useQuery,
@@ -51,9 +50,7 @@ export const useSystemProxyState = () => {
const toggleSystemProxy = async (enabled: boolean) => { const toggleSystemProxy = async (enabled: boolean) => {
// Roll failed optimistic writes back to the latest confirmed state. // Roll failed optimistic writes back to the latest confirmed state.
let confirmed = let confirmed = verge?.enable_system_proxy ?? false
getCacheData<IVergeConfig>(['getVergeConfig'])?.enable_system_proxy ??
false
mutateVerge( mutateVerge(
(prev) => (prev ? { ...prev, enable_system_proxy: enabled } : prev), (prev) => (prev ? { ...prev, enable_system_proxy: enabled } : prev),
false, false,
+16
View File
@@ -1,4 +1,5 @@
import { import {
getAppUptime,
getRuntimeState, getRuntimeState,
type RunState, type RunState,
type RunningMode, type RunningMode,
@@ -8,6 +9,7 @@ import { useQuery } from '@/services/query-client'
import { useVisibility } from './use-visibility' import { useVisibility } from './use-visibility'
export const runStateQueryKey = ['getRuntimeState'] as const export const runStateQueryKey = ['getRuntimeState'] as const
const appUptimeQueryKey = ['appUptime'] as const
/** Fail closed until the first snapshot so TUN never flashes as available. */ /** Fail closed until the first snapshot so TUN never flashes as available. */
const unknownRunState: RunState = { const unknownRunState: RunState = {
@@ -52,3 +54,17 @@ export function useSystemState() {
isLoading, isLoading,
} }
} }
export function useAppUptime() {
const { data: uptime = 0 } = useQuery({
queryKey: appUptimeQueryKey,
queryFn: getAppUptime,
staleTime: 5000,
refetchInterval: 3000,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: 1,
})
return uptime
}
+7 -8
View File
@@ -2,7 +2,7 @@ import { useCallback } from 'react'
import { getVergeConfig, patchVergeConfig } from '@/services/cmds' import { getVergeConfig, patchVergeConfig } from '@/services/cmds'
import { getPreloadConfig, setPreloadConfig } from '@/services/preload' import { getPreloadConfig, setPreloadConfig } from '@/services/preload'
import { getCacheData, setCacheData, useQuery } from '@/services/query-client' import { setCacheData, useQuery } from '@/services/query-client'
export const useVerge = () => { export const useVerge = () => {
const initialVergeConfig = getPreloadConfig() const initialVergeConfig = getPreloadConfig()
@@ -30,13 +30,12 @@ export const useVerge = () => {
void refetch() void refetch()
return return
} }
if (typeof updaterOrData === 'function') { void setCacheData<IVergeConfig>(
const prev = getCacheData<IVergeConfig>(['getVergeConfig']) ['getVergeConfig'],
const next = updaterOrData(prev) typeof updaterOrData === 'function'
setCacheData(['getVergeConfig'], next) ? (current) => updaterOrData(current ?? verge)
} else { : updaterOrData,
setCacheData(['getVergeConfig'], updaterOrData) )
}
} }
const patchVerge = useCallback( const patchVerge = useCallback(
+22 -2
View File
@@ -1,20 +1,40 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { revalidateProfiles } from '@/hooks/use-profiles'
import { runStateQueryKey } from '@/hooks/use-system-state' import { runStateQueryKey } from '@/hooks/use-system-state'
import type { RunState } from '@/services/cmds' import type { RunState } from '@/services/cmds'
import { subscribeVergeEvents } from '@/services/events' import { subscribeVergeEvents } from '@/services/events'
import { revalidateQueries, setCacheDataAsync } from '@/services/query-client' import { revalidateQueries, setCacheData } from '@/services/query-client'
export const useLayoutEvents = ( export const useLayoutEvents = (
handleNotice: (payload: [string, string]) => void, handleNotice: (payload: [string, string]) => void,
) => { ) => {
useEffect(() => { useEffect(() => {
let lastProfileId: string | null = null
let lastProfileUpdateTime = 0
const refreshThrottle = 800
const revalidateKeys = (keys: readonly string[]) => { const revalidateKeys = (keys: readonly string[]) => {
void revalidateQueries(keys.map((key) => [key])) void revalidateQueries(keys.map((key) => [key]))
} }
const handleProfileChanged = (newProfileId: string) => {
const now = Date.now()
if (
lastProfileId === newProfileId &&
now - lastProfileUpdateTime < refreshThrottle
) {
return
}
lastProfileId = newProfileId
lastProfileUpdateTime = now
void revalidateProfiles()
}
return subscribeVergeEvents( return subscribeVergeEvents(
{ {
'profile-changed': handleProfileChanged,
'verge://refresh-profiles': () => void revalidateProfiles(),
'verge://refresh-clash-config': () => { 'verge://refresh-clash-config': () => {
revalidateKeys([ revalidateKeys([
'getProxyView', 'getProxyView',
@@ -36,7 +56,7 @@ export const useLayoutEvents = (
}, },
// Transitions carry the full run-state snapshot, so write it directly to cache. // Transitions carry the full run-state snapshot, so write it directly to cache.
'verge://run-state-changed': (payload) => { 'verge://run-state-changed': (payload) => {
void setCacheDataAsync<RunState>(runStateQueryKey, payload) void setCacheData<RunState>(runStateQueryKey, payload)
}, },
'verge://notice-message': handleNotice, 'verge://notice-message': handleNotice,
}, },
+4 -11
View File
@@ -40,12 +40,11 @@ import {
} from '@/components/profile/profile-viewer' } from '@/components/profile/profile-viewer'
import { ConfigViewer } from '@/components/setting/mods/config-viewer' import { ConfigViewer } from '@/components/setting/mods/config-viewer'
import { useListen } from '@/hooks/use-listen' import { useListen } from '@/hooks/use-listen'
import { useProfiles } from '@/hooks/use-profiles' import { fetchProfilesIntoCache, useProfiles } from '@/hooks/use-profiles'
import { import {
createProfile, createProfile,
deleteProfile, deleteProfile,
enhanceProfiles, enhanceProfiles,
getProfiles,
getRuntimeLogs, getRuntimeLogs,
importProfile, importProfile,
reorderProfile, reorderProfile,
@@ -53,11 +52,7 @@ import {
} from '@/services/cmds' } from '@/services/cmds'
import { subscribeVergeEvents } from '@/services/events' import { subscribeVergeEvents } from '@/services/events'
import { errorDetail, showNotice } from '@/services/notice-service' import { errorDetail, showNotice } from '@/services/notice-service'
import { import { revalidateQuery, useQuery } from '@/services/query-client'
fetchCacheData,
revalidateQueries,
useQuery,
} from '@/services/query-client'
import { import {
useLoadingCache, useLoadingCache,
useSetLoadingCache, useSetLoadingCache,
@@ -175,9 +170,7 @@ const ProfilePage = () => {
debugLog('[紧急刷新] 开始强制刷新所有数据') debugLog('[紧急刷新] 开始强制刷新所有数据')
try { try {
await revalidateQueries([['getProfiles'], ['getRuntimeLogs']]) await Promise.all([revalidateQuery(['getRuntimeLogs']), mutateProfiles()])
await mutateProfiles()
await new Promise((resolve) => setTimeout(resolve, 500)) await new Promise((resolve) => setTimeout(resolve, 500))
await onEnhance(false) await onEnhance(false)
@@ -294,7 +287,7 @@ const ProfilePage = () => {
console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`) console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`)
try { try {
await fetchCacheData(['getProfiles'], getProfiles) await fetchProfilesIntoCache()
await onEnhance(false) await onEnhance(false)
showNotice.error( showNotice.error(
'profiles.page.feedback.notifications.importNeedsRefresh', 'profiles.page.feedback.notifications.importNeedsRefresh',
-8
View File
@@ -26,10 +26,6 @@ export interface SystemContextType {
systemProxyAddress: string systemProxyAddress: string
} }
export interface UptimeContextType {
uptime: number
}
export interface CoreDataStatusContextType { export interface CoreDataStatusContextType {
isCoreDataPending: boolean isCoreDataPending: boolean
} }
@@ -49,7 +45,6 @@ export const ClashConfigContext = createContext<ClashConfigContextType | null>(
null, null,
) )
export const SystemContext = createContext<SystemContextType | null>(null) export const SystemContext = createContext<SystemContextType | null>(null)
export const UptimeContext = createContext<UptimeContextType | null>(null)
export const CoreDataStatusContext = export const CoreDataStatusContext =
createContext<CoreDataStatusContextType | null>(null) createContext<CoreDataStatusContextType | null>(null)
export const RefreshersContext = createContext<RefreshersContextType | null>( export const RefreshersContext = createContext<RefreshersContextType | null>(
@@ -80,9 +75,6 @@ export const useClashConfigData = (): ClashConfigContextType =>
export const useSystemData = (): SystemContextType => export const useSystemData = (): SystemContextType =>
useCtx(SystemContext, 'useSystemData') useCtx(SystemContext, 'useSystemData')
export const useUptimeData = (): UptimeContextType =>
useCtx(UptimeContext, 'useUptimeData')
export const useAppRefreshers = (): RefreshersContextType => export const useAppRefreshers = (): RefreshersContextType =>
useCtx(RefreshersContext, 'useAppRefreshers') useCtx(RefreshersContext, 'useAppRefreshers')
+2 -40
View File
@@ -8,14 +8,9 @@ import {
import { useClashInfo, useRuntimeConfig } from '@/hooks/use-clash' import { useClashInfo, useRuntimeConfig } from '@/hooks/use-clash'
import { runStateQueryKey } from '@/hooks/use-system-state' import { runStateQueryKey } from '@/hooks/use-system-state'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { import { getProxyView, getRuntimeState, getSystemProxy } from '@/services/cmds'
getAppUptime,
getProxyView,
getRuntimeState,
getSystemProxy,
} from '@/services/cmds'
import { subscribeVergeEvents } from '@/services/events' import { subscribeVergeEvents } from '@/services/events'
import { revalidateQueries, useQuery } from '@/services/query-client' import { useQuery } from '@/services/query-client'
import { resolveDisplayedMixedPort } from '@/utils/mixed-port' import { resolveDisplayedMixedPort } from '@/utils/mixed-port'
import { import {
@@ -25,7 +20,6 @@ import {
RefreshersContext, RefreshersContext,
RulesContext, RulesContext,
SystemContext, SystemContext,
UptimeContext,
} from './app-data-context' } from './app-data-context'
const TQ_MIHOMO = { const TQ_MIHOMO = {
@@ -109,14 +103,6 @@ export const AppDataProvider = ({
}) })
const runningMode = runState?.mode const runningMode = runState?.mode
const { data: uptimeData } = useQuery({
queryKey: ['appUptime'],
queryFn: getAppUptime,
...TQ_DEFAULTS,
refetchInterval: 3000,
retry: 1,
})
const refreshProxy = useStableFn(_refetchProxyView) const refreshProxy = useStableFn(_refetchProxyView)
const refreshClashConfig = useStableFn(_refetchClashConfig) const refreshClashConfig = useStableFn(_refetchClashConfig)
const refreshRules = useStableFn(_refetchRules) const refreshRules = useStableFn(_refetchRules)
@@ -124,22 +110,8 @@ export const AppDataProvider = ({
const refreshRuleProviders = useStableFn(_refetchRuleProviders) const refreshRuleProviders = useStableFn(_refetchRuleProviders)
useEffect(() => { useEffect(() => {
let lastProfileId: string | null = null
let lastProfileUpdateTime = 0
let lastProxyUpdateTime = 0 let lastProxyUpdateTime = 0
const refreshThrottle = 800 const refreshThrottle = 800
const handleProfileChanged = (newProfileId: string) => {
const now = Date.now()
if (
lastProfileId === newProfileId &&
now - lastProfileUpdateTime < refreshThrottle
) {
return
}
lastProfileId = newProfileId
lastProfileUpdateTime = now
void revalidateQueries([['getProfiles']])
}
const handleRefreshProxy = () => { const handleRefreshProxy = () => {
const now = Date.now() const now = Date.now()
@@ -148,13 +120,7 @@ export const AppDataProvider = ({
refreshProxy().catch(() => {}) refreshProxy().catch(() => {})
} }
const handleRefreshProfiles = () => {
void revalidateQueries([['getProfiles']])
}
return subscribeVergeEvents({ return subscribeVergeEvents({
'profile-changed': handleProfileChanged,
'verge://refresh-profiles': handleRefreshProfiles,
'verge://refresh-proxy-config': handleRefreshProxy, 'verge://refresh-proxy-config': handleRefreshProxy,
}) })
}, [refreshProxy]) }, [refreshProxy])
@@ -244,8 +210,6 @@ export const AppDataProvider = ({
} }
}, [sysproxy, runningMode, isRunningModePending, verge, displayedMixedPort]) }, [sysproxy, runningMode, isRunningModePending, verge, displayedMixedPort])
const uptimeValue = useMemo(() => ({ uptime: uptimeData || 0 }), [uptimeData])
const coreDataStatusValue = useMemo( const coreDataStatusValue = useMemo(
() => ({ () => ({
isCoreDataPending: isProxyViewPending || isClashConfigPending, isCoreDataPending: isProxyViewPending || isClashConfigPending,
@@ -277,13 +241,11 @@ export const AppDataProvider = ({
<RulesContext value={rulesValue}> <RulesContext value={rulesValue}>
<ClashConfigContext value={clashConfigValue}> <ClashConfigContext value={clashConfigValue}>
<SystemContext value={systemValue}> <SystemContext value={systemValue}>
<UptimeContext value={uptimeValue}>
<CoreDataStatusContext value={coreDataStatusValue}> <CoreDataStatusContext value={coreDataStatusValue}>
<RefreshersContext value={refreshersValue}> <RefreshersContext value={refreshersValue}>
{children} {children}
</RefreshersContext> </RefreshersContext>
</CoreDataStatusContext> </CoreDataStatusContext>
</UptimeContext>
</SystemContext> </SystemContext>
</ClashConfigContext> </ClashConfigContext>
</RulesContext> </RulesContext>
+7 -78
View File
@@ -1,4 +1,3 @@
import { unstable_serialize } from 'swr'
import useSWR, { import useSWR, {
type SWRConfiguration, type SWRConfiguration,
type SWRResponse, type SWRResponse,
@@ -33,19 +32,6 @@ type QueryResult<T> = SWRResponse<T> & {
refetch: () => Promise<{ data: T | undefined }> refetch: () => Promise<{ data: T | undefined }>
} }
const serializeQueryKey = (queryKey: QueryKey) => unstable_serialize(queryKey)
const queryCache = new Map<string, unknown>()
const setCachedData = <T>(queryKey: QueryKey, data: T | undefined) => {
const cacheKey = serializeQueryKey(queryKey)
if (data === undefined) {
queryCache.delete(cacheKey)
} else {
queryCache.set(cacheKey, data)
}
}
export const swrConfig: SWRConfiguration = { export const swrConfig: SWRConfiguration = {
dedupingInterval: 2000, dedupingInterval: 2000,
errorRetryCount: 3, errorRetryCount: 3,
@@ -53,72 +39,29 @@ export const swrConfig: SWRConfiguration = {
revalidateOnFocus: false, revalidateOnFocus: false,
} }
export const getCacheData = <T>(queryKey: QueryKey): T | undefined => {
return queryCache.get(serializeQueryKey(queryKey)) as T | undefined
}
const updateCachedData = <T>(
queryKey: QueryKey,
updaterOrData: QueryDataUpdater<T>,
) => {
const current = getCacheData<T>(queryKey)
const next =
typeof updaterOrData === 'function'
? (updaterOrData as (current: T | undefined) => T | undefined)(current)
: updaterOrData
setCachedData(queryKey, next)
return next
}
export const setCacheData = <T>( export const setCacheData = <T>(
queryKey: QueryKey, queryKey: QueryKey,
updaterOrData: QueryDataUpdater<T>, updaterOrData: QueryDataUpdater<T>,
) => { ) =>
const next = updateCachedData(queryKey, updaterOrData) swrMutate<T>(queryKey, updaterOrData, {
void swrMutate(queryKey, next, {
populateCache: true, populateCache: true,
revalidate: false, revalidate: false,
}) })
return next
}
export const setCacheDataAsync = async <T>( export const revalidateQuery = (queryKey: QueryKey) => swrMutate(queryKey)
queryKey: QueryKey,
updaterOrData: QueryDataUpdater<T>,
) => {
const next = updateCachedData(queryKey, updaterOrData)
await swrMutate(queryKey, next, {
populateCache: true,
revalidate: false,
})
return next
}
export const revalidateQuery = async (queryKey: QueryKey) => {
const data = await swrMutate(queryKey)
if (data !== undefined) {
setCachedData(queryKey, data)
}
return data
}
export const revalidateQueries = (queryKeys: readonly QueryKey[]) => export const revalidateQueries = (queryKeys: readonly QueryKey[]) =>
Promise.all(queryKeys.map(revalidateQuery)) Promise.all(queryKeys.map(revalidateQuery))
export const removeCacheData = (queryKey: QueryKey) => { export const removeCacheData = (queryKey: QueryKey) =>
setCachedData(queryKey, undefined) setCacheData(queryKey, undefined)
return swrMutate(queryKey, undefined, {
populateCache: true,
revalidate: false,
})
}
export const fetchCacheData = async <T>( export const fetchCacheData = async <T>(
queryKey: QueryKey, queryKey: QueryKey,
queryFn: () => Promise<T> | T, queryFn: () => Promise<T> | T,
) => { ) => {
const data = await queryFn() const data = await queryFn()
setCacheData(queryKey, data) await setCacheData(queryKey, data)
return data return data
} }
@@ -144,11 +87,6 @@ export function useQuery<T>(options: QueryOptions<T>): QueryResult<T> {
typeof fallbackDataSource === 'function' typeof fallbackDataSource === 'function'
? (fallbackDataSource as () => T | undefined)() ? (fallbackDataSource as () => T | undefined)()
: fallbackDataSource : fallbackDataSource
const serializedKey = serializeQueryKey(queryKey)
if (enabled && fallbackData !== undefined && !queryCache.has(serializedKey)) {
setCachedData(queryKey, fallbackData)
}
const swr = useSWR<T>(enabled ? queryKey : null, queryFn, { const swr = useSWR<T>(enabled ? queryKey : null, queryFn, {
dedupingInterval: staleTime, dedupingInterval: staleTime,
errorRetryCount: retry === false ? 0 : retry, errorRetryCount: retry === false ? 0 : retry,
@@ -176,21 +114,12 @@ export function useQuery<T>(options: QueryOptions<T>): QueryResult<T> {
revalidateOnReconnect: refetchOnReconnect, revalidateOnReconnect: refetchOnReconnect,
refreshInterval: refetchInterval || 0, refreshInterval: refetchInterval || 0,
refreshWhenHidden: refetchIntervalInBackground ?? false, refreshWhenHidden: refetchIntervalInBackground ?? false,
onSuccess: (data) => {
setCachedData(queryKey, data)
},
}) })
return { return {
...swr, ...swr,
isFetching: swr.isValidating, isFetching: swr.isValidating,
isPending: swr.isLoading, isPending: swr.isLoading,
refetch: async () => { refetch: async () => ({ data: await swr.mutate() }),
const data = await swr.mutate()
if (data !== undefined) {
setCachedData(queryKey, data)
}
return { data }
},
} }
} }