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