//! Shared shape for long-running background work. //! //! Today's [`compression`](crate::services::compression) and [`export`](crate::services::export) //! pipelines each implement their own progress + SSE plumbing. They could converge on the //! trait sketched here so future jobs (analytics, archival, ...) plug into one progress //! pipeline. //! //! This module is intentionally a *sketch*: the existing services are not yet wired to //! it. The aim is to (a) document the convention so new jobs follow it, (b) make the //! refactor mechanical when someone is ready to do it. See `docs/IDEAS.md` — //! "Maintainability principles" — for the rationale. //! //! Example of an eventual implementor: //! //! ```ignore //! struct ZipExport { event_id: Uuid, /* … */ } //! //! impl BackgroundJob for ZipExport { //! fn name(&self) -> &'static str { "zip-export" } //! async fn run(self, ctx: JobContext) -> Result<()> { //! for (i, item) in items.iter().enumerate() { //! ctx.report(percent(i, items.len())).await?; //! // … write to zip … //! } //! Ok(()) //! } //! } //! ``` use anyhow::Result; /// Handle handed to a running job: reports progress and emits SSE events. /// /// Wraps the existing SSE broadcaster and an optional `export_job` row. Implementors /// don't need to know about `state.sse_tx` directly — they call [`JobContext::report`] /// and get the same effect. pub struct JobContext { pub job_id: Option, pub event_kind: &'static str, pub sse_tx: tokio::sync::broadcast::Sender, pub pool: sqlx::PgPool, } impl JobContext { /// Update progress (0..=100) and broadcast an SSE tick. Cheap to call often — /// rate-limit at the call site if a job emits at > 10 Hz. pub async fn report(&self, percent: u8) -> Result<()> { if let Some(job_id) = self.job_id { sqlx::query("UPDATE export_job SET progress_pct = $1 WHERE id = $2") .bind(percent as i16) .bind(job_id) .execute(&self.pool) .await?; } let _ = self.sse_tx.send(crate::state::SseEvent::new( self.event_kind, serde_json::json!({ "progress_pct": percent }).to_string(), )); Ok(()) } } /// One unit of work that publishes progress through a [`JobContext`]. /// /// `run` consumes `self`; spawn with `tokio::spawn` at the caller. Errors propagate; /// the caller is responsible for mapping them to `export_job.error_message` or /// equivalent. Implementors stay small — the trait deliberately has no `cancel` /// or `pause`; we have not needed those yet. #[allow(async_fn_in_trait)] pub trait BackgroundJob: Send + 'static { fn name(&self) -> &'static str; async fn run(self, ctx: JobContext) -> Result<()>; }