Skip to main content

freya_core/lifecycle/
task.rs

1use std::{
2    cell::RefCell,
3    pin::Pin,
4    rc::Rc,
5    sync::{
6        Arc,
7        atomic::Ordering,
8    },
9};
10
11use crate::{
12    current_context::CurrentContext,
13    prelude::current_scope_id,
14    runner::Message,
15    scope_id::ScopeId,
16};
17
18/// Spawn a task attached to the root scope.
19///
20/// Unlike [`spawn`], this task keeps running when the component that started it
21/// unmounts. Use it for app-wide work like initializing a shared cache or a
22/// background synchronization loop. It runs until it finishes, the app exits, or
23/// its [`TaskHandle`] is cancelled.
24///
25/// Spawn it from a hook so rerenders do not create duplicates:
26///
27/// ```rust,no_run
28/// # use freya::prelude::*;
29/// # async fn initialize_shared_cache() {}
30/// # fn app() -> impl IntoElement {
31/// let _cache_task = use_hook(|| {
32///     spawn_forever(async {
33///         initialize_shared_cache().await;
34///     })
35/// });
36///
37/// rect()
38/// # }
39/// ```
40pub fn spawn_forever(future: impl Future<Output = ()> + 'static) -> TaskHandle {
41    CurrentContext::with(|context| {
42        let task_id = TaskId(context.task_id_counter.fetch_add(1, Ordering::Relaxed));
43        context.tasks.borrow_mut().insert(
44            task_id,
45            Rc::new(RefCell::new(Task {
46                scope_id: ScopeId::ROOT,
47                future: Box::pin(future),
48                waker: futures_util::task::waker(Arc::new(TaskWaker {
49                    task_id,
50                    sender: context.sender.clone(),
51                })),
52            })),
53        );
54        context
55            .sender
56            .unbounded_send(Message::PollTask(task_id))
57            .unwrap();
58        task_id.into()
59    })
60}
61
62/// Spawn a task attached to the current component scope.
63///
64/// Use it for async work owned by a component, like handling an event, waiting
65/// for a timer or loading component-specific data. Freya cancels the task when
66/// that component unmounts. The returned [`TaskHandle`] lets you cancel it
67/// earlier.
68///
69/// ```rust,no_run
70/// # use freya::prelude::*;
71/// # async fn save_document() {}
72/// # fn save_button() -> impl IntoElement {
73/// Button::new().child("Save").on_press(|_| {
74///     spawn(async {
75///         save_document().await;
76///     });
77/// })
78/// # }
79/// ```
80pub fn spawn(future: impl Future<Output = ()> + 'static) -> TaskHandle {
81    CurrentContext::with(|context| {
82        let task_id = TaskId(context.task_id_counter.fetch_add(1, Ordering::Relaxed));
83        context.tasks.borrow_mut().insert(
84            task_id,
85            Rc::new(RefCell::new(Task {
86                scope_id: current_scope_id(),
87                future: Box::pin(future),
88                waker: futures_util::task::waker(Arc::new(TaskWaker {
89                    task_id,
90                    sender: context.sender.clone(),
91                })),
92            })),
93        );
94        context
95            .sender
96            .unbounded_send(Message::PollTask(task_id))
97            .unwrap();
98        task_id.into()
99    })
100}
101
102/// A non-owning handle used to cancel a spawned task manually.
103///
104/// Dropping this handle does not cancel the task. Call [`TaskHandle::cancel`]
105/// explicitly, or use [`TaskHandle::owned`] when the task should be cancelled as
106/// its owner is dropped.
107#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
108pub struct TaskHandle(TaskId);
109
110impl From<TaskId> for TaskHandle {
111    fn from(value: TaskId) -> Self {
112        TaskHandle(value)
113    }
114}
115
116impl TaskHandle {
117    /// Cancel the task.
118    ///
119    /// Use it when an event or state change makes an in-progress task no longer
120    /// necessary. This method must run within Freya's current context, use
121    /// [`TaskHandle::try_cancel`] for cleanup that may run outside it.
122    pub fn cancel(&self) {
123        CurrentContext::with(|context| context.tasks.borrow_mut().remove(&self.0));
124    }
125
126    /// Try to cancel the task if Freya's current context is available.
127    ///
128    /// Unlike [`TaskHandle::cancel`], this does nothing when called outside
129    /// Freya's context. Prefer it in destructors and other cleanup paths where a
130    /// context might no longer exist.
131    pub fn try_cancel(&self) {
132        CurrentContext::try_with(|context| context.tasks.borrow_mut().remove(&self.0));
133    }
134
135    /// Upgrade to an [`OwnedTaskHandle`] that cancels the task when its last
136    /// clone is dropped.
137    ///
138    /// Retain the returned handle for as long as the task should run. Useful for
139    /// a task owned by another long-lived value rather than by a component
140    /// scope:
141    ///
142    /// ```rust,no_run
143    /// # use freya::prelude::*;
144    /// # async fn forward_messages() {}
145    /// struct Worker {
146    ///     _task: OwnedTaskHandle,
147    /// }
148    ///
149    /// # fn start_worker() -> Worker {
150    /// let worker = Worker {
151    ///     _task: spawn_forever(forward_messages()).owned(),
152    /// };
153    /// # worker
154    /// # }
155    /// ```
156    pub fn owned(self) -> OwnedTaskHandle {
157        OwnedTaskHandle(Rc::new(InnerOwnedTaskHandle(self)))
158    }
159}
160
161struct InnerOwnedTaskHandle(TaskHandle);
162
163impl Drop for InnerOwnedTaskHandle {
164    fn drop(&mut self) {
165        self.0.try_cancel();
166    }
167}
168
169/// An owning handle that cancels its task when the last clone is dropped.
170///
171/// Use [`TaskHandle::owned`] to create one. Clones share ownership of the same
172/// task, so dropping one only cancels the task when no other clones remain.
173#[derive(Clone)]
174pub struct OwnedTaskHandle(Rc<InnerOwnedTaskHandle>);
175
176impl PartialEq for OwnedTaskHandle {
177    fn eq(&self, other: &Self) -> bool {
178        Rc::ptr_eq(&self.0, &other.0)
179    }
180}
181
182impl OwnedTaskHandle {
183    /// Cancel the owned task immediately.
184    ///
185    /// This method has the same context requirement as [`TaskHandle::cancel`].
186    pub fn cancel(&self) {
187        self.0.0.cancel();
188    }
189
190    /// Try to cancel the owned task if Freya's current context is available.
191    ///
192    /// Use this instead of [`OwnedTaskHandle::cancel`] from cleanup code that
193    /// may run after Freya's context has been removed.
194    pub fn try_cancel(&self) {
195        self.0.0.try_cancel();
196    }
197
198    /// Get a non-owning [`TaskHandle`] for the same task.
199    ///
200    /// The returned handle can cancel the task, but dropping it has no effect
201    /// on the [`OwnedTaskHandle`]'s ownership.
202    pub fn downgrade(&self) -> TaskHandle {
203        self.0.0
204    }
205}
206
207/// Wakes a Freya task by asking the runner to poll it again.
208///
209/// This is a runtime implementation detail, application code normally uses
210/// [`spawn`] or [`spawn_forever`] instead.
211pub struct TaskWaker {
212    task_id: TaskId,
213    sender: futures_channel::mpsc::UnboundedSender<Message>,
214}
215
216impl futures_util::task::ArcWake for TaskWaker {
217    fn wake_by_ref(arc_self: &Arc<Self>) {
218        _ = arc_self
219            .sender
220            .unbounded_send(Message::PollTask(arc_self.task_id));
221    }
222}
223
224/// A future scheduled by Freya's async runtime.
225///
226/// This is a runtime implementation detail stored by the runner. Application
227/// code should use [`TaskHandle`] to interact with spawned tasks.
228pub struct Task {
229    pub scope_id: ScopeId,
230    pub future: Pin<Box<dyn Future<Output = ()>>>,
231    /// Used to notify the runner that this task needs progress.
232    pub waker: futures_util::task::Waker,
233}
234
235/// The opaque identifier of a task scheduled by Freya's async runtime.
236#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
237pub struct TaskId(u64);