Files
clash-verge-rev/src/hooks/use-clash.ts
T
Tunglies 89dc3cb3c8 feat(mutate): single funnel for user-intent commands and invoke gate
Adds src/services/mutate.ts: one path for optimistic apply, services-layer
invoke, Busy handling, failure rollback, and the error toast, with busy
bookkeeping mirrored into the app store. The patch hooks (verge, clash,
profiles) now route through it. Moves the remaining direct invokes
(unlock ×3, DNS ×4) into cmds.ts and turns on the invoke-only-in-services
architecture gate in CI.
2026-09-30 20:39:41 +08:00

153 lines
3.4 KiB
TypeScript

import { useLockFn } from 'ahooks'
import i18n from 'i18next'
import { getVersion } from 'tauri-plugin-mihomo-api'
import {
getClashInfo,
getClashMode,
getRuntimeConfig,
patchClashConfig,
} from '@/services/cmds'
import { mutate } from '@/services/mutate'
import {
revalidateQuery,
setCacheData,
useQuery,
} from '@/services/query-client'
type MutateClashUpdater =
| ((old: IConfigData | undefined) => IConfigData | undefined)
| IConfigData
| undefined
const PORT_KEYS = [
'port',
'socks-port',
'mixed-port',
'redir-port',
'tproxy-port',
] as const
type ClashInfoPatch = Partial<
Pick<
IConfigData,
| 'port'
| 'socks-port'
| 'mixed-port'
| 'redir-port'
| 'tproxy-port'
| 'external-controller'
| 'secret'
>
>
const hasClashInfoPayload = (patch: ClashInfoPatch) =>
PORT_KEYS.some((key) => patch[key] != null) ||
patch['external-controller'] != null ||
patch.secret != null
const validatePortRange = (port: number) => {
if (port < 1000) {
throw new Error(
i18n.t(($) => $.settings.modals.clashPort.messages.portTooLow),
)
}
if (port > 65535) {
throw new Error(
i18n.t(($) => $.settings.modals.clashPort.messages.portTooHigh),
)
}
}
const validatePorts = (patch: ClashInfoPatch) => {
PORT_KEYS.forEach((key) => {
const port = patch[key]
if (!port) return
validatePortRange(port)
})
}
export const useRuntimeConfig = (shouldFetch: boolean = true) => {
return useQuery({
queryKey: ['getRuntimeConfig'],
queryFn: getRuntimeConfig,
enabled: shouldFetch,
})
}
// Fault-tolerant fallback for the current proxy mode, read straight from the
// saved clash config on the backend (bypasses the strict BaseConfig path).
export const useClashMode = (shouldFetch: boolean = true) => {
return useQuery({
queryKey: ['getClashMode'],
queryFn: getClashMode,
enabled: shouldFetch,
})
}
export const useClash = () => {
const { data: clash, refetch } = useRuntimeConfig()
const { data: versionData, refetch: mutateVersion } = useQuery({
queryKey: ['getVersion'],
queryFn: getVersion,
})
const mutateClash = (updater?: MutateClashUpdater, revalidate?: boolean) => {
if (updater === undefined) {
return refetch()
}
void setCacheData<IConfigData>(['getRuntimeConfig'], updater)
if (revalidate !== false) {
return refetch()
}
return Promise.resolve()
}
const patchClash = useLockFn(async (patch: Partial<IConfigData>) => {
await mutate(() => patchClashConfig(patch), {
id: 'patch-clash-config',
revalidate: [['getRuntimeConfig']],
})
})
const version = versionData?.meta
? `${versionData.version} Mihomo`
: versionData?.version || '-'
return {
clash,
version,
mutateClash,
mutateVersion,
patchClash,
}
}
export const useClashInfo = () => {
const { data: clashInfo, refetch: mutateInfo } = useQuery({
queryKey: ['getClashInfo'],
queryFn: getClashInfo,
})
const patchInfo = useLockFn(async (patch: ClashInfoPatch) => {
if (!hasClashInfoPayload(patch)) return
validatePorts(patch)
await mutate(() => patchClashConfig(patch), {
id: 'patch-clash-info',
revalidate: [['getClashInfo'], ['getClashConfig']],
})
})
const invalidateClashConfig = () => revalidateQuery(['getClashConfig'])
return {
clashInfo,
mutateInfo,
patchInfo,
invalidateClashConfig,
}
}