1use std::borrow::Cow;
9use std::cell::{Cell, RefCell};
10use std::collections::HashMap;
11use std::future::Future;
12use std::rc::Rc;
13
14use wasm_bindgen_futures::spawn_local;
15
16use crate::reactive::ScopeId;
17use crate::scope::current_scope_id;
18
19#[derive(Default)]
20struct ScopeTasks {
21 tasks: Vec<Rc<TaskState>>,
22 latest: HashMap<String, Rc<TaskState>>,
23}
24
25struct TaskState {
26 cancelled: Cell<bool>,
27}
28
29#[derive(Clone)]
30pub struct TaskHandle {
31 inner: Rc<TaskState>,
32}
33
34thread_local! {
35 static TASKS: RefCell<HashMap<ScopeId, ScopeTasks>> = RefCell::new(HashMap::new());
36}
37
38impl TaskHandle {
39 fn new() -> Self {
40 Self {
41 inner: Rc::new(TaskState {
42 cancelled: Cell::new(false),
43 }),
44 }
45 }
46
47 fn cancelled() -> Self {
48 let handle = Self::new();
49 handle.cancel();
50 handle
51 }
52
53 pub fn cancel(&self) {
54 self.inner.cancelled.set(true);
55 }
56
57 pub fn is_cancelled(&self) -> bool {
58 self.inner.cancelled.get()
59 }
60}
61
62pub fn spawn(fut: impl Future<Output = ()> + 'static) {
63 spawn_local(fut);
64}
65
66pub fn spawn_scoped(fut: impl Future<Output = ()> + 'static) -> TaskHandle {
67 let scope_id = current_scope_id()
68 .expect("pocopine::spawn_scoped called outside a handler / lifecycle context");
69 spawn_for_scope(scope_id, fut)
70}
71
72pub fn spawn_for_scope(scope_id: ScopeId, fut: impl Future<Output = ()> + 'static) -> TaskHandle {
76 if crate::scope::Scope::find(scope_id).is_none() {
77 return TaskHandle::cancelled();
78 }
79
80 let handle = TaskHandle::new();
81 TASKS.with(|tasks| {
82 tasks
83 .borrow_mut()
84 .entry(scope_id)
85 .or_default()
86 .tasks
87 .push(handle.inner.clone());
88 });
89 let inner = handle.inner.clone();
90 spawn_local(async move {
91 fut.await;
92 let _ = inner;
93 });
94 handle
95}
96
97pub fn spawn_latest(
98 task_name: impl Into<Cow<'static, str>>,
99 fut: impl Future<Output = ()> + 'static,
100) -> TaskHandle {
101 let scope_id = current_scope_id()
102 .expect("pocopine::spawn_latest called outside a handler / lifecycle context");
103 let task_name = task_name.into().into_owned();
104 spawn_latest_for_scope(scope_id, task_name, fut)
105}
106
107pub fn spawn_latest_for_scope(
112 scope_id: ScopeId,
113 task_name: impl Into<Cow<'static, str>>,
114 fut: impl Future<Output = ()> + 'static,
115) -> TaskHandle {
116 if crate::scope::Scope::find(scope_id).is_none() {
117 return TaskHandle::cancelled();
118 }
119
120 let task_name = task_name.into().into_owned();
121 let handle = TaskHandle::new();
122 TASKS.with(|tasks| {
123 let mut tasks = tasks.borrow_mut();
124 let scope_tasks = tasks.entry(scope_id).or_default();
125 if let Some(prev) = scope_tasks
126 .latest
127 .insert(task_name.clone(), handle.inner.clone())
128 {
129 prev.cancelled.set(true);
130 }
131 scope_tasks.tasks.push(handle.inner.clone());
132 });
133 let inner = handle.inner.clone();
134 spawn_local(async move {
135 fut.await;
136 TASKS.with(|tasks| {
137 let mut tasks = tasks.borrow_mut();
138 let Some(scope_tasks) = tasks.get_mut(&scope_id) else {
139 return;
140 };
141 if scope_tasks
142 .latest
143 .get(&task_name)
144 .is_some_and(|current| Rc::ptr_eq(current, &inner))
145 {
146 scope_tasks.latest.remove(&task_name);
147 }
148 });
149 });
150 handle
151}
152
153pub fn clear_scope(scope_id: ScopeId) {
154 TASKS.with(|tasks| {
155 if let Some(scope_tasks) = tasks.borrow_mut().remove(&scope_id) {
156 for task in scope_tasks.tasks {
157 task.cancelled.set(true);
158 }
159 }
160 });
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn spawn_for_scope_returns_cancelled_handle_when_scope_is_gone() {
169 let handle = spawn_for_scope(ScopeId(u64::MAX), async move {});
170
171 assert!(handle.is_cancelled());
172 }
173
174 #[test]
175 fn spawn_latest_for_scope_returns_cancelled_handle_when_scope_is_gone() {
176 let handle = spawn_latest_for_scope(ScopeId(u64::MAX), "search", async move {});
177
178 assert!(handle.is_cancelled());
179 }
180}