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:
Tunglies
2026-10-01 04:20:26 +08:00
parent 4e62db44f8
commit c31ab06983
9 changed files with 152 additions and 34 deletions
+5 -1
View File
@@ -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
+27 -5
View File
@@ -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
}
+10 -2
View File
@@ -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
}
+35 -7
View File
@@ -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
+5 -1
View File
@@ -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
View File
@@ -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()