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.
31pub struct ProgressRun<'a> {
32    /// Reporter receiving lifecycle callbacks for this run.
33    reporter: &'a dyn ProgressReporter,
34    /// Monotonic start time used to compute elapsed durations.
35    started_at: Instant,
36    /// Minimum interval between due-based running callbacks.
37    report_interval: Duration,
38    /// Next monotonic instant at which a due-based running callback may fire.
39    next_running_at: Instant,
40    /// Optional stage metadata attached to every event emitted by this run.
41    stage: Option<ProgressStage>,
42}
43
44impl<'a> ProgressRun<'a> {
45    /// Creates a progress run starting at the current instant.
46    ///
47    /// # Parameters
48    ///
49    /// * `reporter` - Reporter receiving progress events.
50    /// * `report_interval` - Minimum delay between due-based running events.
51    ///
52    /// # Returns
53    ///
54    /// A progress run whose elapsed time is measured from now.
55    #[inline]
56    pub fn new(reporter: &'a dyn ProgressReporter, report_interval: Duration) -> Self {
57        Self::from_start(reporter, report_interval, Instant::now())
58    }
59
60    /// Creates a progress run from an explicit start instant.
61    ///
62    /// # Parameters
63    ///
64    /// * `reporter` - Reporter receiving progress events.
65    /// * `report_interval` - Minimum delay between due-based running events.
66    /// * `started_at` - Monotonic instant representing operation start.
67    ///
68    /// # Returns
69    ///
70    /// A progress run using `started_at` for elapsed-time calculations.
71    #[inline]
72    pub fn from_start(
73        reporter: &'a dyn ProgressReporter,
74        report_interval: Duration,
75        started_at: Instant,
76    ) -> Self {
77        Self {
78            reporter,
79            started_at,
80            report_interval,
81            next_running_at: next_instant(started_at, report_interval),
82            stage: None,
83        }
84    }
85
86    /// Returns a copy configured with stage metadata.
87    ///
88    /// # Parameters
89    ///
90    /// * `stage` - Stage metadata attached to subsequently reported events.
91    ///
92    /// # Returns
93    ///
94    /// This progress run with `stage` recorded.
95    #[inline]
96    pub fn with_stage(mut self, stage: ProgressStage) -> Self {
97        self.stage = Some(stage);
98        self
99    }
100
101    /// Returns a copy with stage metadata removed.
102    ///
103    /// # Returns
104    ///
105    /// This progress run without stage metadata.
106    #[inline]
107    pub fn without_stage(mut self) -> Self {
108        self.stage = None;
109        self
110    }
111
112    /// Reports a started lifecycle event.
113    ///
114    /// # Parameters
115    ///
116    /// * `counters` - Initial counters for the operation.
117    ///
118    /// # Panics
119    ///
120    /// Propagates panics from the configured reporter.
121    #[inline]
122    pub fn report_started(&self, counters: ProgressCounters) {
123        self.report(ProgressPhase::Started, counters);
124    }
125
126    /// Reports a running lifecycle event immediately.
127    ///
128    /// # Parameters
129    ///
130    /// * `counters` - Current counters for the operation.
131    ///
132    /// # Panics
133    ///
134    /// Propagates panics from the configured reporter.
135    #[inline]
136    pub fn report_running(&self, counters: ProgressCounters) {
137        self.report(ProgressPhase::Running, counters);
138    }
139
140    /// Reports a running lifecycle event if the configured interval has passed.
141    ///
142    /// # Parameters
143    ///
144    /// * `counters` - Current counters for the operation.
145    ///
146    /// # Returns
147    ///
148    /// `true` when a running event was emitted, or `false` when the next
149    /// running-event deadline has not been reached.
150    ///
151    /// # Panics
152    ///
153    /// Propagates panics from the configured reporter when an event is due.
154    pub fn report_running_if_due(&mut self, counters: ProgressCounters) -> bool {
155        let now = Instant::now();
156        if now < self.next_running_at {
157            return false;
158        }
159        self.report_with_elapsed(
160            ProgressPhase::Running,
161            counters,
162            now.saturating_duration_since(self.started_at),
163        );
164        self.next_running_at = next_instant(now, self.report_interval);
165        true
166    }
167
168    /// Reports a finished lifecycle event.
169    ///
170    /// # Parameters
171    ///
172    /// * `counters` - Final counters for a successfully completed operation.
173    ///
174    /// # Panics
175    ///
176    /// Propagates panics from the configured reporter.
177    #[inline]
178    pub fn report_finished(&self, counters: ProgressCounters) {
179        self.report(ProgressPhase::Finished, counters);
180    }
181
182    /// Reports a failed lifecycle event.
183    ///
184    /// # Parameters
185    ///
186    /// * `counters` - Final or current counters for a failed operation.
187    ///
188    /// # Panics
189    ///
190    /// Propagates panics from the configured reporter.
191    #[inline]
192    pub fn report_failed(&self, counters: ProgressCounters) {
193        self.report(ProgressPhase::Failed, counters);
194    }
195
196    /// Reports a canceled lifecycle event.
197    ///
198    /// # Parameters
199    ///
200    /// * `counters` - Final or current counters for a canceled operation.
201    ///
202    /// # Panics
203    ///
204    /// Propagates panics from the configured reporter.
205    #[inline]
206    pub fn report_canceled(&self, counters: ProgressCounters) {
207        self.report(ProgressPhase::Canceled, counters);
208    }
209
210    /// Reports a lifecycle event with the run's current elapsed duration.
211    ///
212    /// # Parameters
213    ///
214    /// * `phase` - Lifecycle phase to report.
215    /// * `counters` - Counters carried by the event.
216    ///
217    /// # Panics
218    ///
219    /// Propagates panics from the configured reporter.
220    #[inline]
221    pub fn report(&self, phase: ProgressPhase, counters: ProgressCounters) {
222        self.report_with_elapsed(phase, counters, self.elapsed());
223    }
224
225    /// Reports a lifecycle event with an explicit elapsed duration.
226    ///
227    /// # Parameters
228    ///
229    /// * `phase` - Lifecycle phase to report.
230    /// * `counters` - Counters carried by the event.
231    /// * `elapsed` - Elapsed duration carried by the event.
232    ///
233    /// # Panics
234    ///
235    /// Propagates panics from the configured reporter.
236    pub fn report_with_elapsed(
237        &self,
238        phase: ProgressPhase,
239        counters: ProgressCounters,
240        elapsed: Duration,
241    ) {
242        let event = self.event_with_elapsed(phase, counters, elapsed);
243        self.reporter.report(&event);
244    }
245
246    /// Returns the elapsed duration since this run started.
247    ///
248    /// # Returns
249    ///
250    /// The monotonic elapsed duration for this progress run.
251    #[inline]
252    pub fn elapsed(&self) -> Duration {
253        self.started_at.elapsed()
254    }
255
256    /// Returns the start instant for this run.
257    ///
258    /// # Returns
259    ///
260    /// The monotonic instant used as this run's start time.
261    #[inline]
262    pub const fn started_at(&self) -> Instant {
263        self.started_at
264    }
265
266    /// Returns the configured running-event interval.
267    ///
268    /// # Returns
269    ///
270    /// The minimum delay between due-based running events.
271    #[inline]
272    pub const fn report_interval(&self) -> Duration {
273        self.report_interval
274    }
275
276    /// Returns the optional stage metadata attached to events.
277    ///
278    /// # Returns
279    ///
280    /// `Some(stage)` when stage metadata is configured, otherwise `None`.
281    #[inline]
282    pub const fn stage(&self) -> Option<&ProgressStage> {
283        self.stage.as_ref()
284    }
285
286    /// Builds a progress event with optional stage metadata.
287    ///
288    /// # Parameters
289    ///
290    /// * `phase` - Lifecycle phase for the event.
291    /// * `counters` - Counters carried by the event.
292    /// * `elapsed` - Elapsed duration carried by the event.
293    ///
294    /// # Returns
295    ///
296    /// A progress event ready to be sent to the reporter.
297    fn event_with_elapsed(
298        &self,
299        phase: ProgressPhase,
300        counters: ProgressCounters,
301        elapsed: Duration,
302    ) -> ProgressEvent {
303        let event = ProgressEvent::from_phase(phase, counters, elapsed);
304        match self.stage.clone() {
305            Some(stage) => event.with_stage(stage),
306            None => event,
307        }
308    }
309}
310
311/// Computes the next reporting instant while avoiding overflow panics.
312///
313/// # Parameters
314///
315/// * `base` - Base instant for the deadline.
316/// * `interval` - Duration added to `base`.
317///
318/// # Returns
319///
320/// `base + interval`, or `base` when the addition overflows.
321fn next_instant(base: Instant, interval: Duration) -> Instant {
322    base.checked_add(interval).unwrap_or(base)
323}