1#[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
12use std::sync::{Condvar, Mutex, PoisonError};
13use std::{
14 any::Any,
15 cell::{Cell, RefCell},
16 future::Future,
17 pin::Pin,
18 rc::Rc,
19 sync::{
20 Arc, OnceLock,
21 atomic::{AtomicBool, Ordering},
22 },
23 task::{Context, Poll, Waker},
24 time::Duration,
25};
26
27#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
28use wasm_bindgen::JsCast;
29use web_time::Instant;
30
31use crate::{
32 hooks::{mutableStateOfNeverEqual, remember},
33 runtime::{RuntimeHandle, TaskHandle, current_runtime_handle},
34 state::{MutableState, State},
35};
36
37pub fn spawn_ui_task(future: impl Future<Output = ()> + 'static) -> Option<TaskHandle> {
43 current_runtime_handle().and_then(|runtime| runtime.spawn_ui(future))
44}
45
46#[derive(Clone)]
52pub struct CoroutineScope {
53 inner: Rc<ScopeInner>,
54}
55
56struct ScopeInner {
57 runtime: Option<RuntimeHandle>,
58 tasks: RefCell<Vec<TaskHandle>>,
59 closed: Cell<bool>,
60}
61
62impl Drop for ScopeInner {
63 fn drop(&mut self) {
64 for task in self.tasks.get_mut().drain(..) {
65 task.cancel();
66 }
67 }
68}
69
70struct CompositionScopeOwner(CoroutineScope);
71
72impl Drop for CompositionScopeOwner {
73 fn drop(&mut self) {
74 self.0.inner.closed.set(true);
75 self.0.cancel();
76 }
77}
78
79impl CoroutineScope {
80 pub fn launch(&self, future: impl Future<Output = ()> + 'static) {
83 if self.inner.closed.get() {
84 return;
85 }
86 let Some(runtime) = self.inner.runtime.clone() else {
87 log::warn!("cranpose: a coroutine scope with no runtime dropped its work");
88 return;
89 };
90 self.inner
91 .tasks
92 .borrow_mut()
93 .retain(|task| !task.is_finished());
94 if let Some(handle) = runtime.spawn_ui(future) {
95 self.inner.tasks.borrow_mut().push(handle);
96 }
97 }
98
99 pub fn cancel(&self) {
101 let tasks = std::mem::take(&mut *self.inner.tasks.borrow_mut());
102 for task in tasks {
103 task.cancel();
104 }
105 }
106
107 #[cfg(test)]
108 pub(crate) fn probe_identity(&self) -> usize {
109 Rc::as_ptr(&self.inner) as *const () as usize
110 }
111}
112
113#[expect(non_snake_case, reason = "names follow Compose's casing")]
115#[track_caller]
116pub fn rememberCoroutineScope() -> CoroutineScope {
117 remember(|| {
118 CompositionScopeOwner(CoroutineScope {
119 inner: Rc::new(ScopeInner {
120 runtime: current_runtime_handle(),
121 tasks: RefCell::new(Vec::new()),
122 closed: Cell::new(false),
123 }),
124 })
125 })
126 .with(|owner| owner.0.clone())
127}
128
129pub fn delay(duration: Duration) -> Delay {
135 Delay {
136 deadline: Instant::now() + duration,
137 armed: false,
138 fired: Arc::new(AtomicBool::new(false)),
139 }
140}
141
142pub struct Delay {
144 deadline: Instant,
145 armed: bool,
146 fired: Arc<AtomicBool>,
147}
148
149impl Future for Delay {
150 type Output = ();
151
152 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
153 if self.fired.load(Ordering::Acquire) || Instant::now() >= self.deadline {
154 return Poll::Ready(());
155 }
156 let this = self.get_mut();
157 if !this.armed {
158 this.armed = true;
159 arm(
160 this.deadline,
161 context.waker().clone(),
162 Arc::clone(&this.fired),
163 );
164 }
165 Poll::Pending
166 }
167}
168
169pub async fn interval(period: Duration, mut tick: impl FnMut()) {
174 loop {
175 delay(period).await;
176 tick();
177 }
178}
179
180#[cfg(not(any(
181 all(target_arch = "wasm32", target_os = "unknown"),
182 target_vendor = "apple"
183)))]
184pub(crate) struct Alarm {
185 deadline: Instant,
186 waker: Waker,
187 fired: Arc<AtomicBool>,
188}
189
190fn arm(deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
197 #[cfg(target_os = "espidf")]
198 if current_runtime_handle().is_some() {
199 arm_host_alarm(Alarm {
200 deadline,
201 waker,
202 fired,
203 });
204 return;
205 }
206 timer().arm(deadline, waker, fired);
207}
208
209struct Timer {
210 #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
211 alarms: Mutex<Vec<Alarm>>,
212 #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
213 wake: Condvar,
214}
215
216fn timer() -> &'static Timer {
217 static TIMER: OnceLock<&'static Timer> = OnceLock::new();
218 TIMER.get_or_init(|| {
219 let timer: &'static Timer = Box::leak(Box::new(Timer::new()));
220 timer.start();
221 timer
222 })
223}
224
225#[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
226impl Timer {
227 fn new() -> Self {
228 Self {
229 alarms: Mutex::new(Vec::new()),
230 wake: Condvar::new(),
231 }
232 }
233
234 fn start(&'static self) {
235 std::thread::Builder::new()
236 .name("cranpose-timer".to_string())
237 .spawn(move || self.run())
238 .expect("the timer thread starts");
239 }
240
241 fn run(&self) {
242 let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
243 loop {
244 let now = Instant::now();
245 let mut due = Vec::new();
246 let mut next: Option<Duration> = None;
247 alarms.retain(|alarm| {
248 if alarm.deadline <= now {
249 due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
250 false
251 } else {
252 let remaining = alarm.deadline - now;
253 next = Some(next.map_or(remaining, |current| current.min(remaining)));
254 true
255 }
256 });
257
258 if !due.is_empty() {
259 drop(alarms);
260 for (waker, fired) in due {
261 fired.store(true, Ordering::Release);
262 waker.wake();
263 }
264 alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
265 continue;
266 }
267
268 alarms = match next {
269 Some(timeout) => {
270 self.wake
271 .wait_timeout(alarms, timeout)
272 .unwrap_or_else(PoisonError::into_inner)
273 .0
274 }
275 None => self
276 .wake
277 .wait(alarms)
278 .unwrap_or_else(PoisonError::into_inner),
279 };
280 }
281 }
282
283 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
284 let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
285 alarms.push(Alarm {
286 deadline,
287 waker,
288 fired,
289 });
290 self.wake.notify_one();
291 }
292}
293
294#[cfg(target_vendor = "apple")]
300impl Timer {
301 fn new() -> Self {
302 Self {}
303 }
304
305 fn start(&'static self) {}
306
307 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
308 use dispatch2::{DispatchQoS, DispatchQueue, DispatchTime, GlobalQueueIdentifier};
309
310 let nanos = deadline
311 .saturating_duration_since(Instant::now())
312 .as_nanos()
313 .min(i64::MAX as u128) as i64;
314 let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::QualityOfService(
315 DispatchQoS::UserInteractive,
316 ));
317 let fire = move || {
318 fired.store(true, Ordering::Release);
319 waker.wake();
320 };
321 if queue.after(DispatchTime::NOW.time(nanos), fire).is_err() {
322 log::error!("cranpose: GCD refused a timer; the delay never resolves");
323 }
324 }
325}
326
327#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
328impl Timer {
329 fn new() -> Self {
330 Self {}
331 }
332
333 fn start(&'static self) {}
334
335 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
336 let millis = deadline
337 .saturating_duration_since(Instant::now())
338 .as_millis()
339 .min(i32::MAX as u128) as i32;
340 let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
341 fired.store(true, Ordering::Release);
342 waker.wake();
343 });
344 let scheduled = web_sys::window().and_then(|window| {
345 window
346 .set_timeout_with_callback_and_timeout_and_arguments_0(
347 callback.unchecked_ref(),
348 millis,
349 )
350 .ok()
351 });
352 if scheduled.is_none() {
353 log::warn!("cranpose: no window timer is available; the delay resolves immediately");
354 }
355 }
356}
357
358#[cfg(target_os = "emscripten")]
363impl Timer {
364 fn new() -> Self {
365 Self {}
366 }
367
368 fn start(&'static self) {}
369
370 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
371 arm_host_alarm(Alarm {
372 deadline,
373 waker,
374 fired,
375 });
376 }
377}
378
379#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
382fn arm_host_alarm(alarm: Alarm) {
383 crate::thread_state::runtimes(|state| state.alarms.borrow_mut().push(alarm));
384}
385
386pub fn next_alarm_deadline() -> Option<Instant> {
391 #[cfg(any(target_os = "emscripten", target_os = "espidf"))]
392 return crate::thread_state::try_runtimes(|state| {
393 state
394 .alarms
395 .borrow()
396 .iter()
397 .map(|alarm| alarm.deadline)
398 .min()
399 })
400 .flatten();
401 #[cfg(not(any(target_os = "emscripten", target_os = "espidf")))]
402 None
403}
404
405#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
408pub(crate) fn has_due_alarm() -> bool {
409 next_alarm_deadline().is_some_and(|deadline| deadline <= Instant::now())
410}
411
412#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
414pub(crate) fn fire_due_alarms() {
415 let now = Instant::now();
416 let due: Vec<Alarm> = crate::thread_state::runtimes(|state| {
417 let mut alarms = state.alarms.borrow_mut();
418 if alarms.iter().all(|alarm| alarm.deadline > now) {
419 return Vec::new();
420 }
421 let (due, waiting) = std::mem::take(&mut *alarms)
422 .into_iter()
423 .partition(|alarm| alarm.deadline <= now);
424 *alarms = waiting;
425 due
426 });
427 for alarm in due {
428 alarm.fired.store(true, Ordering::Release);
429 alarm.waker.wake();
430 }
431}
432
433pub struct EventChannel<T: 'static> {
440 shared: Rc<ChannelShared<T>>,
441}
442
443struct ChannelShared<T: 'static> {
444 ready: RefCell<std::collections::VecDeque<T>>,
445 closed: Cell<bool>,
446 delivered: Cell<usize>,
447 wakers: RefCell<Vec<Waker>>,
448}
449
450impl<T: 'static> ChannelShared<T> {
451 fn wake_all(&self) {
452 for waker in self.wakers.borrow_mut().drain(..) {
453 waker.wake();
454 }
455 }
456}
457
458impl<T: 'static> Default for EventChannel<T> {
459 fn default() -> Self {
460 Self::new()
461 }
462}
463
464impl<T: 'static> EventChannel<T> {
465 pub fn new() -> Self {
467 Self {
468 shared: Rc::new(ChannelShared {
469 ready: RefCell::new(std::collections::VecDeque::new()),
470 closed: Cell::new(false),
471 delivered: Cell::new(0),
472 wakers: RefCell::new(Vec::new()),
473 }),
474 }
475 }
476
477 pub fn stream(&self) -> EventStream<T> {
479 EventStream {
480 shared: Rc::clone(&self.shared),
481 }
482 }
483
484 pub fn send(&self, event: T) {
486 if self.shared.closed.get() {
487 return;
488 }
489 self.shared.ready.borrow_mut().push_back(event);
490 self.shared.wake_all();
491 }
492
493 pub fn close(&self) {
495 if self.shared.closed.get() {
496 return;
497 }
498 self.shared.closed.set(true);
499 self.shared.wake_all();
500 }
501
502 pub fn is_closed(&self) -> bool {
504 self.shared.closed.get()
505 }
506
507 pub fn pending(&self) -> usize {
509 self.shared.ready.borrow().len()
510 }
511}
512
513pub struct EventStream<T: 'static> {
519 shared: Rc<ChannelShared<T>>,
520}
521
522impl<T: 'static> Clone for EventStream<T> {
523 fn clone(&self) -> Self {
524 Self {
525 shared: Rc::clone(&self.shared),
526 }
527 }
528}
529
530impl<T: 'static> EventStream<T> {
531 pub fn next(&self) -> EventStreamNext<T> {
534 EventStreamNext {
535 shared: Rc::clone(&self.shared),
536 }
537 }
538
539 pub fn delivered(&self) -> usize {
541 self.shared.delivered.get()
542 }
543}
544
545pub struct EventStreamNext<T: 'static> {
547 shared: Rc<ChannelShared<T>>,
548}
549
550impl<T: 'static> Future for EventStreamNext<T> {
551 type Output = Option<T>;
552
553 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
554 if let Some(event) = self.shared.ready.borrow_mut().pop_front() {
555 self.shared.delivered.set(self.shared.delivered.get() + 1);
556 return Poll::Ready(Some(event));
557 }
558 if self.shared.closed.get() {
559 return Poll::Ready(None);
560 }
561 self.shared
562 .wakers
563 .borrow_mut()
564 .push(context.waker().clone());
565 Poll::Pending
566 }
567}
568
569#[expect(non_snake_case, reason = "names follow Compose's casing")]
575#[track_caller]
576pub fn CollectEvents<T, K>(stream: EventStream<T>, key: K, on_event: impl FnMut(T) + 'static)
577where
578 T: 'static,
579 K: PartialEq + 'static,
580{
581 crate::__launched_effect_async_impl(
582 crate::caller_location_key(),
583 std::panic::Location::caller().into(),
584 key,
585 move |_scope| {
586 let mut on_event = on_event;
587 Box::pin(async move {
588 while let Some(event) = stream.next().await {
589 on_event(event);
590 }
591 })
592 },
593 );
594}
595
596#[expect(non_snake_case, reason = "names follow Compose's casing")]
601#[track_caller]
602pub fn collectAsState<T, K>(stream: EventStream<T>, key: K, initial: T) -> State<T>
603where
604 T: Clone + 'static,
605 K: PartialEq + 'static,
606{
607 let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
608 let sink = state;
609 CollectEvents(stream, key, move |event| sink.set(event));
610 state.as_state()
611}
612
613pub struct EventSender<T: Send + 'static> {
621 #[cfg(not(target_arch = "wasm32"))]
622 dispatcher: crate::runtime::UiDispatcher,
623 bridge: u64,
624 _events: std::marker::PhantomData<fn(T)>,
625}
626
627impl<T: Send + 'static> Clone for EventSender<T> {
628 fn clone(&self) -> Self {
629 Self {
630 #[cfg(not(target_arch = "wasm32"))]
631 dispatcher: self.dispatcher.clone(),
632 bridge: self.bridge,
633 _events: std::marker::PhantomData,
634 }
635 }
636}
637
638impl<T: Send + 'static> EventSender<T> {
639 pub fn send(&self, event: T) {
641 let bridge = self.bridge;
642 #[cfg(not(target_arch = "wasm32"))]
643 self.dispatcher
644 .post(move || deliver_bridged::<T>(bridge, event));
645 #[cfg(target_arch = "wasm32")]
646 deliver_bridged::<T>(bridge, event);
647 }
648}
649
650fn bridge_channel(bridge: u64) -> Option<Rc<dyn Any>> {
651 crate::thread_state::runtimes(|state| state.bridges.borrow().get(&bridge).cloned())
652}
653
654static NEXT_BRIDGE: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(1);
655
656fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
657 let Some(channel) = bridge_channel(bridge) else {
658 log::debug!("event bridge {bridge} is gone, one event dropped");
659 return;
660 };
661 if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
662 channel.send(event);
663 }
664}
665
666struct Bridge<T: Send + 'static> {
667 id: u64,
668 channel: Rc<EventChannel<T>>,
669}
670
671impl<T: Send + 'static> Bridge<T> {
672 fn new() -> Self {
673 let id = NEXT_BRIDGE.fetch_add(1, Ordering::Relaxed);
674 let channel = Rc::new(EventChannel::<T>::new());
675 crate::thread_state::runtimes(|state| {
676 state
677 .bridges
678 .borrow_mut()
679 .insert(id, Rc::clone(&channel) as Rc<dyn Any>)
680 });
681 Self { id, channel }
682 }
683}
684
685impl<T: Send + 'static> Drop for Bridge<T> {
686 fn drop(&mut self) {
687 crate::thread_state::try_runtimes(|state| state.bridges.borrow_mut().remove(&self.id));
688 self.channel.close();
689 }
690}
691
692#[expect(non_snake_case, reason = "names follow Compose's casing")]
700#[track_caller]
701pub fn rememberEventStream<T, K, R, S>(key: K, subscribe: S) -> EventStream<T>
702where
703 T: Send + 'static,
704 K: PartialEq + 'static,
705 R: 'static,
706 S: FnOnce(EventSender<T>) -> R + 'static,
707{
708 let bridge = remember(Bridge::<T>::new);
709 let (id, stream) = bridge.with(|bridge| (bridge.id, bridge.channel.stream()));
710 #[cfg(not(target_arch = "wasm32"))]
711 let dispatcher = current_runtime_handle().map(|runtime| runtime.dispatcher());
712
713 crate::__disposable_effect_impl(crate::caller_location_key(), key, move |scope| {
714 #[cfg(not(target_arch = "wasm32"))]
715 let Some(dispatcher) = dispatcher else {
716 log::warn!("cranpose: an event stream was remembered without a runtime");
717 return scope.on_dispose(|| {});
718 };
719 let registration = subscribe(EventSender {
720 #[cfg(not(target_arch = "wasm32"))]
721 dispatcher,
722 bridge: id,
723 _events: std::marker::PhantomData,
724 });
725 scope.on_dispose(move || drop(registration))
726 });
727
728 stream
729}
730
731#[expect(non_snake_case, reason = "names follow Compose's casing")]
737#[track_caller]
738pub fn produceState<T, K, F>(initial: T, key: K, producer: F) -> State<T>
739where
740 T: Clone + 'static,
741 K: PartialEq + 'static,
742 F: FnOnce(ProduceScope<T>) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
743{
744 let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
745 let handle = ProduceScope { state };
746 crate::__launched_effect_async_impl(
747 crate::caller_location_key(),
748 std::panic::Location::caller().into(),
749 key,
750 move |_scope| producer(handle),
751 );
752 state.as_state()
753}
754
755pub struct ProduceScope<T: Clone + 'static> {
757 state: MutableState<T>,
758}
759
760impl<T: Clone + 'static> ProduceScope<T> {
761 pub fn set(&self, value: T) {
763 self.state.set(value);
764 }
765}
766
767#[cfg(test)]
768#[path = "tests/concurrency_tests.rs"]
769mod tests;
770
771#[cfg(test)]
772#[path = "tests/concurrency_stream_tests.rs"]
773mod stream_tests;
774
775#[cfg(test)]
776#[path = "tests/concurrency_timer_race_tests.rs"]
777mod timer_race_tests;