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);