Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 57 additions & 79 deletions apps/desktop-tauri/src-tauri/src/auto_refresh.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
use std::sync::Mutex;
use std::time::Duration;
#[cfg(test)]
use std::time::Instant;
use std::sync::{Arc, 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<String> {
Expand All @@ -44,85 +57,50 @@ fn powertoys_local_usage_provider_ids(settings: &Settings) -> Vec<String> {
.collect()
}

fn should_refresh(app: &tauri::AppHandle) -> bool {
let settings = Settings::load();
let Some(interval) = refresh_interval(settings.refresh_interval_secs) else {
return false;
pub(crate) fn schedule_refresh_enrichment(settings: &Settings) {
let provider_ids = powertoys_local_usage_provider_ids(settings);
if provider_ids.is_empty() {
return;
}
static ENRICHMENT: OnceLock<Arc<tokio::sync::Mutex<()>>> = OnceLock::new();
let Ok(guard) = Arc::clone(ENRICHMENT.get_or_init(|| Arc::new(tokio::sync::Mutex::new(()))))
.try_lock_owned()
else {
return;
};

let state = app.state::<Mutex<AppState>>();
state
.lock()
.map(|guard| should_refresh_from_state(&guard, interval))
.unwrap_or(false)
tauri::async_runtime::spawn(async move {
let _guard = guard;
crate::commands::refresh_provider_local_usage_cache(provider_ids).await;
});
}

fn refresh_interval(seconds: u64) -> Option<Duration> {
(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,
updated_at: Option<Instant>,
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,
));
assert_eq!(refresh_interval(0), None);
}

#[test]
fn stale_cache_refreshes_after_configured_interval() {
assert!(should_refresh_from_values(
false,
Some(Instant::now() - Duration::from_secs(300)),
300,
));
}
fn fixed_cadence_advances_from_the_scheduled_tick() {
let start = Instant::now();
let interval = Duration::from_secs(100);
let first_tick = start + interval;

#[test]
fn active_refresh_blocks_overlapping_background_refresh() {
assert!(!should_refresh_from_values(true, None, 300));
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]
Expand Down
1 change: 1 addition & 0 deletions apps/desktop-tauri/src-tauri/src/commands/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
30 changes: 30 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -324,6 +328,7 @@ pub async fn update_settings(
) -> Result<SettingsSnapshot, String> {
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();
Expand Down Expand Up @@ -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))
}
Expand All @@ -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!(
Expand Down
Loading