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(all(target_arch = "wasm32", target_os = "unknown"))]
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(
180 all(target_arch = "wasm32", target_os = "unknown"),
181 target_vendor = "apple"
182)))]
183pub(crate) struct Alarm {
184 deadline: Instant,
185 waker: Waker,
186 fired: Arc<AtomicBool>,
187}
188
189struct Timer {
190 #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
191 alarms: Mutex<Vec<Alarm>>,
192 #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
193 wake: Condvar,
194}
195
196fn timer() -> &'static Timer {
197 static TIMER: OnceLock<&'static Timer> = OnceLock::new();
198 TIMER.get_or_init(|| {
199 let timer: &'static Timer = Box::leak(Box::new(Timer::new()));
200 timer.start();
201 timer
202 })
203}
204
205#[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
206impl Timer {
207 fn new() -> Self {
208 Self {
209 alarms: Mutex::new(Vec::new()),
210 wake: Condvar::new(),
211 }
212 }
213
214 fn start(&'static self) {
215 std::thread::Builder::new()
216 .name("cranpose-timer".to_string())
217 .spawn(move || self.run())
218 .expect("the timer thread starts");
219 }
220
221 fn run(&self) {
222 let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
223 loop {
224 let now = Instant::now();
225 let mut due = Vec::new();
226 let mut next: Option<Duration> = None;
227 alarms.retain(|alarm| {
228 if alarm.deadline <= now {
229 due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
230 false
231 } else {
232 let remaining = alarm.deadline - now;
233 next = Some(next.map_or(remaining, |current| current.min(remaining)));
234 true
235 }
236 });
237
238 if !due.is_empty() {
239 drop(alarms);
240 for (waker, fired) in due {
241 fired.store(true, Ordering::Release);
242 waker.wake();
243 }
244 alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
245 continue;
246 }
247
248 alarms = match next {
249 Some(timeout) => {
250 self.wake
251 .wait_timeout(alarms, timeout)
252 .unwrap_or_else(PoisonError::into_inner)
253 .0
254 }
255 None => self
256 .wake
257 .wait(alarms)
258 .unwrap_or_else(PoisonError::into_inner),
259 };
260 }
261 }
262
263 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
264 let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
265 alarms.push(Alarm {
266 deadline,
267 waker,
268 fired,
269 });
270 self.wake.notify_one();
271 }
272}
273
274#[cfg(target_vendor = "apple")]
280impl Timer {
281 fn new() -> Self {
282 Self {}
283 }
284
285 fn start(&'static self) {}
286
287 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
288 use dispatch2::{DispatchQoS, DispatchQueue, DispatchTime, GlobalQueueIdentifier};
289
290 let nanos = deadline
291 .saturating_duration_since(Instant::now())
292 .as_nanos()
293 .min(i64::MAX as u128) as i64;
294 let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::QualityOfService(
295 DispatchQoS::UserInteractive,
296 ));
297 let fire = move || {
298 fired.store(true, Ordering::Release);
299 waker.wake();
300 };
301 if queue.after(DispatchTime::NOW.time(nanos), fire).is_err() {
302 log::error!("cranpose: GCD refused a timer; the delay never resolves");
303 }
304 }
305}
306
307#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
308impl Timer {
309 fn new() -> Self {
310 Self {}
311 }
312
313 fn start(&'static self) {}
314
315 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
316 let millis = deadline
317 .saturating_duration_since(Instant::now())
318 .as_millis()
319 .min(i32::MAX as u128) as i32;
320 let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
321 fired.store(true, Ordering::Release);
322 waker.wake();
323 });
324 let scheduled = web_sys::window().and_then(|window| {
325 window
326 .set_timeout_with_callback_and_timeout_and_arguments_0(
327 callback.unchecked_ref(),
328 millis,
329 )
330 .ok()
331 });
332 if scheduled.is_none() {
333 log::warn!("cranpose: no window timer is available; the delay resolves immediately");
334 }
335 }
336}
337
338#[cfg(target_os = "emscripten")]
343impl Timer {
344 fn new() -> Self {
345 Self {}
346 }
347
348 fn start(&'static self) {}
349
350 fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
351 crate::thread_state::runtimes(|state| {
352 state.alarms.borrow_mut().push(Alarm {
353 deadline,
354 waker,
355 fired,
356 })
357 });
358 }
359}
360
361#[cfg(target_os = "emscripten")]
363pub(crate) fn fire_due_alarms() {
364 let now = Instant::now();
365 let due: Vec<Alarm> = crate::thread_state::runtimes(|state| {
366 let mut alarms = state.alarms.borrow_mut();
367 if alarms.iter().all(|alarm| alarm.deadline > now) {
368 return Vec::new();
369 }
370 let (due, waiting) = std::mem::take(&mut *alarms)
371 .into_iter()
372 .partition(|alarm| alarm.deadline <= now);
373 *alarms = waiting;
374 due
375 });
376 for alarm in due {
377 alarm.fired.store(true, Ordering::Release);
378 alarm.waker.wake();
379 }
380}
381
382pub struct EventChannel<T: 'static> {
389 shared: Rc<ChannelShared<T>>,
390}
391
392struct ChannelShared<T: 'static> {
393 ready: RefCell<std::collections::VecDeque<T>>,
394 closed: std::cell::Cell<bool>,
395 delivered: std::cell::Cell<usize>,
396 wakers: RefCell<Vec<Waker>>,
397}
398
399impl<T: 'static> ChannelShared<T> {
400 fn wake_all(&self) {
401 for waker in self.wakers.borrow_mut().drain(..) {
402 waker.wake();
403 }
404 }
405}
406
407impl<T: 'static> Default for EventChannel<T> {
408 fn default() -> Self {
409 Self::new()
410 }
411}
412
413impl<T: 'static> EventChannel<T> {
414 pub fn new() -> Self {
416 Self {
417 shared: Rc::new(ChannelShared {
418 ready: RefCell::new(std::collections::VecDeque::new()),
419 closed: std::cell::Cell::new(false),
420 delivered: std::cell::Cell::new(0),
421 wakers: RefCell::new(Vec::new()),
422 }),
423 }
424 }
425
426 pub fn stream(&self) -> EventStream<T> {
428 EventStream {
429 shared: Rc::clone(&self.shared),
430 }
431 }
432
433 pub fn send(&self, event: T) {
435 if self.shared.closed.get() {
436 return;
437 }
438 self.shared.ready.borrow_mut().push_back(event);
439 self.shared.wake_all();
440 }
441
442 pub fn close(&self) {
444 if self.shared.closed.get() {
445 return;
446 }
447 self.shared.closed.set(true);
448 self.shared.wake_all();
449 }
450
451 pub fn is_closed(&self) -> bool {
453 self.shared.closed.get()
454 }
455
456 pub fn pending(&self) -> usize {
458 self.shared.ready.borrow().len()
459 }
460}
461
462pub struct EventStream<T: 'static> {
468 shared: Rc<ChannelShared<T>>,
469}
470
471impl<T: 'static> Clone for EventStream<T> {
472 fn clone(&self) -> Self {
473 Self {
474 shared: Rc::clone(&self.shared),
475 }
476 }
477}
478
479impl<T: 'static> EventStream<T> {
480 pub fn next(&self) -> EventStreamNext<T> {
483 EventStreamNext {
484 shared: Rc::clone(&self.shared),
485 }
486 }
487
488 pub fn delivered(&self) -> usize {
490 self.shared.delivered.get()
491 }
492}
493
494pub struct EventStreamNext<T: 'static> {
496 shared: Rc<ChannelShared<T>>,
497}
498
499impl<T: 'static> Future for EventStreamNext<T> {
500 type Output = Option<T>;
501
502 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
503 if let Some(event) = self.shared.ready.borrow_mut().pop_front() {
504 self.shared.delivered.set(self.shared.delivered.get() + 1);
505 return Poll::Ready(Some(event));
506 }
507 if self.shared.closed.get() {
508 return Poll::Ready(None);
509 }
510 self.shared
511 .wakers
512 .borrow_mut()
513 .push(context.waker().clone());
514 Poll::Pending
515 }
516}
517
518#[expect(non_snake_case)]
524#[track_caller]
525pub fn CollectEvents<T, K>(stream: EventStream<T>, key: K, on_event: impl FnMut(T) + 'static)
526where
527 T: 'static,
528 K: PartialEq + 'static,
529{
530 crate::__launched_effect_async_impl(
531 crate::caller_location_key(),
532 std::panic::Location::caller().into(),
533 key,
534 move |_scope| {
535 let mut on_event = on_event;
536 Box::pin(async move {
537 while let Some(event) = stream.next().await {
538 on_event(event);
539 }
540 })
541 },
542 );
543}
544
545#[expect(non_snake_case)]
550#[track_caller]
551pub fn collectAsState<T, K>(stream: EventStream<T>, key: K, initial: T) -> State<T>
552where
553 T: Clone + 'static,
554 K: PartialEq + 'static,
555{
556 let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
557 let sink = state;
558 CollectEvents(stream, key, move |event| sink.set(event));
559 state.as_state()
560}
561
562pub struct EventSender<T: Send + 'static> {
570 #[cfg(not(target_arch = "wasm32"))]
571 dispatcher: crate::runtime::UiDispatcher,
572 bridge: u64,
573 _events: std::marker::PhantomData<fn(T)>,
574}
575
576impl<T: Send + 'static> Clone for EventSender<T> {
577 fn clone(&self) -> Self {
578 Self {
579 #[cfg(not(target_arch = "wasm32"))]
580 dispatcher: self.dispatcher.clone(),
581 bridge: self.bridge,
582 _events: std::marker::PhantomData,
583 }
584 }
585}
586
587impl<T: Send + 'static> EventSender<T> {
588 pub fn send(&self, event: T) {
590 let bridge = self.bridge;
591 #[cfg(not(target_arch = "wasm32"))]
592 self.dispatcher
593 .post(move || deliver_bridged::<T>(bridge, event));
594 #[cfg(target_arch = "wasm32")]
595 deliver_bridged::<T>(bridge, event);
596 }
597}
598
599static NEXT_BRIDGE: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(1);
600
601fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
602 let channel =
603 crate::thread_state::runtimes(|state| state.bridges.borrow().get(&bridge).cloned());
604 let Some(channel) = channel else {
605 log::debug!("event bridge {bridge} is gone, one event dropped");
606 return;
607 };
608 if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
609 channel.send(event);
610 }
611}
612
613struct Bridge<T: Send + 'static> {
614 id: u64,
615 channel: Rc<EventChannel<T>>,
616}
617
618impl<T: Send + 'static> Bridge<T> {
619 fn new() -> Self {
620 let id = NEXT_BRIDGE.fetch_add(1, Ordering::Relaxed);
621 let channel = Rc::new(EventChannel::<T>::new());
622 crate::thread_state::runtimes(|state| {
623 state
624 .bridges
625 .borrow_mut()
626 .insert(id, Rc::clone(&channel) as Rc<dyn std::any::Any>)
627 });
628 Self { id, channel }
629 }
630}
631
632impl<T: Send + 'static> Drop for Bridge<T> {
633 fn drop(&mut self) {
634 crate::thread_state::try_runtimes(|state| state.bridges.borrow_mut().remove(&self.id));
635 self.channel.close();
636 }
637}
638
639#[expect(non_snake_case)]
647#[track_caller]
648pub fn rememberEventStream<T, K, R, S>(key: K, subscribe: S) -> EventStream<T>
649where
650 T: Send + 'static,
651 K: PartialEq + 'static,
652 R: 'static,
653 S: FnOnce(EventSender<T>) -> R + 'static,
654{
655 let bridge = remember(Bridge::<T>::new);
656 let (id, stream) = bridge.with(|bridge| (bridge.id, bridge.channel.stream()));
657 #[cfg(not(target_arch = "wasm32"))]
658 let dispatcher = current_runtime_handle().map(|runtime| runtime.dispatcher());
659
660 crate::__disposable_effect_impl(crate::caller_location_key(), key, move |scope| {
661 #[cfg(not(target_arch = "wasm32"))]
662 let Some(dispatcher) = dispatcher else {
663 log::warn!("cranpose: an event stream was remembered without a runtime");
664 return scope.on_dispose(|| {});
665 };
666 let registration = subscribe(EventSender {
667 #[cfg(not(target_arch = "wasm32"))]
668 dispatcher,
669 bridge: id,
670 _events: std::marker::PhantomData,
671 });
672 scope.on_dispose(move || drop(registration))
673 });
674
675 stream
676}
677
678#[expect(non_snake_case)]
684#[track_caller]
685pub fn produceState<T, K, F>(initial: T, key: K, producer: F) -> State<T>
686where
687 T: Clone + 'static,
688 K: PartialEq + 'static,
689 F: FnOnce(ProduceScope<T>) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
690{
691 let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
692 let handle = ProduceScope { state };
693 crate::__launched_effect_async_impl(
694 crate::caller_location_key(),
695 std::panic::Location::caller().into(),
696 key,
697 move |_scope| producer(handle),
698 );
699 state.as_state()
700}
701
702pub struct ProduceScope<T: Clone + 'static> {
704 state: MutableState<T>,
705}
706
707impl<T: Clone + 'static> ProduceScope<T> {
708 pub fn set(&self, value: T) {
710 self.state.set(value);
711 }
712}
713
714#[cfg(test)]
715#[path = "tests/concurrency_tests.rs"]
716mod tests;
717
718#[cfg(test)]
719#[path = "tests/concurrency_stream_tests.rs"]
720mod stream_tests;
721
722#[cfg(test)]
723#[path = "tests/concurrency_timer_race_tests.rs"]
724mod timer_race_tests;