mod affinity_decay; mod feed_polling; mod scoring; use anyhow::Result; use feedsignal_db::Db; use feedsignal_llm::Llm; use std::sync::Arc; use tokio_cron_scheduler::{Job, JobScheduler}; /// Wires up the recurring jobs described in the design discussion: polling /// subscribed feeds for new articles, scoring them, and decaying topic /// affinities once a day so stale signals fade. Call once at server /// startup, and also runs once immediately so a freshly (re)opened app /// doesn't wait 15 minutes for its first check. pub async fn start_scheduler(db: Arc, llm: Arc) -> Result { let scheduler = JobScheduler::new().await?; { let db = db.clone(); tokio::spawn(async move { if let Err(err) = feed_polling::run(&db).await { tracing::error!(?err, "initial feed poll failed"); } }); } { let db = db.clone(); scheduler .add(Job::new_async("0 */15 * * * *", move |_uuid, _lock| { let db = db.clone(); Box::pin(async move { if let Err(err) = feed_polling::run(&db).await { tracing::error!(?err, "feed polling run failed"); } }) })?) .await?; } { let db = db.clone(); let llm = llm.clone(); scheduler .add(Job::new_async("0 */15 * * * *", move |_uuid, _lock| { let db = db.clone(); let llm = llm.clone(); Box::pin(async move { if let Err(err) = scoring::run(&db, &llm).await { tracing::error!(?err, "scoring pipeline run failed"); } }) })?) .await?; } { let db = db.clone(); scheduler .add(Job::new_async("0 0 4 * * *", move |_uuid, _lock| { let db = db.clone(); Box::pin(async move { if let Err(err) = affinity_decay::run(&db).await { tracing::error!(?err, "affinity decay run failed"); } }) })?) .await?; } scheduler.start().await?; Ok(scheduler) }