Skip to main content

origin_domain/
job.rs

1//! Background jobs. Everything long-running reports progress the same way, so every
2//! product can reuse one progress UI.
3
4use crate::ids::JobId;
5use serde::{Deserialize, Serialize};
6use time::OffsetDateTime;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
10#[serde(rename_all = "snake_case")]
11pub enum JobStatus {
12    Queued,
13    Running,
14    Succeeded,
15    Failed,
16    Cancelled,
17}
18
19impl JobStatus {
20    pub fn is_terminal(self) -> bool {
21        matches!(self, Self::Succeeded | Self::Failed | Self::Cancelled)
22    }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
27pub struct Progress {
28    pub current: u64,
29    /// `None` while the total is not yet known — the UI shows an indeterminate bar.
30    pub total: Option<u64>,
31}
32
33impl Progress {
34    pub fn indeterminate() -> Self {
35        Self {
36            current: 0,
37            total: None,
38        }
39    }
40
41    pub fn of(current: u64, total: u64) -> Self {
42        Self {
43            current,
44            total: Some(total),
45        }
46    }
47
48    /// Completion between `0.0` and `1.0`, if the total is known and non-zero.
49    pub fn ratio(&self) -> Option<f64> {
50        match self.total {
51            Some(total) if total > 0 => Some((self.current as f64 / total as f64).clamp(0.0, 1.0)),
52            _ => None,
53        }
54    }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
59pub struct Job {
60    pub id: JobId,
61    /// Product-defined job kind, e.g. `scan-repository`.
62    pub kind: String,
63    pub status: JobStatus,
64    pub progress: Progress,
65    pub cancelable: bool,
66    #[serde(with = "time::serde::rfc3339")]
67    #[cfg_attr(feature = "ts", ts(type = "string"))]
68    pub started_at: OffsetDateTime,
69    #[serde(with = "time::serde::rfc3339::option")]
70    #[cfg_attr(feature = "ts", ts(type = "string | null"))]
71    pub finished_at: Option<OffsetDateTime>,
72    pub error: Option<String>,
73}
74
75impl Job {
76    pub fn queued(kind: impl Into<String>, started_at: OffsetDateTime) -> Self {
77        Self {
78            id: JobId::generate(),
79            kind: kind.into(),
80            status: JobStatus::Queued,
81            progress: Progress::indeterminate(),
82            cancelable: true,
83            started_at,
84            finished_at: None,
85            error: None,
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn ratio_is_none_while_the_total_is_unknown() {
96        assert_eq!(Progress::indeterminate().ratio(), None);
97    }
98
99    #[test]
100    fn ratio_never_exceeds_one() {
101        assert_eq!(Progress::of(12, 10).ratio(), Some(1.0));
102    }
103}