refactor(frontend): remove redundant intent and bus wrappers

This commit is contained in:
Tunglies
2026-10-06 16:07:11 +08:00
9 changed files with 22 additions and 87 deletions
+3 -2
View File
@@ -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,
}
+7 -25
View File
@@ -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 {
+1 -1
View File
@@ -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') {
+1 -7
View File
@@ -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[]) => {
@@ -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: () => {},
})
+6 -16
View File
@@ -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
-22
View File
@@ -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(
-4
View File
@@ -31,10 +31,6 @@ export const useEventBus = (
dispatch,
handleNotice,
revalidateKeys,
revalidateProfiles: () => {
void revalidateQueries([['getProfiles']])
},
refreshProxyView: () => revalidateKeys(['getProxyView']),
readPendingFailures,
readRunState,
})
+4 -8
View File
@@ -14,12 +14,10 @@ export interface MutateOptions<T> {
/** 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<T> =
| { ok: true; value: T }
| { ok: false; busy: true; value?: T }
export type MutateResult<T> = { 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<T>(
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<T>(
return { ok: true, value }
} catch (error) {
if (options.errorNotice !== false) {
showNotice.error(
options.errorNotice !== undefined ? options.errorNotice : error,
)
showNotice.error(error)
}
throw error
} finally {