mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-25 13:12:07 +08:00
refactor(frontend): remove redundant intent and bus wrappers
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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: () => {},
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -31,10 +31,6 @@ export const useEventBus = (
|
||||
dispatch,
|
||||
handleNotice,
|
||||
revalidateKeys,
|
||||
revalidateProfiles: () => {
|
||||
void revalidateQueries([['getProfiles']])
|
||||
},
|
||||
refreshProxyView: () => revalidateKeys(['getProxyView']),
|
||||
readPendingFailures,
|
||||
readRunState,
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user