Skip to main content

cranpose_core/
concurrency.rs

1//! Composition-scoped structured concurrency.
2//!
3//! `LaunchedEffect` covers work that starts because a key changed. This covers
4//! the rest: work started from an event handler, timed work, work that feeds a
5//! piece of state, and blocking work that must not run on the UI thread.
6//! Everything here is owned by the composition — a scope cancels its tasks when
7//! it leaves, and a timer stops when nothing is waiting on it — so an
8//! application never keeps its own task list or its own "is this still alive"
9//! flag.
10
11use crate::hooks::{mutableStateOf, remember};
12use crate::runtime::{current_runtime_handle, RuntimeHandle, TaskHandle};
13use crate::state::{MutableState, State};
14use std::cell::RefCell;
15use std::future::Future;
16use std::pin::Pin;
17use std::rc::Rc;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::{Arc, OnceLock};
20#[cfg(not(target_arch = "wasm32"))]
21use std::sync::{Condvar, Mutex};
22use std::task::{Context, Poll, Waker};
23use std::time::Duration;
24#[cfg(target_arch = "wasm32")]
25use wasm_bindgen::JsCast;
26use web_time::Instant;
27
28/// Spawns `future` on the current runtime's UI task queue.
29///
30/// Framework-internal: application code launches through a
31/// [`CoroutineScope`](CoroutineScope) so the work is cancelled with its
32/// composition. Returns `None` when there is no runtime on this thread.
33pub fn spawn_ui_task(future: impl Future<Output = ()> + 'static) -> Option<TaskHandle> {
34    current_runtime_handle().and_then(|runtime| runtime.spawn_ui(future))
35}
36
37/// A cancellation scope for work launched outside the composition pass.
38///
39/// Tasks launched through a scope are cancelled when the scope leaves the
40/// composition, so a click handler can start an asynchronous job without the
41/// job outliving the screen that started it.
42#[derive(Clone)]
43pub struct CoroutineScope {
44    inner: Rc<ScopeInner>,
45}
46
47struct ScopeInner {
48    runtime: Option<RuntimeHandle>,
49    tasks: RefCell<Vec<TaskHandle>>,
50}
51
52impl Drop for ScopeInner {
53    fn drop(&mut self) {
54        for task in self.tasks.borrow_mut().drain(..) {
55            task.cancel();
56        }
57    }
58}
59
60impl CoroutineScope {
61    /// Launches `future`, keeping it alive until it finishes or the scope is
62    /// cancelled.
63    pub fn launch(&self, future: impl Future<Output = ()> + 'static) {
64        let Some(runtime) = self.inner.runtime.clone() else {
65            log::warn!("cranpose: a coroutine scope with no runtime dropped its work");
66            return;
67        };
68        // Finished tasks are pruned on every launch so a long-lived screen that
69        // starts many short jobs does not accumulate handles.
70        self.inner
71            .tasks
72            .borrow_mut()
73            .retain(|task| !task.is_finished());
74        if let Some(handle) = runtime.spawn_ui(future) {
75            self.inner.tasks.borrow_mut().push(handle);
76        }
77    }
78
79    /// Cancels every task this scope launched.
80    pub fn cancel(&self) {
81        for task in self.inner.tasks.borrow_mut().drain(..) {
82            task.cancel();
83        }
84    }
85}
86
87/// Remembers a [`CoroutineScope`] bound to this position in the composition.
88#[allow(non_snake_case)]
89pub fn rememberCoroutineScope() -> CoroutineScope {
90    remember(|| CoroutineScope {
91        inner: Rc::new(ScopeInner {
92            runtime: current_runtime_handle(),
93            tasks: RefCell::new(Vec::new()),
94        }),
95    })
96    .with(|scope| scope.clone())
97}
98
99// ---- Timers --------------------------------------------------------------
100
101/// Resolves after `duration` has elapsed.
102///
103/// The wait is served by the framework's timer, which posts the wake-up onto
104/// the runtime's UI queue. Nothing spins and no frames are requested while a
105/// delay is pending, so a one-minute timer costs nothing for a minute.
106pub fn delay(duration: Duration) -> Delay {
107    Delay {
108        deadline: Instant::now() + duration,
109        armed: false,
110        fired: Arc::new(AtomicBool::new(false)),
111    }
112}
113
114/// The future returned by [`delay`].
115pub struct Delay {
116    deadline: Instant,
117    armed: bool,
118    fired: Arc<AtomicBool>,
119}
120
121impl Future for Delay {
122    type Output = ();
123
124    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
125        if self.fired.load(Ordering::Acquire) || Instant::now() >= self.deadline {
126            return Poll::Ready(());
127        }
128        let this = self.get_mut();
129        if !this.armed {
130            this.armed = true;
131            timer().arm(
132                this.deadline,
133                context.waker().clone(),
134                Arc::clone(&this.fired),
135            );
136        }
137        Poll::Pending
138    }
139}
140
141/// Runs `tick` every `period` until the returned future is dropped.
142///
143/// The first tick happens after one full period, matching a repeating timer
144/// rather than a leading-edge one.
145pub async fn interval(period: Duration, mut tick: impl FnMut()) {
146    loop {
147        delay(period).await;
148        tick();
149    }
150}
151
152/// A wake-up the timer owes a waiting future.
153#[cfg(not(target_arch = "wasm32"))]
154struct Alarm {
155    deadline: Instant,
156    waker: Waker,
157    fired: Arc<AtomicBool>,
158}
159
160/// One process-wide timer serving every [`Delay`].
161///
162/// On a threaded target it owns a single thread parked on a condition variable
163/// until the earliest deadline, so an idle application has one sleeping thread
164/// and no periodic work at all. On the web, where there is only one thread, each
165/// alarm is handed to the browser's own `setTimeout`.
166struct Timer {
167    #[cfg(not(target_arch = "wasm32"))]
168    alarms: Mutex<Vec<Alarm>>,
169    #[cfg(not(target_arch = "wasm32"))]
170    wake: Condvar,
171}
172
173fn timer() -> &'static Timer {
174    static TIMER: OnceLock<&'static Timer> = OnceLock::new();
175    TIMER.get_or_init(|| {
176        let timer: &'static Timer = Box::leak(Box::new(Timer::new()));
177        timer.start();
178        timer
179    })
180}
181
182#[cfg(not(target_arch = "wasm32"))]
183impl Timer {
184    fn new() -> Self {
185        Self {
186            alarms: Mutex::new(Vec::new()),
187            wake: Condvar::new(),
188        }
189    }
190
191    fn start(&'static self) {
192        std::thread::Builder::new()
193            .name("cranpose-timer".to_string())
194            .spawn(move || self.run())
195            .expect("the timer thread starts");
196    }
197
198    /// Fires alarms as they come due, sleeping until the next deadline.
199    ///
200    /// The lock is held continuously from taking the due alarms to entering the
201    /// wait, and `wait` releases it atomically. That is what makes an [`arm`]
202    /// racing this loop impossible to miss: it cannot take the lock until the
203    /// thread is already waiting, and the notify then lands.
204    ///
205    /// [`arm`]: Timer::arm
206    fn run(&self) {
207        let mut alarms = self
208            .alarms
209            .lock()
210            .unwrap_or_else(|error| error.into_inner());
211        loop {
212            let now = Instant::now();
213            let mut due = Vec::new();
214            let mut next: Option<Duration> = None;
215            alarms.retain(|alarm| {
216                if alarm.deadline <= now {
217                    due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
218                    false
219                } else {
220                    let remaining = alarm.deadline - now;
221                    next = Some(next.map_or(remaining, |current| current.min(remaining)));
222                    true
223                }
224            });
225
226            if !due.is_empty() {
227                // Waking runs application code, so the lock is released first;
228                // the loop then re-checks rather than sleeping on stale state.
229                drop(alarms);
230                for (waker, fired) in due {
231                    fired.store(true, Ordering::Release);
232                    waker.wake();
233                }
234                alarms = self
235                    .alarms
236                    .lock()
237                    .unwrap_or_else(|error| error.into_inner());
238                continue;
239            }
240
241            alarms = match next {
242                Some(timeout) => {
243                    self.wake
244                        .wait_timeout(alarms, timeout)
245                        .unwrap_or_else(|error| error.into_inner())
246                        .0
247                }
248                None => self
249                    .wake
250                    .wait(alarms)
251                    .unwrap_or_else(|error| error.into_inner()),
252            };
253        }
254    }
255
256    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
257        let mut alarms = self
258            .alarms
259            .lock()
260            .unwrap_or_else(|error| error.into_inner());
261        alarms.push(Alarm {
262            deadline,
263            waker,
264            fired,
265        });
266        self.wake.notify_one();
267    }
268}
269
270#[cfg(target_arch = "wasm32")]
271impl Timer {
272    fn new() -> Self {
273        Self {}
274    }
275
276    fn start(&'static self) {}
277
278    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
279        let millis = deadline
280            .saturating_duration_since(Instant::now())
281            .as_millis()
282            .min(i32::MAX as u128) as i32;
283        let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
284            fired.store(true, Ordering::Release);
285            waker.wake();
286        });
287        let scheduled = web_sys::window().and_then(|window| {
288            window
289                .set_timeout_with_callback_and_timeout_and_arguments_0(
290                    callback.unchecked_ref(),
291                    millis,
292                )
293                .ok()
294        });
295        if scheduled.is_none() {
296            log::warn!("cranpose: no window timer is available; the delay resolves immediately");
297        }
298    }
299}
300
301// ---- Event streams -------------------------------------------------------
302
303/// The producing half of an [`EventStream`].
304///
305/// A service that receives events from outside the composition — a platform
306/// callback, a worker thread, a socket — publishes through a channel, and every
307/// pending collector is woken. This is the shape that replaces
308/// "register an observer, then drain a queue" everywhere in the framework.
309pub struct EventChannel<T: 'static> {
310    shared: Rc<ChannelShared<T>>,
311}
312
313struct ChannelShared<T: 'static> {
314    ready: RefCell<std::collections::VecDeque<T>>,
315    closed: std::cell::Cell<bool>,
316    delivered: std::cell::Cell<usize>,
317    wakers: RefCell<Vec<Waker>>,
318}
319
320impl<T: 'static> ChannelShared<T> {
321    fn wake_all(&self) {
322        for waker in self.wakers.borrow_mut().drain(..) {
323            waker.wake();
324        }
325    }
326}
327
328impl<T: 'static> Default for EventChannel<T> {
329    fn default() -> Self {
330        Self::new()
331    }
332}
333
334impl<T: 'static> EventChannel<T> {
335    /// Creates an open channel.
336    pub fn new() -> Self {
337        Self {
338            shared: Rc::new(ChannelShared {
339                ready: RefCell::new(std::collections::VecDeque::new()),
340                closed: std::cell::Cell::new(false),
341                delivered: std::cell::Cell::new(0),
342                wakers: RefCell::new(Vec::new()),
343            }),
344        }
345    }
346
347    /// The consuming half, handed to collectors.
348    pub fn stream(&self) -> EventStream<T> {
349        EventStream {
350            shared: Rc::clone(&self.shared),
351        }
352    }
353
354    /// Publishes one event and wakes every pending collector.
355    pub fn send(&self, event: T) {
356        if self.shared.closed.get() {
357            return;
358        }
359        self.shared.ready.borrow_mut().push_back(event);
360        self.shared.wake_all();
361    }
362
363    /// Ends the stream. Collectors drain what is queued and then finish.
364    pub fn close(&self) {
365        if self.shared.closed.get() {
366            return;
367        }
368        self.shared.closed.set(true);
369        self.shared.wake_all();
370    }
371
372    /// Whether the channel has been closed.
373    pub fn is_closed(&self) -> bool {
374        self.shared.closed.get()
375    }
376
377    /// How many events are queued but not yet taken.
378    pub fn pending(&self) -> usize {
379        self.shared.ready.borrow().len()
380    }
381}
382
383/// The consuming half of an [`EventChannel`].
384///
385/// Collectors take events one at a time; an event goes to exactly one
386/// collector, so two collectors share the stream rather than each seeing every
387/// event.
388pub struct EventStream<T: 'static> {
389    shared: Rc<ChannelShared<T>>,
390}
391
392impl<T: 'static> Clone for EventStream<T> {
393    fn clone(&self) -> Self {
394        Self {
395            shared: Rc::clone(&self.shared),
396        }
397    }
398}
399
400impl<T: 'static> EventStream<T> {
401    /// Resolves with the next event, or `None` once the stream is closed and
402    /// drained.
403    pub fn next(&self) -> EventStreamNext<T> {
404        EventStreamNext {
405            shared: Rc::clone(&self.shared),
406        }
407    }
408
409    /// How many events this stream has handed out.
410    pub fn delivered(&self) -> usize {
411        self.shared.delivered.get()
412    }
413}
414
415/// The future returned by [`EventStream::next`].
416pub struct EventStreamNext<T: 'static> {
417    shared: Rc<ChannelShared<T>>,
418}
419
420impl<T: 'static> Future for EventStreamNext<T> {
421    type Output = Option<T>;
422
423    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
424        if let Some(event) = self.shared.ready.borrow_mut().pop_front() {
425            self.shared.delivered.set(self.shared.delivered.get() + 1);
426            return Poll::Ready(Some(event));
427        }
428        if self.shared.closed.get() {
429            return Poll::Ready(None);
430        }
431        self.shared
432            .wakers
433            .borrow_mut()
434            .push(context.waker().clone());
435        Poll::Pending
436    }
437}
438
439/// Collects `stream` for as long as this call stays in the composition,
440/// handing each event to `on_event`.
441///
442/// `key` re-starts the collection when it changes, exactly like
443/// `LaunchedEffect`.
444#[allow(non_snake_case)]
445#[track_caller]
446pub fn CollectEvents<T, K>(stream: EventStream<T>, key: K, on_event: impl FnMut(T) + 'static)
447where
448    T: 'static,
449    K: PartialEq + 'static,
450{
451    crate::__launched_effect_async_impl(
452        crate::location_key(file!(), line!(), column!()),
453        std::panic::Location::caller().into(),
454        key,
455        move |_scope| {
456            let mut on_event = on_event;
457            Box::pin(async move {
458                while let Some(event) = stream.next().await {
459                    on_event(event);
460                }
461            })
462        },
463    );
464}
465
466/// Collects `stream` into state, starting at `initial`.
467///
468/// The composition reads the latest value the stream produced, and recomposes
469/// when a new one arrives.
470#[allow(non_snake_case)]
471#[track_caller]
472pub fn collectAsState<T, K>(stream: EventStream<T>, key: K, initial: T) -> State<T>
473where
474    T: Clone + 'static,
475    K: PartialEq + 'static,
476{
477    let state = remember(|| mutableStateOf(initial)).with(|state| *state);
478    let sink = state;
479    CollectEvents(stream, key, move |event| sink.set(event));
480    state.as_state()
481}
482
483// ---- Cross-thread event bridges ------------------------------------------
484
485/// A `Send` publishing handle for a composition-scoped [`EventStream`].
486///
487/// Platform services publish events from whatever thread they run on — a JNI
488/// callback, a worker, a socket reader. The sender hops each event onto the UI
489/// thread through the runtime's dispatcher and pushes it into the stream the
490/// composition is collecting, so no service and no application ever writes that
491/// hop again.
492pub struct EventSender<T: Send + 'static> {
493    /// The hop onto the UI thread. The web runs the whole application on one
494    /// thread, so there is nothing to hop and no dispatcher to carry.
495    #[cfg(not(target_arch = "wasm32"))]
496    dispatcher: crate::runtime::UiDispatcher,
497    bridge: u64,
498    _events: std::marker::PhantomData<fn(T)>,
499}
500
501impl<T: Send + 'static> Clone for EventSender<T> {
502    fn clone(&self) -> Self {
503        Self {
504            #[cfg(not(target_arch = "wasm32"))]
505            dispatcher: self.dispatcher.clone(),
506            bridge: self.bridge,
507            _events: std::marker::PhantomData,
508        }
509    }
510}
511
512impl<T: Send + 'static> EventSender<T> {
513    /// Publishes `event` to the composition that owns this bridge.
514    pub fn send(&self, event: T) {
515        let bridge = self.bridge;
516        #[cfg(not(target_arch = "wasm32"))]
517        self.dispatcher
518            .post(move || deliver_bridged::<T>(bridge, event));
519        #[cfg(target_arch = "wasm32")]
520        deliver_bridged::<T>(bridge, event);
521    }
522}
523
524thread_local! {
525    /// Live bridges on this UI thread, by id. A bridge is registered while its
526    /// composition holds it and removed when that slot is discarded.
527    static BRIDGES: RefCell<std::collections::HashMap<u64, Rc<dyn std::any::Any>>> =
528        RefCell::new(std::collections::HashMap::new());
529}
530
531static NEXT_BRIDGE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
532
533fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
534    let channel = BRIDGES.with(|bridges| bridges.borrow().get(&bridge).cloned());
535    let Some(channel) = channel else {
536        // The composition that asked for these events is gone; dropping the
537        // event here is the whole point of scoping the stream to it.
538        return;
539    };
540    if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
541        channel.send(event);
542    }
543}
544
545/// The composition-owned half of a bridge: a channel registered for delivery
546/// for exactly as long as the composition holds it.
547struct Bridge<T: Send + 'static> {
548    id: u64,
549    channel: Rc<EventChannel<T>>,
550}
551
552impl<T: Send + 'static> Bridge<T> {
553    fn new() -> Self {
554        let id = NEXT_BRIDGE.fetch_add(1, Ordering::Relaxed);
555        let channel = Rc::new(EventChannel::<T>::new());
556        BRIDGES.with(|bridges| {
557            bridges
558                .borrow_mut()
559                .insert(id, Rc::clone(&channel) as Rc<dyn std::any::Any>)
560        });
561        Self { id, channel }
562    }
563}
564
565impl<T: Send + 'static> Drop for Bridge<T> {
566    fn drop(&mut self) {
567        BRIDGES.with(|bridges| bridges.borrow_mut().remove(&self.id));
568        self.channel.close();
569    }
570}
571
572/// Turns a platform subscription into a composition-scoped [`EventStream`].
573///
574/// `subscribe` receives a `Send` [`EventSender`] and returns whatever
575/// registration handle the service uses; that handle is dropped — unsubscribing
576/// the service — when `key` changes or the composition leaves. This is the one
577/// place the framework bridges "a service publishes from another thread" to
578/// "a composition collects".
579#[allow(non_snake_case)]
580pub fn rememberEventStream<T, K, R, S>(key: K, subscribe: S) -> EventStream<T>
581where
582    T: Send + 'static,
583    K: PartialEq + 'static,
584    R: 'static,
585    S: FnOnce(EventSender<T>) -> R + 'static,
586{
587    let bridge = remember(Bridge::<T>::new);
588    let (id, stream) = bridge.with(|bridge| (bridge.id, bridge.channel.stream()));
589    #[cfg(not(target_arch = "wasm32"))]
590    let dispatcher = current_runtime_handle().map(|runtime| runtime.dispatcher());
591
592    crate::__disposable_effect_impl(
593        crate::location_key(file!(), line!(), column!()),
594        key,
595        move |scope| {
596            #[cfg(not(target_arch = "wasm32"))]
597            let Some(dispatcher) = dispatcher
598            else {
599                log::warn!("cranpose: an event stream was remembered without a runtime");
600                return scope.on_dispose(|| {});
601            };
602            let registration = subscribe(EventSender {
603                #[cfg(not(target_arch = "wasm32"))]
604                dispatcher,
605                bridge: id,
606                _events: std::marker::PhantomData,
607            });
608            scope.on_dispose(move || drop(registration))
609        },
610    );
611
612    stream
613}
614
615// ---- Blocking work -------------------------------------------------------
616
617/// Runs `work` off the UI thread and resolves with its result on the UI thread.
618///
619/// This is the escape hatch for genuinely blocking work — parsing a large file,
620/// a synchronous provider call — that must not stall composition. On the web
621/// there is one thread, so `work` runs inline; callers keep the unit of work
622/// small enough that this is honest on every target.
623#[allow(non_snake_case)]
624pub async fn withBlocking<T, F>(work: F) -> T
625where
626    T: Send + 'static,
627    F: FnOnce() -> T + Send + 'static,
628{
629    #[cfg(not(target_arch = "wasm32"))]
630    {
631        let slot: Arc<Mutex<Option<T>>> = Arc::new(Mutex::new(None));
632        let done = Arc::new(AtomicBool::new(false));
633        let wakers: Arc<Mutex<Vec<Waker>>> = Arc::new(Mutex::new(Vec::new()));
634
635        let worker_slot = Arc::clone(&slot);
636        let worker_done = Arc::clone(&done);
637        let worker_wakers = Arc::clone(&wakers);
638        BlockingPool::get().submit(Box::new(move || {
639            let value = work();
640            *worker_slot
641                .lock()
642                .unwrap_or_else(|error| error.into_inner()) = Some(value);
643            worker_done.store(true, Ordering::Release);
644            for waker in worker_wakers
645                .lock()
646                .unwrap_or_else(|error| error.into_inner())
647                .drain(..)
648            {
649                waker.wake();
650            }
651        }));
652
653        BlockingWork { slot, done, wakers }.await
654    }
655    #[cfg(target_arch = "wasm32")]
656    {
657        work()
658    }
659}
660
661/// Runs `work` off the UI thread and hands its result to `on_ui` on the UI
662/// thread.
663///
664/// The callback shape of [`withBlocking`], for the code that is not already in
665/// a coroutine: an event handler, a button, anything that wants to start some
666/// blocking work and carry on. Both share the same pool, so an application
667/// that uses one, the other, or both never spends more than one set of threads
668/// on blocking work.
669///
670/// Without a runtime — a unit test, a tool — `work` runs inline and `on_ui`
671/// follows it, so a caller behaves the same either way.
672///
673/// ```rust,ignore
674/// launchBlocking(
675///     move || std::fs::read(path),
676///     move |bytes| document.set(bytes.ok()),
677/// );
678/// ```
679#[allow(non_snake_case)]
680pub fn launchBlocking<T>(work: impl FnOnce() -> T + Send + 'static, on_ui: impl FnOnce(T) + 'static)
681where
682    T: Send + 'static,
683{
684    let Some(runtime) = current_runtime_handle() else {
685        on_ui(work());
686        return;
687    };
688    let Some(continuation) = runtime.register_ui_cont(on_ui) else {
689        return;
690    };
691    let dispatcher = runtime.dispatcher();
692    #[cfg(not(target_arch = "wasm32"))]
693    BlockingPool::get().submit(Box::new(move || {
694        dispatcher.post_invoke(continuation, work());
695    }));
696    #[cfg(target_arch = "wasm32")]
697    dispatcher.post_invoke(continuation, work());
698}
699
700/// The pool [`withBlocking`] runs its work on.
701///
702/// A thread per call is what this replaced. It costs a stack and a kernel
703/// thread for every file read, and an application that starts a hundred of
704/// them — a scanner walking a folder, a list loading thumbnails — starts a
705/// hundred threads. Reusing them costs one channel send.
706///
707/// The pool grows on demand rather than being sized up front: work handed to
708/// it blocks by definition, so a fixed count of workers would let one slow
709/// read hold up every other. A new worker starts only when every existing one
710/// is busy, and it is kept afterwards. The cap is what stops a runaway from
711/// spawning without end; past it, work queues.
712#[cfg(not(target_arch = "wasm32"))]
713struct BlockingPool {
714    sender: std::sync::mpsc::Sender<BlockingJob>,
715    receiver: Arc<Mutex<std::sync::mpsc::Receiver<BlockingJob>>>,
716    state: Arc<Mutex<PoolState>>,
717}
718
719/// What the pool knows about itself.
720///
721/// `outstanding` counts work submitted and not yet finished - queued as well as
722/// running. Counting only what a worker has already picked up would be a number
723/// that lags every submission: the job that needs a new worker is precisely the
724/// one no worker has looked at yet, so a pool deciding from that count refuses
725/// to grow exactly when it must.
726#[cfg(not(target_arch = "wasm32"))]
727#[derive(Clone, Copy, Default)]
728struct PoolState {
729    alive: usize,
730    outstanding: usize,
731}
732
733#[cfg(not(target_arch = "wasm32"))]
734type BlockingJob = Box<dyn FnOnce() + Send + 'static>;
735
736/// How far the pool may grow. Blocking work is waiting, not computing, so this
737/// is not a core count; it is the point past which an application is doing
738/// something other than what this is for.
739#[cfg(not(target_arch = "wasm32"))]
740const MAX_BLOCKING_WORKERS: usize = 64;
741
742/// A pool that cannot grow runs nothing, and one that grows without bound is
743/// not a pool. Checked here rather than in a test, because the answer cannot
744/// change at run time.
745#[cfg(not(target_arch = "wasm32"))]
746const _: () = assert!(MAX_BLOCKING_WORKERS > 0 && MAX_BLOCKING_WORKERS <= 256);
747
748#[cfg(not(target_arch = "wasm32"))]
749impl BlockingPool {
750    fn get() -> &'static BlockingPool {
751        static POOL: OnceLock<BlockingPool> = OnceLock::new();
752        POOL.get_or_init(BlockingPool::new)
753    }
754
755    fn new() -> BlockingPool {
756        let (sender, receiver) = std::sync::mpsc::channel();
757        BlockingPool {
758            sender,
759            receiver: Arc::new(Mutex::new(receiver)),
760            state: Arc::new(Mutex::new(PoolState::default())),
761        }
762    }
763
764    fn submit(&self, job: BlockingJob) {
765        if self.take_slot() {
766            self.start_worker();
767        }
768        // A closed channel means every worker is gone, which only happens if
769        // the process is tearing down. Running the work here is still correct.
770        if let Err(returned) = self.sender.send(job) {
771            self.release_slot();
772            (returned.0)();
773        }
774    }
775
776    /// Records one more piece of outstanding work, and answers whether it needs
777    /// a worker of its own.
778    fn take_slot(&self) -> bool {
779        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
780        state.outstanding += 1;
781        let grow = state.alive < state.outstanding && state.alive < MAX_BLOCKING_WORKERS;
782        if grow {
783            state.alive += 1;
784        }
785        grow
786    }
787
788    fn release_slot(&self) {
789        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
790        state.outstanding = state.outstanding.saturating_sub(1);
791    }
792
793    /// Starts a worker for a slot [`take_slot`](Self::take_slot) already claimed.
794    fn start_worker(&self) {
795        let receiver = Arc::clone(&self.receiver);
796        let counters = Arc::clone(&self.state);
797        let started = std::thread::Builder::new()
798            .name("cranpose-blocking".to_string())
799            .spawn(move || loop {
800                let job = {
801                    let queue = receiver.lock().unwrap_or_else(|error| error.into_inner());
802                    queue.recv()
803                };
804                let Ok(job) = job else {
805                    break;
806                };
807                job();
808                let mut counters = counters.lock().unwrap_or_else(|error| error.into_inner());
809                counters.outstanding = counters.outstanding.saturating_sub(1);
810            });
811        if started.is_err() {
812            let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
813            state.alive -= 1;
814        }
815    }
816}
817
818#[cfg(not(target_arch = "wasm32"))]
819struct BlockingWork<T> {
820    slot: Arc<Mutex<Option<T>>>,
821    done: Arc<AtomicBool>,
822    wakers: Arc<Mutex<Vec<Waker>>>,
823}
824
825#[cfg(not(target_arch = "wasm32"))]
826impl<T> Future for BlockingWork<T> {
827    type Output = T;
828
829    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
830        if self.done.load(Ordering::Acquire) {
831            if let Some(value) = self
832                .slot
833                .lock()
834                .unwrap_or_else(|error| error.into_inner())
835                .take()
836            {
837                return Poll::Ready(value);
838            }
839        }
840        self.wakers
841            .lock()
842            .unwrap_or_else(|error| error.into_inner())
843            .push(context.waker().clone());
844        // The worker may have finished between the check and the registration.
845        if self.done.load(Ordering::Acquire) {
846            if let Some(value) = self
847                .slot
848                .lock()
849                .unwrap_or_else(|error| error.into_inner())
850                .take()
851            {
852                return Poll::Ready(value);
853            }
854        }
855        Poll::Pending
856    }
857}
858
859// ---- Produced state ------------------------------------------------------
860
861/// Runs `producer` when `key` changes and exposes what it publishes as state.
862///
863/// The Compose `produceState` contract: the producer receives a handle it uses
864/// to publish values, and is cancelled when the key changes or the composition
865/// leaves.
866#[allow(non_snake_case)]
867#[track_caller]
868pub fn produceState<T, K, F>(initial: T, key: K, producer: F) -> State<T>
869where
870    T: Clone + 'static,
871    K: PartialEq + 'static,
872    F: FnOnce(ProduceScope<T>) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
873{
874    let state = remember(|| mutableStateOf(initial)).with(|state| *state);
875    let handle = ProduceScope { state };
876    crate::__launched_effect_async_impl(
877        crate::location_key(file!(), line!(), column!()),
878        std::panic::Location::caller().into(),
879        key,
880        move |_scope| producer(handle),
881    );
882    state.as_state()
883}
884
885/// The publishing half handed to a [`produceState`] producer.
886pub struct ProduceScope<T: Clone + 'static> {
887    state: MutableState<T>,
888}
889
890impl<T: Clone + 'static> ProduceScope<T> {
891    /// Publishes `value` to the produced state.
892    pub fn set(&self, value: T) {
893        self.state.set(value);
894    }
895}
896
897#[cfg(test)]
898mod tests {
899    use super::*;
900
901    #[test]
902    fn a_delay_resolves_after_its_deadline() {
903        let started = Instant::now();
904        pollster::block_on(delay(Duration::from_millis(30)));
905        assert!(started.elapsed() >= Duration::from_millis(25));
906    }
907
908    #[test]
909    fn many_delays_share_one_timer_and_all_fire() {
910        let started = Instant::now();
911        pollster::block_on(async {
912            for _ in 0..4 {
913                delay(Duration::from_millis(5)).await;
914            }
915        });
916        assert!(started.elapsed() >= Duration::from_millis(15));
917    }
918
919    #[test]
920    fn an_elapsed_delay_is_ready_without_arming_the_timer() {
921        let mut future = Box::pin(Delay {
922            deadline: Instant::now() - Duration::from_millis(1),
923            armed: false,
924            fired: Arc::new(AtomicBool::new(false)),
925        });
926        let waker = Waker::noop().clone();
927        assert!(future
928            .as_mut()
929            .poll(&mut Context::from_waker(&waker))
930            .is_ready());
931    }
932}
933
934#[cfg(test)]
935mod stream_tests {
936    use super::*;
937
938    #[test]
939    fn a_channel_wakes_its_collector_and_ends_when_closed() {
940        let channel: EventChannel<u32> = EventChannel::new();
941        let stream = channel.stream();
942
943        let mut pending = Box::pin(stream.next());
944        let waker = Waker::noop().clone();
945        let mut context = Context::from_waker(&waker);
946        assert!(pending.as_mut().poll(&mut context).is_pending());
947
948        channel.send(7);
949        assert_eq!(pending.as_mut().poll(&mut context), Poll::Ready(Some(7)));
950
951        channel.send(8);
952        channel.close();
953        // Queued events survive the close, then the stream finishes.
954        assert_eq!(pollster::block_on(stream.next()), Some(8));
955        assert_eq!(pollster::block_on(stream.next()), None);
956        assert_eq!(stream.delivered(), 2);
957    }
958
959    #[test]
960    fn an_event_goes_to_exactly_one_collector() {
961        let channel: EventChannel<u32> = EventChannel::new();
962        let first = channel.stream();
963        let second = first.clone();
964        channel.send(1);
965        channel.close();
966        assert_eq!(pollster::block_on(first.next()), Some(1));
967        assert_eq!(pollster::block_on(second.next()), None);
968    }
969
970    #[test]
971    fn sending_after_close_is_ignored() {
972        let channel: EventChannel<u32> = EventChannel::new();
973        let stream = channel.stream();
974        channel.close();
975        channel.send(1);
976        assert_eq!(pollster::block_on(stream.next()), None);
977        assert_eq!(channel.pending(), 0);
978    }
979
980    #[test]
981    fn blocking_work_resolves_with_its_result() {
982        let doubled = pollster::block_on(withBlocking(|| 21 * 2));
983        assert_eq!(doubled, 42);
984    }
985}
986
987#[cfg(test)]
988mod timer_race_tests {
989    use super::*;
990
991    /// Arming from many threads while the timer loop is between firing and
992    /// sleeping must never lose a wake-up. Before the timer held its lock
993    /// across that boundary, a delay armed in that window slept forever.
994    #[test]
995    fn concurrent_arming_never_loses_a_wake_up() {
996        let rounds = 40;
997        let threads: Vec<_> = (0..8)
998            .map(|worker| {
999                std::thread::spawn(move || {
1000                    for round in 0..rounds {
1001                        let millis = 1 + ((worker + round) % 5) as u64;
1002                        pollster::block_on(delay(Duration::from_millis(millis)));
1003                    }
1004                })
1005            })
1006            .collect();
1007        for thread in threads {
1008            thread.join().expect("every waiter is woken");
1009        }
1010    }
1011
1012    #[cfg(not(target_arch = "wasm32"))]
1013    #[test]
1014    fn blocking_work_reuses_its_threads_instead_of_one_per_call() {
1015        use std::collections::HashSet;
1016        use std::sync::mpsc;
1017
1018        // A pool of this test's own: the process-wide one is shared with every
1019        // other test in the binary, and they run at the same time, so what it
1020        // has grown to is not this test's to assert on.
1021        let pool = BlockingPool::new();
1022
1023        // Serial calls: each one finds an idle worker, so the pool never grows
1024        // past the one thread. A thread per call would report as many distinct
1025        // thread ids as there were calls.
1026        let (sender, receiver) = mpsc::channel();
1027        for _ in 0..16 {
1028            let done = Arc::new((Mutex::new(false), Condvar::new()));
1029            let waiter = Arc::clone(&done);
1030            let sender = sender.clone();
1031            pool.submit(Box::new(move || {
1032                let _ = sender.send(std::thread::current().id());
1033                let (lock, signal) = &*done;
1034                *lock.lock().unwrap_or_else(|error| error.into_inner()) = true;
1035                signal.notify_all();
1036            }));
1037            let (lock, signal) = &*waiter;
1038            let mut finished = lock.lock().unwrap_or_else(|error| error.into_inner());
1039            while !*finished {
1040                finished = signal
1041                    .wait(finished)
1042                    .unwrap_or_else(|error| error.into_inner());
1043            }
1044        }
1045        drop(sender);
1046
1047        let threads: HashSet<_> = receiver.iter().collect();
1048        assert!(
1049            threads.len() < 16,
1050            "sixteen serial jobs used {} threads; the pool is not reusing them",
1051            threads.len()
1052        );
1053    }
1054
1055    #[cfg(not(target_arch = "wasm32"))]
1056    #[test]
1057    fn blocking_work_grows_so_one_slow_job_cannot_hold_up_another() {
1058        // The whole reason the pool grows: work handed to it blocks, so a
1059        // worker sitting in a long read must not be the only one there is.
1060        let pool = BlockingPool::new();
1061        let started = Arc::new((Mutex::new(0usize), Condvar::new()));
1062        let release = Arc::new((Mutex::new(false), Condvar::new()));
1063
1064        for _ in 0..4 {
1065            let started = Arc::clone(&started);
1066            let release = Arc::clone(&release);
1067            pool.submit(Box::new(move || {
1068                {
1069                    let (count, signal) = &*started;
1070                    *count.lock().unwrap_or_else(|error| error.into_inner()) += 1;
1071                    signal.notify_all();
1072                }
1073                let (held, signal) = &*release;
1074                let mut go = held.lock().unwrap_or_else(|error| error.into_inner());
1075                while !*go {
1076                    go = signal.wait(go).unwrap_or_else(|error| error.into_inner());
1077                }
1078            }));
1079        }
1080
1081        // All four must be running at once, which cannot happen unless the
1082        // pool grew for them.
1083        let (count, signal) = &*started;
1084        let mut running = count.lock().unwrap_or_else(|error| error.into_inner());
1085        while *running < 4 {
1086            running = signal
1087                .wait(running)
1088                .unwrap_or_else(|error| error.into_inner());
1089        }
1090        drop(running);
1091
1092        let (held, signal) = &*release;
1093        *held.lock().unwrap_or_else(|error| error.into_inner()) = true;
1094        signal.notify_all();
1095    }
1096}