Skip to main content

qubit_progress/runtime/
progress_run.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/// `ProgressRun` 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///     NoOpProgressReporter,
39///     ProgressCounters,
40///     ProgressRun,
41/// };
42///
43/// let reporter = NoOpProgressReporter;
44/// let mut progress = ProgressRun::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 ProgressRun<'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> ProgressRun<'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    /// # Panics
179    ///
180    /// Propagates panics from the configured reporter when an event is due.
181    pub fn report_running_if_due(&mut self, counters: ProgressCounters) -> bool {
182        let now = Instant::now();
183        if now < self.next_running_at {
184            return false;
185        }
186        self.report_with_elapsed(
187            ProgressPhase::Running,
188            counters,
189            now.saturating_duration_since(self.started_at),
190        );
191        self.next_running_at = next_instant(now, self.report_interval);
192        true
193    }
194
195    /// Reports a finished lifecycle event.
196    ///
197    /// # Parameters
198    ///
199    /// * `counters` - Final counters for a successfully completed operation.
200    ///
201    /// # Panics
202    ///
203    /// Propagates panics from the configured reporter.
204    #[inline]
205    pub fn report_finished(&self, counters: ProgressCounters) {
206        self.report(ProgressPhase::Finished, counters);
207    }
208
209    /// Reports a failed lifecycle event.
210    ///
211    /// # Parameters
212    ///
213    /// * `counters` - Final or current counters for a failed operation.
214    ///
215    /// # Panics
216    ///
217    /// Propagates panics from the configured reporter.
218    #[inline]
219    pub fn report_failed(&self, counters: ProgressCounters) {
220        self.report(ProgressPhase::Failed, counters);
221    }
222
223    /// Reports a canceled lifecycle event.
224    ///
225    /// # Parameters
226    ///
227    /// * `counters` - Final or current counters for a canceled operation.
228    ///
229    /// # Panics
230    ///
231    /// Propagates panics from the configured reporter.
232    #[inline]
233    pub fn report_canceled(&self, counters: ProgressCounters) {
234        self.report(ProgressPhase::Canceled, counters);
235    }
236
237    /// Reports a lifecycle event with the run's current elapsed duration.
238    ///
239    /// # Parameters
240    ///
241    /// * `phase` - Lifecycle phase to report.
242    /// * `counters` - Counters carried by the event.
243    ///
244    /// # Panics
245    ///
246    /// Propagates panics from the configured reporter.
247    #[inline]
248    pub fn report(&self, phase: ProgressPhase, counters: ProgressCounters) {
249        self.report_with_elapsed(phase, counters, self.elapsed());
250    }
251
252    /// Reports a lifecycle event with an explicit elapsed duration.
253    ///
254    /// # Parameters
255    ///
256    /// * `phase` - Lifecycle phase to report.
257    /// * `counters` - Counters carried by the event.
258    /// * `elapsed` - Elapsed duration carried by the event.
259    ///
260    /// # Panics
261    ///
262    /// Propagates panics from the configured reporter.
263    pub fn report_with_elapsed(
264        &self,
265        phase: ProgressPhase,
266        counters: ProgressCounters,
267        elapsed: Duration,
268    ) {
269        let event = self.event_with_elapsed(phase, counters, elapsed);
270        self.reporter.report(&event);
271    }
272
273    /// Returns the elapsed duration since this run started.
274    ///
275    /// # Returns
276    ///
277    /// The monotonic elapsed duration for this progress run.
278    #[inline]
279    pub fn elapsed(&self) -> Duration {
280        self.started_at.elapsed()
281    }
282
283    /// Returns the start instant for this run.
284    ///
285    /// # Returns
286    ///
287    /// The monotonic instant used as this run's start time.
288    #[inline]
289    pub const fn started_at(&self) -> Instant {
290        self.started_at
291    }
292
293    /// Returns the configured running-event interval.
294    ///
295    /// # Returns
296    ///
297    /// The minimum delay between due-based running events.
298    #[inline]
299    pub const fn report_interval(&self) -> Duration {
300        self.report_interval
301    }
302
303    /// Returns the optional stage metadata attached to events.
304    ///
305    /// # Returns
306    ///
307    /// `Some(stage)` when stage metadata is configured, otherwise `None`.
308    #[inline]
309    pub const fn stage(&self) -> Option<&ProgressStage> {
310        self.stage.as_ref()
311    }
312
313    /// Builds a progress event with optional stage metadata.
314    ///
315    /// # Parameters
316    ///
317    /// * `phase` - Lifecycle phase for the event.
318    /// * `counters` - Counters carried by the event.
319    /// * `elapsed` - Elapsed duration carried by the event.
320    ///
321    /// # Returns
322    ///
323    /// A progress event ready to be sent to the reporter.
324    fn event_with_elapsed(
325        &self,
326        phase: ProgressPhase,
327        counters: ProgressCounters,
328        elapsed: Duration,
329    ) -> ProgressEvent {
330        let event = ProgressEvent::from_phase(phase, counters, elapsed);
331        match self.stage.clone() {
332            Some(stage) => event.with_stage(stage),
333            None => event,
334        }
335    }
336}
337
338/// Computes the next reporting instant while avoiding overflow panics.
339///
340/// # Parameters
341///
342/// * `base` - Base instant for the deadline.
343/// * `interval` - Duration added to `base`.
344///
345/// # Returns
346///
347/// `base + interval`, or `base` when the addition overflows.
348fn next_instant(base: Instant, interval: Duration) -> Instant {
349    base.checked_add(interval).unwrap_or(base)
350}