Skip to main content

appcore_filemaker/
control.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: control.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/30 05:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/30 05:00:00 by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13
14use serde::{Deserialize, Serialize};
15
16use crate::{ErrorCode, FileMakerError, Result};
17
18/// Stable operation phase reported at cooperative boundaries.
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum ProgressPhase {
22    /// Parsing and expanding a template.
23    Compile,
24    /// Binding typed data and patches.
25    Bind,
26    /// Expanding bound and repeated element instances.
27    BindElements,
28    /// Measuring and resolving geometry.
29    Layout,
30    /// Collision/reflow iteration.
31    Reflow,
32    /// Preflight inspection.
33    Preflight,
34    /// Encoding output.
35    Export,
36}
37
38/// Bounded progress notification.
39#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
40pub struct ProgressEvent {
41    /// Current phase.
42    pub phase: ProgressPhase,
43    /// Completed deterministic work units.
44    pub completed: u64,
45    /// Known upper bound, when meaningful.
46    pub total: Option<u64>,
47}
48
49/// Observer invoked synchronously; implementations must return promptly.
50pub trait ProgressObserver: Send + Sync {
51    /// Receives one progress event without influencing compiler behavior.
52    fn report(&self, event: &ProgressEvent);
53}
54
55/// Cheap cloneable cooperative cancellation flag.
56#[derive(Clone, Debug, Default)]
57pub struct CancellationToken(Arc<AtomicBool>);
58
59impl CancellationToken {
60    /// Requests cancellation for every clone of this token.
61    pub fn cancel(&self) {
62        self.0.store(true, Ordering::Release);
63    }
64
65    /// Returns whether cancellation has been requested.
66    #[must_use]
67    pub fn is_cancelled(&self) -> bool {
68        self.0.load(Ordering::Acquire)
69    }
70
71    /// Converts a cancellation request into the stable controlled error.
72    pub fn check(&self) -> Result<()> {
73        if self.is_cancelled() {
74            return Err(FileMakerError::new(
75                ErrorCode::Cancelled,
76                "operation cancelled at a cooperative boundary",
77            ));
78        }
79        Ok(())
80    }
81}
82
83/// Cancellation and progress controls shared by one operation pipeline.
84#[derive(Clone, Default)]
85pub struct OperationControl {
86    cancellation: CancellationToken,
87    observer: Option<Arc<dyn ProgressObserver>>,
88}
89
90impl OperationControl {
91    /// Creates controls around the supplied cancellation token.
92    #[must_use]
93    pub fn new(cancellation: CancellationToken) -> Self {
94        Self {
95            cancellation,
96            observer: None,
97        }
98    }
99
100    /// Installs a synchronous progress observer.
101    #[must_use]
102    pub fn with_observer(mut self, observer: Arc<dyn ProgressObserver>) -> Self {
103        self.observer = Some(observer);
104        self
105    }
106
107    /// Returns the shared cancellation token.
108    #[must_use]
109    pub fn cancellation(&self) -> &CancellationToken {
110        &self.cancellation
111    }
112
113    /// Checks cancellation, then emits a bounded progress event.
114    pub fn checkpoint(
115        &self,
116        phase: ProgressPhase,
117        completed: u64,
118        total: Option<u64>,
119    ) -> Result<()> {
120        self.cancellation.check()?;
121        if let Some(observer) = &self.observer {
122            observer.report(&ProgressEvent {
123                phase,
124                completed,
125                total,
126            });
127            self.cancellation.check()?;
128        }
129        Ok(())
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn cancellation_is_shared_and_controlled() {
139        let token = CancellationToken::default();
140        let clone = token.clone();
141        token.cancel();
142        assert_eq!(clone.check().unwrap_err().code(), ErrorCode::Cancelled);
143    }
144}