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}