Skip to main content

ferroui_reactiveui_testing/
scheduler_extensions.rs

1//! `SchedulerExtensions` (`ReactiveUI.Testing/SchedulerExtensions.cs`).
2
3use ferroui_reactive::concurrency::IScheduler;
4use ferroui_reactive::disposables::Disposable;
5use ferroui_reactive_testing::TestScheduler;
6use ferroui_reactiveui::{PanicException, RxSchedulers};
7use ferroui_system::threading::tasks::Task;
8use ferroui_system::IDisposable;
9use std::rc::Rc;
10
11/// Restores the schedulers of [`RxSchedulers`] when it is dropped: the `using` block of upstream's
12/// `With`, which restores them also when the block throws (here, panics).
13struct RestoreOnDrop(Rc<dyn IDisposable>);
14
15impl Drop for RestoreOnDrop {
16    fn drop(&mut self) {
17        self.0.dispose();
18    }
19}
20
21/// Runs code with a scheduler as both schedulers of [`RxSchedulers`], the main thread scheduler
22/// and the task pool scheduler (`SchedulerExtensions`).
23///
24/// It is implemented for a scheduler handle (`Rc<dyn IScheduler>`, `Rc<S>`) and for the
25/// [`TestScheduler`] of `ferroui-reactive-testing`; the block receives the scheduler it was called
26/// on.
27///
28/// Deviation (DEVIATIONS.md, ReactiveUI): the schedulers of [`RxSchedulers`] are per thread, so the
29/// scheduler is the scheduler of the current thread only.
30pub trait SchedulerExtensions {
31    /// The scheduler as the handle that [`RxSchedulers`] holds.
32    fn as_scheduler(&self) -> Rc<dyn IScheduler>;
33
34    /// Makes the scheduler both schedulers of [`RxSchedulers`] until the returned disposable is
35    /// disposed, which restores the schedulers set before (`WithScheduler`).
36    fn with_scheduler(&self) -> Rc<dyn IDisposable> {
37        let previous_main_thread = RxSchedulers::main_thread_scheduler();
38        let previous_task_pool = RxSchedulers::task_pool_scheduler();
39
40        let scheduler = self.as_scheduler();
41        RxSchedulers::set_main_thread_scheduler(scheduler.clone());
42        RxSchedulers::set_task_pool_scheduler(scheduler);
43
44        Disposable::create(move || {
45            RxSchedulers::set_main_thread_scheduler(previous_main_thread);
46            RxSchedulers::set_task_pool_scheduler(previous_task_pool);
47        })
48    }
49
50    /// Runs `block` with the scheduler as both schedulers of [`RxSchedulers`] and returns its
51    /// result; the schedulers set before are restored afterwards, also when `block` panics
52    /// (`With<TRet>(Func<T, TRet>)`, and `With(Action<T>)` with a block that returns `()`).
53    fn with<R>(&self, block: impl FnOnce(&Self) -> R) -> R {
54        let _restore = RestoreOnDrop(self.with_scheduler());
55        block(self)
56    }
57
58    /// Runs `block` with the scheduler as both schedulers of [`RxSchedulers`] until the task it
59    /// returns ends, and gives the outcome of that task (`WithAsync<TRet>(Func<T, Task<TRet>>)`,
60    /// and `WithAsync(Func<T, Task>)` with a `Task<()>`).
61    ///
62    /// A panic of `block` ends the returned task with the error of the panic
63    /// ([`PanicException`]), as an exception thrown in an `async` method faults its task; the
64    /// schedulers are restored in every case.
65    fn with_async<R: Clone + 'static>(&self, block: impl FnOnce(&Self) -> Task<R>) -> Task<R> {
66        let restore = self.with_scheduler();
67        let task = match PanicException::catch(|| block(self)) {
68            Ok(task) => task,
69            Err((error, _payload)) => {
70                restore.dispose();
71                return Task::from_exception(error);
72            }
73        };
74        Task::spawn_local(async move {
75            let outcome = task.await;
76            restore.dispose();
77            outcome
78        })
79    }
80}
81
82impl SchedulerExtensions for Rc<dyn IScheduler> {
83    fn as_scheduler(&self) -> Rc<dyn IScheduler> {
84        self.clone()
85    }
86}
87
88impl<S: IScheduler + 'static> SchedulerExtensions for Rc<S> {
89    fn as_scheduler(&self) -> Rc<dyn IScheduler> {
90        self.clone()
91    }
92}
93
94impl SchedulerExtensions for TestScheduler {
95    fn as_scheduler(&self) -> Rc<dyn IScheduler> {
96        self.scheduler()
97    }
98}