1use crate::{hash_key, with_current_composer, Key, RuntimeHandle, TaskHandle};
2#[cfg(not(target_arch = "wasm32"))]
3use std::cell::{Cell, RefCell};
4use std::future::Future;
5use std::hash::Hash;
6use std::pin::Pin;
7#[cfg(not(target_arch = "wasm32"))]
8use std::rc::Rc;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11
12#[derive(Default)]
13struct LaunchedEffectState {
14 key: Option<Key>,
15 cancel: Option<LaunchedEffectCancellation>,
16}
17
18struct LaunchedEffectCancellation {
19 #[cfg(not(target_arch = "wasm32"))]
20 runtime: RuntimeHandle,
21 active: Arc<AtomicBool>,
22 #[cfg(not(target_arch = "wasm32"))]
23 continuations: Rc<RefCell<Vec<u64>>>,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct TaskSite {
34 pub file: &'static str,
35 pub line: u32,
36}
37
38impl TaskSite {
39 pub const fn new(file: &'static str, line: u32) -> TaskSite {
41 TaskSite { file, line }
42 }
43}
44
45impl Default for TaskSite {
46 fn default() -> TaskSite {
47 TaskSite::new("unknown", 0)
48 }
49}
50
51impl std::fmt::Display for TaskSite {
52 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(formatter, "{}:{}", self.file, self.line)
54 }
55}
56
57impl From<&'static std::panic::Location<'static>> for TaskSite {
58 fn from(location: &'static std::panic::Location<'static>) -> TaskSite {
60 TaskSite::new(location.file(), location.line())
61 }
62}
63
64#[derive(Default)]
65struct LaunchedEffectAsyncState {
66 key: Option<Key>,
67 cancel: Option<LaunchedEffectCancellation>,
68 task: Option<TaskHandle>,
69 site: TaskSite,
70}
71
72impl LaunchedEffectState {
73 fn should_run(&self, key: Key) -> bool {
74 match self.key {
75 Some(current) => current != key,
76 None => true,
77 }
78 }
79
80 fn set_key(&mut self, key: Key) {
81 self.key = Some(key);
82 }
83
84 fn launch(
85 &mut self,
86 runtime: RuntimeHandle,
87 effect: impl FnOnce(LaunchedEffectScope) + 'static,
88 ) {
89 self.cancel_current();
90 let active = Arc::new(AtomicBool::new(true));
91 #[cfg(not(target_arch = "wasm32"))]
92 let continuations = Rc::new(RefCell::new(Vec::new()));
93 self.cancel = Some(LaunchedEffectCancellation {
94 #[cfg(not(target_arch = "wasm32"))]
95 runtime: runtime.clone(),
96 active: Arc::clone(&active),
97 #[cfg(not(target_arch = "wasm32"))]
98 continuations: Rc::clone(&continuations),
99 });
100 let scope = LaunchedEffectScope {
101 active: Arc::clone(&active),
102 runtime: runtime.clone(),
103 #[cfg(not(target_arch = "wasm32"))]
104 continuations,
105 };
106 runtime.enqueue_ui_task(Box::new(move || effect(scope)));
107 }
108
109 fn cancel_current(&mut self) {
110 if let Some(cancel) = self.cancel.take() {
111 cancel.cancel();
112 }
113 }
114}
115
116impl LaunchedEffectCancellation {
117 fn cancel(&self) {
118 self.active.store(false, Ordering::SeqCst);
119 #[cfg(not(target_arch = "wasm32"))]
120 {
121 let mut pending = self.continuations.borrow_mut();
122 for id in pending.drain(..) {
123 self.runtime.cancel_ui_cont(id);
124 }
125 }
126 }
127}
128
129impl LaunchedEffectAsyncState {
130 fn should_run(&self, key: Key) -> bool {
131 match self.key {
132 Some(current) => current != key,
133 None => true,
134 }
135 }
136
137 fn set_key(&mut self, key: Key) {
138 self.key = Some(key);
139 }
140
141 fn set_site(&mut self, site: TaskSite) {
142 self.site = site;
143 }
144
145 fn launch(
146 &mut self,
147 runtime: RuntimeHandle,
148 mk_future: impl FnOnce(LaunchedEffectScope) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
149 ) {
150 self.cancel_current();
151 let active = Arc::new(AtomicBool::new(true));
152 #[cfg(not(target_arch = "wasm32"))]
153 let continuations = Rc::new(RefCell::new(Vec::new()));
154 self.cancel = Some(LaunchedEffectCancellation {
155 #[cfg(not(target_arch = "wasm32"))]
156 runtime: runtime.clone(),
157 active: Arc::clone(&active),
158 #[cfg(not(target_arch = "wasm32"))]
159 continuations: Rc::clone(&continuations),
160 });
161 let scope = LaunchedEffectScope {
162 active: Arc::clone(&active),
163 runtime: runtime.clone(),
164 #[cfg(not(target_arch = "wasm32"))]
165 continuations,
166 };
167 let future = mk_future(scope.clone());
168 let active_flag = Arc::clone(&scope.active);
169 crate::label_next_ui_task(self.site.to_string());
170 match runtime.spawn_ui(async move {
171 future.await;
172 active_flag.store(false, Ordering::SeqCst);
173 }) {
174 Some(handle) => {
175 self.task = Some(handle);
176 }
177 None => {
178 active.store(false, Ordering::SeqCst);
179 self.cancel = None;
180 }
181 }
182 }
183
184 fn cancel_current(&mut self) {
185 if let Some(handle) = self.task.take() {
186 handle.cancel();
187 }
188 if let Some(cancel) = self.cancel.take() {
189 cancel.cancel();
190 }
191 }
192}
193
194impl Drop for LaunchedEffectState {
195 fn drop(&mut self) {
196 self.cancel_current();
197 }
198}
199
200impl Drop for LaunchedEffectAsyncState {
201 fn drop(&mut self) {
202 self.cancel_current();
203 }
204}
205
206#[derive(Clone)]
207pub struct LaunchedEffectScope {
208 active: Arc<AtomicBool>,
209 runtime: RuntimeHandle,
210 #[cfg(not(target_arch = "wasm32"))]
211 continuations: Rc<RefCell<Vec<u64>>>,
212}
213
214impl LaunchedEffectScope {
215 #[cfg(not(target_arch = "wasm32"))]
216 fn track_continuation(&self, id: u64) {
217 self.continuations.borrow_mut().push(id);
218 }
219
220 #[cfg(not(target_arch = "wasm32"))]
221 fn release_continuation(&self, id: u64) {
222 let mut continuations = self.continuations.borrow_mut();
223 if let Some(index) = continuations.iter().position(|entry| *entry == id) {
224 continuations.remove(index);
225 }
226 }
227
228 pub fn is_active(&self) -> bool {
229 self.active.load(Ordering::SeqCst)
230 }
231
232 pub fn runtime(&self) -> RuntimeHandle {
233 self.runtime.clone()
234 }
235
236 pub fn launch(&self, task: impl FnOnce(LaunchedEffectScope) + 'static) {
242 if !self.is_active() {
243 return;
244 }
245 let scope = self.clone();
246 self.runtime.enqueue_ui_task(Box::new(move || {
247 if scope.is_active() {
248 task(scope);
249 }
250 }));
251 }
252
253 pub fn post_ui(&self, task: impl FnOnce() + 'static) {
258 if !self.is_active() {
259 return;
260 }
261 let active = Arc::clone(&self.active);
262 self.runtime.enqueue_ui_task(Box::new(move || {
263 if active.load(Ordering::SeqCst) {
264 task();
265 }
266 }));
267 }
268
269 #[cfg(not(target_arch = "wasm32"))]
276 pub fn launch_background<T, Work, Ui, Fut>(&self, work: Work, on_ui: Ui)
277 where
278 T: Send + 'static,
279 Work: FnOnce(CancelToken) -> Fut + Send + 'static,
280 Fut: Future<Output = T> + Send + 'static,
281 Ui: FnOnce(T) + 'static,
282 {
283 if !self.is_active() {
284 return;
285 }
286 let dispatcher = self.runtime.dispatcher();
287 let active_for_thread = Arc::clone(&self.active);
288 let continuation_scope = self.clone();
289 let continuation_active = Arc::clone(&self.active);
290 let id_cell = Rc::new(Cell::new(0));
291 let id_for_closure = Rc::clone(&id_cell);
292 let continuation = move |value: T| {
293 let id = id_for_closure.get();
294 continuation_scope.release_continuation(id);
295 if continuation_active.load(Ordering::SeqCst) {
296 on_ui(value);
297 }
298 };
299
300 let Some(cont_id) = self.runtime.register_ui_cont(continuation) else {
301 return;
302 };
303 id_cell.set(cont_id);
304 self.track_continuation(cont_id);
305
306 std::thread::spawn(move || {
307 let token = CancelToken::new(Arc::clone(&active_for_thread));
308 let value = pollster::block_on(work(token.clone()));
309 if token.is_cancelled() {
310 return;
311 }
312 dispatcher.post_invoke(cont_id, value);
313 });
314 }
315
316 #[cfg(target_arch = "wasm32")]
323 pub fn launch_background<T, Work, Ui, Fut>(&self, work: Work, on_ui: Ui)
324 where
325 T: 'static,
326 Work: FnOnce(CancelToken) -> Fut + 'static,
327 Fut: Future<Output = T> + 'static,
328 Ui: FnOnce(T) + 'static,
329 {
330 if !self.is_active() {
331 return;
332 }
333 let active_for_task = Arc::clone(&self.active);
334 let scope = self.clone();
335 wasm_bindgen_futures::spawn_local(async move {
336 let token = CancelToken::new(Arc::clone(&active_for_task));
337 let value = work(token.clone()).await;
338 if token.is_cancelled() {
339 return;
340 }
341 scope.post_ui(move || {
342 if token.is_active() {
343 on_ui(value);
344 }
345 });
346 });
347 }
348}
349
350#[derive(Clone)]
351pub struct CancelToken {
357 active: Arc<AtomicBool>,
358}
359
360impl CancelToken {
361 fn new(active: Arc<AtomicBool>) -> Self {
362 Self { active }
363 }
364
365 pub fn is_cancelled(&self) -> bool {
367 !self.active.load(Ordering::SeqCst)
368 }
369
370 pub fn is_active(&self) -> bool {
372 self.active.load(Ordering::SeqCst)
373 }
374}
375
376pub fn __launched_effect_impl<K, F>(group_key: Key, keys: K, effect: F)
377where
378 K: Hash,
379 F: FnOnce(LaunchedEffectScope) + 'static,
380{
381 with_current_composer(|composer| {
384 composer.with_group(group_key, |composer| {
385 let key_hash = hash_key(&keys);
386 let state = composer.remember_effect::<LaunchedEffectState>();
387 if state.with(|state| state.should_run(key_hash)) {
388 state.update(|state| state.set_key(key_hash));
389 let runtime = composer.runtime_handle();
390 let state_for_effect = state.clone();
391 let mut effect_opt = Some(effect);
392 composer.register_side_effect(move || {
393 if let Some(effect) = effect_opt.take() {
394 state_for_effect.update(|state| state.launch(runtime.clone(), effect));
395 }
396 });
397 }
398 });
399 });
400}
401
402#[macro_export]
403macro_rules! LaunchedEffect {
404 ($keys:expr, $effect:expr) => {
405 $crate::__launched_effect_impl(
406 $crate::location_key(file!(), line!(), column!()),
407 $keys,
408 $effect,
409 )
410 };
411}
412
413pub fn __launched_effect_async_impl<K, F>(group_key: Key, site: TaskSite, keys: K, mk_future: F)
416where
417 K: Hash,
418 F: FnOnce(LaunchedEffectScope) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
419{
420 with_current_composer(|composer| {
421 composer.with_group(group_key, |composer| {
422 let key_hash = hash_key(&keys);
423 let state = composer.remember_effect::<LaunchedEffectAsyncState>();
424 if state.with(|state| state.should_run(key_hash)) {
425 state.update(|state| {
426 state.set_key(key_hash);
427 state.set_site(site);
428 });
429 let runtime = composer.runtime_handle();
430 let state_for_effect = state.clone();
431 let mut mk_future_opt = Some(mk_future);
432 composer.register_side_effect(move || {
433 if let Some(mk_future) = mk_future_opt.take() {
434 state_for_effect.update(|state| {
435 state.launch(runtime.clone(), mk_future);
436 });
437 }
438 });
439 }
440 });
441 });
442}
443
444#[macro_export]
445macro_rules! LaunchedEffectAsync {
446 ($keys:expr, $future:expr) => {
447 $crate::__launched_effect_async_impl(
448 $crate::location_key(file!(), line!(), column!()),
449 $crate::TaskSite::new(file!(), line!()),
450 $keys,
451 $future,
452 )
453 };
454}