mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-24 20:52:15 +08:00
refactor(profiles): route profile operations through the mutate funnel
Ports profile create/import/reorder/delete/update/patch/save-file and enhance calls onto the funnel; retry and editor flows keep their own error handling with errorNotice:false. Per-uid ids keep the update worker pool concurrent under the funnel's in-flight dedupe.
This commit is contained in:
@@ -26,6 +26,7 @@ import { useNavigate } from 'react-router'
|
||||
|
||||
import { useAppRefreshers } from '@/providers/app-data-context'
|
||||
import { updateProfile } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { isValidUrl } from '@/utils/network'
|
||||
import { openExternalUrl } from '@/utils/open-external-url'
|
||||
@@ -297,7 +298,10 @@ export const HomeProfileCard = ({
|
||||
|
||||
setUpdating(true)
|
||||
try {
|
||||
await updateProfile(current.uid, current.option)
|
||||
await mutate(() => updateProfile(current.uid, current.option), {
|
||||
id: `update-profile:${current.uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
onProfileUpdated?.()
|
||||
|
||||
// 刷新首页数据
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
readProfileFile,
|
||||
saveProfileFile,
|
||||
} from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
@@ -472,7 +473,11 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
setCurrData(nextData)
|
||||
}
|
||||
|
||||
if (!(await saveProfileFile(property, nextData))) {
|
||||
const result = await mutate(() => saveProfileFile(property, nextData), {
|
||||
id: `save-profile-file:${property}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!(result.ok && result.value === true)) {
|
||||
await fetchContent()
|
||||
onClose()
|
||||
return
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
updateProfile,
|
||||
viewProfile,
|
||||
} from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import {
|
||||
useProfileLoadingCache,
|
||||
@@ -369,7 +370,10 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const onOpenFile = useLockFn(async () => {
|
||||
setAnchorEl(null)
|
||||
try {
|
||||
await viewProfile(itemData.uid)
|
||||
await mutate(() => viewProfile(itemData.uid), {
|
||||
id: 'view-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
} catch (err) {
|
||||
showNotice.error(err)
|
||||
}
|
||||
@@ -398,7 +402,10 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
try {
|
||||
const payload = Object.keys(option).length > 0 ? option : undefined
|
||||
await updateProfile(itemData.uid, payload)
|
||||
await mutate(() => updateProfile(itemData.uid, payload), {
|
||||
id: `update-profile:${itemData.uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
|
||||
void mutateProfiles()
|
||||
} catch {
|
||||
@@ -586,7 +593,11 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
|
||||
const handleSaveProfileDocument = useLockFn(async () => {
|
||||
const currentValue = profileDocument.value
|
||||
if (!(await saveProfileFile(uid, currentValue))) {
|
||||
const result = await mutate(() => saveProfileFile(uid, currentValue), {
|
||||
id: `save-profile-file:${uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!(result.ok && result.value === true)) {
|
||||
await profileDocument.reload()
|
||||
return
|
||||
}
|
||||
@@ -597,7 +608,11 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const handleSaveMergeDocument = useLockFn(async () => {
|
||||
const mergeUid = option?.merge ?? ''
|
||||
const currentValue = mergeDocument.value
|
||||
if (!(await saveProfileFile(mergeUid, currentValue))) {
|
||||
const result = await mutate(() => saveProfileFile(mergeUid, currentValue), {
|
||||
id: `save-profile-file:${mergeUid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!(result.ok && result.value === true)) {
|
||||
await mergeDocument.reload()
|
||||
return
|
||||
}
|
||||
@@ -608,7 +623,14 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const handleSaveScriptDocument = useLockFn(async () => {
|
||||
const scriptUid = option?.script ?? ''
|
||||
const currentValue = scriptDocument.value
|
||||
if (!(await saveProfileFile(scriptUid, currentValue))) {
|
||||
const result = await mutate(
|
||||
() => saveProfileFile(scriptUid, currentValue),
|
||||
{
|
||||
id: `save-profile-file:${scriptUid}`,
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
if (!(result.ok && result.value === true)) {
|
||||
await scriptDocument.reload()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { EditorViewer } from '@/components/profile/editor-viewer'
|
||||
import { useEditorDocument } from '@/hooks/use-editor-document'
|
||||
import { viewProfile, readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
|
||||
import { EnhanceHint } from './enhance-hint'
|
||||
@@ -63,7 +64,10 @@ export const ProfileMore = (props: Props) => {
|
||||
const onOpenFile = useLockFn(async () => {
|
||||
setAnchorEl(null)
|
||||
try {
|
||||
await viewProfile(id)
|
||||
await mutate(() => viewProfile(id), {
|
||||
id: 'view-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
} catch (err) {
|
||||
showNotice.error(err)
|
||||
}
|
||||
@@ -96,7 +100,11 @@ export const ProfileMore = (props: Props) => {
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
const currentValue = document.value
|
||||
if (!(await saveProfileFile(id, currentValue))) {
|
||||
const result = await mutate(() => saveProfileFile(id, currentValue), {
|
||||
id: `save-profile-file:${id}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!(result.ok && result.value === true)) {
|
||||
await document.reload()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { BaseDialog, Switch } from '@/components/base'
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import { createProfile, patchProfile } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { version } from '@root/package.json'
|
||||
|
||||
@@ -128,26 +129,38 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
|
||||
if (!isRemote) {
|
||||
if (openType === 'new') {
|
||||
await createProfile(item, fileDataRef.current)
|
||||
await mutate(() => createProfile(item, fileDataRef.current), {
|
||||
id: 'create-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
} else {
|
||||
if (!form.uid) {
|
||||
throw new Error(
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await patchProfile(form.uid, item)
|
||||
await mutate(() => patchProfile(form.uid, item), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (openType === 'new') {
|
||||
await createProfile(item, fileDataRef.current)
|
||||
await mutate(() => createProfile(item, fileDataRef.current), {
|
||||
id: 'create-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
} else {
|
||||
if (!form.uid) {
|
||||
throw new Error(
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await patchProfile(form.uid, item)
|
||||
await mutate(() => patchProfile(form.uid, item), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
showNotice.info(
|
||||
@@ -164,16 +177,31 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
|
||||
}
|
||||
|
||||
if (openType === 'new') {
|
||||
await createProfile(retryItem, fileDataRef.current)
|
||||
await mutate(
|
||||
() => createProfile(retryItem, fileDataRef.current),
|
||||
{
|
||||
id: 'create-profile',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
if (!form.uid) {
|
||||
throw new Error(
|
||||
t('profiles.modals.profileForm.errors.uidMissing'),
|
||||
)
|
||||
}
|
||||
await patchProfile(form.uid, retryItem)
|
||||
await mutate(() => patchProfile(form.uid, retryItem), {
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
|
||||
await patchProfile(form.uid, { option: originalOptions })
|
||||
await mutate(
|
||||
() => patchProfile(form.uid, { option: originalOptions }),
|
||||
{
|
||||
id: 'patch-profile',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
showNotice.success(
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { BaseSearchBox, MonacoEditor } from '@/components/base'
|
||||
import { ProxyItem } from '@/components/profile/proxy-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
@@ -329,7 +330,11 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
if (!(await saveProfileFile(property, currData))) {
|
||||
const result = await mutate(() => saveProfileFile(property, currData), {
|
||||
id: `save-profile-file:${property}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!(result.ok && result.value === true)) {
|
||||
await fetchContent()
|
||||
onClose()
|
||||
return
|
||||
|
||||
@@ -32,6 +32,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { BaseSearchBox, MonacoEditor, Switch } from '@/components/base'
|
||||
import { RuleItem } from '@/components/profile/rule-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
@@ -570,7 +571,11 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
try {
|
||||
if (!(await saveProfileFile(property, currData))) {
|
||||
const result = await mutate(() => saveProfileFile(property, currData), {
|
||||
id: `save-profile-file:${property}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!(result.ok && result.value === true)) {
|
||||
await fetchContent()
|
||||
onClose()
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@/components/base'
|
||||
import { useClash } from '@/hooks/use-clash'
|
||||
import { enhanceProfiles } from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import getSystem from '@/utils/get-system'
|
||||
import { areValidIpCidrs } from '@/utils/network'
|
||||
@@ -129,7 +130,10 @@ export function TunViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
)
|
||||
setOpen(false)
|
||||
showNotice.success('settings.modals.tun.messages.applied')
|
||||
void enhanceProfiles().catch((err: any) => {
|
||||
void mutate(() => enhanceProfiles(), {
|
||||
id: 'enhance-profiles',
|
||||
errorNotice: false,
|
||||
}).catch((err: any) => {
|
||||
showNotice.error(err)
|
||||
})
|
||||
} catch (err: any) {
|
||||
|
||||
+52
-15
@@ -49,6 +49,7 @@ import {
|
||||
reorderProfile,
|
||||
updateProfile,
|
||||
} from '@/services/cmds'
|
||||
import { mutate } from '@/services/mutate'
|
||||
import { errorDetail, showNotice } from '@/services/notice-service'
|
||||
import { revalidateQuery, useQuery } from '@/services/query-client'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
@@ -140,16 +141,22 @@ const ProfilePage = () => {
|
||||
},
|
||||
} as IProfileItem
|
||||
const data = await readTextFile(file)
|
||||
await createProfile(item, data)
|
||||
await mutateProfiles()
|
||||
await mutate(() => createProfile(item, data), {
|
||||
id: 'create-profile',
|
||||
errorNotice: false,
|
||||
revalidate: [['getProfiles']],
|
||||
})
|
||||
}
|
||||
await enhanceProfiles()
|
||||
await mutate(() => enhanceProfiles(), {
|
||||
id: 'enhance-profiles',
|
||||
errorNotice: false,
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
void unlisten.then((cleanup) => cleanup())
|
||||
}
|
||||
}, [mutateProfiles])
|
||||
}, [])
|
||||
|
||||
const onEmergencyRefresh = useLockFn(async () => {
|
||||
debugLog('[紧急刷新] 开始强制刷新所有数据')
|
||||
@@ -211,7 +218,10 @@ const ProfilePage = () => {
|
||||
await performRobustRefresh()
|
||||
}
|
||||
try {
|
||||
await importProfile(url)
|
||||
await mutate(() => importProfile(url), {
|
||||
id: 'import-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
await handleImportSuccess('shared.feedback.notifications.importSuccess')
|
||||
} catch (initialErr) {
|
||||
console.warn('[订阅导入] 首次导入失败:', initialErr)
|
||||
@@ -224,10 +234,17 @@ const ProfilePage = () => {
|
||||
|
||||
showNotice.info('profiles.page.feedback.notifications.importRetry')
|
||||
try {
|
||||
await importProfile(url, {
|
||||
with_proxy: false,
|
||||
self_proxy: true,
|
||||
})
|
||||
await mutate(
|
||||
() =>
|
||||
importProfile(url, {
|
||||
with_proxy: false,
|
||||
self_proxy: true,
|
||||
}),
|
||||
{
|
||||
id: 'import-profile',
|
||||
errorNotice: false,
|
||||
},
|
||||
)
|
||||
await handleImportSuccess(
|
||||
'shared.feedback.notifications.importWithClashProxy',
|
||||
)
|
||||
@@ -308,7 +325,10 @@ const ProfilePage = () => {
|
||||
if (activeId == null || overId == null || activeId === overId) return
|
||||
|
||||
try {
|
||||
await reorderProfile(activeId, overId)
|
||||
await mutate(() => reorderProfile(activeId, overId), {
|
||||
id: 'reorder-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
mutateProfiles()
|
||||
} catch (error) {
|
||||
setProfileDndRevision((revision) => revision + 1)
|
||||
@@ -339,7 +359,10 @@ const ProfilePage = () => {
|
||||
if (outcome.status === 'valid') {
|
||||
currentProfileRef.current = profile
|
||||
void mutateLogs().catch(() => {})
|
||||
void closeAllConnections().catch(() => {})
|
||||
void mutate(() => closeAllConnections(), {
|
||||
id: 'close-all-connections',
|
||||
errorNotice: false,
|
||||
}).catch(() => {})
|
||||
|
||||
if (
|
||||
notifySuccess &&
|
||||
@@ -465,7 +488,11 @@ const ProfilePage = () => {
|
||||
setActivatings((prev) => [...new Set([...prev, ...currentProfiles])])
|
||||
|
||||
try {
|
||||
if (!(await enhanceProfiles())) return
|
||||
const result = await mutate(() => enhanceProfiles(), {
|
||||
id: 'enhance-profiles',
|
||||
errorNotice: false,
|
||||
})
|
||||
if (!(result.ok && result.value === true)) return
|
||||
mutateLogs()
|
||||
if (notifySuccess) {
|
||||
showNotice.success(
|
||||
@@ -484,7 +511,10 @@ const ProfilePage = () => {
|
||||
const current = profiles.current === uid
|
||||
try {
|
||||
setActivatings([...(current ? currentActivatings() : []), uid])
|
||||
await deleteProfile(uid)
|
||||
await mutate(() => deleteProfile(uid), {
|
||||
id: 'delete-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
mutateProfiles()
|
||||
mutateLogs()
|
||||
if (current) {
|
||||
@@ -519,7 +549,11 @@ const ProfilePage = () => {
|
||||
|
||||
const updateOne = async (uid: string) => {
|
||||
try {
|
||||
await updateProfile(uid)
|
||||
// Per-uid id: the worker pool must not dedupe distinct subscriptions.
|
||||
await mutate(() => updateProfile(uid), {
|
||||
id: `update-profile:${uid}`,
|
||||
errorNotice: false,
|
||||
})
|
||||
throttleMutate()
|
||||
} catch (err: any) {
|
||||
console.error(`更新订阅 ${uid} 失败:`, err)
|
||||
@@ -613,7 +647,10 @@ const ProfilePage = () => {
|
||||
setActivatings((prev) => [...new Set([...prev, ...currentActivating])])
|
||||
|
||||
for (const uid of selectedProfiles) {
|
||||
await deleteProfile(uid)
|
||||
await mutate(() => deleteProfile(uid), {
|
||||
id: 'delete-profile',
|
||||
errorNotice: false,
|
||||
})
|
||||
}
|
||||
|
||||
await mutateProfiles()
|
||||
|
||||
Reference in New Issue
Block a user