1#[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
12use std::sync::{Condvar, Mutex, PoisonError};
13use std::{
14 cell::{Cell, RefCell},
15 future::Future,
16 pin::Pin,
17 rc::Rc,
18 sync::{
19 Arc, OnceLock,
20 atomic::{AtomicBool, Ordering},
21 },
22 task::{Context, Poll, Waker},
23 time::Duration,
24};
25
26#[cfg(target_arch = "wasm32")]
27use wasm_bindgen::JsCast;
28use web_time::Instant;
29
30use crate::{
31 hooks::{mutableStateOfNeverEqual, remember},
32 runtime::{RuntimeHandle, TaskHandle, current_runtime_handle},
33 state::{MutableState, State},
34};
35
36pub fn spawn_ui_task(future: impl Future<Output = ()> + 'static) -> Option<TaskHandle> {
42 current_runtime_handle().and_then(|runtime| runtime.spawn_ui(future))
43}
44
45#[derive(Clone)]
51pub struct CoroutineScope {
52 inner: Rc<ScopeInner>,
53}
54
55struct ScopeInner {
56 runtime: Option<RuntimeHandle>,
57 tasks: RefCell<Vec<TaskHandle>>,
58 closed: Cell<bool>,
59}
60
61impl Drop for ScopeInner {
62 fn drop(&mut self) {
63 for task in self.tasks.get_mut().drain(..) {
64 task.cancel();
65 }
66 }
67}
68
69struct CompositionScopeOwner(CoroutineScope);
70
71impl Drop for CompositionScopeOwner {
72 fn drop(&mut self) {
73 self.0.inner.closed.set(true);
74 self.0.cancel();
75 }
76}
77
78impl CoroutineScope {
79 pub fn launch(&self, future: impl Future<Output = ()> + 'static) {
82 if self.inner.closed.get() {
83 return;
84 }
85 let Some(runtime) = self.inner.runtime.clone() else {
86 log::warn!("cranpose: a coroutine scope with no runtime dropped its work");
87 return;
88 };
89 self.inner
90 .tasks
91 .borrow_mut()
92 .retain(|task| !task.is_finished());
93 if let Some(handle) = runtime.spawn_ui(future) {
94 self.inner.tasks.borrow_mut().push(handle);
95 }
96 }
97
98 pub fn cancel(&self) {
100 let tasks = std::mem::take(&mut *self.inner.tasks.borrow_mut());
101 for task in tasks {
102 task.cancel();
103 }
104 }
105
106 #[cfg(test)]
107 pub(crate) fn probe_identity(&self) -> usize {
108 Rc::as_ptr(&self.inner) as *const () as usize
109 }
110}
111
112#[expect(non_snake_case)]
114#[track_caller]
115pub fn rememberCoroutineScope() -> CoroutineScope {
116 remember(|| {
117 CompositionScopeOwner(CoroutineScope {
118 inner: Rc::new(ScopeInner {
119 runtime: current_runtime_handle(),
120 tasks: RefCell::new(Vec::new()),
121 closed: Cell::new(false),
122 }),
123 })
124 })
125 .with(|owner| owner.0.clone())
126}
127
128pub fn delay(duration: Duration) -> Delay {
134 Delay {
135 deadline: Instant::now() + duration,
136 armed: false,
137 fired: Arc::new(AtomicBool::new(false)),
138 }
139}
140
141pub struct Delay {
143 deadline: Instant,
144 armed: bool,
145 fired: Arc<AtomicBool>,
146}
147
148impl Future for Delay {
149 type Output = ();
150
151 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
152 if self.fired.load(Ordering::Acquire) || Instant::now() >= self.deadline {
153 return Poll::Ready(());
154 }
155 let this = self.get_mut();
156 if !this.armed {
157 this.armed = true;
158 timer().arm(
159 this.deadline,
160 context.waker().clone(),
161 Arc::clone(&this.fired),
162 );
163 }
164 Poll::Pending
165 }
166}
167
168pub async fn interval(period: Duration, mut tick: impl FnMut()) {
173 loop {
174 delay(period).await;
175 tick();
176 }
177}
178
179#[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
180struct Alarm {
181 deadline: Instant,
182 waker: Waker,
183 fired: Arc<AtomicBool>,
184}
185
186struct Timer {
187 #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
188 alarms: Mutex<Vec<Alarm>>,
189 #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
190 wake: Condvar,
191}
192
193fn timer() -> &'static Timer {
194 static TIMER: OnceLock<&'static Timer> = OnceLock::new();
195 TIMER.get_or_init(|| {
196 let timer: &'static Timer = Box::leak(Box::new(Timer::new()));
197 timer.start();
198 timer
199 })
200}
201
202#[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
203impl Timer {
204 fn new() -> Self {
205 Self {
206 alarms: Mutex::new(Vec::new()),
207 wake: Condvar::new(),
208 }
209 }
210
211 fn start(&'static self) {
212 std::thread::Builder::new()
213 .name("cranpose-timer".to_string())
214 .spawn(move || self.run())
215 .expect("the timer thread starts");
216 }
217
218 fn run(&self) {
219 let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
220 loop {
221 let now = Instant::now();
222 let mut due = Vec::new();
223 let mut next: Option<Duration> = None;
224 alarms.retain(|alarm| {
225 if alarm.deadline <= now {
226 due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
227 false
228 } else {
229 let remaining = alarm.deadline - now;
230 next = Some(next.map_or(remaining, |current| current.min(remaining)));
231 true
232 }
233 });
234
235 if !due.is_empty() {
236 drop(alarms);
237 for (waker, fired) in due {
238 fired.store(true, Ordering::Release);
239 waker.wake();
240 }
241 alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
242 continue;
243 }
244
245 alarms = match next {
246 Some(timeout) => {
247 self.wake
248 .wait_timeout(alarms, timeout)
249 .unwrap_or_else(PoisonError::into_inner)
250 .0
251 }
252 None => self
253 .wake
254 .wait(alarms)
255 .unwrap_or_else(PoisonError::into_inner),
256 };
257 }
258 }
259
260 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
261 let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
262 alarms.push(Alarm {
263 deadline,
264 waker,
265 fired,
266 });
267 self.wake.notify_one();
268 }
269}
270
271#[cfg(target_vendor = "apple")]
277impl Timer {
278 fn new() -> Self {
279 Self {}
280 }
281
282 fn start(&'static self) {}
283
284 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
285 use dispatch2::{DispatchQoS, DispatchQueue, DispatchTime, GlobalQueueIdentifier};
286
287 let nanos = deadline
288 .saturating_duration_since(Instant::now())
289 .as_nanos()
290 .min(i64::MAX as u128) as i64;
291 let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::QualityOfService(
292 DispatchQoS::UserInteractive,
293 ));
294 let fire = move || {
295 fired.store(true, Ordering::Release);
296 waker.wake();
297 };
298 if queue.after(DispatchTime::NOW.time(nanos), fire).is_err() {
299 log::error!("cranpose: GCD refused a timer; the delay never resolves");
300 }
301 }
302}
303
304#[cfg(target_arch = "wasm32")]
305impl Timer {
306 fn new() -> Self {
307 Self {}
308 }
309
310 fn start(&'static self) {}
311
312 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
313 let millis = deadline
314 .saturating_duration_since(Instant::now())
315 .as_millis()
316 .min(i32::MAX as u128) as i32;
317 let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
318 fired.store(true, Ordering::Release);
319 waker.wake();
320 });
321 let scheduled = web_sys::window().and_then(|window| {
322 window
323 .set_timeout_with_callback_and_timeout_and_arguments_0(
324 callback.unchecked_ref(),
325 millis,
326 )
327 .ok()
328 });
329 if scheduled.is_none() {
330 log::warn!("cranpose: no window timer is available; the delay resolves immediately");
331 }
332 }
333}
334
335pub struct EventChannel<T: 'static> {
342 shared: Rc<ChannelShared<T>>,
343}
344
345struct ChannelShared<T: 'static> {
346 ready: RefCell<std::collections::VecDeque<T>>,
347 closed: std::cell::Cell<bool>,
348 delivered: std::cell::Cell<usize>,
349 wakers: RefCell<Vec<Waker>>,
350}
351
352impl<T: 'static> ChannelShared<T> {
353 fn wake_all(&self) {
354 for waker in self.wakers.borrow_mut().drain(..) {
355 waker.wake();
356 }
357 }
358}
359
360impl<T: 'static> Default for EventChannel<T> {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366impl<T: 'static> EventChannel<T> {
367 pub fn new() -> Self {
369 Self {
370 shared: Rc::new(ChannelShared {
371 ready: RefCell::new(std::collections::VecDeque::new()),
372 closed: std::cell::Cell::new(false),
373 delivered: std::cell::Cell::new(0),
374 wakers: RefCell::new(Vec::new()),
375 }),
376 }
377 }
378
379 pub fn stream(&self) -> EventStream<T> {
381 EventStream {
382 shared: Rc::clone(&self.shared),
383 }
384 }
385
386 pub fn send(&self, event: T) {
388 if self.shared.closed.get() {
389 return;
390 }
391 self.shared.ready.borrow_mut().push_back(event);
392 self.shared.wake_all();
393 }
394
395 pub fn close(&self) {
397 if self.shared.closed.get() {
398 return;
399 }
400 self.shared.closed.set(true);
401 self.shared.wake_all();
402 }
403
404 pub fn is_closed(&self) -> bool {
406 self.shared.closed.get()
407 }
408
409 pub fn pending(&self) -> usize {
411 self.shared.ready.borrow().len()
412 }
413}
414
415pub struct EventStream<T: 'static> {
421 shared: Rc<ChannelShared<T>>,
422}
423
424impl<T: 'static> Clone for EventStream<T> {
425 fn clone(&self) -> Self {
426 Self {
427 shared: Rc::clone(&self.shared),
428 }
429 }
430}
431
432impl<T: 'static> EventStream<T> {
433 pub fn next(&self) -> EventStreamNext<T> {
436 EventStreamNext {
437 shared: Rc::clone(&self.shared),
438 }
439 }
440
441 pub fn delivered(&self) -> usize {
443 self.shared.delivered.get()
444 }
445}
446
447pub struct EventStreamNext<T: 'static> {
449 shared: Rc<ChannelShared<T>>,
450}
451
452impl<T: 'static> Future for EventStreamNext<T> {
453 type Output = Option<T>;
454
455 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
456 if let Some(event) = self.shared.ready.borrow_mut().pop_front() {
457 self.shared.delivered.set(self.shared.delivered.get() + 1);
458 return Poll::Ready(Some(event));
459 }
460 if self.shared.closed.get() {
461 return Poll::Ready(None);
462 }
463 self.shared
464 .wakers
465 .borrow_mut()
466 .push(context.waker().clone());
467 Poll::Pending
468 }
469}
470
471#[expect(non_snake_case)]
477#[track_caller]
478pub fn CollectEvents<T, K>(stream: EventStream<T>, key: K, on_event: impl FnMut(T) + 'static)
479where
480 T: 'static,
481 K: PartialEq + 'static,
482{
483 crate::__launched_effect_async_impl(
484 crate::caller_location_key(),
485 std::panic::Location::caller().into(),
486 key,
487 move |_scope| {
488 let mut on_event = on_event;
489 Box::pin(async move {
490 while let Some(event) = stream.next().await {
491 on_event(event);
492 }
493 })
494 },
495 );
496}
497
498#[expect(non_snake_case)]
503#[track_caller]
504pub fn collectAsState<T, K>(stream: EventStream<T>, key: K, initial: T) -> State<T>
505where
506 T: Clone + 'static,
507 K: PartialEq + 'static,
508{
509 let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
510 let sink = state;
511 CollectEvents(stream, key, move |event| sink.set(event));
512 state.as_state()
513}
514
515pub struct EventSender<T: Send + 'static> {
523 #[cfg(not(target_arch = "wasm32"))]
524 dispatcher: crate::runtime::UiDispatcher,
525 bridge: u64,
526 _events: std::marker::PhantomData<fn(T)>,
527}
528
529impl<T: Send + 'static> Clone for EventSender<T> {
530 fn clone(&self) -> Self {
531 Self {
532 #[cfg(not(target_arch = "wasm32"))]
533 dispatcher: self.dispatcher.clone(),
534 bridge: self.bridge,
535 _events: std::marker::PhantomData,
536 }
537 }
538}
539
540impl<T: Send + 'static> EventSender<T> {
541 pub fn send(&self, event: T) {
543 let bridge = self.bridge;
544 #[cfg(not(target_arch = "wasm32"))]
545 self.dispatcher
546 .post(move || deliver_bridged::<T>(bridge, event));
547 #[cfg(target_arch = "wasm32")]
548 deliver_bridged::<T>(bridge, event);
549 }
550}
551
552thread_local! {
553 static BRIDGES: RefCell<std::collections::HashMap<u64, Rc<dyn std::any::Any>>> =
554 RefCell::new(std::collections::HashMap::new());
555}
556
557static NEXT_BRIDGE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
558
559fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
560 let channel = BRIDGES.with(|bridges| bridges.borrow().get(&bridge).cloned());
561 let Some(channel) = channel else {
562 log::debug!("event bridge {bridge} is gone, one event dropped");
563 return;
564 };
565 if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
566 channel.send(event);
567 }
568}
569
570struct Bridge<T: Send + 'static> {
571 id: u64,
572 channel: Rc<EventChannel<T>>,
573}
574
575impl<T: Send + 'static> Bridge<T> {
576 fn new() -> Self {
577 let id = NEXT_BRIDGE.fetch_add(1, Ordering::Relaxed);
578 let channel = Rc::new(EventChannel::<T>::new());
579 BRIDGES.with(|bridges| {
580 bridges
581 .borrow_mut()
582 .insert(id, Rc::clone(&channel) as Rc<dyn std::any::Any>)
583 });
584 Self { id, channel }
585 }
586}
587
588impl<T: Send + 'static> Drop for Bridge<T> {
589 fn drop(&mut self) {
590 BRIDGES.with(|bridges| bridges.borrow_mut().remove(&self.id));
591 self.channel.close();
592 }
593}
594
595#[expect(non_snake_case)]
603#[track_caller]
604pub fn rememberEventStream<T, K, R, S>(key: K, subscribe: S) -> EventStream<T>
605where
606 T: Send + 'static,
607 K: PartialEq + 'static,
608 R: 'static,
609 S: FnOnce(EventSender<T>) -> R + 'static,
610{
611 let bridge = remember(Bridge::<T>::new);
612 let (id, stream) = bridge.with(|bridge| (bridge.id, bridge.channel.stream()));
613 #[cfg(not(target_arch = "wasm32"))]
614 let dispatcher = current_runtime_handle().map(|runtime| runtime.dispatcher());
615
616 crate::__disposable_effect_impl(crate::caller_location_key(), key, move |scope| {
617 #[cfg(not(target_arch = "wasm32"))]
618 let Some(dispatcher) = dispatcher else {
619 log::warn!("cranpose: an event stream was remembered without a runtime");
620 return scope.on_dispose(|| {});
621 };
622 let registration = subscribe(EventSender {
623 #[cfg(not(target_arch = "wasm32"))]
624 dispatcher,
625 bridge: id,
626 _events: std::marker::PhantomData,
627 });
628 scope.on_dispose(move || drop(registration))
629 });
630
631 stream
632}
633
634#[expect(non_snake_case)]
640#[track_caller]
641pub fn produceState<T, K, F>(initial: T, key: K, producer: F) -> State<T>
642where
643 T: Clone + 'static,
644 K: PartialEq + 'static,
645 F: FnOnce(ProduceScope<T>) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
646{
647 let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
648 let handle = ProduceScope { state };
649 crate::__launched_effect_async_impl(
650 crate::caller_location_key(),
651 std::panic::Location::caller().into(),
652 key,
653 move |_scope| producer(handle),
654 );
655 state.as_state()
656}
657
658pub struct ProduceScope<T: Clone + 'static> {
660 state: MutableState<T>,
661}
662
663impl<T: Clone + 'static> ProduceScope<T> {
664 pub fn set(&self, value: T) {
666 self.state.set(value);
667 }
668}
669
670#[cfg(test)]
671#[path = "tests/concurrency_tests.rs"]
672mod tests;
673
674#[cfg(test)]
675#[path = "tests/concurrency_stream_tests.rs"]
676mod stream_tests;
677
678#[cfg(test)]
679#[path = "tests/concurrency_timer_race_tests.rs"]
680mod timer_race_tests;