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(any(target_arch = "wasm32", target_vendor = "apple")))]
12use std::sync::{Condvar, Mutex, PoisonError};
13use std::{
14    any::Any,
15    cell::{Cell, RefCell},
16    future::Future,
17    pin::Pin,
18    rc::Rc,
19    sync::{
20        Arc, OnceLock,
21        atomic::{AtomicBool, Ordering},
22    },
23    task::{Context, Poll, Waker},
24    time::Duration,
25};
26
27#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
28use wasm_bindgen::JsCast;
29use web_time::Instant;
30
31use crate::{
32    hooks::{mutableStateOfNeverEqual, remember},
33    runtime::{RuntimeHandle, TaskHandle, current_runtime_handle},
34    state::{MutableState, State},
35};
36
37/// Spawns `future` on the current runtime's UI task queue.
38///
39/// Framework-internal: application code launches through a
40/// [`CoroutineScope`] so the work is cancelled with its
41/// composition. Returns `None` when there is no runtime on this thread.
42pub fn spawn_ui_task(future: impl Future<Output = ()> + 'static) -> Option<TaskHandle> {
43    current_runtime_handle().and_then(|runtime| runtime.spawn_ui(future))
44}
45
46/// A cancellation scope for work launched outside the composition pass.
47///
48/// Tasks launched through a scope are cancelled when the scope leaves the
49/// composition, so a click handler can start an asynchronous job without the
50/// job outliving the screen that started it.
51#[derive(Clone)]
52pub struct CoroutineScope {
53    inner: Rc<ScopeInner>,
54}
55
56struct ScopeInner {
57    runtime: Option<RuntimeHandle>,
58    tasks: RefCell<Vec<TaskHandle>>,
59    closed: Cell<bool>,
60}
61
62impl Drop for ScopeInner {
63    fn drop(&mut self) {
64        for task in self.tasks.get_mut().drain(..) {
65            task.cancel();
66        }
67    }
68}
69
70struct CompositionScopeOwner(CoroutineScope);
71
72impl Drop for CompositionScopeOwner {
73    fn drop(&mut self) {
74        self.0.inner.closed.set(true);
75        self.0.cancel();
76    }
77}
78
79impl CoroutineScope {
80    /// Launches `future`, keeping it alive until it finishes or the scope is
81    /// cancelled.
82    pub fn launch(&self, future: impl Future<Output = ()> + 'static) {
83        if self.inner.closed.get() {
84            return;
85        }
86        let Some(runtime) = self.inner.runtime.clone() else {
87            log::warn!("cranpose: a coroutine scope with no runtime dropped its work");
88            return;
89        };
90        self.inner
91            .tasks
92            .borrow_mut()
93            .retain(|task| !task.is_finished());
94        if let Some(handle) = runtime.spawn_ui(future) {
95            self.inner.tasks.borrow_mut().push(handle);
96        }
97    }
98
99    /// Cancels every task this scope launched.
100    pub fn cancel(&self) {
101        let tasks = std::mem::take(&mut *self.inner.tasks.borrow_mut());
102        for task in tasks {
103            task.cancel();
104        }
105    }
106
107    #[cfg(test)]
108    pub(crate) fn probe_identity(&self) -> usize {
109        Rc::as_ptr(&self.inner) as *const () as usize
110    }
111}
112
113/// Remembers a [`CoroutineScope`] bound to this position in the composition.
114#[expect(non_snake_case, reason = "names follow Compose's casing")]
115#[track_caller]
116pub fn rememberCoroutineScope() -> CoroutineScope {
117    remember(|| {
118        CompositionScopeOwner(CoroutineScope {
119            inner: Rc::new(ScopeInner {
120                runtime: current_runtime_handle(),
121                tasks: RefCell::new(Vec::new()),
122                closed: Cell::new(false),
123            }),
124        })
125    })
126    .with(|owner| owner.0.clone())
127}
128
129/// Resolves after `duration` has elapsed.
130///
131/// The wait is served by the framework's timer, which posts the wake-up onto
132/// the runtime's UI queue. Nothing spins and no frames are requested while a
133/// delay is pending, so a one-minute timer costs nothing for a minute.
134pub fn delay(duration: Duration) -> Delay {
135    Delay {
136        deadline: Instant::now() + duration,
137        armed: false,
138        fired: Arc::new(AtomicBool::new(false)),
139    }
140}
141
142/// The future returned by [`delay`].
143pub struct Delay {
144    deadline: Instant,
145    armed: bool,
146    fired: Arc<AtomicBool>,
147}
148
149impl Future for Delay {
150    type Output = ();
151
152    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
153        if self.fired.load(Ordering::Acquire) || Instant::now() >= self.deadline {
154            return Poll::Ready(());
155        }
156        let this = self.get_mut();
157        if !this.armed {
158            this.armed = true;
159            arm(
160                this.deadline,
161                context.waker().clone(),
162                Arc::clone(&this.fired),
163            );
164        }
165        Poll::Pending
166    }
167}
168
169/// Runs `tick` every `period` until the returned future is dropped.
170///
171/// The first tick happens after one full period, matching a repeating timer
172/// rather than a leading-edge one.
173pub async fn interval(period: Duration, mut tick: impl FnMut()) {
174    loop {
175        delay(period).await;
176        tick();
177    }
178}
179
180#[cfg(not(any(
181    all(target_arch = "wasm32", target_os = "unknown"),
182    target_vendor = "apple"
183)))]
184pub(crate) struct Alarm {
185    deadline: Instant,
186    waker: Waker,
187    fired: Arc<AtomicBool>,
188}
189
190/// Arms the alarm of a [`Delay`]. On ESP-IDF, a delay on a thread that runs
191/// a composition waits in that thread's runtime, as under Emscripten: its
192/// host fires it as it drains the UI work and wakes for it through
193/// [`next_alarm_deadline`], so no timer thread takes the 9 KB of RAM its
194/// stack and task need. A delay on any other thread waits on the timer
195/// thread.
196fn arm(deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
197    #[cfg(target_os = "espidf")]
198    if current_runtime_handle().is_some() {
199        arm_host_alarm(Alarm {
200            deadline,
201            waker,
202            fired,
203        });
204        return;
205    }
206    timer().arm(deadline, waker, fired);
207}
208
209struct Timer {
210    #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
211    alarms: Mutex<Vec<Alarm>>,
212    #[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
213    wake: Condvar,
214}
215
216fn timer() -> &'static Timer {
217    static TIMER: OnceLock<&'static Timer> = OnceLock::new();
218    TIMER.get_or_init(|| {
219        let timer: &'static Timer = Box::leak(Box::new(Timer::new()));
220        timer.start();
221        timer
222    })
223}
224
225#[cfg(not(any(target_arch = "wasm32", target_vendor = "apple")))]
226impl Timer {
227    fn new() -> Self {
228        Self {
229            alarms: Mutex::new(Vec::new()),
230            wake: Condvar::new(),
231        }
232    }
233
234    fn start(&'static self) {
235        std::thread::Builder::new()
236            .name("cranpose-timer".to_string())
237            .spawn(move || self.run())
238            .expect("the timer thread starts");
239    }
240
241    fn run(&self) {
242        let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
243        loop {
244            let now = Instant::now();
245            let mut due = Vec::new();
246            let mut next: Option<Duration> = None;
247            alarms.retain(|alarm| {
248                if alarm.deadline <= now {
249                    due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
250                    false
251                } else {
252                    let remaining = alarm.deadline - now;
253                    next = Some(next.map_or(remaining, |current| current.min(remaining)));
254                    true
255                }
256            });
257
258            if !due.is_empty() {
259                drop(alarms);
260                for (waker, fired) in due {
261                    fired.store(true, Ordering::Release);
262                    waker.wake();
263                }
264                alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
265                continue;
266            }
267
268            alarms = match next {
269                Some(timeout) => {
270                    self.wake
271                        .wait_timeout(alarms, timeout)
272                        .unwrap_or_else(PoisonError::into_inner)
273                        .0
274                }
275                None => self
276                    .wake
277                    .wait(alarms)
278                    .unwrap_or_else(PoisonError::into_inner),
279            };
280        }
281    }
282
283    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
284        let mut alarms = self.alarms.lock().unwrap_or_else(PoisonError::into_inner);
285        alarms.push(Alarm {
286            deadline,
287            waker,
288            fired,
289        });
290        self.wake.notify_one();
291    }
292}
293
294/// Apple platforms hand each deadline to Grand Central Dispatch on the
295/// user-interactive queue. A thread sleeping on a condition variable at the
296/// default quality of service wakes about 8 ms late there, as macOS coalesces
297/// its timers, so a one-frame `delay` often missed its frame; GCD at
298/// user-interactive quality of service wakes within a millisecond.
299#[cfg(target_vendor = "apple")]
300impl Timer {
301    fn new() -> Self {
302        Self {}
303    }
304
305    fn start(&'static self) {}
306
307    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
308        use dispatch2::{DispatchQoS, DispatchQueue, DispatchTime, GlobalQueueIdentifier};
309
310        let nanos = deadline
311            .saturating_duration_since(Instant::now())
312            .as_nanos()
313            .min(i64::MAX as u128) as i64;
314        let queue = DispatchQueue::global_queue(GlobalQueueIdentifier::QualityOfService(
315            DispatchQoS::UserInteractive,
316        ));
317        let fire = move || {
318            fired.store(true, Ordering::Release);
319            waker.wake();
320        };
321        if queue.after(DispatchTime::NOW.time(nanos), fire).is_err() {
322            log::error!("cranpose: GCD refused a timer; the delay never resolves");
323        }
324    }
325}
326
327#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
328impl Timer {
329    fn new() -> Self {
330        Self {}
331    }
332
333    fn start(&'static self) {}
334
335    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
336        let millis = deadline
337            .saturating_duration_since(Instant::now())
338            .as_millis()
339            .min(i32::MAX as u128) as i32;
340        let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
341            fired.store(true, Ordering::Release);
342            waker.wake();
343        });
344        let scheduled = web_sys::window().and_then(|window| {
345            window
346                .set_timeout_with_callback_and_timeout_and_arguments_0(
347                    callback.unchecked_ref(),
348                    millis,
349                )
350                .ok()
351        });
352        if scheduled.is_none() {
353            log::warn!("cranpose: no window timer is available; the delay resolves immediately");
354        }
355    }
356}
357
358// Under Emscripten there is no browser timer to call back and no thread to
359// wait, so alarms wait in the thread's runtime state, which the runtime
360// checks each time it drains its UI work. The host that embeds Cranpose,
361// such as a game engine, drives those drains with its frames.
362#[cfg(target_os = "emscripten")]
363impl Timer {
364    fn new() -> Self {
365        Self {}
366    }
367
368    fn start(&'static self) {}
369
370    fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
371        arm_host_alarm(Alarm {
372            deadline,
373            waker,
374            fired,
375        });
376    }
377}
378
379/// Puts `alarm` in the thread's runtime state, for the runtime to fire as it
380/// drains its UI work.
381#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
382fn arm_host_alarm(alarm: Alarm) {
383    crate::thread_state::runtimes(|state| state.alarms.borrow_mut().push(alarm));
384}
385
386/// When the earliest delay waiting in this thread's runtime is due, for a
387/// host that runs its own loop to wake then and drain the UI work. `None`
388/// when none waits, and on platforms whose delays wait on a timer of their
389/// own.
390pub fn next_alarm_deadline() -> Option<Instant> {
391    #[cfg(any(target_os = "emscripten", target_os = "espidf"))]
392    return crate::thread_state::try_runtimes(|state| {
393        state
394            .alarms
395            .borrow()
396            .iter()
397            .map(|alarm| alarm.deadline)
398            .min()
399    })
400    .flatten();
401    #[cfg(not(any(target_os = "emscripten", target_os = "espidf")))]
402    None
403}
404
405/// Whether a delay waiting in this thread's runtime is due, which makes UI
406/// work pending.
407#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
408pub(crate) fn has_due_alarm() -> bool {
409    next_alarm_deadline().is_some_and(|deadline| deadline <= Instant::now())
410}
411
412/// Wakes the delays whose deadline passed.
413#[cfg(any(target_os = "emscripten", target_os = "espidf"))]
414pub(crate) fn fire_due_alarms() {
415    let now = Instant::now();
416    let due: Vec<Alarm> = crate::thread_state::runtimes(|state| {
417        let mut alarms = state.alarms.borrow_mut();
418        if alarms.iter().all(|alarm| alarm.deadline > now) {
419            return Vec::new();
420        }
421        let (due, waiting) = std::mem::take(&mut *alarms)
422            .into_iter()
423            .partition(|alarm| alarm.deadline <= now);
424        *alarms = waiting;
425        due
426    });
427    for alarm in due {
428        alarm.fired.store(true, Ordering::Release);
429        alarm.waker.wake();
430    }
431}
432
433/// The producing half of an [`EventStream`].
434///
435/// A service that receives events from outside the composition — a platform
436/// callback, a worker thread, a socket — publishes through a channel, and every
437/// pending collector is woken. This is the shape that replaces
438/// "register an observer, then drain a queue" everywhere in the framework.
439pub struct EventChannel<T: 'static> {
440    shared: Rc<ChannelShared<T>>,
441}
442
443struct ChannelShared<T: 'static> {
444    ready: RefCell<std::collections::VecDeque<T>>,
445    closed: Cell<bool>,
446    delivered: Cell<usize>,
447    wakers: RefCell<Vec<Waker>>,
448}
449
450impl<T: 'static> ChannelShared<T> {
451    fn wake_all(&self) {
452        for waker in self.wakers.borrow_mut().drain(..) {
453            waker.wake();
454        }
455    }
456}
457
458impl<T: 'static> Default for EventChannel<T> {
459    fn default() -> Self {
460        Self::new()
461    }
462}
463
464impl<T: 'static> EventChannel<T> {
465    /// Creates an open channel.
466    pub fn new() -> Self {
467        Self {
468            shared: Rc::new(ChannelShared {
469                ready: RefCell::new(std::collections::VecDeque::new()),
470                closed: Cell::new(false),
471                delivered: Cell::new(0),
472                wakers: RefCell::new(Vec::new()),
473            }),
474        }
475    }
476
477    /// The consuming half, handed to collectors.
478    pub fn stream(&self) -> EventStream<T> {
479        EventStream {
480            shared: Rc::clone(&self.shared),
481        }
482    }
483
484    /// Publishes one event and wakes every pending collector.
485    pub fn send(&self, event: T) {
486        if self.shared.closed.get() {
487            return;
488        }
489        self.shared.ready.borrow_mut().push_back(event);
490        self.shared.wake_all();
491    }
492
493    /// Ends the stream. Collectors drain what is queued and then finish.
494    pub fn close(&self) {
495        if self.shared.closed.get() {
496            return;
497        }
498        self.shared.closed.set(true);
499        self.shared.wake_all();
500    }
501
502    /// Whether the channel has been closed.
503    pub fn is_closed(&self) -> bool {
504        self.shared.closed.get()
505    }
506
507    /// How many events are queued but not yet taken.
508    pub fn pending(&self) -> usize {
509        self.shared.ready.borrow().len()
510    }
511}
512
513/// The consuming half of an [`EventChannel`].
514///
515/// Collectors take events one at a time; an event goes to exactly one
516/// collector, so two collectors share the stream rather than each seeing every
517/// event.
518pub struct EventStream<T: 'static> {
519    shared: Rc<ChannelShared<T>>,
520}
521
522impl<T: 'static> Clone for EventStream<T> {
523    fn clone(&self) -> Self {
524        Self {
525            shared: Rc::clone(&self.shared),
526        }
527    }
528}
529
530impl<T: 'static> EventStream<T> {
531    /// Resolves with the next event, or `None` once the stream is closed and
532    /// drained.
533    pub fn next(&self) -> EventStreamNext<T> {
534        EventStreamNext {
535            shared: Rc::clone(&self.shared),
536        }
537    }
538
539    /// How many events this stream has handed out.
540    pub fn delivered(&self) -> usize {
541        self.shared.delivered.get()
542    }
543}
544
545/// The future returned by [`EventStream::next`].
546pub struct EventStreamNext<T: 'static> {
547    shared: Rc<ChannelShared<T>>,
548}
549
550impl<T: 'static> Future for EventStreamNext<T> {
551    type Output = Option<T>;
552
553    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
554        if let Some(event) = self.shared.ready.borrow_mut().pop_front() {
555            self.shared.delivered.set(self.shared.delivered.get() + 1);
556            return Poll::Ready(Some(event));
557        }
558        if self.shared.closed.get() {
559            return Poll::Ready(None);
560        }
561        self.shared
562            .wakers
563            .borrow_mut()
564            .push(context.waker().clone());
565        Poll::Pending
566    }
567}
568
569/// Collects `stream` for as long as this call stays in the composition,
570/// handing each event to `on_event`.
571///
572/// `key` re-starts the collection when it changes, exactly like
573/// `LaunchedEffect`.
574#[expect(non_snake_case, reason = "names follow Compose's casing")]
575#[track_caller]
576pub fn CollectEvents<T, K>(stream: EventStream<T>, key: K, on_event: impl FnMut(T) + 'static)
577where
578    T: 'static,
579    K: PartialEq + 'static,
580{
581    crate::__launched_effect_async_impl(
582        crate::caller_location_key(),
583        std::panic::Location::caller().into(),
584        key,
585        move |_scope| {
586            let mut on_event = on_event;
587            Box::pin(async move {
588                while let Some(event) = stream.next().await {
589                    on_event(event);
590                }
591            })
592        },
593    );
594}
595
596/// Collects `stream` into state, starting at `initial`.
597///
598/// The composition reads the latest value the stream produced, and recomposes
599/// when a new one arrives.
600#[expect(non_snake_case, reason = "names follow Compose's casing")]
601#[track_caller]
602pub fn collectAsState<T, K>(stream: EventStream<T>, key: K, initial: T) -> State<T>
603where
604    T: Clone + 'static,
605    K: PartialEq + 'static,
606{
607    let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
608    let sink = state;
609    CollectEvents(stream, key, move |event| sink.set(event));
610    state.as_state()
611}
612
613/// A `Send` publishing handle for a composition-scoped [`EventStream`].
614///
615/// Platform services publish events from whatever thread they run on — a JNI
616/// callback, a worker, a socket reader. The sender hops each event onto the UI
617/// thread through the runtime's dispatcher and pushes it into the stream the
618/// composition is collecting, so no service and no application ever writes that
619/// hop again.
620pub struct EventSender<T: Send + 'static> {
621    #[cfg(not(target_arch = "wasm32"))]
622    dispatcher: crate::runtime::UiDispatcher,
623    bridge: u64,
624    _events: std::marker::PhantomData<fn(T)>,
625}
626
627impl<T: Send + 'static> Clone for EventSender<T> {
628    fn clone(&self) -> Self {
629        Self {
630            #[cfg(not(target_arch = "wasm32"))]
631            dispatcher: self.dispatcher.clone(),
632            bridge: self.bridge,
633            _events: std::marker::PhantomData,
634        }
635    }
636}
637
638impl<T: Send + 'static> EventSender<T> {
639    /// Publishes `event` to the composition that owns this bridge.
640    pub fn send(&self, event: T) {
641        let bridge = self.bridge;
642        #[cfg(not(target_arch = "wasm32"))]
643        self.dispatcher
644            .post(move || deliver_bridged::<T>(bridge, event));
645        #[cfg(target_arch = "wasm32")]
646        deliver_bridged::<T>(bridge, event);
647    }
648}
649
650fn bridge_channel(bridge: u64) -> Option<Rc<dyn Any>> {
651    crate::thread_state::runtimes(|state| state.bridges.borrow().get(&bridge).cloned())
652}
653
654static NEXT_BRIDGE: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(1);
655
656fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
657    let Some(channel) = bridge_channel(bridge) else {
658        log::debug!("event bridge {bridge} is gone, one event dropped");
659        return;
660    };
661    if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
662        channel.send(event);
663    }
664}
665
666struct Bridge<T: Send + 'static> {
667    id: u64,
668    channel: Rc<EventChannel<T>>,
669}
670
671impl<T: Send + 'static> Bridge<T> {
672    fn new() -> Self {
673        let id = NEXT_BRIDGE.fetch_add(1, Ordering::Relaxed);
674        let channel = Rc::new(EventChannel::<T>::new());
675        crate::thread_state::runtimes(|state| {
676            state
677                .bridges
678                .borrow_mut()
679                .insert(id, Rc::clone(&channel) as Rc<dyn Any>)
680        });
681        Self { id, channel }
682    }
683}
684
685impl<T: Send + 'static> Drop for Bridge<T> {
686    fn drop(&mut self) {
687        crate::thread_state::try_runtimes(|state| state.bridges.borrow_mut().remove(&self.id));
688        self.channel.close();
689    }
690}
691
692/// Turns a platform subscription into a composition-scoped [`EventStream`].
693///
694/// `subscribe` receives a `Send` [`EventSender`] and returns whatever
695/// registration handle the service uses; that handle is dropped — unsubscribing
696/// the service — when `key` changes or the composition leaves. This is the one
697/// place the framework bridges "a service publishes from another thread" to
698/// "a composition collects".
699#[expect(non_snake_case, reason = "names follow Compose's casing")]
700#[track_caller]
701pub fn rememberEventStream<T, K, R, S>(key: K, subscribe: S) -> EventStream<T>
702where
703    T: Send + 'static,
704    K: PartialEq + 'static,
705    R: 'static,
706    S: FnOnce(EventSender<T>) -> R + 'static,
707{
708    let bridge = remember(Bridge::<T>::new);
709    let (id, stream) = bridge.with(|bridge| (bridge.id, bridge.channel.stream()));
710    #[cfg(not(target_arch = "wasm32"))]
711    let dispatcher = current_runtime_handle().map(|runtime| runtime.dispatcher());
712
713    crate::__disposable_effect_impl(crate::caller_location_key(), key, move |scope| {
714        #[cfg(not(target_arch = "wasm32"))]
715        let Some(dispatcher) = dispatcher else {
716            log::warn!("cranpose: an event stream was remembered without a runtime");
717            return scope.on_dispose(|| {});
718        };
719        let registration = subscribe(EventSender {
720            #[cfg(not(target_arch = "wasm32"))]
721            dispatcher,
722            bridge: id,
723            _events: std::marker::PhantomData,
724        });
725        scope.on_dispose(move || drop(registration))
726    });
727
728    stream
729}
730
731/// Runs `producer` when `key` changes and exposes what it publishes as state.
732///
733/// The Compose `produceState` contract: the producer receives a handle it uses
734/// to publish values, and is cancelled when the key changes or the composition
735/// leaves.
736#[expect(non_snake_case, reason = "names follow Compose's casing")]
737#[track_caller]
738pub fn produceState<T, K, F>(initial: T, key: K, producer: F) -> State<T>
739where
740    T: Clone + 'static,
741    K: PartialEq + 'static,
742    F: FnOnce(ProduceScope<T>) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
743{
744    let state = remember(|| mutableStateOfNeverEqual(initial)).with(|state| *state);
745    let handle = ProduceScope { state };
746    crate::__launched_effect_async_impl(
747        crate::caller_location_key(),
748        std::panic::Location::caller().into(),
749        key,
750        move |_scope| producer(handle),
751    );
752    state.as_state()
753}
754
755/// The publishing half handed to a [`produceState`] producer.
756pub struct ProduceScope<T: Clone + 'static> {
757    state: MutableState<T>,
758}
759
760impl<T: Clone + 'static> ProduceScope<T> {
761    /// Publishes `value` to the produced state.
762    pub fn set(&self, value: T) {
763        self.state.set(value);
764    }
765}
766
767#[cfg(test)]
768#[path = "tests/concurrency_tests.rs"]
769mod tests;
770
771#[cfg(test)]
772#[path = "tests/concurrency_stream_tests.rs"]
773mod stream_tests;
774
775#[cfg(test)]
776#[path = "tests/concurrency_timer_race_tests.rs"]
777mod timer_race_tests;