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 task_site::TaskLabel,
36};
37
38pub fn spawn_ui_task(future: impl Future<Output = ()> + 'static) -> Option<TaskHandle> {
44 current_runtime_handle().and_then(|runtime| runtime.spawn_ui(future))
45}
46
47#[derive(Clone)]
53pub struct CoroutineScope {
54 inner: Rc<ScopeInner>,
55}
56
57struct ScopeInner {
58 runtime: Option<RuntimeHandle>,
59 tasks: RefCell<Vec<TaskHandle>>,
60 closed: Cell<bool>,
61}
62
63impl Drop for ScopeInner {
64 fn drop(&mut self) {
65 for task in self.tasks.get_mut().drain(..) {
66 task.cancel();
67 }
68 }
69}
70
71struct CompositionScopeOwner(CoroutineScope);
72
73impl Drop for CompositionScopeOwner {
74 fn drop(&mut self) {
75 self.0.close();
76 }
77}
78
79impl CoroutineScope {
80 pub(crate) fn new(runtime: Option<RuntimeHandle>) -> Self {
81 Self {
82 inner: Rc::new(ScopeInner {
83 runtime,
84 tasks: RefCell::new(Vec::new()),
85 closed: Cell::new(false),
86 }),
87 }
88 }
89
90 pub fn launch(&self, future: impl Future<Output = ()> + 'static) {
93 self.launch_labelled(Box::pin(future), TaskLabel::Unnamed);
94 }
95
96 pub(crate) fn launch_labelled(
97 &self,
98 future: Pin<Box<dyn Future<Output = ()>>>,
99 label: TaskLabel,
100 ) {
101 if self.inner.closed.get() {
102 return;
103 }
104 let Some(runtime) = self.inner.runtime.as_ref() else {
105 log::warn!("cranpose: a coroutine scope with no runtime dropped its work");
106 return;
107 };
108 self.inner
109 .tasks
110 .borrow_mut()
111 .retain(|task| !task.is_finished());
112 if let Some(handle) = runtime.spawn_labelled(future, label) {
113 self.inner.tasks.borrow_mut().push(handle);
114 }
115 }
116
117 pub(crate) fn close(&self) {
119 self.inner.closed.set(true);
120 self.cancel();
121 }
122
123 pub fn cancel(&self) {
125 let tasks = std::mem::take(&mut *self.inner.tasks.borrow_mut());
126 for task in tasks {
127 task.cancel();
128 }
129 }
130
131 #[cfg(test)]
132 pub(crate) fn probe_identity(&self) -> usize {
133 Rc::as_ptr(&self.inner) as *const () as usize
134 }
135}
136
137#[expect(non_snake_case, reason = "names follow Compose's casing")]
139#[track_caller]
140pub fn rememberCoroutineScope() -> CoroutineScope {
141 remember(|| CompositionScopeOwner(CoroutineScope::new(current_runtime_handle())))
142 .with(|owner| owner.0.clone())
143}
144
145pub fn delay(duration: Duration) -> Delay {
151 Delay {
152 deadline: Instant::now() + duration,
153 armed: false,
154 fired: Arc::new(AtomicBool::new(false)),
155 }
156}
157
158pub struct Delay {
160 deadline: Instant,
161 armed: bool,
162 fired: Arc<AtomicBool>,
163}
164
165impl Future for Delay {
166 type Output = ();
167
168 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
169 if self.fired.load(Ordering::Acquire) || Instant::now() >= self.deadline {
170 return Poll::Ready(());
171 }
172 let this = self.get_mut();
173 if !this.armed {
174 this.armed = true;
175 arm(
176 this.deadline,
177 context.waker().clone(),
178 Arc::clone(&this.fired),
179 );
180 }
181 Poll::Pending
182 }
183}
184
185pub async fn interval(period: Duration, mut tick: impl FnMut()) {
190 loop {
191 delay(period).await;
192 tick();
193 }
194}
195
196#[cfg(not(any(
197 all(target_arch = "wasm32", target_os = "unknown"),
198 target_vendor = "apple"
199)))]
200pub(crate) struct Alarm {
201 deadline: Instant,
202 waker: Waker,
203 fired: Arc<AtomicBool>,
204}
205
206fn arm(deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
213 #[cfg(target_os = "espidf")]
214 if current_runtime_handle().is_some() {
215 arm_host_alarm(Alarm {
216 deadline,
217 waker,
218 fired,
219 });
220 return;
221 }
222 timer().arm(deadline, waker, fired);
223}
224
225struct Timer {
226 #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
227 alarms: Mutex<Vec<Alarm>>,
228 #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
229 wake: Condvar,
230}
231
232fn timer() -> &'static Timer {
233 static TIMER: OnceLock<&'static Timer> = OnceLock::new();
234 TIMER.get_or_init(|| {
235 let timer: &'static Timer = Box::leak(Box::new(Timer::new()));
236 timer.start();
237 timer
238 })
239}
240
241#[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
242impl Timer {
243 fn new() -> Self {
244 Self {
245 alarms: Mutex::new(Vec::new()),
246 wake: Condvar::new(),
247 }
248 }
249
250 fn start(&'static self) {
251 std::thread::Builder::new()
252 .name("cranpose-timer".to_string())
253 .spawn(move || self.run())
254 .expect("the timer thread starts");
255 }
256
257 fn run(&self) {
258 let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
259 loop {
260 let now = Instant::now();
261 let mut due = Vec::new();
262 let mut next: Option<Duration> = None;
263 alarms.retain(|alarm| {
264 if alarm.deadline <= now {
265 due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
266 false
267 } else {
268 let remaining = alarm.deadline - now;
269 next = Some(next.map_or(remaining, |current| current.min(remaining)));
270 true
271 }
272 });
273
274 if !due.is_empty() {
275 drop(alarms);
276 for (waker, fired) in due {
277 fired.store(true, Ordering::Release);
278 waker.wake();
279 }
280 alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
281 continue;
282 }
283
284 alarms = match next {
285 Some(timeout) => {
286 self.wake
287 .wait_timeout(alarms, timeout)
288 .unwrap_or_else(PoisonError::into_inner)
289 .0
290 }
291 None => self
292 .wake
293 .wait(alarms)
294 .unwrap_or_else(PoisonError::into_inner),
295 };
296 }
297 }
298
299 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
300 let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
301 alarms.push(Alarm {
302 deadline,
303 waker,
304 fired,
305 });
306 self.wake.notify_one();
307 }
308}
309
310#[cfg(target_vendor = "apple")]
316impl Timer {
317 fn new() -> Self {
318 Self {}
319 }
320
321 fn start(&'static self) {}
322
323 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
324 use dispatch2::{DispatchQoS, DispatchQueue, DispatchTime, GlobalQueueIdentifier};
325
326 let nanos = deadline
327 .saturating_duration_since(Instant::now())
328 .as_nanos()
329 .min(i64::MAX as u128) as i64;
330 let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::QualityOfService(
331 DispatchQoS::UserInteractive,
332 ));
333 let fire = move || {
334 fired.store(true, Ordering::Release);
335 waker.wake();
336 };
337 if queue.after(DispatchTime::NOW.time(nanos), fire).is_err() {
338 log::error!("cranpose: GCD refused a timer; the delay never resolves");
339 }
340 }
341}
342
343#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
344impl Timer {
345 fn new() -> Self {
346 Self {}
347 }
348
349 fn start(&'static self) {}
350
351 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
352 let millis = deadline
353 .saturating_duration_since(Instant::now())
354 .as_millis()
355 .min(i32::MAX as u128) as i32;
356 let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
357 fired.store(true, Ordering::Release);
358 waker.wake();
359 });
360 let scheduled = web_sys::window().and_then(|window| {
361 window
362 .set_timeout_with_callback_and_timeout_and_arguments_0(
363 callback.unchecked_ref(),
364 millis,
365 )
366 .ok()
367 });
368 if scheduled.is_none() {
369 log::warn!("cranpose: no window timer is available; the delay resolves immediately");
370 }
371 }
372}
373
374#[cfg(target_os = "emscripten")]
379impl Timer {
380 fn new() -> Self {
381 Self {}
382 }
383
384 fn start(&'static self) {}
385
386 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
387 arm_host_alarm(Alarm {
388 deadline,
389 waker,
390 fired,
391 });
392 }
393}
394
395#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
398fn arm_host_alarm(alarm: Alarm) {
399 crate::thread_state::runtimes(|state| state.alarms.borrow_mut().push(alarm));
400}
401
402pub fn next_alarm_deadline() -> Option<Instant> {
407 #[cfg(any(target_os = "emscripten", target_os = "espidf"))]
408 return crate::thread_state::try_runtimes(|state| {
409 state
410 .alarms
411 .borrow()
412 .iter()
413 .map(|alarm| alarm.deadline)
414 .min()
415 })
416 .flatten();
417 #[cfg(not(any(target_os = "emscripten", target_os = "espidf")))]
418 None
419}
420
421#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
424pub(crate) fn has_due_alarm() -> bool {
425 next_alarm_deadline().is_some_and(|deadline| deadline <= Instant::now())
426}
427
428#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
430pub(crate) fn fire_due_alarms() {
431 let now = Instant::now();
432 let due: Vec<Alarm> = crate::thread_state::runtimes(|state| {
433 let mut alarms = state.alarms.borrow_mut();
434 if alarms.iter().all(|alarm| alarm.deadline > now) {
435 return Vec::new();
436 }
437 let (due, waiting) = std::mem::take(&mut *alarms)
438 .into_iter()
439 .partition(|alarm| alarm.deadline <= now);
440 *alarms = waiting;
441 due
442 });
443 for alarm in due {
444 alarm.fired.store(true, Ordering::Release);
445 alarm.waker.wake();
446 }
447}
448
449pub struct EventChannel<T: 'static> {
456 shared: Rc<ChannelShared<T>>,
457}
458
459struct ChannelShared<T: 'static> {
460 ready: RefCell<std::collections::VecDeque<T>>,
461 closed: Cell<bool>,
462 delivered: Cell<usize>,
463 wakers: RefCell<Vec<Waker>>,
464}
465
466impl<T: 'static> ChannelShared<T> {
467 fn wake_all(&self) {
468 for waker in self.wakers.borrow_mut().drain(..) {
469 waker.wake();
470 }
471 }
472}
473
474impl<T: 'static> Default for EventChannel<T> {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480impl<T: 'static> EventChannel<T> {
481 pub fn new() -> Self {
483 Self {
484 shared: Rc::new(ChannelShared {
485 ready: RefCell::new(std::collections::VecDeque::new()),
486 closed: Cell::new(false),
487 delivered: Cell::new(0),
488 wakers: RefCell::new(Vec::new()),
489 }),
490 }
491 }
492
493 pub fn stream(&self) -> EventStream<T> {
495 EventStream {
496 shared: Rc::clone(&self.shared),
497 }
498 }
499
500 pub fn send(&self, event: T) {
502 if self.shared.closed.get() {
503 return;
504 }
505 self.shared.ready.borrow_mut().push_back(event);
506 self.shared.wake_all();
507 }
508
509 pub fn close(&self) {
511 if self.shared.closed.get() {
512 return;
513 }
514 self.shared.closed.set(true);
515 self.shared.wake_all();
516 }
517
518 pub fn is_closed(&self) -> bool {
520 self.shared.closed.get()
521 }
522
523 pub fn pending(&self) -> usize {
525 self.shared.ready.borrow().len()
526 }
527}
528
529pub struct EventStream<T: 'static> {
535 shared: Rc<ChannelShared<T>>,
536}
537
538impl<T: 'static> Clone for EventStream<T> {
539 fn clone(&self) -> Self {
540 Self {
541 shared: Rc::clone(&self.shared),
542 }
543 }
544}
545
546impl<T: 'static> EventStream<T> {
547 pub fn next(&self) -> EventStreamNext<T> {
550 EventStreamNext {
551 shared: Rc::clone(&self.shared),
552 }
553 }
554
555 pub fn delivered(&self) -> usize {
557 self.shared.delivered.get()
558 }
559}
560
561pub struct EventStreamNext<T: 'static> {
563 shared: Rc<ChannelShared<T>>,
564}
565
566impl<T: 'static> Future for EventStreamNext<T> {
567 type Output = Option<T>;
568
569 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
570 if let Some(event) = self.shared.ready.borrow_mut().pop_front() {
571 self.shared.delivered.set(self.shared.delivered.get() + 1);
572 return Poll::Ready(Some(event));
573 }
574 if self.shared.closed.get() {
575 return Poll::Ready(None);
576 }
577 self.shared
578 .wakers
579 .borrow_mut()
580 .push(context.waker().clone());
581 Poll::Pending
582 }
583}
584
585#[expect(non_snake_case, reason = "names follow Compose's casing")]
591#[track_caller]
592pub fn CollectEvents<T, K>(stream: EventStream<T>, key: K, on_event: impl FnMut(T) + 'static)
593where
594 T: 'static,
595 K: PartialEq + 'static,
596{
597 crate::__launched_effect_impl(
598 crate::caller_location_key(),
599 std::panic::Location::caller().into(),
600 key,
601 async move |_| {
602 let mut on_event = on_event;
603 while let Some(event) = stream.next().await {
604 on_event(event);
605 }
606 },
607 );
608}
609
610#[expect(non_snake_case, reason = "names follow Compose's casing")]
615#[track_caller]
616pub fn collectAsState<T, K>(stream: EventStream<T>, key: K, initial: T) -> State<T>
617where
618 T: Clone + 'static,
619 K: PartialEq + 'static,
620{
621 let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
622 let sink = state;
623 CollectEvents(stream, key, move |event| sink.set(event));
624 state.as_state()
625}
626
627pub struct EventSender<T: Send + 'static> {
635 #[cfg(not(target_arch = "wasm32"))]
636 dispatcher: crate::runtime::UiDispatcher,
637 bridge: u64,
638 _events: std::marker::PhantomData<fn(T)>,
639}
640
641impl<T: Send + 'static> Clone for EventSender<T> {
642 fn clone(&self) -> Self {
643 Self {
644 #[cfg(not(target_arch = "wasm32"))]
645 dispatcher: self.dispatcher.clone(),
646 bridge: self.bridge,
647 _events: std::marker::PhantomData,
648 }
649 }
650}
651
652impl<T: Send + 'static> EventSender<T> {
653 pub fn send(&self, event: T) {
655 let bridge = self.bridge;
656 #[cfg(not(target_arch = "wasm32"))]
657 self.dispatcher
658 .post(move || deliver_bridged::<T>(bridge, event));
659 #[cfg(target_arch = "wasm32")]
660 deliver_bridged::<T>(bridge, event);
661 }
662}
663
664fn bridge_channel(bridge: u64) -> Option<Rc<dyn Any>> {
665 crate::thread_state::runtimes(|state| state.bridges.borrow().get(&bridge).cloned())
666}
667
668static NEXT_BRIDGE: crate::ProcessIds = crate::ProcessIds::starting_at(1);
669
670fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
671 let Some(channel) = bridge_channel(bridge) else {
672 log::debug!("event bridge {bridge} is gone, one event dropped");
673 return;
674 };
675 if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
676 channel.send(event);
677 }
678}
679
680struct Bridge<T: Send + 'static> {
681 id: u64,
682 channel: Rc<EventChannel<T>>,
683}
684
685impl<T: Send + 'static> Bridge<T> {
686 fn new() -> Self {
687 let id = NEXT_BRIDGE.next();
688 let channel = Rc::new(EventChannel::<T>::new());
689 crate::thread_state::runtimes(|state| {
690 state
691 .bridges
692 .borrow_mut()
693 .insert(id, Rc::clone(&channel) as Rc<dyn Any>)
694 });
695 Self { id, channel }
696 }
697}
698
699impl<T: Send + 'static> Drop for Bridge<T> {
700 fn drop(&mut self) {
701 crate::thread_state::try_runtimes(|state| state.bridges.borrow_mut().remove(&self.id));
702 self.channel.close();
703 }
704}
705
706#[expect(non_snake_case, reason = "names follow Compose's casing")]
714#[track_caller]
715pub fn rememberEventStream<T, K, R, S>(key: K, subscribe: S) -> EventStream<T>
716where
717 T: Send + 'static,
718 K: PartialEq + 'static,
719 R: 'static,
720 S: FnOnce(EventSender<T>) -> R + 'static,
721{
722 let bridge = remember(Bridge::<T>::new);
723 let (id, stream) = bridge.with(|bridge| (bridge.id, bridge.channel.stream()));
724 #[cfg(not(target_arch = "wasm32"))]
725 let dispatcher = current_runtime_handle().map(|runtime| runtime.dispatcher());
726
727 crate::__disposable_effect_impl(crate::caller_location_key(), key, move |scope| {
728 #[cfg(not(target_arch = "wasm32"))]
729 let Some(dispatcher) = dispatcher else {
730 log::warn!("cranpose: an event stream was remembered without a runtime");
731 return scope.on_dispose(|| {});
732 };
733 let registration = subscribe(EventSender {
734 #[cfg(not(target_arch = "wasm32"))]
735 dispatcher,
736 bridge: id,
737 _events: std::marker::PhantomData,
738 });
739 scope.on_dispose(move || drop(registration))
740 });
741
742 stream
743}
744
745#[expect(non_snake_case, reason = "names follow Compose's casing")]
751#[track_caller]
752pub fn produceState<T, K, F, Fut>(initial: T, key: K, producer: F) -> State<T>
753where
754 T: Clone + 'static,
755 K: PartialEq + 'static,
756 F: FnOnce(ProduceScope<T>) -> Fut + 'static,
757 Fut: Future<Output = ()> + 'static,
758{
759 let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
760 let handle = ProduceScope { state };
761 crate::__launched_effect_impl(
762 crate::caller_location_key(),
763 std::panic::Location::caller().into(),
764 key,
765 move |_scope| producer(handle),
766 );
767 state.as_state()
768}
769
770pub struct ProduceScope<T: Clone + 'static> {
772 state: MutableState<T>,
773}
774
775impl<T: Clone + 'static> ProduceScope<T> {
776 pub fn set(&self, value: T) {
778 self.state.set(value);
779 }
780}
781
782#[cfg(test)]
783#[path = "tests/concurrency_tests.rs"]
784mod tests;
785
786#[cfg(test)]
787#[path = "tests/concurrency_stream_tests.rs"]
788mod stream_tests;
789
790#[cfg(test)]
791#[path = "tests/concurrency_timer_race_tests.rs"]
792mod timer_race_tests;