cranpose_core/
launched_effect.rs1#[cfg(not(target_arch = "wasm32"))]
2use std::cell::{Cell, RefCell};
3#[cfg(not(target_arch = "wasm32"))]
4use std::rc::Rc;
5use std::{
6 future::Future,
7 sync::{
8 Arc,
9 atomic::{AtomicBool, Ordering},
10 },
11};
12
13use crate::{
14 CoroutineScope, Key, Owned, RuntimeHandle, TaskSite, effect_key::EffectKey,
15 task_site::TaskLabel, with_current_composer,
16};
17
18#[derive(Default)]
19struct LaunchedEffectState {
20 key: Option<EffectKey>,
21 scope: Option<LaunchedEffectScope>,
22}
23
24impl LaunchedEffectState {
25 fn should_run<K: PartialEq + 'static>(&self, keys: &K) -> bool {
26 self.key.as_ref().is_none_or(|current| !current.holds(keys))
27 }
28}
29
30impl Drop for LaunchedEffectState {
31 fn drop(&mut self) {
32 if let Some(scope) = self.scope.take() {
33 scope.cancel();
34 }
35 }
36}
37
38#[derive(Clone)]
45pub struct LaunchedEffectScope {
46 active: Arc<AtomicBool>,
47 runtime: RuntimeHandle,
48 coroutines: CoroutineScope,
49 #[cfg(not(target_arch = "wasm32"))]
50 continuations: Rc<RefCell<Vec<u64>>>,
51}
52
53impl LaunchedEffectScope {
54 fn new(runtime: RuntimeHandle) -> Self {
55 Self {
56 active: Arc::new(AtomicBool::new(true)),
57 coroutines: CoroutineScope::new(Some(runtime.clone())),
58 runtime,
59 #[cfg(not(target_arch = "wasm32"))]
60 continuations: Rc::default(),
61 }
62 }
63
64 fn cancel(&self) {
65 self.active.store(false, Ordering::SeqCst);
66 self.coroutines.close();
67 #[cfg(not(target_arch = "wasm32"))]
68 {
69 let pending = std::mem::take(&mut *self.continuations.borrow_mut());
70 for id in pending {
71 self.runtime.cancel_ui_cont(id);
72 }
73 }
74 }
75
76 #[cfg(not(target_arch = "wasm32"))]
77 fn release_continuation(&self, id: u64) {
78 self.continuations.borrow_mut().retain(|entry| *entry != id);
79 }
80
81 pub fn is_active(&self) -> bool {
84 self.active.load(Ordering::SeqCst)
85 }
86
87 pub fn runtime(&self) -> RuntimeHandle {
89 self.runtime.clone()
90 }
91
92 #[cfg(feature = "internal")]
102 pub async fn with_frame_nanos<R>(&self, on_frame: impl FnOnce(u64) -> R) -> R {
103 on_frame(self.runtime.frame_clock().next_frame().await)
104 }
105
106 #[cfg(feature = "internal")]
109 pub async fn with_frame_millis<R>(&self, on_frame: impl FnOnce(u64) -> R) -> R {
110 self.with_frame_nanos(|nanos| on_frame(nanos / 1_000_000))
111 .await
112 }
113
114 pub fn launch(&self, future: impl Future<Output = ()> + 'static) {
118 self.coroutines.launch(future);
119 }
120
121 #[cfg(not(target_arch = "wasm32"))]
128 pub fn launch_background<T, Work, Ui, Fut>(&self, work: Work, on_ui: Ui)
129 where
130 T: Send + 'static,
131 Work: FnOnce(CancelToken) -> Fut + Send + 'static,
132 Fut: Future<Output = T> + Send + 'static,
133 Ui: FnOnce(T) + 'static,
134 {
135 if !self.is_active() {
136 return;
137 }
138 let dispatcher = self.runtime.dispatcher();
139 let active_for_thread = Arc::clone(&self.active);
140 let continuation_scope = self.clone();
141 let id_cell = Rc::new(Cell::new(0));
142 let id_for_closure = Rc::clone(&id_cell);
143 let continuation = move |value: T| {
144 continuation_scope.release_continuation(id_for_closure.get());
145 if continuation_scope.is_active() {
146 on_ui(value);
147 }
148 };
149
150 let Some(cont_id) = self.runtime.register_ui_cont(continuation) else {
151 return;
152 };
153 id_cell.set(cont_id);
154 self.continuations.borrow_mut().push(cont_id);
155
156 std::thread::spawn(move || {
157 let token = CancelToken::new(Arc::clone(&active_for_thread));
158 let value = pollster::block_on(work(token.clone()));
159 if token.is_cancelled() {
160 return;
161 }
162 dispatcher.post_invoke(cont_id, value);
163 });
164 }
165
166 #[cfg(target_arch = "wasm32")]
172 pub fn launch_background<T, Work, Ui, Fut>(&self, work: Work, on_ui: Ui)
173 where
174 T: 'static,
175 Work: FnOnce(CancelToken) -> Fut + 'static,
176 Fut: Future<Output = T> + 'static,
177 Ui: FnOnce(T) + 'static,
178 {
179 if !self.is_active() {
180 return;
181 }
182 let token = CancelToken::new(Arc::clone(&self.active));
183 self.launch(async move {
184 let value = work(token.clone()).await;
185 if token.is_active() {
186 on_ui(value);
187 }
188 });
189 }
190}
191
192#[derive(Clone)]
193pub struct CancelToken {
199 active: Arc<AtomicBool>,
200}
201
202impl CancelToken {
203 fn new(active: Arc<AtomicBool>) -> Self {
204 Self { active }
205 }
206
207 pub fn is_cancelled(&self) -> bool {
209 !self.active.load(Ordering::SeqCst)
210 }
211
212 pub fn is_active(&self) -> bool {
214 self.active.load(Ordering::SeqCst)
215 }
216}
217
218fn restart(state: Owned<LaunchedEffectState>, runtime: RuntimeHandle) -> LaunchedEffectScope {
222 let scope = LaunchedEffectScope::new(runtime);
223 let previous = state.update(|state| state.scope.replace(scope.clone()));
224 if let Some(previous) = previous {
225 previous.cancel();
226 }
227 scope
228}
229
230#[doc(hidden)]
237pub fn __launched_effect_impl<K, F, Fut>(group_key: Key, site: TaskSite, keys: K, block: F)
238where
239 K: PartialEq + 'static,
240 F: FnOnce(LaunchedEffectScope) -> Fut + 'static,
241 Fut: Future<Output = ()> + 'static,
242{
243 with_current_composer(|composer| {
244 let state = composer.remember_effect::<LaunchedEffectState>(group_key);
245 if state.with(|state| state.should_run(&keys)) {
246 let key = EffectKey::new(keys);
247 state.update(|state| state.key = Some(key));
248 let runtime = composer.runtime_handle();
249 composer.register_side_effect(move || {
250 let scope = restart(state, runtime);
251 let future = Box::pin(block(scope.clone()));
252 scope
253 .coroutines
254 .launch_labelled(future, TaskLabel::Site(site));
255 });
256 }
257 });
258}
259
260#[expect(non_snake_case, reason = "names follow Compose's casing")]
278#[track_caller]
279pub fn LaunchedEffect<K, F, Fut>(keys: K, block: F)
280where
281 K: PartialEq + 'static,
282 F: FnOnce(LaunchedEffectScope) -> Fut + 'static,
283 Fut: Future<Output = ()> + 'static,
284{
285 __launched_effect_impl(
286 crate::caller_location_key(),
287 std::panic::Location::caller().into(),
288 keys,
289 block,
290 );
291}