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