1#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
13
14mod pool;
15
16use std::any::Any;
17use std::collections::VecDeque;
18use std::fmt;
19use std::panic::{AssertUnwindSafe, catch_unwind};
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
22use std::time::Duration;
23
24use serde::{Deserialize, Serialize};
25use serde_json::{Value, json};
26
27pub type Apply<S> = Box<dyn FnOnce(&mut S) -> Result<Value, String> + Send>;
29
30pub fn apply<S>(f: impl FnOnce(&mut S) -> Result<Value, String> + Send + 'static) -> Apply<S> {
32 Box::new(f)
33}
34
35pub type Wake = Arc<dyn Fn() + Send + Sync>;
38
39const HISTORY: usize = 50;
41const WAKE_STEP: f64 = 0.005;
43const MAX_MESSAGE: usize = 512;
45
46fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
47 m.lock().unwrap_or_else(PoisonError::into_inner)
48}
49
50mod clock {
52 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
53 pub type Instant = std::time::Instant;
54 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
55 pub fn now() -> Option<Instant> {
56 Some(Instant::now())
57 }
58
59 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
60 #[derive(Clone, Copy, Debug)]
61 pub struct Instant;
62 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
63 impl Instant {
64 pub fn elapsed(&self) -> std::time::Duration {
65 std::time::Duration::ZERO
66 }
67 }
68 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
69 pub fn now() -> Option<Instant> {
70 None
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
76#[serde(transparent)]
77pub struct JobId(pub u64);
78
79impl fmt::Display for JobId {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "{}", self.0)
82 }
83}
84
85#[derive(Debug, Clone, Default)]
87pub struct CancelToken(Arc<AtomicBool>);
88
89impl CancelToken {
90 pub fn cancel(&self) {
91 self.0.store(true, Ordering::Relaxed);
92 }
93 pub fn is_cancelled(&self) -> bool {
94 self.0.load(Ordering::Relaxed)
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum Status {
102 Queued,
104 Running,
105 Ready,
107 Done,
109 Failed,
110 Cancelled,
111}
112
113impl Status {
114 pub fn is_finished(self) -> bool {
115 matches!(self, Status::Done | Status::Failed | Status::Cancelled)
116 }
117}
118
119struct State {
120 status: Status,
121 fraction: Option<f64>,
122 message: String,
123 started: Option<clock::Instant>,
124 woken_fraction: f64,
125 update_error: Option<String>,
127}
128
129struct Shared {
130 cancel: CancelToken,
131 state: Mutex<State>,
132 wake: Arc<Mutex<Option<Wake>>>,
133}
134
135impl Shared {
136 fn wake(&self) {
137 let w = lock(&self.wake).clone();
138 if let Some(w) = w {
139 let _ = catch_unwind(AssertUnwindSafe(|| w()));
141 }
142 }
143
144 fn begin(&self) -> bool {
146 {
147 let mut s = lock(&self.state);
148 if s.status != Status::Queued {
149 return false;
150 }
151 s.status = Status::Running;
152 s.started = clock::now();
153 }
154 self.wake();
155 true
156 }
157
158 fn set_status(&self, status: Status) {
159 lock(&self.state).status = status;
160 }
161
162 fn status(&self) -> Status {
163 lock(&self.state).status
164 }
165}
166
167pub struct Progress {
169 shared: Arc<Shared>,
170}
171
172impl Progress {
173 pub fn set(&self, fraction: f64) -> bool {
175 self.update(Some(fraction), None)
176 }
177
178 pub fn steps(&self, done: u64, total: u64) -> bool {
180 let f = if total == 0 { 0.0 } else { done as f64 / total as f64 };
181 self.update(Some(f), None)
182 }
183
184 pub fn message(&self, message: impl Into<String>) {
186 self.update(None, Some(message.into()));
187 }
188
189 pub fn report(&self, fraction: f64, message: impl Into<String>) -> bool {
191 self.update(Some(fraction), Some(message.into()))
192 }
193
194 pub fn is_cancelled(&self) -> bool {
195 self.shared.cancel.is_cancelled()
196 }
197
198 pub fn token(&self) -> CancelToken {
200 self.shared.cancel.clone()
201 }
202
203 fn update(&self, fraction: Option<f64>, message: Option<String>) -> bool {
204 let wake = {
205 let mut s = lock(&self.shared.state);
206 let mut wake = false;
207 if let Some(f) = fraction.filter(|f| !f.is_nan()) {
208 let f = f.clamp(0.0, 1.0);
209 s.fraction = Some(f);
210 if (f - s.woken_fraction).abs() >= WAKE_STEP || f >= 1.0 {
211 s.woken_fraction = f;
212 wake = true;
213 }
214 }
215 if let Some(mut m) = message {
216 if let Some((i, _)) = m.char_indices().nth(MAX_MESSAGE) {
217 m.truncate(i);
218 }
219 if m != s.message {
220 s.message = m;
221 wake = true;
222 }
223 }
224 wake
225 };
226 if wake {
227 self.shared.wake();
228 }
229 !self.is_cancelled()
230 }
231}
232
233pub enum Step<S> {
235 Continue,
237 Done(Apply<S>),
238 Failed(String),
239 Wait,
242}
243
244enum Outcome<S> {
245 Apply(Apply<S>),
246 Failed(String),
247 Cancelled,
248 Update(Apply<S>),
250}
251
252struct Inbox<S> {
253 items: Mutex<VecDeque<(JobId, Outcome<S>)>>,
254 cv: Condvar,
255}
256
257impl<S> Inbox<S> {
258 fn push(&self, id: JobId, outcome: Outcome<S>) {
259 lock(&self.items).push_back((id, outcome));
260 self.cv.notify_all();
261 }
262
263 fn finish(&self, id: JobId, outcome: Outcome<S>, shared: &Shared) {
267 let mut items = lock(&self.items);
268 items.push_back((id, outcome));
269 shared.set_status(Status::Ready);
270 drop(items);
271 self.cv.notify_all();
272 }
273}
274
275struct Entry {
276 id: JobId,
277 kind: &'static str,
278 label: String,
279 shared: Arc<Shared>,
280}
281
282type StepFn<S> = Box<dyn FnMut(&Progress) -> Step<S> + Send>;
283
284struct SteppedEntry<S> {
285 id: JobId,
286 shared: Arc<Shared>,
287 step: StepFn<S>,
288}
289
290pub struct Updates<S> {
296 id: JobId,
297 inbox: Arc<Inbox<S>>,
298 shared: Arc<Shared>,
299}
300
301impl<S> Clone for Updates<S> {
302 fn clone(&self) -> Self {
303 Self { id: self.id, inbox: self.inbox.clone(), shared: self.shared.clone() }
304 }
305}
306
307impl<S> Updates<S> {
308 pub fn send(&self, apply: Apply<S>) -> bool {
312 self.inbox.push(self.id, Outcome::Update(apply));
313 self.shared.wake();
314 !self.shared.cancel.is_cancelled()
315 }
316}
317
318pub struct JobBuilder<'a, S: 'static> {
320 jobs: &'a mut Jobs<S>,
321 kind: &'static str,
322 label: String,
323 here: bool,
324}
325
326impl<S: 'static> JobBuilder<'_, S> {
327 pub fn here(mut self) -> Self {
331 self.here = true;
332 self
333 }
334
335 pub fn spawn<I: Send + 'static>(self, input: I, work: impl FnOnce(I, &Progress) -> Result<Apply<S>, String> + Send + 'static) -> JobId {
337 self.jobs.launch(self.kind, self.label, self.here, input, move |i, p, _| work(i, p))
338 }
339
340 pub fn spawn_streaming<I: Send + 'static>(
342 self,
343 input: I,
344 work: impl FnOnce(I, &Progress, &Updates<S>) -> Result<Apply<S>, String> + Send + 'static,
345 ) -> JobId {
346 self.jobs.launch(self.kind, self.label, self.here, input, work)
347 }
348
349 pub fn spawn_stepped<I: Send + 'static>(self, input: I, step: impl FnMut(&mut I, &Progress) -> Step<S> + Send + 'static) -> JobId {
352 self.jobs.launch_stepped(self.kind, self.label, self.here, input, step)
353 }
354}
355
356fn wait_a_moment() {
358 #[cfg(not(target_arch = "wasm32"))]
359 std::thread::sleep(Duration::from_millis(1));
360 #[cfg(target_arch = "wasm32")]
361 std::hint::spin_loop();
362}
363
364struct ReadyItem<S> {
365 entry: Entry,
366 outcome: Outcome<S>,
367}
368
369#[must_use = "apply it, or finished jobs are lost"]
371pub struct Ready<S> {
372 items: Vec<ReadyItem<S>>,
373}
374
375impl<S> Ready<S> {
376 pub fn is_empty(&self) -> bool {
377 self.items.is_empty()
378 }
379
380 pub fn apply(self, state: &mut S) -> Vec<JobRecord> {
383 let mut out = Vec::new();
384 for ReadyItem { entry, outcome } in self.items {
385 if let Outcome::Update(apply) = outcome {
386 if lock(&entry.shared.state).update_error.is_some() {
388 continue;
389 }
390 let failed = match catch_unwind(AssertUnwindSafe(|| apply(state))) {
391 Ok(Ok(_)) => None,
392 Ok(Err(e)) => Some(e),
393 Err(p) => {
394 let msg = panic_message(p.as_ref());
395 log::error!("applying an update of job `{}` panicked: {msg}", entry.label);
396 Some(format!("internal error while applying the result ({msg})"))
397 }
398 };
399 if let Some(e) = failed {
400 lock(&entry.shared.state).update_error = Some(e);
401 entry.shared.cancel.cancel();
402 }
403 continue;
404 }
405 let cancelled = entry.shared.cancel.is_cancelled();
406 let update_error = lock(&entry.shared.state).update_error.take();
407 let (status, message, result) = match outcome {
408 _ if update_error.is_some() => (Status::Failed, update_error.clone().unwrap_or_default(), Value::Null),
410 _ if cancelled => (Status::Cancelled, String::new(), Value::Null),
411 Outcome::Cancelled | Outcome::Update(_) => (Status::Cancelled, String::new(), Value::Null),
412 Outcome::Failed(e) => (Status::Failed, e, Value::Null),
413 Outcome::Apply(apply) => match catch_unwind(AssertUnwindSafe(|| apply(state))) {
414 Ok(Ok(v)) => (Status::Done, lock(&entry.shared.state).message.clone(), v),
415 Ok(Err(e)) => (Status::Failed, e, Value::Null),
416 Err(p) => {
417 let msg = panic_message(p.as_ref());
418 log::error!("applying job `{}` panicked: {msg}", entry.label);
419 (Status::Failed, format!("internal error while applying the result ({msg})"), Value::Null)
420 }
421 },
422 };
423 entry.shared.set_status(status);
424 let seconds = lock(&entry.shared.state).started.map(|t| t.elapsed().as_secs_f64());
425 out.push(JobRecord { id: entry.id, kind: entry.kind.to_string(), label: entry.label, status, message, result, seconds });
426 }
427 out
428 }
429}
430
431#[derive(Debug, Clone, PartialEq, Serialize)]
433pub struct JobInfo {
434 pub id: JobId,
435 pub kind: String,
436 pub label: String,
437 pub status: Status,
438 pub fraction: Option<f64>,
440 pub message: String,
441 pub elapsed_secs: Option<f64>,
442 pub eta_secs: Option<f64>,
444 pub cancellable: bool,
445}
446
447#[derive(Debug, Clone, PartialEq, Serialize)]
449pub struct JobRecord {
450 pub id: JobId,
451 pub kind: String,
452 pub label: String,
453 pub status: Status,
455 pub message: String,
457 pub result: Value,
459 pub seconds: Option<f64>,
460}
461
462fn panic_message(payload: &(dyn Any + Send)) -> String {
463 payload
464 .downcast_ref::<&str>()
465 .map(|s| (*s).to_string())
466 .or_else(|| payload.downcast_ref::<String>().cloned())
467 .unwrap_or_else(|| "unknown error".into())
468}
469
470pub struct Jobs<S: 'static> {
472 pool: Option<pool::Pool>,
473 entries: Vec<Entry>,
474 stepped: Vec<SteppedEntry<S>>,
475 steps_per_drain: usize,
476 inbox: Arc<Inbox<S>>,
477 history: VecDeque<JobRecord>,
478 next_id: u64,
479 wake: Arc<Mutex<Option<Wake>>>,
480}
481
482impl<S: 'static> Default for Jobs<S> {
483 fn default() -> Self {
484 Self::new()
485 }
486}
487
488impl<S: 'static> Jobs<S> {
489 pub fn new() -> Self {
491 if cfg!(target_arch = "wasm32") {
492 return Self::inline();
493 }
494 let n = std::thread::available_parallelism().map(|n| n.get().saturating_sub(1)).unwrap_or(1).clamp(1, 8);
495 Self::with_workers(n)
496 }
497
498 pub fn with_workers(n: usize) -> Self {
500 Self::build(Some(pool::Pool::new(n)))
501 }
502
503 pub fn inline() -> Self {
506 Self::build(None)
507 }
508
509 fn build(pool: Option<pool::Pool>) -> Self {
510 Self {
511 pool,
512 entries: vec![],
513 stepped: vec![],
514 steps_per_drain: 4,
515 inbox: Arc::new(Inbox { items: Mutex::new(VecDeque::new()), cv: Condvar::new() }),
516 history: VecDeque::new(),
517 next_id: 1,
518 wake: Arc::default(),
519 }
520 }
521
522 pub fn workers(&self) -> usize {
524 self.pool.as_ref().map_or(0, pool::Pool::workers)
525 }
526
527 pub fn set_steps_per_drain(&mut self, n: usize) {
529 self.steps_per_drain = n.max(1);
530 }
531
532 pub fn set_wake(&mut self, wake: impl Fn() + Send + Sync + 'static) {
534 *lock(&self.wake) = Some(Arc::new(wake));
535 }
536
537 fn register(&mut self, kind: &'static str, label: String) -> (JobId, Arc<Shared>) {
538 let id = JobId(self.next_id);
539 self.next_id = self.next_id.saturating_add(1);
540 let shared = Arc::new(Shared {
541 cancel: CancelToken::default(),
542 state: Mutex::new(State {
543 status: Status::Queued,
544 fraction: None,
545 message: String::new(),
546 started: None,
547 woken_fraction: 0.0,
548 update_error: None,
549 }),
550 wake: self.wake.clone(),
551 });
552 self.entries.push(Entry { id, kind, label, shared: shared.clone() });
553 (id, shared)
554 }
555
556 pub fn next_id(&self) -> JobId {
559 JobId(self.next_id)
560 }
561
562 pub fn job(&mut self, kind: &'static str, label: impl Into<String>) -> JobBuilder<'_, S> {
565 JobBuilder { jobs: self, kind, label: label.into(), here: false }
566 }
567
568 pub fn spawn<I: Send + 'static>(
571 &mut self,
572 kind: &'static str,
573 label: impl Into<String>,
574 input: I,
575 work: impl FnOnce(I, &Progress) -> Result<Apply<S>, String> + Send + 'static,
576 ) -> JobId {
577 self.job(kind, label).spawn(input, work)
578 }
579
580 fn launch<I: Send + 'static>(
581 &mut self,
582 kind: &'static str,
583 label: String,
584 here: bool,
585 input: I,
586 work: impl FnOnce(I, &Progress, &Updates<S>) -> Result<Apply<S>, String> + Send + 'static,
587 ) -> JobId {
588 let (id, shared) = self.register(kind, label);
589 let inbox = self.inbox.clone();
590 let updates = Updates { id, inbox: inbox.clone(), shared: shared.clone() };
591 let task = move || {
592 if !shared.begin() {
593 return;
594 }
595 let progress = Progress { shared: shared.clone() };
596 let r = catch_unwind(AssertUnwindSafe(|| work(input, &progress, &updates)));
597 let cancelled = shared.cancel.is_cancelled();
598 let outcome = match r {
599 _ if cancelled => Outcome::Cancelled,
600 Ok(Ok(apply)) => Outcome::Apply(apply),
601 Ok(Err(e)) => Outcome::Failed(e),
602 Err(p) => {
603 let msg = panic_message(p.as_ref());
604 log::error!("background job panicked: {msg}");
605 Outcome::Failed(format!("internal error ({msg}); the job was stopped"))
606 }
607 };
608 inbox.finish(id, outcome, &shared);
609 shared.wake();
610 };
611 match &mut self.pool {
612 Some(pool) if !here => pool.submit(Box::new(task)),
613 _ => task(),
614 }
615 id
616 }
617
618 pub fn spawn_stepped<I: Send + 'static>(
623 &mut self,
624 kind: &'static str,
625 label: impl Into<String>,
626 input: I,
627 step: impl FnMut(&mut I, &Progress) -> Step<S> + Send + 'static,
628 ) -> JobId {
629 self.job(kind, label).spawn_stepped(input, step)
630 }
631
632 fn launch_stepped<I: Send + 'static>(
633 &mut self,
634 kind: &'static str,
635 label: String,
636 here: bool,
637 input: I,
638 mut step: impl FnMut(&mut I, &Progress) -> Step<S> + Send + 'static,
639 ) -> JobId {
640 let mut input = input;
641 if self.pool.is_some() || here {
642 return self.launch(kind, label, here, (), move |(), p, _| {
643 loop {
644 if p.is_cancelled() {
645 return Err("cancelled".into());
646 }
647 match step(&mut input, p) {
648 Step::Continue => {}
649 Step::Done(apply) => return Ok(apply),
650 Step::Failed(e) => return Err(e),
651 Step::Wait => wait_a_moment(),
652 }
653 }
654 });
655 }
656 let (id, shared) = self.register(kind, label);
657 self.stepped.push(SteppedEntry { id, shared, step: Box::new(move |p| step(&mut input, p)) });
658 id
659 }
660
661 pub fn needs_frames(&self) -> bool {
663 !self.stepped.is_empty()
664 }
665
666 pub fn is_busy(&self) -> bool {
668 !self.entries.is_empty()
669 }
670
671 pub fn advance(&mut self, steps: usize) -> bool {
675 self.drive(steps.max(1))
676 }
677
678 fn drive(&mut self, steps: usize) -> bool {
680 let inbox = self.inbox.clone();
681 let mut worked = false;
682 self.stepped.retain_mut(|job| {
683 let status = job.shared.status();
684 if status == Status::Queued && !job.shared.begin() {
685 return false;
686 }
687 if status != Status::Queued && status != Status::Running {
688 return false;
689 }
690 let progress = Progress { shared: job.shared.clone() };
691 for _ in 0..steps {
692 if job.shared.cancel.is_cancelled() {
693 inbox.finish(job.id, Outcome::Cancelled, &job.shared);
694 return false;
695 }
696 let outcome = match catch_unwind(AssertUnwindSafe(|| (job.step)(&progress))) {
697 Ok(Step::Continue) => {
698 worked = true;
699 continue;
700 }
701 Ok(Step::Wait) => return true,
702 Ok(Step::Done(apply)) => Outcome::Apply(apply),
703 Ok(Step::Failed(e)) => Outcome::Failed(e),
704 Err(p) => Outcome::Failed(format!("internal error ({}); the job was stopped", panic_message(p.as_ref()))),
705 };
706 worked = true;
707 inbox.finish(job.id, outcome, &job.shared);
708 return false;
709 }
710 true
711 });
712 worked
713 }
714
715 pub fn drain_ready(&mut self, state: &mut S) -> Vec<JobRecord> {
721 let ready = self.take_ready();
722 let out = ready.apply(state);
723 self.remember(&out);
724 out
725 }
726
727 pub fn take_ready(&mut self) -> Ready<S> {
732 if !self.stepped.is_empty() {
733 self.drive(self.steps_per_drain);
734 }
735 let taken: Vec<(JobId, Outcome<S>)> = lock(&self.inbox.items).drain(..).collect();
736 let mut items = Vec::with_capacity(taken.len());
737 for (id, outcome) in taken {
738 let last = !matches!(outcome, Outcome::Update(_));
739 let Some(pos) = self.entries.iter().position(|e| e.id == id) else { continue };
740 let entry = if last {
741 self.entries.remove(pos)
742 } else {
743 let Some(e) = self.entries.get(pos) else { continue };
744 Entry { id: e.id, kind: e.kind, label: e.label.clone(), shared: e.shared.clone() }
745 };
746 items.push(ReadyItem { entry, outcome });
747 }
748 Ready { items }
749 }
750
751 pub fn remember(&mut self, records: &[JobRecord]) {
753 for rec in records {
754 self.history.push_back(rec.clone());
755 if self.history.len() > HISTORY {
756 self.history.pop_front();
757 }
758 }
759 }
760
761 pub fn status(&self, id: JobId) -> Option<Status> {
763 if let Some(e) = self.entries.iter().find(|e| e.id == id) {
764 return Some(e.shared.status());
765 }
766 self.history.iter().rev().find(|r| r.id == id).map(|r| r.status)
767 }
768
769 pub fn wait(&mut self, id: JobId, timeout: Duration) -> bool {
773 let Some(shared) = self.entries.iter().find(|e| e.id == id).map(|e| e.shared.clone()) else { return true };
774 if self.stepped.iter().any(|j| j.id == id) {
775 let mut budget: u64 = 10_000_000;
776 let deadline = clock::now();
777 while self.stepped.iter().any(|j| j.id == id) && budget > 0 {
778 self.drive(1);
779 budget -= 1;
780 if deadline.is_some_and(|t| t.elapsed() >= timeout) {
781 break;
782 }
783 }
784 }
785 let deadline = clock::now();
786 let mut items = lock(&self.inbox.items);
787 loop {
788 if !matches!(shared.status(), Status::Queued | Status::Running) {
789 return true;
790 }
791 let left = match deadline {
792 Some(t) => timeout.saturating_sub(t.elapsed()),
793 None => return false,
794 };
795 if left.is_zero() {
796 return false;
797 }
798 items = self.inbox.cv.wait_timeout(items, left.min(Duration::from_millis(50))).unwrap_or_else(PoisonError::into_inner).0;
799 }
800 }
801
802 pub fn cancel(&mut self, id: JobId) -> bool {
805 let Some(e) = self.entries.iter().find(|e| e.id == id) else { return false };
806 e.shared.cancel.cancel();
807 let was_queued = {
808 let mut s = lock(&e.shared.state);
809 let q = s.status == Status::Queued;
810 if q {
811 s.status = Status::Ready;
812 }
813 q
814 };
815 if was_queued {
816 self.inbox.push(id, Outcome::Cancelled);
817 }
818 e.shared.wake();
819 true
820 }
821
822 pub fn cancel_all(&mut self) {
824 let ids: Vec<JobId> = self.entries.iter().map(|e| e.id).collect();
825 for id in ids {
826 self.cancel(id);
827 }
828 }
829
830 pub fn wait_all(&mut self, timeout: Duration) -> bool {
834 let deadline = clock::now().map(|t| (t, timeout));
835 let mut budget: u64 = 10_000_000;
836 while !self.stepped.is_empty() && budget > 0 {
837 self.drive(1);
838 budget -= 1;
839 if deadline.is_some_and(|(t, d)| t.elapsed() >= d) {
840 break;
841 }
842 }
843 let mut items = lock(&self.inbox.items);
844 loop {
845 let pending = self.entries.iter().any(|e| matches!(e.shared.status(), Status::Queued | Status::Running));
846 if !pending {
847 return true;
848 }
849 let left = match deadline {
850 Some((t, d)) => d.saturating_sub(t.elapsed()),
851 None => return false,
852 };
853 if left.is_zero() {
854 return false;
855 }
856 items = self.inbox.cv.wait_timeout(items, left.min(Duration::from_millis(50))).unwrap_or_else(PoisonError::into_inner).0;
857 }
858 }
859
860 pub fn list(&self) -> Vec<JobInfo> {
862 self.entries
863 .iter()
864 .map(|e| {
865 let s = lock(&e.shared.state);
866 let elapsed = s.started.map(|t| t.elapsed().as_secs_f64());
867 let eta = match (elapsed, s.fraction) {
868 (Some(el), Some(f)) if f > 0.0 && f < 1.0 && el > 0.0 => Some(el * (1.0 - f) / f),
869 (_, Some(f)) if f >= 1.0 => Some(0.0),
870 _ => None,
871 };
872 JobInfo {
873 id: e.id,
874 kind: e.kind.to_string(),
875 label: e.label.clone(),
876 status: s.status,
877 fraction: s.fraction,
878 message: s.message.clone(),
879 elapsed_secs: elapsed,
880 eta_secs: eta,
881 cancellable: matches!(s.status, Status::Queued | Status::Running) && !e.shared.cancel.is_cancelled(),
882 }
883 })
884 .collect()
885 }
886
887 pub fn recent(&self) -> impl DoubleEndedIterator<Item = &JobRecord> {
889 self.history.iter()
890 }
891
892 pub fn to_json(&self) -> Value {
894 json!({"active": self.list(), "recent": self.history})
895 }
896}
897
898impl<S: 'static> Drop for Jobs<S> {
899 fn drop(&mut self) {
900 for e in &self.entries {
901 e.shared.cancel.cancel();
902 }
903 }
904}