use chrono::{DateTime, Utc};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::time::{self, Duration};
use crate::db::Database;
use crate::{obtain_authenticated_client, sync_local_to_db, sync_remote_to_db_delta};
#[derive(Debug, Clone)]
pub enum SyncCommand {
TriggerImmediateSync,
SetWindowActive(bool),
}
#[derive(Debug, Clone)]
pub enum SyncEvent {
SyncStarted { is_manual: bool },
SyncFinished(Result<DateTime<Utc>, String>),
}
pub struct SyncManager {
cmd_tx: mpsc::Sender<SyncCommand>,
}
impl SyncManager {
pub fn spawn(event_tx: mpsc::Sender<SyncEvent>) -> Arc<Self> {
Self::spawn_with_db_path(event_tx, "task_lists.db")
}
pub fn spawn_with_db_path(
event_tx: mpsc::Sender<SyncEvent>,
db_path: impl Into<String>,
) -> Arc<Self> {
let (cmd_tx, cmd_rx) = mpsc::channel(32);
let path = db_path.into();
tokio::spawn(async move {
run_sync_actor(cmd_rx, event_tx, path).await;
});
Arc::new(Self { cmd_tx })
}
pub fn trigger_sync(&self) {
let _ = self.cmd_tx.try_send(SyncCommand::TriggerImmediateSync);
}
pub fn set_window_active(&self, active: bool) {
let _ = self.cmd_tx.try_send(SyncCommand::SetWindowActive(active));
}
}
async fn run_sync_actor(
mut cmd_rx: mpsc::Receiver<SyncCommand>,
event_tx: mpsc::Sender<SyncEvent>,
db_path: String,
) {
let mut is_active = true;
let mut last_sync_time: Option<DateTime<Utc>> = None;
let active_duration = Duration::from_secs(30);
let idle_duration = Duration::from_secs(180);
let mut timer = time::interval(active_duration);
timer.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
loop {
tokio::select! {
cmd = cmd_rx.recv() => {
match cmd {
Some(SyncCommand::TriggerImmediateSync) => {
let _ = event_tx.send(SyncEvent::SyncStarted { is_manual: true }).await;
let res = perform_single_sync(&mut last_sync_time, &db_path).await.map_err(|e| e.to_string());
let _ = event_tx.send(SyncEvent::SyncFinished(res)).await;
timer.reset();
}
Some(SyncCommand::SetWindowActive(active)) => {
let became_active = active && !is_active;
is_active = active;
let new_duration = if is_active { active_duration } else { idle_duration };
timer = time::interval(new_duration);
timer.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
if became_active {
tracing::info!("🪟 Window focused! Triggering immediate background delta sync...");
let _ = event_tx.send(SyncEvent::SyncStarted { is_manual: false }).await;
let res = perform_single_sync(&mut last_sync_time, &db_path).await.map_err(|e| e.to_string());
let _ = event_tx.send(SyncEvent::SyncFinished(res)).await;
}
}
None => break,
}
}
_ = timer.tick() => {
let _ = event_tx.send(SyncEvent::SyncStarted { is_manual: false }).await;
let res = perform_single_sync(&mut last_sync_time, &db_path).await.map_err(|e| e.to_string());
let _ = event_tx.send(SyncEvent::SyncFinished(res)).await;
}
}
}
}
async fn perform_single_sync(
last_sync_time: &mut Option<DateTime<Utc>>,
db_path: &str,
) -> crate::Result<DateTime<Utc>> {
let sync_start = Utc::now();
let client = obtain_authenticated_client().await?;
let path = db_path.to_string();
let mut db = tokio::task::spawn_blocking(move || Database::new(&path)).await??;
sync_local_to_db(&client, &mut db).await?;
sync_remote_to_db_delta(&client, &mut db, *last_sync_time).await?;
*last_sync_time = Some(sync_start);
Ok(sync_start)
}