Skip to main content

qubit_progress/
progress.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::time::{
11    Duration,
12    Instant,
13};
14
15use crate::{
16    model::{
17        ProgressCounters,
18        ProgressEvent,
19        ProgressPhase,
20        ProgressStage,
21    },
22    reporter::ProgressReporter,
23};
24
25/// Tracks one progress-producing operation and reports lifecycle events.
26///
27/// `Progress` owns no operation-specific counters. Callers keep their own
28/// domain state and pass freshly built [`ProgressCounters`] when reporting.
29/// The run only manages elapsed time, periodic running-event throttling,
30/// optional stage metadata, and forwarding immutable events to a reporter.
31///
32/// # Examples
33///
34/// ```
35/// use std::time::Duration;
36///
37/// use qubit_progress::{
38///     ProgressCounters,
39///     Progress,
40///     WriterProgressReporter,
41/// };
42///
43/// let reporter = WriterProgressReporter::from_writer(std::io::stdout());
44/// let mut progress = Progress::new(&reporter, Duration::from_secs(5));
45///
46/// progress.report_started(ProgressCounters::new(Some(2)));
47///
48/// let running = ProgressCounters::new(Some(2))
49///     .with_completed_count(1)
50///     .with_active_count(1);
51/// let _reported = progress.report_running_if_due(running);
52///
53/// let finished = ProgressCounters::new(Some(2))
54///     .with_completed_count(2)
55///     .with_succeeded_count(2);
56/// progress.report_finished(finished);
57/// ```
58pub struct Progress<'a> {
59    /// Reporter receiving lifecycle callbacks for this run.
60    reporter: &'a dyn ProgressReporter,
61    /// Monotonic start time used to compute elapsed durations.
62    started_at: Instant,
63    /// Minimum interval between due-based running callbacks.
64    report_interval: Duration,
65    /// Next monotonic instant at which a due-based running callback may fire.
66    next_running_at: Instant,
67    /// Optional stage metadata attached to every event emitted by this run.
68    stage: Option<ProgressStage>,
69}
70
71impl<'a> Progress<'a> {
72    /// Creates a progress run starting at the current instant.
73    ///
74    /// # Parameters
75    ///
76    /// * `reporter` - Reporter receiving progress events.
77    /// * `report_interval` - Minimum delay between due-based running events.
78    ///
79    /// # Returns
80    ///
81    /// A progress run whose elapsed time is measured from now.
82    #[inline]
83    pub fn new(reporter: &'a dyn ProgressReporter, report_interval: Duration) -> Self {
84        Self::from_start(reporter, report_interval, Instant::now())
85    }
86
87    /// Creates a progress run from an explicit start instant.
88    ///
89    /// # Parameters
90    ///
91    /// * `reporter` - Reporter receiving progress events.
92    /// * `report_interval` - Minimum delay between due-based running events.
93    /// * `started_at` - Monotonic instant representing operation start.
94    ///
95    /// # Returns
96    ///
97    /// A progress run using `started_at` for elapsed-time calculations.
98    #[inline]
99    pub fn from_start(
100        reporter: &'a dyn ProgressReporter,
101        report_interval: Duration,
102        started_at: Instant,
103    ) -> Self {
104        Self {
105            reporter,
106            started_at,
107            report_interval,
108            next_running_at: next_instant(started_at, report_interval),
109            stage: None,
110        }
111    }
112
113    /// Returns a copy configured with stage metadata.
114    ///
115    /// # Parameters
116    ///
117    /// * `stage` - Stage metadata attached to subsequently reported events.
118    ///
119    /// # Returns
120    ///
121    /// This progress run with `stage` recorded.
122    #[inline]
123    pub fn with_stage(mut self, stage: ProgressStage) -> Self {
124        self.stage = Some(stage);
125        self
126    }
127
128    /// Returns a copy with stage metadata removed.
129    ///
130    /// # Returns
131    ///
132    /// This progress run without stage metadata.
133    #[inline]
134    pub fn without_stage(mut self) -> Self {
135        self.stage = None;
136        self
137    }
138
139    /// Reports a started lifecycle event.
140    ///
141    /// # Parameters
142    ///
143    /// * `counters` - Initial counters for the operation.
144    ///
145    /// # Panics
146    ///
147    /// Propagates panics from the configured reporter.
148    #[inline]
149    pub fn report_started(&self, counters: ProgressCounters) {
150        self.report(ProgressPhase::Started, counters);
151    }
152
153    /// Reports a running lifecycle event immediately.
154    ///
155    /// # Parameters
156    ///
157    /// * `counters` - Current counters for the operation.
158    ///
159    /// # Panics
160    ///
161    /// Propagates panics from the configured reporter.
162    #[inline]
163    pub fn report_running(&self, counters: ProgressCounters) {
164        self.report(ProgressPhase::Running, counters);
165    }
166
167    /// Reports a running lifecycle event if the configured interval has passed.
168    ///
169    /// # Parameters
170    ///
171    /// * `counters` - Current counters for the operation.
172    ///
173    /// # Returns
174    ///
175    /// `true` when a running event was emitted, or `false` when the next
176    /// running-event deadline has not been reached.
177    ///
178    /// This method does not block waiting for the next deadline. It returns
179    /// immediately when not due, and when due it synchronously calls the
180    /// configured reporter. Any blocking behavior therefore comes from the
181    /// reporter implementation.
182    ///
183    /// # Panics
184    ///
185    /// Propagates panics from the configured reporter when an event is due.
186    pub fn report_running_if_due(&mut self, counters: ProgressCounters) -> bool {
187        let now = Instant::now();
188        if now < self.next_running_at {
189            return false;
190        }
191        self.report_with_elapsed(
192            ProgressPhase::Running,
193            counters,
194            now.saturating_duration_since(self.started_at),
195        );
196        self.next_running_at = next_instant(now, self.report_interval);
197        true
198    }
199
200    /// Reports a finished lifecycle event.
201    ///
202    /// # Parameters
203    ///
204    /// * `counters` - Final counters for a successfully completed operation.
205    ///
206    /// # Panics
207    ///
208    /// Propagates panics from the configured reporter.
209    #[inline]
210    pub fn report_finished(&self, counters: ProgressCounters) {
211        self.report(ProgressPhase::Finished, counters);
212    }
213
214    /// Reports a failed lifecycle event.
215    ///
216    /// # Parameters
217    ///
218    /// * `counters` - Final or current counters for a failed operation.
219    ///
220    /// # Panics
221    ///
222    /// Propagates panics from the configured reporter.
223    #[inline]
224    pub fn report_failed(&self, counters: ProgressCounters) {
225        self.report(ProgressPhase::Failed, counters);
226    }
227
228    /// Reports a canceled lifecycle event.
229    ///
230    /// # Parameters
231    ///
232    /// * `counters` - Final or current counters for a canceled operation.
233    ///
234    /// # Panics
235    ///
236    /// Propagates panics from the configured reporter.
237    #[inline]
238    pub fn report_canceled(&self, counters: ProgressCounters) {
239        self.report(ProgressPhase::Canceled, counters);
240    }
241
242    /// Reports a lifecycle event with the run's current elapsed duration.
243    ///
244    /// # Parameters
245    ///
246    /// * `phase` - Lifecycle phase to report.
247    /// * `counters` - Counters carried by the event.
248    ///
249    /// # Panics
250    ///
251    /// Propagates panics from the configured reporter.
252    #[inline]
253    pub fn report(&self, phase: ProgressPhase, counters: ProgressCounters) {
254        self.report_with_elapsed(phase, counters, self.elapsed());
255    }
256
257    /// Reports a lifecycle event with an explicit elapsed duration.
258    ///
259    /// # Parameters
260    ///
261    /// * `phase` - Lifecycle phase to report.
262    /// * `counters` - Counters carried by the event.
263    /// * `elapsed` - Elapsed duration carried by the event.
264    ///
265    /// # Panics
266    ///
267    /// Propagates panics from the configured reporter.
268    pub fn report_with_elapsed(
269        &self,
270        phase: ProgressPhase,
271        counters: ProgressCounters,
272        elapsed: Duration,
273    ) {
274        let event = self.event_with_elapsed(phase, counters, elapsed);
275        self.reporter.report(&event);
276    }
277
278    /// Returns the elapsed duration since this run started.
279    ///
280    /// # Returns
281    ///
282    /// The monotonic elapsed duration for this progress run.
283    #[inline]
284    pub fn elapsed(&self) -> Duration {
285        self.started_at.elapsed()
286    }
287
288    /// Returns the start instant for this run.
289    ///
290    /// # Returns
291    ///
292    /// The monotonic instant used as this run's start time.
293    #[inline]
294    pub const fn started_at(&self) -> Instant {
295        self.started_at
296    }
297
298    /// Returns the configured running-event interval.
299    ///
300    /// # Returns
301    ///
302    /// The minimum delay between due-based running events.
303    #[inline]
304    pub const fn report_interval(&self) -> Duration {
305        self.report_interval
306    }
307
308    /// Returns the optional stage metadata attached to events.
309    ///
310    /// # Returns
311    ///
312    /// `Some(stage)` when stage metadata is configured, otherwise `None`.
313    #[inline]
314    pub const fn stage(&self) -> Option<&ProgressStage> {
315        self.stage.as_ref()
316    }
317
318    /// Builds a progress event with optional stage metadata.
319    ///
320    /// # Parameters
321    ///
322    /// * `phase` - Lifecycle phase for the event.
323    /// * `counters` - Counters carried by the event.
324    /// * `elapsed` - Elapsed duration carried by the event.
325    ///
326    /// # Returns
327    ///
328    /// A progress event ready to be sent to the reporter.
329    fn event_with_elapsed(
330        &self,
331        phase: ProgressPhase,
332        counters: ProgressCounters,
333        elapsed: Duration,
334    ) -> ProgressEvent {
335        let event = ProgressEvent::from_phase(phase, counters, elapsed);
336        match self.stage.clone() {
337            Some(stage) => event.with_stage(stage),
338            None => event,
339        }
340    }
341}
342
343/// Computes the next reporting instant while avoiding overflow panics.
344///
345/// # Parameters
346///
347/// * `base` - Base instant for the deadline.
348/// * `interval` - Duration added to `base`.
349///
350/// # Returns
351///
352/// `base + interval`, or `base` when the addition overflows.
353fn next_instant(base: Instant, interval: Duration) -> Instant {
354    base.checked_add(interval).unwrap_or(base)
355}