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