This document describes the timer-based scheduling system that powers automatic profile updates in Clash Verge Rev. The system uses a persistent scheduler to trigger periodic updates of remote subscription profiles based on user-configured intervals. This document covers the Timer singleton, task lifecycle management, profile update scheduling, retry logic, and integration with other subsystems.
For information about manual profile updates and the profile UI, see Profile Frontend UI. For the underlying profile data structures, see Profile Backend Architecture.
The timer system consists of three major components working together:
Sources: src-tauri/src/core/timer.rs1-477 src-tauri/src/config/prfitem.rs80-125 src-tauri/src/feat/profile.rs1-222
The Timer struct is implemented as a singleton and contains the following key fields:
| Field | Type | Purpose |
|---|---|---|
delay_timer | Arc<RwLock<DelayTimer>> | Underlying scheduler from delay-timer crate |
timer_map | Arc<RwLock<HashMap<String, TimerTask>>> | Maps profile UIDs to task metadata |
timer_count | AtomicU64 | Atomic counter for generating unique task IDs |
initialized | AtomicBool | Thread-safe flag to prevent re-initialization |
Sources: src-tauri/src/core/timer.rs28-40
Each scheduled task is tracked with metadata:
The timer_map associates profile UIDs (as String keys) with their corresponding TimerTask instances. This allows the system to track which profiles have active timers and their configurations.
Sources: src-tauri/src/core/timer.rs20-26
Sources: src-tauri/src/core/timer.rs56-143
The initialization process:
compare_exchange on the initialized flag to ensure single initializationrefresh() to compute the desired timer state from profilesProfile auto-update behavior is controlled by fields in PrfOption:
| Field | Type | Description |
|---|---|---|
update_interval | Option<u64> | Update interval in minutes (0 = disabled) |
allow_auto_update | Option<bool> | Master switch for auto-updates (default: true via default_allow_auto_update) |
A profile will have an active timer task registered if:
allow_auto_update is Some(true) (defaults to true for backwards compatibility)update_interval is Some(minutes) where minutes > 0itype is "remote"Sources: src-tauri/src/core/timer.rs235-260 src-tauri/src/config/prfitem.rs112-114 src-tauri/src/config/prfitem.rs581-584
The gen_map() function scans all profiles and builds a map of UIDs to intervals:
The gen_diff() function compares the current timer_map with the desired state from gen_map() and produces a DiffFlag for each change:
| Scenario | DiffFlag variant | Action taken in refresh() |
|---|---|---|
| Task exists, interval unchanged | (none) | No operation |
| Task exists, interval changed | Mod(task_id, new_interval) | Remove old task, add new task |
| Task exists, no longer needed | Del(task_id) | Remove from DelayTimer and timer_map |
| Task missing, needed | Add(task_id, interval) | Register in DelayTimer and timer_map |
Sources: src-tauri/src/core/timer.rs263-323 src-tauri/src/core/timer.rs472-477
async_task() call sequence per scheduled firing
Sources: src-tauri/src/core/timer.rs423-469
The add_task() method creates a recurring task with TaskBuilder:
Key behaviors:
set_maximum_parallel_runnable_num(1) prevents overlapping updates for the same profiletokio::time::timeoutauto_refresh parameter passed to feat::update_profile is true only if uid is the currently active profileSources: src-tauri/src/core/timer.rs326-353 src-tauri/src/core/timer.rs423-459
When feat::update_profile() executes, it attempts multiple HTTP strategies in sequence via perform_profile_update():
update_profile() and perform_profile_update() execution flow
Sources: src-tauri/src/feat/profile.rs66-222
| Attempt | PrfOption configuration | Network path |
|---|---|---|
| 1 (Direct) | self_proxy=false, with_proxy=false | No proxy — direct HTTP |
| 2 (Clash Proxy) | self_proxy=true, with_proxy=false | Routes through the local Clash/Mihomo core (ProxyType::Localhost) |
| 3 (System Proxy) | self_proxy=false, with_proxy=true | Uses the OS-configured system proxy (ProxyType::System) |
The proxy type selection happens inside PrfItem::from_url() via NetworkManager::get_with_interrupt(). After a successful fallback, the "update_with_clash_proxy" notice is sent. If all three attempts fail and the update was manually triggered (is_manual_trigger=true), "update_failed_even_with_clash" is sent instead.
Sources: src-tauri/src/feat/profile.rs100-187 src-tauri/src/config/prfitem.rs256-302
The refresh() method is called when timer configuration may have changed:
Trigger Points:
Timer::init() at application startuppatch_profile() detects changes to update_interval or allow_auto_update in a profile's PrfOptionSources: src-tauri/src/cmd/profile.rs411-443 src-tauri/src/core/timer.rs146-232
patch_profile() → Timer::refresh() sequence
Sources: src-tauri/src/cmd/profile.rs411-443 src-tauri/src/core/timer.rs146-232
The refresh is asynchronous (spawned via AsyncHandler::spawn) to avoid blocking the command handler. Changes are applied immediately to the DelayTimer, and the timer_map is updated to reflect the new state.
The timer system emits events to notify the frontend of update progress:
| Event helper | Trigger point | Frontend purpose |
|---|---|---|
Handle::notify_profile_update_started(uid) | Before feat::update_profile() is called | Show loading spinner on profile card |
Handle::notify_profile_update_completed(uid) | After update finishes (success or error) | Clear spinner, revalidate profile data |
Handle::notify_timer_updated(uid) | After Timer::refresh() succeeds due to interval change | Refresh "next update" countdown display |
Sources: src-tauri/src/core/timer.rs412-419 src-tauri/src/cmd/profile.rs429-437
Timer → Frontend event pathway
Sources: src-tauri/src/core/timer.rs412-459
The frontend can subscribe to these events to display loading indicators, trigger SWR revalidation, and update profile card state.
The get_next_update_time() method on Timer and its corresponding Tauri command allow the frontend to query when a profile will next update.
The calculation is:
next_update_unix = profile.updated + (interval_minutes * 60)
Where profile.updated is the Unix timestamp of the last successful download stored in PrfItem::updated, and interval_minutes comes from the TimerTask in timer_map.
The method returns None if the profile has no active timer entry (i.e., auto-update is not configured), or if updated is 0.
Sources: src-tauri/src/core/timer.rs356-409 src-tauri/src/cmd/profile.rs481-486
timer_count uses AtomicU64 for lock-free task ID generationdelay_timer and timer_map use RwLock for concurrent read accesscompare_exchange on initialized ensures single initializationSources: src-tauri/src/core/timer.rs28-65
Sources: src-tauri/src/core/timer.rs482-516 src-tauri/src/feat/profile.rs133-225
await pointsmaximum_parallel_runnable_num(1) prevents resource contentionSources: src-tauri/src/core/timer.rs172-263 src-tauri/src/core/timer.rs366-402
Refresh this wiki