From 67b35a3015e834d832127034b7b5213214bf65e5 Mon Sep 17 00:00:00 2001 From: NessZerra Date: Sun, 12 Jul 2026 09:09:35 +0700 Subject: [PATCH 1/2] Fix refresh scheduling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src-tauri/src/auto_refresh.rs | 97 +++++++++++-------- .../src-tauri/src/commands/providers.rs | 1 + .../src-tauri/src/commands/settings.rs | 30 ++++++ 3 files changed, 90 insertions(+), 38 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/auto_refresh.rs b/apps/desktop-tauri/src-tauri/src/auto_refresh.rs index 01809a2ee9..7409ea0037 100644 --- a/apps/desktop-tauri/src-tauri/src/auto_refresh.rs +++ b/apps/desktop-tauri/src-tauri/src/auto_refresh.rs @@ -1,34 +1,47 @@ -use std::sync::Mutex; -use std::time::Duration; -#[cfg(test)] -use std::time::Instant; +use std::sync::OnceLock; +use std::time::{Duration, Instant}; use codexbar::settings::Settings; -use tauri::Manager; - -use crate::state::AppState; const AUTO_REFRESH_POLL_INTERVAL: Duration = Duration::from_secs(15); pub fn install(app: tauri::AppHandle) { tauri::async_runtime::spawn(async move { + let mut schedule: Option<(Duration, Instant)> = None; loop { - if should_refresh(&app) - && crate::commands::do_refresh_providers_if_stale(&app) - .await - .is_ok() - { - refresh_powertoys_local_usage_cache().await; + let interval = refresh_interval(Settings::load().refresh_interval_secs); + match interval { + None => schedule = None, + Some(interval) => { + let now = Instant::now(); + let scheduled_at = schedule + .filter(|(scheduled_interval, _)| *scheduled_interval == interval) + .map(|(_, scheduled_at)| scheduled_at) + .unwrap_or(now); + if now >= scheduled_at { + let _ = crate::commands::do_refresh_providers_if_stale(&app).await; + schedule = Some(( + interval, + next_fixed_tick(scheduled_at, Instant::now(), interval), + )); + } + } } tokio::time::sleep(AUTO_REFRESH_POLL_INTERVAL).await; } }); } -async fn refresh_powertoys_local_usage_cache() { - let settings = Settings::load(); - let provider_ids = powertoys_local_usage_provider_ids(&settings); - crate::commands::refresh_provider_local_usage_cache(provider_ids).await; +fn next_fixed_tick( + previous_scheduled_at: Instant, + completed_at: Instant, + interval: Duration, +) -> Instant { + let mut scheduled_at = previous_scheduled_at + interval; + while scheduled_at <= completed_at { + scheduled_at += interval; + } + scheduled_at } fn powertoys_local_usage_provider_ids(settings: &Settings) -> Vec { @@ -44,33 +57,25 @@ fn powertoys_local_usage_provider_ids(settings: &Settings) -> Vec { .collect() } -fn should_refresh(app: &tauri::AppHandle) -> bool { - let settings = Settings::load(); - let Some(interval) = refresh_interval(settings.refresh_interval_secs) else { - return false; - }; - - let state = app.state::>(); - state - .lock() - .map(|guard| should_refresh_from_state(&guard, interval)) - .unwrap_or(false) +pub(crate) fn schedule_refresh_enrichment(settings: &Settings) { + let provider_ids = powertoys_local_usage_provider_ids(settings); + if provider_ids.is_empty() { + return; + } + tauri::async_runtime::spawn(async move { + static ENRICHMENT: OnceLock> = OnceLock::new(); + let _guard = ENRICHMENT + .get_or_init(|| tokio::sync::Mutex::new(())) + .lock() + .await; + crate::commands::refresh_provider_local_usage_cache(provider_ids).await; + }); } fn refresh_interval(seconds: u64) -> Option { (seconds > 0).then(|| Duration::from_secs(seconds)) } -fn should_refresh_from_state(state: &AppState, interval: Duration) -> bool { - if state.is_refreshing { - return false; - } - match state.provider_cache_updated_at { - Some(updated_at) => updated_at.elapsed() >= interval, - None => true, - } -} - #[cfg(test)] pub(crate) fn should_refresh_from_values( is_refreshing: bool, @@ -125,6 +130,22 @@ mod tests { assert!(!should_refresh_from_values(true, None, 300)); } + #[test] + fn fixed_cadence_advances_from_the_scheduled_tick() { + let start = Instant::now(); + let interval = Duration::from_secs(100); + let first_tick = start + interval; + + assert_eq!( + next_fixed_tick(first_tick, first_tick + Duration::from_secs(60), interval), + start + Duration::from_secs(200) + ); + assert_eq!( + next_fixed_tick(first_tick, first_tick + Duration::from_secs(260), interval), + start + Duration::from_secs(400) + ); + } + #[test] fn powertoys_local_usage_refresh_only_includes_supported_enabled_providers() { let mut settings = Settings::default(); diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index ffef079984..241e4fbaff 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -181,6 +181,7 @@ async fn do_refresh_providers_with_policy( update_tray_and_notifications(app, &state, &inputs.settings, &inputs.token_accounts)?; events::emit_refresh_complete(app, enabled_count, error_count); + crate::auto_refresh::schedule_refresh_enrichment(&inputs.settings); Ok(()) } diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index 45bc905f3a..ee8f4949c8 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -56,6 +56,10 @@ pub struct SettingsUpdate { } impl SettingsUpdate { + fn refreshes_provider_data(&self) -> bool { + self.enabled_providers.is_some() + } + fn notifies_float_bar(&self) -> bool { self.enabled_providers.is_some() || self.refresh_interval_secs.is_some() @@ -324,6 +328,7 @@ pub async fn update_settings( ) -> Result { let mut settings = Settings::load(); let notify_float_bar = patch.notifies_float_bar(); + let refresh_provider_data = patch.refreshes_provider_data(); let clear_local_usage_cache = patch.codex_custom_sessions_dirs.is_some(); let rebuild_tray_menu = patch.rebuilds_tray_menu(); let refresh_tray_presentation = patch.refreshes_tray_presentation(); @@ -352,6 +357,12 @@ pub async fn update_settings( // settings live — e.g. the Display tab's window-scale slider takes effect // immediately instead of only after the PopOut is reopened. events::emit_settings_changed(&app); + if refresh_provider_data { + let app = app.clone(); + tauri::async_runtime::spawn(async move { + let _ = crate::commands::do_refresh_providers(&app).await; + }); + } Ok(SettingsSnapshot::from(settings)) } @@ -360,6 +371,25 @@ pub async fn update_settings( mod tests { use super::*; + #[test] + fn only_data_affecting_settings_refresh_providers() { + assert!( + SettingsUpdate { + enabled_providers: Some(vec!["codex".to_string()]), + ..Default::default() + } + .refreshes_provider_data() + ); + assert!( + !SettingsUpdate { + provider_metrics: Some(Default::default()), + tray_icon_mode: Some("single".to_string()), + ..Default::default() + } + .refreshes_provider_data() + ); + } + #[test] fn display_settings_that_affect_tray_trigger_presentation_refresh() { assert!( From 69b63909a7fbf5f1901877538c112c062c244530 Mon Sep 17 00:00:00 2001 From: NessZerra Date: Sun, 12 Jul 2026 09:13:30 +0700 Subject: [PATCH 2/2] Keep refresh enrichment single-flight Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src-tauri/src/auto_refresh.rs | 61 +++---------------- 1 file changed, 9 insertions(+), 52 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/auto_refresh.rs b/apps/desktop-tauri/src-tauri/src/auto_refresh.rs index 7409ea0037..91d26dd773 100644 --- a/apps/desktop-tauri/src-tauri/src/auto_refresh.rs +++ b/apps/desktop-tauri/src-tauri/src/auto_refresh.rs @@ -1,4 +1,4 @@ -use std::sync::OnceLock; +use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; use codexbar::settings::Settings; @@ -62,12 +62,14 @@ pub(crate) fn schedule_refresh_enrichment(settings: &Settings) { if provider_ids.is_empty() { return; } + static ENRICHMENT: OnceLock>> = OnceLock::new(); + let Ok(guard) = Arc::clone(ENRICHMENT.get_or_init(|| Arc::new(tokio::sync::Mutex::new(())))) + .try_lock_owned() + else { + return; + }; tauri::async_runtime::spawn(async move { - static ENRICHMENT: OnceLock> = OnceLock::new(); - let _guard = ENRICHMENT - .get_or_init(|| tokio::sync::Mutex::new(())) - .lock() - .await; + let _guard = guard; crate::commands::refresh_provider_local_usage_cache(provider_ids).await; }); } @@ -76,58 +78,13 @@ fn refresh_interval(seconds: u64) -> Option { (seconds > 0).then(|| Duration::from_secs(seconds)) } -#[cfg(test)] -pub(crate) fn should_refresh_from_values( - is_refreshing: bool, - updated_at: Option, - interval_secs: u64, -) -> bool { - let Some(interval) = refresh_interval(interval_secs) else { - return false; - }; - if is_refreshing { - return false; - } - updated_at - .map(|updated| updated.elapsed() >= interval) - .unwrap_or(true) -} - #[cfg(test)] mod tests { use super::*; #[test] fn manual_refresh_setting_disables_background_refresh() { - assert!(!should_refresh_from_values(false, None, 0)); - } - - #[test] - fn missing_cache_triggers_background_refresh() { - assert!(should_refresh_from_values(false, None, 300)); - } - - #[test] - fn fresh_cache_does_not_refresh_before_interval() { - assert!(!should_refresh_from_values( - false, - Some(Instant::now() - Duration::from_secs(299)), - 300, - )); - } - - #[test] - fn stale_cache_refreshes_after_configured_interval() { - assert!(should_refresh_from_values( - false, - Some(Instant::now() - Duration::from_secs(300)), - 300, - )); - } - - #[test] - fn active_refresh_blocks_overlapping_background_refresh() { - assert!(!should_refresh_from_values(true, None, 300)); + assert_eq!(refresh_interval(0), None); } #[test]