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, PoisonError};
13use std::{
14    cell::{Cell, RefCell},
15    future::Future,
16    pin::Pin,
17    rc::Rc,
18    sync::{
19        Arc, OnceLock,
20        atomic::{AtomicBool, Ordering},
21    },
22    task::{Context, Poll, Waker},
23    time::Duration,
24};
25
26#[cfg(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#[expect(non_snake_case)]
114#[track_caller]
115pub fn rememberCoroutineScope() -> CoroutineScope {
116    remember(|| {
117        CompositionScopeOwner(CoroutineScope {
118            inner: Rc::new(ScopeInner {
119                runtime: current_runtime_handle(),
120                tasks: RefCell::new(Vec::new()),
121                closed: Cell::new(false),
122            }),
123        })
124    })
125    .with(|owner| owner.0.clone())
126}
127
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.alarms.lock().unwrap_or_else(PoisonError::into_inner);
220        loop {
221            let now = Instant::now();
222            let mut due = Vec::new();
223            let mut next: Option<Duration> = None;
224            alarms.retain(|alarm| {
225                if alarm.deadline <= now {
226                    due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
227                    false
228                } else {
229                    let remaining = alarm.deadline - now;
230                    next = Some(next.map_or(remaining, |current| current.min(remaining)));
231                    true
232                }
233            });
234
235            if !due.is_empty() {
236                drop(alarms);
237                for (waker, fired) in due {
238                    fired.store(true, Ordering::Release);
239                    waker.wake();
240                }
241                alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
242                continue;
243            }
244
245            alarms = match next {
246                Some(timeout) => {
247                    self.wake
248                        .wait_timeout(alarms, timeout)
249                        .unwrap_or_else(PoisonError::into_inner)
250                        .0
251                }
252                None => self
253                    .wake
254                    .wait(alarms)
255                    .unwrap_or_else(PoisonError::into_inner),
256            };
257        }
258    }
259
260    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
261        let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
262        alarms.push(Alarm {
263            deadline,
264            waker,
265            fired,
266        });
267        self.wake.notify_one();
268    }
269}
270
271#[cfg(target_arch = "wasm32")]
272impl Timer {
273    fn new() -> Self {
274        Self {}
275    }
276
277    fn start(&'static self) {}
278
279    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
280        let millis = deadline
281            .saturating_duration_since(Instant::now())
282            .as_millis()
283            .min(i32::MAX as u128) as i32;
284        let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
285            fired.store(true, Ordering::Release);
286            waker.wake();
287        });
288        let scheduled = web_sys::window().and_then(|window| {
289            window
290                .set_timeout_with_callback_and_timeout_and_arguments_0(
291                    callback.unchecked_ref(),
292                    millis,
293                )
294                .ok()
295        });
296        if scheduled.is_none() {
297            log::warn!("cranpose: no window timer is available; the delay resolves immediately");
298        }
299    }
300}
301
302/// The producing half of an [`EventStream`].
303///
304/// A service that receives events from outside the composition — a platform
305/// callback, a worker thread, a socket — publishes through a channel, and every
306/// pending collector is woken. This is the shape that replaces
307/// "register an observer, then drain a queue" everywhere in the framework.
308pub struct EventChannel<T: 'static> {
309    shared: Rc<ChannelShared<T>>,
310}
311
312struct ChannelShared<T: 'static> {
313    ready: RefCell<std::collections::VecDeque<T>>,
314    closed: std::cell::Cell<bool>,
315    delivered: std::cell::Cell<usize>,
316    wakers: RefCell<Vec<Waker>>,
317}
318
319impl<T: 'static> ChannelShared<T> {
320    fn wake_all(&self) {
321        for waker in self.wakers.borrow_mut().drain(..) {
322            waker.wake();
323        }
324    }
325}
326
327impl<T: 'static> Default for EventChannel<T> {
328    fn default() -> Self {
329        Self::new()
330    }
331}
332
333impl<T: 'static> EventChannel<T> {
334    /// Creates an open channel.
335    pub fn new() -> Self {
336        Self {
337            shared: Rc::new(ChannelShared {
338                ready: RefCell::new(std::collections::VecDeque::new()),
339                closed: std::cell::Cell::new(false),
340                delivered: std::cell::Cell::new(0),
341                wakers: RefCell::new(Vec::new()),
342            }),
343        }
344    }
345
346    /// The consuming half, handed to collectors.
347    pub fn stream(&self) -> EventStream<T> {
348        EventStream {
349            shared: Rc::clone(&self.shared),
350        }
351    }
352
353    /// Publishes one event and wakes every pending collector.
354    pub fn send(&self, event: T) {
355        if self.shared.closed.get() {
356            return;
357        }
358        self.shared.ready.borrow_mut().push_back(event);
359        self.shared.wake_all();
360    }
361
362    /// Ends the stream. Collectors drain what is queued and then finish.
363    pub fn close(&self) {
364        if self.shared.closed.get() {
365            return;
366        }
367        self.shared.closed.set(true);
368        self.shared.wake_all();
369    }
370
371    /// Whether the channel has been closed.
372    pub fn is_closed(&self) -> bool {
373        self.shared.closed.get()
374    }
375
376    /// How many events are queued but not yet taken.
377    pub fn pending(&self) -> usize {
378        self.shared.ready.borrow().len()
379    }
380}
381
382/// The consuming half of an [`EventChannel`].
383///
384/// Collectors take events one at a time; an event goes to exactly one
385/// collector, so two collectors share the stream rather than each seeing every
386/// event.
387pub struct EventStream<T: 'static> {
388    shared: Rc<ChannelShared<T>>,
389}
390
391impl<T: 'static> Clone for EventStream<T> {
392    fn clone(&self) -> Self {
393        Self {
394            shared: Rc::clone(&self.shared),
395        }
396    }
397}
398
399impl<T: 'static> EventStream<T> {
400    /// Resolves with the next event, or `None` once the stream is closed and
401    /// drained.
402    pub fn next(&self) -> EventStreamNext<T> {
403        EventStreamNext {
404            shared: Rc::clone(&self.shared),
405        }
406    }
407
408    /// How many events this stream has handed out.
409    pub fn delivered(&self) -> usize {
410        self.shared.delivered.get()
411    }
412}
413
414/// The future returned by [`EventStream::next`].
415pub struct EventStreamNext<T: 'static> {
416    shared: Rc<ChannelShared<T>>,
417}
418
419impl<T: 'static> Future for EventStreamNext<T> {
420    type Output = Option<T>;
421
422    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
423        if let Some(event) = self.shared.ready.borrow_mut().pop_front() {
424            self.shared.delivered.set(self.shared.delivered.get() + 1);
425            return Poll::Ready(Some(event));
426        }
427        if self.shared.closed.get() {
428            return Poll::Ready(None);
429        }
430        self.shared
431            .wakers
432            .borrow_mut()
433            .push(context.waker().clone());
434        Poll::Pending
435    }
436}
437
438/// Collects `stream` for as long as this call stays in the composition,
439/// handing each event to `on_event`.
440///
441/// `key` re-starts the collection when it changes, exactly like
442/// `LaunchedEffect`.
443#[expect(non_snake_case)]
444#[track_caller]
445pub fn CollectEvents<T, K>(stream: EventStream<T>, key: K, on_event: impl FnMut(T) + 'static)
446where
447    T: 'static,
448    K: PartialEq + 'static,
449{
450    crate::__launched_effect_async_impl(
451        crate::caller_location_key(),
452        std::panic::Location::caller().into(),
453        key,
454        move |_scope| {
455            let mut on_event = on_event;
456            Box::pin(async move {
457                while let Some(event) = stream.next().await {
458                    on_event(event);
459                }
460            })
461        },
462    );
463}
464
465/// Collects `stream` into state, starting at `initial`.
466///
467/// The composition reads the latest value the stream produced, and recomposes
468/// when a new one arrives.
469#[expect(non_snake_case)]
470#[track_caller]
471pub fn collectAsState<T, K>(stream: EventStream<T>, key: K, initial: T) -> State<T>
472where
473    T: Clone + 'static,
474    K: PartialEq + 'static,
475{
476    let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
477    let sink = state;
478    CollectEvents(stream, key, move |event| sink.set(event));
479    state.as_state()
480}
481
482/// A `Send` publishing handle for a composition-scoped [`EventStream`].
483///
484/// Platform services publish events from whatever thread they run on — a JNI
485/// callback, a worker, a socket reader. The sender hops each event onto the UI
486/// thread through the runtime's dispatcher and pushes it into the stream the
487/// composition is collecting, so no service and no application ever writes that
488/// hop again.
489pub struct EventSender<T: Send + 'static> {
490    #[cfg(not(target_arch = "wasm32"))]
491    dispatcher: crate::runtime::UiDispatcher,
492    bridge: u64,
493    _events: std::marker::PhantomData<fn(T)>,
494}
495
496impl<T: Send + 'static> Clone for EventSender<T> {
497    fn clone(&self) -> Self {
498        Self {
499            #[cfg(not(target_arch = "wasm32"))]
500            dispatcher: self.dispatcher.clone(),
501            bridge: self.bridge,
502            _events: std::marker::PhantomData,
503        }
504    }
505}
506
507impl<T: Send + 'static> EventSender<T> {
508    /// Publishes `event` to the composition that owns this bridge.
509    pub fn send(&self, event: T) {
510        let bridge = self.bridge;
511        #[cfg(not(target_arch = "wasm32"))]
512        self.dispatcher
513            .post(move || deliver_bridged::<T>(bridge, event));
514        #[cfg(target_arch = "wasm32")]
515        deliver_bridged::<T>(bridge, event);
516    }
517}
518
519thread_local! {
520    static BRIDGES: RefCell<std::collections::HashMap<u64, Rc<dyn std::any::Any>>> =
521        RefCell::new(std::collections::HashMap::new());
522}
523
524static NEXT_BRIDGE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
525
526fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
527    let channel = BRIDGES.with(|bridges| bridges.borrow().get(&bridge).cloned());
528    let Some(channel) = channel else {
529        log::debug!("event bridge {bridge} is gone, one event dropped");
530        return;
531    };
532    if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
533        channel.send(event);
534    }
535}
536
537struct Bridge<T: Send + 'static> {
538    id: u64,
539    channel: Rc<EventChannel<T>>,
540}
541
542impl<T: Send + 'static> Bridge<T> {
543    fn new() -> Self {
544        let id = NEXT_BRIDGE.fetch_add(1, Ordering::Relaxed);
545        let channel = Rc::new(EventChannel::<T>::new());
546        BRIDGES.with(|bridges| {
547            bridges
548                .borrow_mut()
549                .insert(id, Rc::clone(&channel) as Rc<dyn std::any::Any>)
550        });
551        Self { id, channel }
552    }
553}
554
555impl<T: Send + 'static> Drop for Bridge<T> {
556    fn drop(&mut self) {
557        BRIDGES.with(|bridges| bridges.borrow_mut().remove(&self.id));
558        self.channel.close();
559    }
560}
561
562/// Turns a platform subscription into a composition-scoped [`EventStream`].
563///
564/// `subscribe` receives a `Send` [`EventSender`] and returns whatever
565/// registration handle the service uses; that handle is dropped — unsubscribing
566/// the service — when `key` changes or the composition leaves. This is the one
567/// place the framework bridges "a service publishes from another thread" to
568/// "a composition collects".
569#[expect(non_snake_case)]
570#[track_caller]
571pub fn rememberEventStream<T, K, R, S>(key: K, subscribe: S) -> EventStream<T>
572where
573    T: Send + 'static,
574    K: PartialEq + 'static,
575    R: 'static,
576    S: FnOnce(EventSender<T>) -> R + 'static,
577{
578    let bridge = remember(Bridge::<T>::new);
579    let (id, stream) = bridge.with(|bridge| (bridge.id, bridge.channel.stream()));
580    #[cfg(not(target_arch = "wasm32"))]
581    let dispatcher = current_runtime_handle().map(|runtime| runtime.dispatcher());
582
583    crate::__disposable_effect_impl(crate::caller_location_key(), key, move |scope| {
584        #[cfg(not(target_arch = "wasm32"))]
585        let Some(dispatcher) = dispatcher else {
586            log::warn!("cranpose: an event stream was remembered without a runtime");
587            return scope.on_dispose(|| {});
588        };
589        let registration = subscribe(EventSender {
590            #[cfg(not(target_arch = "wasm32"))]
591            dispatcher,
592            bridge: id,
593            _events: std::marker::PhantomData,
594        });
595        scope.on_dispose(move || drop(registration))
596    });
597
598    stream
599}
600
601/// Runs `work` off the UI thread and resolves with its result on the UI thread.
602///
603/// This is the escape hatch for genuinely blocking work — parsing a large file,
604/// a synchronous provider call — that must not stall composition. On the web
605/// there is one thread, so `work` runs inline; callers keep the unit of work
606/// small enough that this is honest on every target.
607#[expect(non_snake_case)]
608pub async fn withBlocking<T, F>(work: F) -> T
609where
610    T: Send + 'static,
611    F: FnOnce() -> T + Send + 'static,
612{
613    #[cfg(not(target_arch = "wasm32"))]
614    {
615        let slot: Arc<Mutex<Option<T>>> = Arc::new(Mutex::new(None));
616        let done = Arc::new(AtomicBool::new(false));
617        let wakers: Arc<Mutex<Vec<Waker>>> = Arc::new(Mutex::new(Vec::new()));
618
619        let worker_slot = Arc::clone(&slot);
620        let worker_done = Arc::clone(&done);
621        let worker_wakers = Arc::clone(&wakers);
622        BlockingPool::get().submit(Box::new(move || {
623            let value = work();
624            *worker_slot.lock().unwrap_or_else(PoisonError::into_inner) = Some(value);
625            worker_done.store(true, Ordering::Release);
626            for waker in worker_wakers
627                .lock()
628                .unwrap_or_else(PoisonError::into_inner)
629                .drain(..)
630            {
631                waker.wake();
632            }
633        }));
634
635        BlockingWork { slot, done, wakers }.await
636    }
637    #[cfg(target_arch = "wasm32")]
638    {
639        work()
640    }
641}
642
643/// Runs `work` off the UI thread and hands its result to `on_ui` on the UI
644/// thread.
645///
646/// The callback shape of [`withBlocking`], for the code that is not already in
647/// a coroutine: an event handler, a button, anything that wants to start some
648/// blocking work and carry on. Both share the same pool, so an application
649/// that uses one, the other, or both never spends more than one set of threads
650/// on blocking work.
651///
652/// Without a runtime — a unit test, a tool — `work` runs inline and `on_ui`
653/// follows it, so a caller behaves the same either way.
654///
655/// ```rust,ignore
656/// launchBlocking(
657///     move || std::fs::read(path),
658///     move |bytes| document.set(bytes.ok()),
659/// );
660/// ```
661#[expect(non_snake_case)]
662pub fn launchBlocking<T>(work: impl FnOnce() -> T + Send + 'static, on_ui: impl FnOnce(T) + 'static)
663where
664    T: Send + 'static,
665{
666    let Some(runtime) = current_runtime_handle() else {
667        on_ui(work());
668        return;
669    };
670    let Some(continuation) = runtime.register_ui_cont(on_ui) else {
671        return;
672    };
673    let dispatcher = runtime.dispatcher();
674    #[cfg(not(target_arch = "wasm32"))]
675    BlockingPool::get().submit(Box::new(move || {
676        dispatcher.post_invoke(continuation, work());
677    }));
678    #[cfg(target_arch = "wasm32")]
679    dispatcher.post_invoke(continuation, work());
680}
681
682#[cfg(not(target_arch = "wasm32"))]
683struct BlockingPool {
684    sender: std::sync::mpsc::Sender<BlockingJob>,
685    receiver: Arc<Mutex<std::sync::mpsc::Receiver<BlockingJob>>>,
686    state: Arc<Mutex<PoolState>>,
687}
688
689#[cfg(not(target_arch = "wasm32"))]
690#[derive(Clone, Copy, Default)]
691struct PoolState {
692    alive: usize,
693    outstanding: usize,
694}
695
696#[cfg(not(target_arch = "wasm32"))]
697type BlockingJob = Box<dyn FnOnce() + Send + 'static>;
698
699#[cfg(not(target_arch = "wasm32"))]
700const MAX_BLOCKING_WORKERS: usize = 64;
701
702#[cfg(not(target_arch = "wasm32"))]
703const _: () = assert!(MAX_BLOCKING_WORKERS > 0 && MAX_BLOCKING_WORKERS <= 256);
704
705#[cfg(not(target_arch = "wasm32"))]
706impl BlockingPool {
707    fn get() -> &'static BlockingPool {
708        static POOL: OnceLock<BlockingPool> = OnceLock::new();
709        POOL.get_or_init(BlockingPool::new)
710    }
711
712    fn new() -> BlockingPool {
713        let (sender, receiver) = std::sync::mpsc::channel();
714        BlockingPool {
715            sender,
716            receiver: Arc::new(Mutex::new(receiver)),
717            state: Arc::new(Mutex::new(PoolState::default())),
718        }
719    }
720
721    fn submit(&self, job: BlockingJob) {
722        if self.take_slot() {
723            self.start_worker();
724        }
725        if let Err(returned) = self.sender.send(job) {
726            self.release_slot();
727            (returned.0)();
728        }
729    }
730
731    fn take_slot(&self) -> bool {
732        let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
733        state.outstanding += 1;
734        let grow = state.alive < state.outstanding && state.alive < MAX_BLOCKING_WORKERS;
735        if grow {
736            state.alive += 1;
737        }
738        grow
739    }
740
741    fn release_slot(&self) {
742        let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
743        state.outstanding = state.outstanding.saturating_sub(1);
744    }
745
746    fn start_worker(&self) {
747        let receiver = Arc::clone(&self.receiver);
748        let counters = Arc::clone(&self.state);
749        let started = std::thread::Builder::new()
750            .name("cranpose-blocking".to_string())
751            .spawn(move || {
752                loop {
753                    let job = {
754                        let queue = receiver.lock().unwrap_or_else(PoisonError::into_inner);
755                        queue.recv()
756                    };
757                    let Ok(job) = job else {
758                        break;
759                    };
760                    job();
761                    let mut counters = counters.lock().unwrap_or_else(PoisonError::into_inner);
762                    counters.outstanding = counters.outstanding.saturating_sub(1);
763                }
764            });
765        if started.is_err() {
766            let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
767            state.alive -= 1;
768        }
769    }
770}
771
772#[cfg(not(target_arch = "wasm32"))]
773struct BlockingWork<T> {
774    slot: Arc<Mutex<Option<T>>>,
775    done: Arc<AtomicBool>,
776    wakers: Arc<Mutex<Vec<Waker>>>,
777}
778
779#[cfg(not(target_arch = "wasm32"))]
780impl<T> Future for BlockingWork<T> {
781    type Output = T;
782
783    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
784        if self.done.load(Ordering::Acquire)
785            && let Some(value) = self
786                .slot
787                .lock()
788                .unwrap_or_else(PoisonError::into_inner)
789                .take()
790        {
791            return Poll::Ready(value);
792        }
793        self.wakers
794            .lock()
795            .unwrap_or_else(PoisonError::into_inner)
796            .push(context.waker().clone());
797        if self.done.load(Ordering::Acquire)
798            && let Some(value) = self
799                .slot
800                .lock()
801                .unwrap_or_else(PoisonError::into_inner)
802                .take()
803        {
804            return Poll::Ready(value);
805        }
806        Poll::Pending
807    }
808}
809
810/// Runs `producer` when `key` changes and exposes what it publishes as state.
811///
812/// The Compose `produceState` contract: the producer receives a handle it uses
813/// to publish values, and is cancelled when the key changes or the composition
814/// leaves.
815#[expect(non_snake_case)]
816#[track_caller]
817pub fn produceState<T, K, F>(initial: T, key: K, producer: F) -> State<T>
818where
819    T: Clone + 'static,
820    K: PartialEq + 'static,
821    F: FnOnce(ProduceScope<T>) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
822{
823    let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
824    let handle = ProduceScope { state };
825    crate::__launched_effect_async_impl(
826        crate::caller_location_key(),
827        std::panic::Location::caller().into(),
828        key,
829        move |_scope| producer(handle),
830    );
831    state.as_state()
832}
833
834/// The publishing half handed to a [`produceState`] producer.
835pub struct ProduceScope<T: Clone + 'static> {
836    state: MutableState<T>,
837}
838
839impl<T: Clone + 'static> ProduceScope<T> {
840    /// Publishes `value` to the produced state.
841    pub fn set(&self, value: T) {
842        self.state.set(value);
843    }
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849
850    #[test]
851    fn a_delay_resolves_after_its_deadline() {
852        let started = Instant::now();
853        pollster::block_on(delay(Duration::from_millis(30)));
854        assert!(started.elapsed() >= Duration::from_millis(25));
855    }
856
857    #[test]
858    fn many_delays_share_one_timer_and_all_fire() {
859        let started = Instant::now();
860        pollster::block_on(async {
861            for _ in 0..4 {
862                delay(Duration::from_millis(5)).await;
863            }
864        });
865        assert!(started.elapsed() >= Duration::from_millis(15));
866    }
867
868    #[test]
869    fn an_elapsed_delay_is_ready_without_arming_the_timer() {
870        let mut future = Box::pin(Delay {
871            deadline: Instant::now() - Duration::from_millis(1),
872            armed: false,
873            fired: Arc::new(AtomicBool::new(false)),
874        });
875        let waker = Waker::noop().clone();
876        assert!(
877            future
878                .as_mut()
879                .poll(&mut Context::from_waker(&waker))
880                .is_ready()
881        );
882    }
883}
884
885#[cfg(test)]
886mod stream_tests {
887    use super::*;
888
889    #[test]
890    fn a_channel_wakes_its_collector_and_ends_when_closed() {
891        let channel: EventChannel<u32> = EventChannel::new();
892        let stream = channel.stream();
893
894        let mut pending = Box::pin(stream.next());
895        let waker = Waker::noop().clone();
896        let mut context = Context::from_waker(&waker);
897        assert!(pending.as_mut().poll(&mut context).is_pending());
898
899        channel.send(7);
900        assert_eq!(pending.as_mut().poll(&mut context), Poll::Ready(Some(7)));
901
902        channel.send(8);
903        channel.close();
904        assert_eq!(pollster::block_on(stream.next()), Some(8));
905        assert_eq!(pollster::block_on(stream.next()), None);
906        assert_eq!(stream.delivered(), 2);
907    }
908
909    #[test]
910    fn an_event_goes_to_exactly_one_collector() {
911        let channel: EventChannel<u32> = EventChannel::new();
912        let first = channel.stream();
913        let second = first.clone();
914        channel.send(1);
915        channel.close();
916        assert_eq!(pollster::block_on(first.next()), Some(1));
917        assert_eq!(pollster::block_on(second.next()), None);
918    }
919
920    #[test]
921    fn sending_after_close_is_ignored() {
922        let channel: EventChannel<u32> = EventChannel::new();
923        let stream = channel.stream();
924        channel.close();
925        channel.send(1);
926        assert_eq!(pollster::block_on(stream.next()), None);
927        assert_eq!(channel.pending(), 0);
928    }
929
930    #[test]
931    fn blocking_work_resolves_with_its_result() {
932        let doubled = pollster::block_on(withBlocking(|| 21 * 2));
933        assert_eq!(doubled, 42);
934    }
935}
936
937#[cfg(test)]
938mod timer_race_tests {
939    use super::*;
940
941    #[test]
942    fn concurrent_arming_never_loses_a_wake_up() {
943        let rounds = 40;
944        let threads: Vec<_> = (0..8)
945            .map(|worker| {
946                std::thread::spawn(move || {
947                    for round in 0..rounds {
948                        let millis = 1 + ((worker + round) % 5) as u64;
949                        pollster::block_on(delay(Duration::from_millis(millis)));
950                    }
951                })
952            })
953            .collect();
954        for thread in threads {
955            thread.join().expect("every waiter is woken");
956        }
957    }
958
959    #[cfg(not(target_arch = "wasm32"))]
960    #[test]
961    fn blocking_work_reuses_its_threads_instead_of_one_per_call() {
962        use std::{collections::HashSet, sync::mpsc};
963
964        let pool = BlockingPool::new();
965
966        let (sender, receiver) = mpsc::channel();
967        for _ in 0..16 {
968            let done = Arc::new((Mutex::new(false), Condvar::new()));
969            let waiter = Arc::clone(&done);
970            let sender = sender.clone();
971            pool.submit(Box::new(move || {
972                let _ = sender.send(std::thread::current().id());
973                let (lock, signal) = &*done;
974                *lock.lock().unwrap_or_else(PoisonError::into_inner) = true;
975                signal.notify_all();
976            }));
977            let (lock, signal) = &*waiter;
978            let mut finished = lock.lock().unwrap_or_else(PoisonError::into_inner);
979            while !*finished {
980                finished = signal
981                    .wait(finished)
982                    .unwrap_or_else(PoisonError::into_inner);
983            }
984        }
985        drop(sender);
986
987        let threads: HashSet<_> = receiver.iter().collect();
988        assert!(
989            threads.len() < 16,
990            "sixteen serial jobs used {} threads; the pool is not reusing them",
991            threads.len()
992        );
993    }
994
995    #[cfg(not(target_arch = "wasm32"))]
996    #[test]
997    fn blocking_work_grows_so_one_slow_job_cannot_hold_up_another() {
998        let pool = BlockingPool::new();
999        let started = Arc::new((Mutex::new(0usize), Condvar::new()));
1000        let release = Arc::new((Mutex::new(false), Condvar::new()));
1001
1002        for _ in 0..4 {
1003            let started = Arc::clone(&started);
1004            let release = Arc::clone(&release);
1005            pool.submit(Box::new(move || {
1006                {
1007                    let (count, signal) = &*started;
1008                    *count.lock().unwrap_or_else(PoisonError::into_inner) += 1;
1009                    signal.notify_all();
1010                }
1011                let (held, signal) = &*release;
1012                let mut go = held.lock().unwrap_or_else(PoisonError::into_inner);
1013                while !*go {
1014                    go = signal.wait(go).unwrap_or_else(PoisonError::into_inner);
1015                }
1016            }));
1017        }
1018
1019        let (count, signal) = &*started;
1020        let mut running = count.lock().unwrap_or_else(PoisonError::into_inner);
1021        while *running < 4 {
1022            running = signal.wait(running).unwrap_or_else(PoisonError::into_inner);
1023        }
1024        drop(running);
1025
1026        let (held, signal) = &*release;
1027        *held.lock().unwrap_or_else(PoisonError::into_inner) = true;
1028        signal.notify_all();
1029    }
1030}