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::{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::{mutableStateOfNeverEqual, 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    closed: Cell<bool>,
59}
60
61impl Drop for ScopeInner {
62    fn drop(&mut self) {
63        for task in self.tasks.get_mut().drain(..) {
64            task.cancel();
65        }
66    }
67}
68
69struct CompositionScopeOwner(CoroutineScope);
70
71impl Drop for CompositionScopeOwner {
72    fn drop(&mut self) {
73        self.0.inner.closed.set(true);
74        self.0.cancel();
75    }
76}
77
78impl CoroutineScope {
79    /// Launches `future`, keeping it alive until it finishes or the scope is
80    /// cancelled.
81    pub fn launch(&self, future: impl Future<Output = ()> + 'static) {
82        if self.inner.closed.get() {
83            return;
84        }
85        let Some(runtime) = self.inner.runtime.clone() else {
86            log::warn!("cranpose: a coroutine scope with no runtime dropped its work");
87            return;
88        };
89        self.inner
90            .tasks
91            .borrow_mut()
92            .retain(|task| !task.is_finished());
93        if let Some(handle) = runtime.spawn_ui(future) {
94            self.inner.tasks.borrow_mut().push(handle);
95        }
96    }
97
98    /// Cancels every task this scope launched.
99    pub fn cancel(&self) {
100        let tasks = std::mem::take(&mut *self.inner.tasks.borrow_mut());
101        for task in tasks {
102            task.cancel();
103        }
104    }
105
106    #[cfg(test)]
107    pub(crate) fn probe_identity(&self) -> usize {
108        Rc::as_ptr(&self.inner) as *const () as usize
109    }
110}
111
112/// Remembers a [`CoroutineScope`] bound to this position in the composition.
113#[allow(non_snake_case)]
114#[track_caller]
115pub fn rememberCoroutineScope() -> CoroutineScope {
116    remember(|| {
117        CompositionScopeOwner(CoroutineScope {
118            inner: Rc::new(ScopeInner {
119                runtime: current_runtime_handle(),
120                tasks: RefCell::new(Vec::new()),
121                closed: Cell::new(false),
122            }),
123        })
124    })
125    .with(|owner| owner.0.clone())
126}
127
128/// Resolves after `duration` has elapsed.
129///
130/// The wait is served by the framework's timer, which posts the wake-up onto
131/// the runtime's UI queue. Nothing spins and no frames are requested while a
132/// delay is pending, so a one-minute timer costs nothing for a minute.
133pub fn delay(duration: Duration) -> Delay {
134    Delay {
135        deadline: Instant::now() + duration,
136        armed: false,
137        fired: Arc::new(AtomicBool::new(false)),
138    }
139}
140
141/// The future returned by [`delay`].
142pub struct Delay {
143    deadline: Instant,
144    armed: bool,
145    fired: Arc<AtomicBool>,
146}
147
148impl Future for Delay {
149    type Output = ();
150
151    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
152        if self.fired.load(Ordering::Acquire) || Instant::now() >= self.deadline {
153            return Poll::Ready(());
154        }
155        let this = self.get_mut();
156        if !this.armed {
157            this.armed = true;
158            timer().arm(
159                this.deadline,
160                context.waker().clone(),
161                Arc::clone(&this.fired),
162            );
163        }
164        Poll::Pending
165    }
166}
167
168/// Runs `tick` every `period` until the returned future is dropped.
169///
170/// The first tick happens after one full period, matching a repeating timer
171/// rather than a leading-edge one.
172pub async fn interval(period: Duration, mut tick: impl FnMut()) {
173    loop {
174        delay(period).await;
175        tick();
176    }
177}
178
179#[cfg(not(target_arch = "wasm32"))]
180struct Alarm {
181    deadline: Instant,
182    waker: Waker,
183    fired: Arc<AtomicBool>,
184}
185
186struct Timer {
187    #[cfg(not(target_arch = "wasm32"))]
188    alarms: Mutex<Vec<Alarm>>,
189    #[cfg(not(target_arch = "wasm32"))]
190    wake: Condvar,
191}
192
193fn timer() -> &'static Timer {
194    static TIMER: OnceLock<&'static Timer> = OnceLock::new();
195    TIMER.get_or_init(|| {
196        let timer: &'static Timer = Box::leak(Box::new(Timer::new()));
197        timer.start();
198        timer
199    })
200}
201
202#[cfg(not(target_arch = "wasm32"))]
203impl Timer {
204    fn new() -> Self {
205        Self {
206            alarms: Mutex::new(Vec::new()),
207            wake: Condvar::new(),
208        }
209    }
210
211    fn start(&'static self) {
212        std::thread::Builder::new()
213            .name("cranpose-timer".to_string())
214            .spawn(move || self.run())
215            .expect("the timer thread starts");
216    }
217
218    fn run(&self) {
219        let mut alarms = self
220            .alarms
221            .lock()
222            .unwrap_or_else(|error| error.into_inner());
223        loop {
224            let now = Instant::now();
225            let mut due = Vec::new();
226            let mut next: Option<Duration> = None;
227            alarms.retain(|alarm| {
228                if alarm.deadline <= now {
229                    due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
230                    false
231                } else {
232                    let remaining = alarm.deadline - now;
233                    next = Some(next.map_or(remaining, |current| current.min(remaining)));
234                    true
235                }
236            });
237
238            if !due.is_empty() {
239                drop(alarms);
240                for (waker, fired) in due {
241                    fired.store(true, Ordering::Release);
242                    waker.wake();
243                }
244                alarms = self
245                    .alarms
246                    .lock()
247                    .unwrap_or_else(|error| error.into_inner());
248                continue;
249            }
250
251            alarms = match next {
252                Some(timeout) => {
253                    self.wake
254                        .wait_timeout(alarms, timeout)
255                        .unwrap_or_else(|error| error.into_inner())
256                        .0
257                }
258                None => self
259                    .wake
260                    .wait(alarms)
261                    .unwrap_or_else(|error| error.into_inner()),
262            };
263        }
264    }
265
266    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
267        let mut alarms = self
268            .alarms
269            .lock()
270            .unwrap_or_else(|error| error.into_inner());
271        alarms.push(Alarm {
272            deadline,
273            waker,
274            fired,
275        });
276        self.wake.notify_one();
277    }
278}
279
280#[cfg(target_arch = "wasm32")]
281impl Timer {
282    fn new() -> Self {
283        Self {}
284    }
285
286    fn start(&'static self) {}
287
288    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
289        let millis = deadline
290            .saturating_duration_since(Instant::now())
291            .as_millis()
292            .min(i32::MAX as u128) as i32;
293        let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
294            fired.store(true, Ordering::Release);
295            waker.wake();
296        });
297        let scheduled = web_sys::window().and_then(|window| {
298            window
299                .set_timeout_with_callback_and_timeout_and_arguments_0(
300                    callback.unchecked_ref(),
301                    millis,
302                )
303                .ok()
304        });
305        if scheduled.is_none() {
306            log::warn!("cranpose: no window timer is available; the delay resolves immediately");
307        }
308    }
309}
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::caller_location_key(),
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(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
486    let sink = state;
487    CollectEvents(stream, key, move |event| sink.set(event));
488    state.as_state()
489}
490
491/// A `Send` publishing handle for a composition-scoped [`EventStream`].
492///
493/// Platform services publish events from whatever thread they run on — a JNI
494/// callback, a worker, a socket reader. The sender hops each event onto the UI
495/// thread through the runtime's dispatcher and pushes it into the stream the
496/// composition is collecting, so no service and no application ever writes that
497/// hop again.
498pub struct EventSender<T: Send + 'static> {
499    #[cfg(not(target_arch = "wasm32"))]
500    dispatcher: crate::runtime::UiDispatcher,
501    bridge: u64,
502    _events: std::marker::PhantomData<fn(T)>,
503}
504
505impl<T: Send + 'static> Clone for EventSender<T> {
506    fn clone(&self) -> Self {
507        Self {
508            #[cfg(not(target_arch = "wasm32"))]
509            dispatcher: self.dispatcher.clone(),
510            bridge: self.bridge,
511            _events: std::marker::PhantomData,
512        }
513    }
514}
515
516impl<T: Send + 'static> EventSender<T> {
517    /// Publishes `event` to the composition that owns this bridge.
518    pub fn send(&self, event: T) {
519        let bridge = self.bridge;
520        #[cfg(not(target_arch = "wasm32"))]
521        self.dispatcher
522            .post(move || deliver_bridged::<T>(bridge, event));
523        #[cfg(target_arch = "wasm32")]
524        deliver_bridged::<T>(bridge, event);
525    }
526}
527
528thread_local! {
529    static BRIDGES: RefCell<std::collections::HashMap<u64, Rc<dyn std::any::Any>>> =
530        RefCell::new(std::collections::HashMap::new());
531}
532
533static NEXT_BRIDGE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
534
535fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
536    let channel = BRIDGES.with(|bridges| bridges.borrow().get(&bridge).cloned());
537    let Some(channel) = channel else {
538        log::debug!("event bridge {bridge} is gone, one event dropped");
539        return;
540    };
541    if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
542        channel.send(event);
543    }
544}
545
546struct Bridge<T: Send + 'static> {
547    id: u64,
548    channel: Rc<EventChannel<T>>,
549}
550
551impl<T: Send + 'static> Bridge<T> {
552    fn new() -> Self {
553        let id = NEXT_BRIDGE.fetch_add(1, Ordering::Relaxed);
554        let channel = Rc::new(EventChannel::<T>::new());
555        BRIDGES.with(|bridges| {
556            bridges
557                .borrow_mut()
558                .insert(id, Rc::clone(&channel) as Rc<dyn std::any::Any>)
559        });
560        Self { id, channel }
561    }
562}
563
564impl<T: Send + 'static> Drop for Bridge<T> {
565    fn drop(&mut self) {
566        BRIDGES.with(|bridges| bridges.borrow_mut().remove(&self.id));
567        self.channel.close();
568    }
569}
570
571/// Turns a platform subscription into a composition-scoped [`EventStream`].
572///
573/// `subscribe` receives a `Send` [`EventSender`] and returns whatever
574/// registration handle the service uses; that handle is dropped — unsubscribing
575/// the service — when `key` changes or the composition leaves. This is the one
576/// place the framework bridges "a service publishes from another thread" to
577/// "a composition collects".
578#[allow(non_snake_case)]
579#[track_caller]
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(crate::caller_location_key(), key, move |scope| {
593        #[cfg(not(target_arch = "wasm32"))]
594        let Some(dispatcher) = dispatcher else {
595            log::warn!("cranpose: an event stream was remembered without a runtime");
596            return scope.on_dispose(|| {});
597        };
598        let registration = subscribe(EventSender {
599            #[cfg(not(target_arch = "wasm32"))]
600            dispatcher,
601            bridge: id,
602            _events: std::marker::PhantomData,
603        });
604        scope.on_dispose(move || drop(registration))
605    });
606
607    stream
608}
609
610/// Runs `work` off the UI thread and resolves with its result on the UI thread.
611///
612/// This is the escape hatch for genuinely blocking work — parsing a large file,
613/// a synchronous provider call — that must not stall composition. On the web
614/// there is one thread, so `work` runs inline; callers keep the unit of work
615/// small enough that this is honest on every target.
616#[allow(non_snake_case)]
617pub async fn withBlocking<T, F>(work: F) -> T
618where
619    T: Send + 'static,
620    F: FnOnce() -> T + Send + 'static,
621{
622    #[cfg(not(target_arch = "wasm32"))]
623    {
624        let slot: Arc<Mutex<Option<T>>> = Arc::new(Mutex::new(None));
625        let done = Arc::new(AtomicBool::new(false));
626        let wakers: Arc<Mutex<Vec<Waker>>> = Arc::new(Mutex::new(Vec::new()));
627
628        let worker_slot = Arc::clone(&slot);
629        let worker_done = Arc::clone(&done);
630        let worker_wakers = Arc::clone(&wakers);
631        BlockingPool::get().submit(Box::new(move || {
632            let value = work();
633            *worker_slot
634                .lock()
635                .unwrap_or_else(|error| error.into_inner()) = Some(value);
636            worker_done.store(true, Ordering::Release);
637            for waker in worker_wakers
638                .lock()
639                .unwrap_or_else(|error| error.into_inner())
640                .drain(..)
641            {
642                waker.wake();
643            }
644        }));
645
646        BlockingWork { slot, done, wakers }.await
647    }
648    #[cfg(target_arch = "wasm32")]
649    {
650        work()
651    }
652}
653
654/// Runs `work` off the UI thread and hands its result to `on_ui` on the UI
655/// thread.
656///
657/// The callback shape of [`withBlocking`], for the code that is not already in
658/// a coroutine: an event handler, a button, anything that wants to start some
659/// blocking work and carry on. Both share the same pool, so an application
660/// that uses one, the other, or both never spends more than one set of threads
661/// on blocking work.
662///
663/// Without a runtime — a unit test, a tool — `work` runs inline and `on_ui`
664/// follows it, so a caller behaves the same either way.
665///
666/// ```rust,ignore
667/// launchBlocking(
668///     move || std::fs::read(path),
669///     move |bytes| document.set(bytes.ok()),
670/// );
671/// ```
672#[allow(non_snake_case)]
673pub fn launchBlocking<T>(work: impl FnOnce() -> T + Send + 'static, on_ui: impl FnOnce(T) + 'static)
674where
675    T: Send + 'static,
676{
677    let Some(runtime) = current_runtime_handle() else {
678        on_ui(work());
679        return;
680    };
681    let Some(continuation) = runtime.register_ui_cont(on_ui) else {
682        return;
683    };
684    let dispatcher = runtime.dispatcher();
685    #[cfg(not(target_arch = "wasm32"))]
686    BlockingPool::get().submit(Box::new(move || {
687        dispatcher.post_invoke(continuation, work());
688    }));
689    #[cfg(target_arch = "wasm32")]
690    dispatcher.post_invoke(continuation, work());
691}
692
693#[cfg(not(target_arch = "wasm32"))]
694struct BlockingPool {
695    sender: std::sync::mpsc::Sender<BlockingJob>,
696    receiver: Arc<Mutex<std::sync::mpsc::Receiver<BlockingJob>>>,
697    state: Arc<Mutex<PoolState>>,
698}
699
700#[cfg(not(target_arch = "wasm32"))]
701#[derive(Clone, Copy, Default)]
702struct PoolState {
703    alive: usize,
704    outstanding: usize,
705}
706
707#[cfg(not(target_arch = "wasm32"))]
708type BlockingJob = Box<dyn FnOnce() + Send + 'static>;
709
710#[cfg(not(target_arch = "wasm32"))]
711const MAX_BLOCKING_WORKERS: usize = 64;
712
713#[cfg(not(target_arch = "wasm32"))]
714const _: () = assert!(MAX_BLOCKING_WORKERS > 0 && MAX_BLOCKING_WORKERS <= 256);
715
716#[cfg(not(target_arch = "wasm32"))]
717impl BlockingPool {
718    fn get() -> &'static BlockingPool {
719        static POOL: OnceLock<BlockingPool> = OnceLock::new();
720        POOL.get_or_init(BlockingPool::new)
721    }
722
723    fn new() -> BlockingPool {
724        let (sender, receiver) = std::sync::mpsc::channel();
725        BlockingPool {
726            sender,
727            receiver: Arc::new(Mutex::new(receiver)),
728            state: Arc::new(Mutex::new(PoolState::default())),
729        }
730    }
731
732    fn submit(&self, job: BlockingJob) {
733        if self.take_slot() {
734            self.start_worker();
735        }
736        if let Err(returned) = self.sender.send(job) {
737            self.release_slot();
738            (returned.0)();
739        }
740    }
741
742    fn take_slot(&self) -> bool {
743        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
744        state.outstanding += 1;
745        let grow = state.alive < state.outstanding && state.alive < MAX_BLOCKING_WORKERS;
746        if grow {
747            state.alive += 1;
748        }
749        grow
750    }
751
752    fn release_slot(&self) {
753        let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
754        state.outstanding = state.outstanding.saturating_sub(1);
755    }
756
757    fn start_worker(&self) {
758        let receiver = Arc::clone(&self.receiver);
759        let counters = Arc::clone(&self.state);
760        let started = std::thread::Builder::new()
761            .name("cranpose-blocking".to_string())
762            .spawn(move || {
763                loop {
764                    let job = {
765                        let queue = receiver.lock().unwrap_or_else(|error| error.into_inner());
766                        queue.recv()
767                    };
768                    let Ok(job) = job else {
769                        break;
770                    };
771                    job();
772                    let mut counters = counters.lock().unwrap_or_else(|error| error.into_inner());
773                    counters.outstanding = counters.outstanding.saturating_sub(1);
774                }
775            });
776        if started.is_err() {
777            let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
778            state.alive -= 1;
779        }
780    }
781}
782
783#[cfg(not(target_arch = "wasm32"))]
784struct BlockingWork<T> {
785    slot: Arc<Mutex<Option<T>>>,
786    done: Arc<AtomicBool>,
787    wakers: Arc<Mutex<Vec<Waker>>>,
788}
789
790#[cfg(not(target_arch = "wasm32"))]
791impl<T> Future for BlockingWork<T> {
792    type Output = T;
793
794    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
795        if self.done.load(Ordering::Acquire)
796            && let Some(value) = self
797                .slot
798                .lock()
799                .unwrap_or_else(|error| error.into_inner())
800                .take()
801        {
802            return Poll::Ready(value);
803        }
804        self.wakers
805            .lock()
806            .unwrap_or_else(|error| error.into_inner())
807            .push(context.waker().clone());
808        if self.done.load(Ordering::Acquire)
809            && let Some(value) = self
810                .slot
811                .lock()
812                .unwrap_or_else(|error| error.into_inner())
813                .take()
814        {
815            return Poll::Ready(value);
816        }
817        Poll::Pending
818    }
819}
820
821/// Runs `producer` when `key` changes and exposes what it publishes as state.
822///
823/// The Compose `produceState` contract: the producer receives a handle it uses
824/// to publish values, and is cancelled when the key changes or the composition
825/// leaves.
826#[allow(non_snake_case)]
827#[track_caller]
828pub fn produceState<T, K, F>(initial: T, key: K, producer: F) -> State<T>
829where
830    T: Clone + 'static,
831    K: PartialEq + 'static,
832    F: FnOnce(ProduceScope<T>) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
833{
834    let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
835    let handle = ProduceScope { state };
836    crate::__launched_effect_async_impl(
837        crate::caller_location_key(),
838        std::panic::Location::caller().into(),
839        key,
840        move |_scope| producer(handle),
841    );
842    state.as_state()
843}
844
845/// The publishing half handed to a [`produceState`] producer.
846pub struct ProduceScope<T: Clone + 'static> {
847    state: MutableState<T>,
848}
849
850impl<T: Clone + 'static> ProduceScope<T> {
851    /// Publishes `value` to the produced state.
852    pub fn set(&self, value: T) {
853        self.state.set(value);
854    }
855}
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860
861    #[test]
862    fn a_delay_resolves_after_its_deadline() {
863        let started = Instant::now();
864        pollster::block_on(delay(Duration::from_millis(30)));
865        assert!(started.elapsed() >= Duration::from_millis(25));
866    }
867
868    #[test]
869    fn many_delays_share_one_timer_and_all_fire() {
870        let started = Instant::now();
871        pollster::block_on(async {
872            for _ in 0..4 {
873                delay(Duration::from_millis(5)).await;
874            }
875        });
876        assert!(started.elapsed() >= Duration::from_millis(15));
877    }
878
879    #[test]
880    fn an_elapsed_delay_is_ready_without_arming_the_timer() {
881        let mut future = Box::pin(Delay {
882            deadline: Instant::now() - Duration::from_millis(1),
883            armed: false,
884            fired: Arc::new(AtomicBool::new(false)),
885        });
886        let waker = Waker::noop().clone();
887        assert!(
888            future
889                .as_mut()
890                .poll(&mut Context::from_waker(&waker))
891                .is_ready()
892        );
893    }
894}
895
896#[cfg(test)]
897mod stream_tests {
898    use super::*;
899
900    #[test]
901    fn a_channel_wakes_its_collector_and_ends_when_closed() {
902        let channel: EventChannel<u32> = EventChannel::new();
903        let stream = channel.stream();
904
905        let mut pending = Box::pin(stream.next());
906        let waker = Waker::noop().clone();
907        let mut context = Context::from_waker(&waker);
908        assert!(pending.as_mut().poll(&mut context).is_pending());
909
910        channel.send(7);
911        assert_eq!(pending.as_mut().poll(&mut context), Poll::Ready(Some(7)));
912
913        channel.send(8);
914        channel.close();
915        assert_eq!(pollster::block_on(stream.next()), Some(8));
916        assert_eq!(pollster::block_on(stream.next()), None);
917        assert_eq!(stream.delivered(), 2);
918    }
919
920    #[test]
921    fn an_event_goes_to_exactly_one_collector() {
922        let channel: EventChannel<u32> = EventChannel::new();
923        let first = channel.stream();
924        let second = first.clone();
925        channel.send(1);
926        channel.close();
927        assert_eq!(pollster::block_on(first.next()), Some(1));
928        assert_eq!(pollster::block_on(second.next()), None);
929    }
930
931    #[test]
932    fn sending_after_close_is_ignored() {
933        let channel: EventChannel<u32> = EventChannel::new();
934        let stream = channel.stream();
935        channel.close();
936        channel.send(1);
937        assert_eq!(pollster::block_on(stream.next()), None);
938        assert_eq!(channel.pending(), 0);
939    }
940
941    #[test]
942    fn blocking_work_resolves_with_its_result() {
943        let doubled = pollster::block_on(withBlocking(|| 21 * 2));
944        assert_eq!(doubled, 42);
945    }
946}
947
948#[cfg(test)]
949mod timer_race_tests {
950    use super::*;
951
952    #[test]
953    fn concurrent_arming_never_loses_a_wake_up() {
954        let rounds = 40;
955        let threads: Vec<_> = (0..8)
956            .map(|worker| {
957                std::thread::spawn(move || {
958                    for round in 0..rounds {
959                        let millis = 1 + ((worker + round) % 5) as u64;
960                        pollster::block_on(delay(Duration::from_millis(millis)));
961                    }
962                })
963            })
964            .collect();
965        for thread in threads {
966            thread.join().expect("every waiter is woken");
967        }
968    }
969
970    #[cfg(not(target_arch = "wasm32"))]
971    #[test]
972    fn blocking_work_reuses_its_threads_instead_of_one_per_call() {
973        use std::{collections::HashSet, sync::mpsc};
974
975        let pool = BlockingPool::new();
976
977        let (sender, receiver) = mpsc::channel();
978        for _ in 0..16 {
979            let done = Arc::new((Mutex::new(false), Condvar::new()));
980            let waiter = Arc::clone(&done);
981            let sender = sender.clone();
982            pool.submit(Box::new(move || {
983                let _ = sender.send(std::thread::current().id());
984                let (lock, signal) = &*done;
985                *lock.lock().unwrap_or_else(|error| error.into_inner()) = true;
986                signal.notify_all();
987            }));
988            let (lock, signal) = &*waiter;
989            let mut finished = lock.lock().unwrap_or_else(|error| error.into_inner());
990            while !*finished {
991                finished = signal
992                    .wait(finished)
993                    .unwrap_or_else(|error| error.into_inner());
994            }
995        }
996        drop(sender);
997
998        let threads: HashSet<_> = receiver.iter().collect();
999        assert!(
1000            threads.len() < 16,
1001            "sixteen serial jobs used {} threads; the pool is not reusing them",
1002            threads.len()
1003        );
1004    }
1005
1006    #[cfg(not(target_arch = "wasm32"))]
1007    #[test]
1008    fn blocking_work_grows_so_one_slow_job_cannot_hold_up_another() {
1009        let pool = BlockingPool::new();
1010        let started = Arc::new((Mutex::new(0usize), Condvar::new()));
1011        let release = Arc::new((Mutex::new(false), Condvar::new()));
1012
1013        for _ in 0..4 {
1014            let started = Arc::clone(&started);
1015            let release = Arc::clone(&release);
1016            pool.submit(Box::new(move || {
1017                {
1018                    let (count, signal) = &*started;
1019                    *count.lock().unwrap_or_else(|error| error.into_inner()) += 1;
1020                    signal.notify_all();
1021                }
1022                let (held, signal) = &*release;
1023                let mut go = held.lock().unwrap_or_else(|error| error.into_inner());
1024                while !*go {
1025                    go = signal.wait(go).unwrap_or_else(|error| error.into_inner());
1026                }
1027            }));
1028        }
1029
1030        let (count, signal) = &*started;
1031        let mut running = count.lock().unwrap_or_else(|error| error.into_inner());
1032        while *running < 4 {
1033            running = signal
1034                .wait(running)
1035                .unwrap_or_else(|error| error.into_inner());
1036        }
1037        drop(running);
1038
1039        let (held, signal) = &*release;
1040        *held.lock().unwrap_or_else(|error| error.into_inner()) = true;
1041        signal.notify_all();
1042    }
1043}