diff --git a/src-tauri/src/feat/effects.rs b/src-tauri/src/feat/effects.rs index 93177df3b..e273bb8da 100644 --- a/src-tauri/src/feat/effects.rs +++ b/src-tauri/src/feat/effects.rs @@ -1,19 +1,20 @@ use crate::config::IVerge; use std::collections::BTreeSet; +/// Variant order defines execution dependency order. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub(super) enum Effect { RestartCore, ClashConfig, Autostart, + Language, SystemProxy, - TrayIcon, Hotkey, TrayMenu, + TrayIcon, TrayTooltip, TrayClick, Lightweight, - Language, LogLevel, LogFile, } diff --git a/src-tauri/src/feat/executor.rs b/src-tauri/src/feat/executor.rs index c301a6975..375a1e227 100644 --- a/src-tauri/src/feat/executor.rs +++ b/src-tauri/src/feat/executor.rs @@ -20,22 +20,6 @@ pub(super) enum Patch<'a> { Clash(&'a Mapping), } -const EFFECT_ORDER: &[Effect] = &[ - Effect::RestartCore, - Effect::ClashConfig, - Effect::Autostart, - Effect::Language, - Effect::SystemProxy, - Effect::Hotkey, - Effect::TrayMenu, - Effect::TrayIcon, - Effect::TrayTooltip, - Effect::TrayClick, - Effect::Lightweight, - Effect::LogLevel, - Effect::LogFile, -]; - async fn ensure_restart(manager: &CoreManager) -> Result<()> { Config::generate().await?; let previous = manager.current_core_readiness_generation(); @@ -143,15 +127,13 @@ pub(super) async fn apply(config_write: &MutexGuard<'_, ()>, patch: Patch<'_>, e Patch::Clash(patch) => clash.edit_draft(|draft| draft.patch_config(patch)), } let result: Result<()> = async { - for &effect in EFFECT_ORDER { - if effects.contains(&effect) { - let result = Box::pin(ensure_effect(effect, verge_patch, manager, &update)).await; - if matches!(patch, Patch::Clash(_)) && matches!(effect, Effect::TrayMenu | Effect::TrayIcon) { - // Tray failures must not reject an applied Clash mode change. - logging_error!(Type::Tray, result); - } else { - result?; - } + for effect in effects.iter().copied() { + let result = Box::pin(ensure_effect(effect, verge_patch, manager, &update)).await; + if matches!(patch, Patch::Clash(_)) && matches!(effect, Effect::TrayMenu | Effect::TrayIcon) { + // Tray failures must not reject an applied Clash mode change. + logging_error!(Type::Tray, result); + } else { + result?; } } match patch { diff --git a/src/hooks/use-profiles.ts b/src/hooks/use-profiles.ts index 8cf654765..306013afa 100644 --- a/src/hooks/use-profiles.ts +++ b/src/hooks/use-profiles.ts @@ -56,7 +56,7 @@ export const useProfiles = () => { if (!result.ok) { // Backend Busy keeps local state untouched, as before. - return result.value ?? { status: 'busy' } + return result.value } const outcome = result.value if (outcome.status !== 'valid' && outcome.status !== 'busy') { diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index aee5dc484..85d7d78b2 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -530,13 +530,7 @@ const ProfilePage = () => { const loadingCache = useProfileLoadingCache() const { updateRevisions: completedUpdateRevisions, timerRevisions } = useProfileUpdates() - const setStoreLoadingProfiles = useSetProfileLoading() - const setLoadingProfiles = useCallback( - (uids: string[], loading: boolean) => { - setStoreLoadingProfiles(uids, loading) - }, - [setStoreLoadingProfiles], - ) + const setLoadingProfiles = useSetProfileLoading() const runProfileUpdates = useCallback( async (uids: string[]) => { diff --git a/src/services/bus/create-bus-handlers.test.ts b/src/services/bus/create-bus-handlers.test.ts index 43d910e40..33bd6d602 100644 --- a/src/services/bus/create-bus-handlers.test.ts +++ b/src/services/bus/create-bus-handlers.test.ts @@ -54,8 +54,6 @@ const mountBus = async () => { notice(status, message, (key) => key, vi.fn()) }, revalidateKeys: (keys) => void revalidateQueries(keys.map((key) => [key])), - revalidateProfiles: () => {}, - refreshProxyView: () => {}, readPendingFailures: () => {}, readRunState: () => {}, }) diff --git a/src/services/bus/create-bus-handlers.ts b/src/services/bus/create-bus-handlers.ts index 985951e51..957589ea7 100644 --- a/src/services/bus/create-bus-handlers.ts +++ b/src/services/bus/create-bus-handlers.ts @@ -20,22 +20,12 @@ export interface BusHandlerDeps { /** Notice-table entry point; layout owns t/navigate wiring. */ handleNotice: (payload: [string, string]) => void revalidateKeys: (keys: readonly string[]) => void - revalidateProfiles: () => void - refreshProxyView: () => void /** Re-read the backend pending-failure snapshot into the store. */ readPendingFailures: () => void /** Re-read the run-state snapshot into the store. */ readRunState: () => void } -export interface EventBus { - handlers: BusHandlers - /** Re-read event-only state after listeners are live (initial race window). */ - onSubscribed: () => void - /** The window regained focus: replay a possibly recovered startup error. */ - onWindowFocus: () => void -} - /** * Pure per-event handler table for the bus. Handlers may trigger documented * side effects (SWR revalidation, notice toasts) but write store state only @@ -43,7 +33,7 @@ export interface EventBus { * throttles below live in the returned closure, mirroring the previous * scattered listeners. */ -export const createEventBus = (deps: BusHandlerDeps): EventBus => { +export const createEventBus = (deps: BusHandlerDeps) => { let lastProfileId: string | null = null let lastProfileChangeTime = 0 let lastProxyRefreshTime = 0 @@ -59,14 +49,14 @@ export const createEventBus = (deps: BusHandlerDeps): EventBus => { } lastProfileId = newProfileId lastProfileChangeTime = now - deps.revalidateProfiles() + deps.revalidateKeys(['getProfiles']) } const handleRefreshProxyConfig = () => { const now = Date.now() if (now - lastProxyRefreshTime <= refreshThrottle) return lastProxyRefreshTime = now - deps.refreshProxyView() + deps.revalidateKeys(['getProxyView']) } const handlers: BusHandlers = { @@ -87,7 +77,7 @@ export const createEventBus = (deps: BusHandlerDeps): EventBus => { 'getSystemProxy', 'getAutotemProxy', ]), - 'verge://refresh-profiles': () => deps.revalidateProfiles(), + 'verge://refresh-profiles': () => deps.revalidateKeys(['getProfiles']), 'verge://refresh-proxy-config': handleRefreshProxyConfig, 'verge://notice-message': (payload) => deps.handleNotice(payload), 'profile-changed': handleProfileChanged, @@ -97,7 +87,7 @@ export const createEventBus = (deps: BusHandlerDeps): EventBus => { deps.dispatch({ type: 'profileUpdate/started', uid }), 'profile-update-completed': ({ uid }) => { deps.dispatch({ type: 'profileUpdate/completed', uid }) - deps.revalidateProfiles() + deps.revalidateKeys(['getProfiles']) }, 'verge://run-state-changed': (payload) => { deps.dispatch({ type: 'runState/loaded', runState: payload }) @@ -106,7 +96,7 @@ export const createEventBus = (deps: BusHandlerDeps): EventBus => { }, 'verge://pending-failures-changed': () => deps.readPendingFailures(), [TEST_ALL_EVENT]: () => deps.dispatch({ type: 'testAll/requested' }), - } as BusHandlers + } const onSubscribed = () => { // Run state is read after subscribing so an event racing the store's diff --git a/src/services/bus/use-event-bus.test.ts b/src/services/bus/use-event-bus.test.ts index 13ba8f138..106503d02 100644 --- a/src/services/bus/use-event-bus.test.ts +++ b/src/services/bus/use-event-bus.test.ts @@ -45,28 +45,6 @@ beforeEach(() => { listen.mockImplementation(() => Promise.resolve(() => {})) }) -it('registers exactly one listener per contract event plus test-all', () => { - useEventBus(handleNotice) - - const names = listen.mock.calls.map(([name]) => name) - expect(names.sort()).toEqual( - [ - 'verge://refresh-clash-config', - 'verge://refresh-verge-config', - 'verge://refresh-profiles', - 'verge://refresh-proxy-config', - 'verge://notice-message', - 'profile-changed', - 'verge://timer-updated', - 'profile-update-started', - 'profile-update-completed', - 'verge://run-state-changed', - 'verge://pending-failures-changed', - 'verge://test-all', - ].sort(), - ) -}) - it('drains event-only state only after every listener is registered', async () => { const registrations: Array<() => void> = [] listen.mockImplementation( diff --git a/src/services/bus/use-event-bus.ts b/src/services/bus/use-event-bus.ts index 8c22e10ed..ea64c0100 100644 --- a/src/services/bus/use-event-bus.ts +++ b/src/services/bus/use-event-bus.ts @@ -31,10 +31,6 @@ export const useEventBus = ( dispatch, handleNotice, revalidateKeys, - revalidateProfiles: () => { - void revalidateQueries([['getProfiles']]) - }, - refreshProxyView: () => revalidateKeys(['getProxyView']), readPendingFailures, readRunState, }) diff --git a/src/services/mutate.ts b/src/services/mutate.ts index 269629a90..decf95bf9 100644 --- a/src/services/mutate.ts +++ b/src/services/mutate.ts @@ -14,12 +14,10 @@ export interface MutateOptions { /** Shown after a fulfilled result (most flows leave this to backend notices). */ successNotice?: MutateNoticeInput /** `false` suppresses the default error toast. */ - errorNotice?: false | MutateNoticeInput + errorNotice?: false } -export type MutateResult = - | { ok: true; value: T } - | { ok: false; busy: true; value?: T } +export type MutateResult = { ok: true; value: T } | { ok: false; value: T } /** ValidationOutcome-shaped results report backend-side Busy rejection this way. */ const isBusyOutcome = (value: unknown): boolean => @@ -44,7 +42,7 @@ export async function mutate( try { const value = await invoke() if (isBusyOutcome(value)) { - return { ok: false, busy: true, value } + return { ok: false, value } } options.onFulfilled?.(value) if (options.revalidate?.length) { @@ -56,9 +54,7 @@ export async function mutate( return { ok: true, value } } catch (error) { if (options.errorNotice !== false) { - showNotice.error( - options.errorNotice !== undefined ? options.errorNotice : error, - ) + showNotice.error(error) } throw error } finally {