mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 02:32:09 +08:00
refactor(frontend): trim unused mutation and busy state APIs
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { revalidateQueries } from '@/services/query-client'
|
||||
import type { AppStoreAction } from '@/store/app-state'
|
||||
|
||||
/** Anything a notice call accepts: translation key, ReactNode, or failure payload. */
|
||||
type MutateNoticeInput = Parameters<typeof showNotice.error>[0]
|
||||
@@ -8,18 +7,12 @@ type MutateNoticeInput = Parameters<typeof showNotice.error>[0]
|
||||
export interface MutateOptions<T> {
|
||||
/** Commands with the same identity execute in request order. */
|
||||
id: string
|
||||
/** Applied before the command runs; undone by `rollback` on failure. */
|
||||
optimistic?: () => void
|
||||
/** Restores pre-optimistic state when the command rejects. */
|
||||
rollback?: () => void | Promise<void>
|
||||
/** Runs after a fulfilled (non-busy) result, e.g. cache merges. */
|
||||
onFulfilled?: (value: T) => void
|
||||
/** SWR keys revalidated after a fulfilled result. */
|
||||
revalidate?: readonly (readonly unknown[] | string)[]
|
||||
/** Shown after a fulfilled result (most flows leave this to backend notices). */
|
||||
successNotice?: MutateNoticeInput
|
||||
/** Shown when the command resolves with `{ status: 'busy' }`. */
|
||||
busyNotice?: MutateNoticeInput
|
||||
/** `false` suppresses the default error toast. */
|
||||
errorNotice?: false | MutateNoticeInput
|
||||
}
|
||||
@@ -35,20 +28,6 @@ const isBusyOutcome = (value: unknown): boolean =>
|
||||
(value as { status?: unknown }).status === 'busy'
|
||||
|
||||
const inFlight = new Map<string, Promise<void>>()
|
||||
let storeDispatch: ((action: AppStoreAction) => void) | null = null
|
||||
|
||||
/** The app store registers its dispatcher so the funnel can record busy state. */
|
||||
export const bindStoreDispatch = (
|
||||
dispatch: ((action: AppStoreAction) => void) | null,
|
||||
) => {
|
||||
storeDispatch = dispatch
|
||||
}
|
||||
|
||||
/**
|
||||
* The single funnel for "user intent → backend command": optimistic apply,
|
||||
* invoke through the services layer, Busy handling, failure rollback, and the
|
||||
* error toast, in that order, for every mutating call site.
|
||||
*/
|
||||
export async function mutate<T>(
|
||||
invoke: () => Promise<T>,
|
||||
options: MutateOptions<T>,
|
||||
@@ -61,15 +40,10 @@ export async function mutate<T>(
|
||||
})
|
||||
inFlight.set(id, settled)
|
||||
if (previous) await previous
|
||||
storeDispatch?.({ type: 'busy/started', id })
|
||||
|
||||
try {
|
||||
options.optimistic?.()
|
||||
const value = await invoke()
|
||||
if (isBusyOutcome(value)) {
|
||||
if (options.busyNotice !== undefined) {
|
||||
showNotice.warning(options.busyNotice)
|
||||
}
|
||||
return { ok: false, busy: true, value }
|
||||
}
|
||||
options.onFulfilled?.(value)
|
||||
@@ -81,7 +55,6 @@ export async function mutate<T>(
|
||||
}
|
||||
return { ok: true, value }
|
||||
} catch (error) {
|
||||
await options.rollback?.()
|
||||
if (options.errorNotice !== false) {
|
||||
showNotice.error(
|
||||
options.errorNotice !== undefined ? options.errorNotice : error,
|
||||
@@ -91,6 +64,5 @@ export async function mutate<T>(
|
||||
} finally {
|
||||
release()
|
||||
if (inFlight.get(id) === settled) inFlight.delete(id)
|
||||
storeDispatch?.({ type: 'busy/settled', id })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ export interface AppStoreState {
|
||||
readonly profileUpdates: ProfileUpdatesState
|
||||
/** Bumped once per test-all request; test items run their delay test per bump. */
|
||||
readonly testAllCounter: number
|
||||
/** Mutate-funnel bookkeeping: in-flight intent ids. */
|
||||
readonly busy: ReadonlySet<string>
|
||||
}
|
||||
|
||||
export const initialAppStoreState: AppStoreState = {
|
||||
@@ -34,7 +32,6 @@ export const initialAppStoreState: AppStoreState = {
|
||||
timerRevisions: new Map(),
|
||||
},
|
||||
testAllCounter: 0,
|
||||
busy: new Set(),
|
||||
}
|
||||
|
||||
export type AppStoreAction =
|
||||
@@ -46,8 +43,6 @@ export type AppStoreAction =
|
||||
| { type: 'profileUpdate/timerTick'; uid: string }
|
||||
| { type: 'profileLoading/set'; uids: readonly string[]; loading: boolean }
|
||||
| { type: 'testAll/requested' }
|
||||
| { type: 'busy/started'; id: string }
|
||||
| { type: 'busy/settled'; id: string }
|
||||
|
||||
const bumpRevision = (
|
||||
map: ReadonlyMap<string, number>,
|
||||
@@ -125,17 +120,5 @@ export function appStoreReducer(
|
||||
return withLoading(state, action.uids, action.loading)
|
||||
case 'testAll/requested':
|
||||
return { ...state, testAllCounter: state.testAllCounter + 1 }
|
||||
case 'busy/started': {
|
||||
if (state.busy.has(action.id)) return state
|
||||
const busy = new Set(state.busy)
|
||||
busy.add(action.id)
|
||||
return { ...state, busy }
|
||||
}
|
||||
case 'busy/settled': {
|
||||
if (!state.busy.has(action.id)) return state
|
||||
const busy = new Set(state.busy)
|
||||
busy.delete(action.id)
|
||||
return { ...state, busy }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useReducer } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import { bindStoreDispatch } from '@/services/mutate'
|
||||
|
||||
import {
|
||||
type AppStoreAction,
|
||||
appStoreReducer,
|
||||
@@ -34,11 +32,6 @@ export const AppStoreProvider = ({ children }: { children?: ReactNode }) => {
|
||||
[reads],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
bindStoreDispatch(dispatch)
|
||||
return () => bindStoreDispatch(null)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const readRunState = () => {
|
||||
void reads.readRunState().catch(() => {})
|
||||
|
||||
Reference in New Issue
Block a user