1use std::fmt;
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
8
9use crate::{ActionInput, MessageKind, Object};
10
11pub type JobWork = Arc<dyn Fn(&JobContext) -> Result<(), String> + Send + Sync>;
14
15pub type JobWaker = Arc<dyn Fn() + Send + Sync>;
17
18#[derive(Clone, Debug, PartialEq)]
20#[non_exhaustive]
21#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
22pub enum JobChange {
23 Save(Object),
25 Delete(Object),
27 Tell(String, MessageKind),
29}
30
31#[derive(Default)]
32struct Shared {
33 progress: Mutex<(Option<f32>, String)>,
35 cancel: AtomicBool,
36 changes: Mutex<Vec<JobChange>>,
37 end: Mutex<Option<Result<(), String>>>,
39 waker: Mutex<Option<JobWaker>>,
40}
41
42fn locked<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
43 mutex.lock().unwrap_or_else(PoisonError::into_inner)
45}
46
47#[cfg_attr(feature = "live", ravenrs_live::expose)]
48impl Shared {
49 fn wake(&self) {
50 let waker = locked(&self.waker).clone();
51 if let Some(waker) = waker {
52 waker();
53 }
54 }
55}
56
57#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
77pub struct JobContext {
78 selected: Vec<Object>,
79 input: ActionInput,
80 shared: Arc<Shared>,
81}
82
83impl fmt::Debug for JobContext {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.debug_struct("JobContext").field("selected", &self.selected.len()).finish_non_exhaustive()
86 }
87}
88
89#[cfg_attr(feature = "live", ravenrs_live::expose)]
90impl JobContext {
91 pub fn selected(&self) -> &[Object] {
93 &self.selected
94 }
95
96 pub fn input(&self) -> &ActionInput {
98 &self.input
99 }
100
101 pub fn progress(&self, done: f32, doing: impl Into<String>) {
103 *locked(&self.shared.progress) = (Some(done.clamp(0.0, 1.0)), doing.into());
104 self.shared.wake();
105 }
106
107 pub fn doing(&self, doing: impl Into<String>) {
109 *locked(&self.shared.progress) = (None, doing.into());
110 self.shared.wake();
111 }
112
113 pub fn cancelled(&self) -> bool {
115 self.shared.cancel.load(Ordering::Relaxed)
116 }
117
118 pub fn save(&self, record: Object) {
120 self.hand(JobChange::Save(record));
121 }
122
123 pub fn delete(&self, record: Object) {
125 self.hand(JobChange::Delete(record));
126 }
127
128 pub fn tell(&self, text: impl Into<String>, kind: MessageKind) {
130 self.hand(JobChange::Tell(text.into(), kind));
131 }
132
133 fn hand(&self, change: JobChange) {
134 locked(&self.shared.changes).push(change);
135 self.shared.wake();
136 }
137}
138
139static NEXT_JOB: AtomicU64 = AtomicU64::new(1);
140
141#[derive(Clone)]
143#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
144pub struct Job {
145 id: u64,
146 action: String,
147 caption: String,
148 shared: Arc<Shared>,
149}
150
151impl fmt::Debug for Job {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 f.debug_struct("Job").field("id", &self.id).field("action", &self.action).finish_non_exhaustive()
154 }
155}
156
157#[cfg_attr(feature = "live", ravenrs_live::expose)]
158impl Job {
159 pub(crate) fn start(
162 action: &str,
163 caption: &str,
164 work: JobWork,
165 selected: Vec<Object>,
166 input: ActionInput,
167 waker: Option<JobWaker>,
168 ) -> Self {
169 let shared = Arc::new(Shared { waker: Mutex::new(waker), ..Shared::default() });
170 let job = Self {
171 id: NEXT_JOB.fetch_add(1, Ordering::Relaxed),
172 action: action.to_owned(),
173 caption: caption.to_owned(),
174 shared: shared.clone(),
175 };
176 let cx = JobContext { selected, input, shared };
177 let run = move || {
178 struct Ending(Arc<Shared>, Option<Result<(), String>>);
180 impl Drop for Ending {
181 fn drop(&mut self) {
182 let end =
183 self.1.take().unwrap_or_else(|| Err("the work stopped unexpectedly".to_owned()));
184 *locked(&self.0.end) = Some(end);
185 self.0.wake();
186 }
187 }
188 let mut ending = Ending(cx.shared.clone(), None);
189 ending.1 = Some(work(&cx));
190 };
191 #[cfg(not(target_arch = "wasm32"))]
192 {
193 let name = format!("job {}", job.action);
194 if let Err(error) = std::thread::Builder::new().name(name).spawn(run) {
195 *locked(&job.shared.end) = Some(Err(error.to_string()));
196 }
197 }
198 #[cfg(target_arch = "wasm32")]
199 run();
200 job
201 }
202
203 pub fn id(&self) -> u64 {
205 self.id
206 }
207
208 pub fn action(&self) -> &str {
210 &self.action
211 }
212
213 pub fn caption(&self) -> &str {
215 &self.caption
216 }
217
218 pub fn progress(&self) -> (Option<f32>, String) {
220 locked(&self.shared.progress).clone()
221 }
222
223 pub fn cancel(&self) {
225 self.shared.cancel.store(true, Ordering::Relaxed);
226 }
227
228 pub fn is_cancelled(&self) -> bool {
230 self.shared.cancel.load(Ordering::Relaxed)
231 }
232
233 pub(crate) fn set_waker(&self, waker: Option<JobWaker>) {
235 *locked(&self.shared.waker) = waker;
236 }
237
238 pub(crate) fn take_changes(&self) -> Vec<JobChange> {
240 std::mem::take(&mut *locked(&self.shared.changes))
241 }
242
243 pub(crate) fn take_end(&self) -> Option<Result<(), String>> {
245 let end = locked(&self.shared.end).take()?;
246 if !locked(&self.shared.changes).is_empty() {
248 *locked(&self.shared.end) = Some(end);
249 return None;
250 }
251 Some(end)
252 }
253}