mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 11:02:08 +08:00
816fdccd5f630032a5b4ae3d9198e1adbce30a2f
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69b76cc6a9 |
refactor(frontend): contract flow (#8131)
* feat(contract): export wire contract from backend and generate frontend types Adds a zero-dependency `frontend-contract` bin that serializes the real NoticeStatus variants and FrontendEvent payloads, scripts/gen-contract.mjs which emits the committed src/services/contract.ts, and a CI job that regenerates it and fails on a non-empty diff. events.ts now derives its event map from the generated contract instead of hand-typed shapes. * feat(events): route all backend events through a single event bus and store Adds src/services/bus (the only listen/emit site: one handler per contract event plus the frontend-internal test-all event) and the React context/reducer app store. Migrates the five scattered subscriptions (layout events, pending failures, proxy refresh, profile update/timer, test-all), deletes the old glue (events.ts, use-listen, use-layout-events, LoadingCacheProvider), moves run state into the store, and adds an architecture gate script wired into CI. Behavior is unchanged; the run-state query's poll/focus safety net is preserved by the store provider. * refactor(notice): make the notice table exhaustive and drop dead wire entries D5 cleanup, frontend and backend in one commit: removes the six config_validate::* statuses that never receive a backend send and the set_config::ok send (enum variants, golden list 41→34, send point in restart_clash_core), their frontend handlers, and the five i18n keys only those handlers used. The notice table is now typed as a Record over the generated NoticeStatus union, so a status without a frontend entry fails to compile. The info status (sent by the silent updater for breaking change notices, previously dropped with a console warning) now shows an info toast — the one deliberate behavior delta. * feat(mutate): single funnel for user-intent commands and invoke gate Adds src/services/mutate.ts: one path for optimistic apply, services-layer invoke, Busy handling, failure rollback, and the error toast, with busy bookkeeping mirrored into the app store. The patch hooks (verge, clash, profiles) now route through it. Moves the remaining direct invokes (unlock ×3, DNS ×4) into cmds.ts and turns on the invoke-only-in-services architecture gate in CI. * refactor(settings): route settings-domain commands through the mutate funnel Ports port saving, webdav/local backup operations, core change/restart/ upgrade, service install/uninstall/repair flows, the system-proxy toggle queue, the DNS override toggle, and app lifecycle commands onto the single funnel. Bespoke flows keep their own error handling via errorNotice:false; simple flows gain the funnel's default error toast and revalidation. * 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. * refactor(proxies): route proxy/home/misc commands through the mutate funnel Ports mode switching (page and home card), chain runtime updates, node selection with tray sync, selection recording, provider updates and syncs, and lightweight-mode entry onto the funnel; every site keeps its existing optimistic/revert and error-handling behavior. * docs: add frontend architecture guardrails for src Layering rules for the contract flow: generated contract as the only wire-typing source, event bus as the only listen/emit site, pure-reducer store for event-derived state, the mutate funnel for user intent, and invoke only in the services layer — each backed by the CI gates. * fix(events): close run-state race and restore single error toasts Self-review findings: the bus now re-reads the run-state snapshot in onSubscribed so an event racing the store's initial read cannot leave a stale snapshot (the ordering the old layout listener guaranteed), and the patch hooks pass errorNotice:false so GuardState/page handlers remain the only error toast, matching the pre-migration behavior. * fix(frontend): preserve queued intents and fresh runtime snapshots * refactor(frontend): trim unused mutation and busy state APIs * docs(frontend): focus guardrails on architecture ownership |
||
|
|
807f9a206d | chore: trim excessive comments | ||
|
|
daadb126e0 |
feat: allow manually unfixing proxy nodes (#7756)
* feat(proxy): add ability to clear persisted node selection for groups * feat(hooks): make proxy selection operations asynchronous * chore: update tauri-plugin-mihomo deps * refactor: rename clear_selected_node to forget_selected_node * refactor(hooks): rename isFixed to isFixedProxy in useProxySelection * docs: update changelog with manual node cancellation support * chore(layout): remove dev-only mihomo websocket debugging interval |
||
|
|
3d8d7731e1 |
fix: node selection persistence (#7620)
* fix(core): put the user's node choices back when the core starts mihomo restores the node picked in each group from the `cache.db` it keeps in the directory it was started against. The app relied on that entirely, and it does not hold: `profile.store-selected` comes from a merge template a user is free to replace, and a service older than the durable runtime generation starts every core in a directory nothing has ever run in. Both end the same way — every `select` group falls back to the first entry in its `proxies:` list. The app already knows better. It records each selection in the profile, and `activate_selected_nodes` already reconciles that record against the running core — it was only ever called when switching profiles, never when a core came up. Call it from the three places a core process becomes ready. Repeat calls supersede each other, so no coordination is needed between the start paths. That makes the profile authoritative, which turns any selection it does not know about into one the next start silently undoes. The chain proxy moved a group without recording it, so recording is now one shared hook that both it and the proxy list go through. Restoration is spawned, not awaited: it polls the core until its groups are readable, and a start must not wait for that. * fix(tray): record the node the tray switched to Restoring the profile's selections when a core starts made the profile authoritative, which turns any selection it never learned about into one the next start silently undoes. The proxy list and the chain proxy both record; the tray did not. So switching node from the tray survived only until the next restart, and then reverted to whatever the profile last knew — a regression the restoration itself introduced, and only for the tray. `record_selected_node` is the backend counterpart of the frontend's `useRecordSelection`. Failing to record does not fail the switch: the core is already on the new node by then, and reporting the switch as failed would be wrong. It is logged instead. * fix(core): restoring selections must not prune what the core has not loaded Putting the profile's selections back when a core starts reused the reconciliation written for profile switches, and that one also *repairs*: a record whose group is absent from two snapshots a second apart is dropped, and the reduced list is written to profiles.yaml. After a profile switch the configuration is loaded, so absence is evidence. After a core start it is not — a provider-backed group can be empty or missing for seconds — and a record pruned on that evidence is gone for good, because the provider finishing later cannot bring it back. Restoring now keeps the records and only applies what it can match; pruning stays with the caller entitled to conclude it. The restore is also asynchronous: it reads the profile, polls the core until its groups are readable, then puts its selections. A choice the user makes inside that window is newer than what it captured, so letting it finish pushed the core back to the older node while the profile already held the newer one. Recording a selection now supersedes any activation in flight — from the tray, and from the UI and chain proxy through `patch_profile`. * fix(core): activate the selections of the profile that is actually running The activation worker read the profiles draft. A profile switch stages its target there before validating it, so a switch that fails — leaving the previous profile running — could still have handed an activation the rejected profile's selections, which it would then apply to whichever same-named groups the running profile has. Reading the committed state removes the window. The switch that succeeds commits before it activates, so nothing changes for that caller; what changes is that a restore running at core start can no longer observe a switch that never happened. * fix(core): stop a restoration from undoing a choice made while it runs Two gaps in making the profile authoritative, both found by adversarial review of the commits that introduced it. A restoration reads the profile's selections once, then polls the core until its groups can be read — seconds, on a cold start. A selection made inside that window is newer than what the restoration is carrying, and nothing told the restoration so: it went on to apply the older choice over the user's. The UI accepted the new node and traffic snapped back to the old one, with the profile and the running core then disagreeing. Recording a selection now abandons any restoration still in flight, through the same generation counter every other superseding caller uses. The other gap is the home card in global mode, which passed `skipConfigSave` and so recorded nothing. That was harmless while nothing re-applied the profile; now it means the next start undoes the choice. It was the only caller that ever set the flag, so the flag is gone rather than flipped — a parameter whose one use was a bug is not worth keeping. * fix(core): wait for the selections that can be put back, keep trying for the rest Two gaps in restoring the profile's selections when a core starts, both found by adversarial review. Restoring re-checked once, a second later, and stopped. That is enough after a profile switch, which happens with the configuration already loaded. A core start is the opposite case: a provider-backed group is present but empty until its provider finishes loading, and a `select` can fail while the core is still settling. Neither produces an activation, so the restore reported success having applied nothing and the group stayed on the first entry of its `proxies:` list — the outcome restoring exists to prevent. It now keeps looking, on a deadline, at what the core is actually on rather than at what it managed to send. Restoring was also spawned, and the caller enables the system proxy the moment the start returns — so traffic could be pointed at a core still on the wrong node. The first pass is now awaited, bounded, and the rest keeps settling in the background: a core that will not answer delays a start rather than blocking it, because being proxied through the wrong node beats not being proxied at all. TUN is deliberately not covered, and cannot be by anything here: it is configured in the core's own file, so it carries traffic from the moment the process starts. What puts a TUN user on the right node from the first packet is the core's own `cache.db`, which is why the service keeps an owner's runtime generation across restarts. `supersede_selected_activation` and `cancel_selected_node_activation` were the same function under two names, added independently for the same reason, and `patch_profile` called both. One survives, called once, after the profile is written — the only placement that needs to exist, since an activation that read the older profile is cancelled by it and one starting afterwards reads the newer one. * fix(core): record a selection by naming it, not by resending the list Recording a node selection sent the whole selection list, built from the profile the frontend had rendered. Two selections made before that list refreshed were therefore built from the same stale snapshot, and the later write dropped the earlier group's choice. Since a core start re-applies whatever the profile holds, the dropped one came back on the next restart — the profile and the running core disagreeing, in the direction the user would least expect. The tray already avoided this, because it went through `record_selected_node`, which merges one group into the profile inside `with_data_modify`. Exposing that as a command and pointing the frontend at it leaves one way to record a selection instead of two, and removes the list from the wire: what a caller knows is the group and the node, and that is now all it says. `useRecordSelection` no longer reads the profile at all, which also makes it stable across renders rather than changing identity whenever the profile does. * test(core): make every way of declining to stage answer for itself `CoreRestarted` was added to `StageRejection` upstream and reached production without a test. It inherited the right verdict — every `RestartRequired` is matched together — but nothing said so, and nothing would have noticed if it had inherited the wrong one. The list of rejections now goes through a wrapper whose `match` is exhaustive, so the next variant added upstream fails to compile here. That is the part worth having: it does not prove the list is complete, but it does mean nobody can add a way to decline without being sent to this test to say what it should lead to. * build(deps): pin the service protocol to v2.6.1 The pin tracked `feat/stage-runtime` because the protocol change it carried had no tag yet. It has one now, and a branch pin makes the build irreproducible: an upstream force-push silently changes what this compiles against. v2.6.1 carries the runtime generation an owner keeps across restarts, which is what lets the core hold on to the `cache.db` its node selections and fake-ip leases live in. The protocol itself is untouched — epoch, revision and both compatibility floors are byte-identical to v2.6.0 — so this is a fix release in both directions: an older service still works with this app, and this app still works with an older service, only without the durable generation. The lockfile change is the two lines that name it. Reaching for `cargo update -p` instead re-resolved a subtree and quietly downgraded `windows-sys`, `windows-core` and `getrandom` along the way. |