1use serde::{Deserialize, Serialize};
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::Arc;
12
13#[derive(Clone, Debug, Default)]
15pub struct CancelToken {
16 flag: Arc<AtomicBool>,
17}
18
19impl CancelToken {
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 pub fn cancel(&self) {
26 self.flag.store(true, Ordering::SeqCst);
27 }
28
29 pub fn is_cancelled(&self) -> bool {
30 self.flag.load(Ordering::SeqCst)
31 }
32
33 pub fn check(&self) -> Result<(), Cancelled> {
35 if self.is_cancelled() {
36 Err(Cancelled)
37 } else {
38 Ok(())
39 }
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct Cancelled;
47
48impl std::fmt::Display for Cancelled {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(f, "cancelled")
51 }
52}
53
54impl std::error::Error for Cancelled {}
55
56#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct Progress {
60 pub phase: String,
62 pub message: String,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub detail: Option<String>,
67 pub current: u64,
69 pub total: u64,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub operation_id: Option<String>,
74}
75
76pub trait ProgressSink {
78 fn progress(&mut self, progress: &Progress);
79}
80
81#[derive(Clone, Copy, Debug, Default)]
83pub struct NullSink;
84
85impl ProgressSink for NullSink {
86 fn progress(&mut self, _progress: &Progress) {}
87}
88
89#[derive(Clone, Debug, Default)]
91pub struct RecordingSink {
92 pub events: Vec<Progress>,
93}
94
95impl ProgressSink for RecordingSink {
96 fn progress(&mut self, progress: &Progress) {
97 self.events.push(progress.clone());
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct WorkBudget {
108 chunk: u64,
109 used: u64,
110 next_boundary: u64,
111}
112
113impl Default for WorkBudget {
114 fn default() -> Self {
115 Self::new(Self::DEFAULT_CHUNK)
116 }
117}
118
119impl WorkBudget {
120 pub const DEFAULT_CHUNK: u64 = 1024;
121
122 pub fn new(chunk: u64) -> Self {
124 let chunk = chunk.max(1);
125 Self { chunk, used: 0, next_boundary: chunk }
126 }
127
128 pub fn chunk(&self) -> u64 {
129 self.chunk
130 }
131
132 pub fn used(&self) -> u64 {
134 self.used
135 }
136
137 pub fn step(&mut self, units: u64) -> bool {
140 self.used = self.used.saturating_add(units);
141 if self.used >= self.next_boundary {
142 let crossed = (self.used - self.next_boundary) / self.chunk + 1;
143 self.next_boundary = self.next_boundary.saturating_add(crossed * self.chunk);
144 true
145 } else {
146 false
147 }
148 }
149}
150
151pub struct Runner<'a> {
154 token: CancelToken,
155 sink: &'a mut dyn ProgressSink,
156 budget: WorkBudget,
157 operation_id: Option<String>,
158}
159
160impl<'a> Runner<'a> {
161 pub fn new(token: CancelToken, sink: &'a mut dyn ProgressSink, budget: WorkBudget) -> Self {
162 Self { token, sink, budget, operation_id: None }
163 }
164
165 pub fn detached(sink: &'a mut dyn ProgressSink) -> Self {
167 Self::new(CancelToken::new(), sink, WorkBudget::default())
168 }
169
170 pub fn with_operation(mut self, operation_id: impl Into<String>) -> Self {
171 self.operation_id = Some(operation_id.into());
172 self
173 }
174
175 pub fn token(&self) -> &CancelToken {
176 &self.token
177 }
178
179 pub fn budget(&self) -> &WorkBudget {
180 &self.budget
181 }
182
183 pub fn stage(&mut self, phase: &str, message: &str, current: u64, total: u64) -> Result<(), Cancelled> {
185 self.publish(phase, message, None, current, total);
186 self.token.check()
187 }
188
189 pub fn stage_detail(
191 &mut self,
192 phase: &str,
193 message: &str,
194 detail: &str,
195 current: u64,
196 total: u64,
197 ) -> Result<(), Cancelled> {
198 self.publish(phase, message, Some(detail.to_string()), current, total);
199 self.token.check()
200 }
201
202 pub fn work(&mut self, units: u64, phase: &str, current: u64, total: u64) -> Result<(), Cancelled> {
205 if self.budget.step(units) {
206 self.publish(phase, phase, None, current, total);
207 self.token.check()?;
208 }
209 Ok(())
210 }
211
212 fn publish(&mut self, phase: &str, message: &str, detail: Option<String>, current: u64, total: u64) {
213 let progress = Progress {
214 phase: phase.to_string(),
215 message: message.to_string(),
216 detail,
217 current,
218 total,
219 operation_id: self.operation_id.clone(),
220 };
221 self.sink.progress(&progress);
222 }
223}
224