Skip to main content

pax_runtime_api/
async_runtime.rs

1//! Executor-neutral task lifetimes and bounded delivery to the UI thread.
2//!
3//! A scope is local authority. Only its producers, guarded publishers and task
4//! reporters can cross threads. A successful send means accepted, not delivered.
5use crate::{properties::SharedPropertyValue, Interpolatable, Property};
6use std::{
7    any::Any,
8    cell::{Cell, RefCell},
9    collections::{HashMap, HashSet, VecDeque},
10    future::Future,
11    marker::PhantomData,
12    pin::Pin,
13    rc::{Rc, Weak},
14    sync::{
15        atomic::{AtomicUsize, Ordering},
16        Arc, Mutex, MutexGuard,
17    },
18    task::{Context, Poll, Waker},
19    time::Duration,
20};
21
22pub(crate) fn lock<T>(value: &Mutex<T>) -> MutexGuard<'_, T> {
23    value.lock().unwrap_or_else(|error| error.into_inner())
24}
25
26/// Application resource limits. Counts do not bound the byte size of user values.
27#[derive(Clone, Debug)]
28pub struct AsyncLimits {
29    pub scopes: usize,
30    pub tasks: usize,
31    pub publishers: usize,
32    pub endpoints: usize,
33    pub message_slots: usize,
34    pub channel_capacity: usize,
35    pub callbacks_per_turn: usize,
36    pub callback_time: Duration,
37}
38impl Default for AsyncLimits {
39    fn default() -> Self {
40        Self {
41            scopes: 1024,
42            tasks: 1024,
43            publishers: 1024,
44            endpoints: 1024,
45            message_slots: 4096,
46            channel_capacity: 32,
47            callbacks_per_turn: 64,
48            callback_time: Duration::from_millis(2),
49        }
50    }
51}
52
53/// New work was rejected before it could start or reserve resources.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum AsyncError {
56    Closed,
57    Limit(&'static str),
58    ZeroCapacity,
59}
60impl std::fmt::Display for AsyncError {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Self::Closed => f.write_str("the work lifetime is closed"),
64            Self::Limit(name) => write!(f, "application {name} limit reached"),
65            Self::ZeroCapacity => f.write_str("a UI channel needs at least one slot"),
66        }
67    }
68}
69impl std::error::Error for AsyncError {}
70
71/// A rejected publication retains the caller's value.
72#[derive(Debug, PartialEq, Eq)]
73pub struct Closed<T>(pub T);
74/// A nonblocking channel send either retains the value or accepts it exactly once.
75#[derive(Debug, PartialEq, Eq)]
76pub enum TrySendError<T> {
77    Full(T),
78    Closed(T),
79}
80
81type Payload = Box<dyn Any + Send>;
82type Wake = Arc<dyn Fn() + Send + Sync>;
83
84// The chain is locked from the application toward the leaf. Revocation only
85// needs its own gate: it cannot return while an earlier commit holds that gate.
86#[derive(Clone)]
87pub(crate) struct Authority(Vec<Arc<Mutex<bool>>>);
88impl Authority {
89    fn root() -> Self {
90        Self(vec![Arc::new(Mutex::new(true))])
91    }
92    fn child(&self) -> Self {
93        let mut gates = self.0.clone();
94        gates.push(Arc::new(Mutex::new(true)));
95        Self(gates)
96    }
97    pub(crate) fn acquire(&self) -> Option<Vec<MutexGuard<'_, bool>>> {
98        let mut guards = Vec::with_capacity(self.0.len());
99        for gate in &self.0 {
100            let guard = lock(gate);
101            if !*guard {
102                return None;
103            }
104            guards.push(guard);
105        }
106        Some(guards)
107    }
108    fn live(&self) -> bool {
109        self.acquire().is_some()
110    }
111    fn revoke(&self) {
112        *lock(self.0.last().unwrap()) = false;
113    }
114}
115
116#[derive(Default)]
117struct Ready {
118    order: VecDeque<u64>,
119    set: HashSet<u64>,
120}
121struct Hub {
122    ready: Mutex<Ready>,
123    wake: Mutex<Wake>,
124}
125impl Hub {
126    fn notify(&self, id: u64) {
127        let wake = {
128            let mut ready = lock(&self.ready);
129            let was_empty = ready.order.is_empty();
130            if ready.set.insert(id) {
131                ready.order.push_back(id);
132            }
133            was_empty
134        };
135        if wake {
136            self.wake();
137        }
138    }
139    fn wake(&self) {
140        let wake = lock(&self.wake).clone();
141        wake();
142    }
143    fn forget(&self, id: u64) {
144        let mut ready = lock(&self.ready);
145        ready.set.remove(&id);
146        ready.order.retain(|queued| *queued != id);
147    }
148    fn prepend(&self, ids: impl IntoIterator<Item = u64>) {
149        let ids: Vec<_> = ids.into_iter().collect();
150        let mut ready = lock(&self.ready);
151        for id in ids.into_iter().rev() {
152            ready.order.retain(|queued| *queued != id);
153            ready.set.insert(id);
154            ready.order.push_front(id);
155        }
156    }
157    fn take(&self) -> Vec<u64> {
158        let mut ready = lock(&self.ready);
159        ready.set.clear();
160        ready.order.drain(..).collect()
161    }
162}
163
164struct QueueState {
165    values: VecDeque<Payload>,
166    closed: bool,
167    producers: usize,
168    waiters: HashMap<usize, Waker>,
169    next_waiter: usize,
170}
171struct Queue {
172    id: u64,
173    capacity: usize,
174    authority: Authority,
175    state: Mutex<QueueState>,
176    hub: std::sync::Weak<Hub>,
177}
178impl Queue {
179    fn notify(&self) {
180        if lock(&self.state).closed {
181            return;
182        }
183        if let Some(hub) = self.hub.upgrade() {
184            hub.notify(self.id);
185        }
186    }
187    fn send(&self, value: Payload) -> Result<(), TrySendError<Payload>> {
188        let Some(authority) = self.authority.acquire() else {
189            return Err(TrySendError::Closed(value));
190        };
191        let mut state = lock(&self.state);
192        if state.closed {
193            return Err(TrySendError::Closed(value));
194        }
195        if state.values.len() == self.capacity {
196            return Err(TrySendError::Full(value));
197        }
198        state.values.push_back(value);
199        drop(state);
200        drop(authority);
201        self.notify();
202        Ok(())
203    }
204    fn pop(&self) -> Option<Payload> {
205        let (value, waiters) = {
206            let mut state = lock(&self.state);
207            (state.values.pop_front(), std::mem::take(&mut state.waiters))
208        };
209        for waiter in waiters.into_values() {
210            waiter.wake();
211        }
212        value
213    }
214    fn close(&self) {
215        let (values, waiters) = {
216            let mut state = lock(&self.state);
217            state.closed = true;
218            (
219                std::mem::take(&mut state.values),
220                std::mem::take(&mut state.waiters),
221            )
222        };
223        drop(values);
224        for waiter in waiters.into_values() {
225            waiter.wake();
226        }
227    }
228    fn finished(&self) -> bool {
229        let state = lock(&self.state);
230        state.closed || (state.producers == 0 && state.values.is_empty())
231    }
232}
233
234/// Transferable producer for a bounded UI channel. Its callback stays local.
235pub struct UiSender<T> {
236    queue: Arc<Queue>,
237    _payload: PhantomData<Mutex<T>>,
238}
239impl<T> Clone for UiSender<T> {
240    fn clone(&self) -> Self {
241        lock(&self.queue.state).producers += 1;
242        Self {
243            queue: self.queue.clone(),
244            _payload: PhantomData,
245        }
246    }
247}
248impl<T> Drop for UiSender<T> {
249    fn drop(&mut self) {
250        let last = {
251            let mut state = lock(&self.queue.state);
252            state.producers -= 1;
253            state.producers == 0
254        };
255        if last {
256            self.queue.notify();
257        }
258    }
259}
260impl<T: Send + 'static> UiSender<T> {
261    /// Enqueue immediately, reporting Full or Closed without losing the value.
262    pub fn try_send(&self, value: T) -> Result<(), TrySendError<T>> {
263        self.queue
264            .send(Box::new(value))
265            .map_err(|error| match error {
266                TrySendError::Full(value) => TrySendError::Full(*value.downcast().unwrap()),
267                TrySendError::Closed(value) => TrySendError::Closed(*value.downcast().unwrap()),
268            })
269    }
270    /// Wait for capacity without blocking the UI or an executor thread.
271    pub fn send(&self, value: T) -> SendFuture<'_, T> {
272        SendFuture {
273            sender: self,
274            value: Some(value),
275            waiter: None,
276        }
277    }
278}
279
280/// Capacity wait owned by one send attempt; dropping it unregisters its waker.
281pub struct SendFuture<'a, T> {
282    sender: &'a UiSender<T>,
283    value: Option<T>,
284    waiter: Option<usize>,
285}
286impl<T> Unpin for SendFuture<'_, T> {}
287impl<T> Drop for SendFuture<'_, T> {
288    fn drop(&mut self) {
289        if let Some(id) = self.waiter {
290            let removed = lock(&self.sender.queue.state).waiters.remove(&id);
291            drop(removed);
292        }
293    }
294}
295impl<T: Send + 'static> Future for SendFuture<'_, T> {
296    type Output = Result<(), Closed<T>>;
297    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
298        let queue = self.sender.queue.clone();
299        let waker = cx.waker().clone();
300        let Some(authority) = queue.authority.acquire() else {
301            return Poll::Ready(Err(Closed(
302                self.value.take().expect("send polled after completion"),
303            )));
304        };
305        let mut state = lock(&queue.state);
306        if state.closed {
307            return Poll::Ready(Err(Closed(
308                self.value.take().expect("send polled after completion"),
309            )));
310        }
311        if state.values.len() < queue.capacity {
312            let previous = self.waiter.take().and_then(|id| state.waiters.remove(&id));
313            state.values.push_back(Box::new(
314                self.value.take().expect("send polled after completion"),
315            ));
316            drop(state);
317            drop(authority);
318            drop(previous);
319            queue.notify();
320            return Poll::Ready(Ok(()));
321        }
322        let id = *self.waiter.get_or_insert_with(|| {
323            state.next_waiter += 1;
324            state.next_waiter
325        });
326        let previous = state.waiters.insert(id, waker);
327        drop(state);
328        drop(authority);
329        drop(previous);
330        Poll::Pending
331    }
332}
333
334/// Single-use producer. Dropping it unused releases the callback on the UI turn.
335pub struct Completion<T>(UiSender<T>);
336impl<T: Send + 'static> Completion<T> {
337    pub fn complete(self, value: T) -> Result<(), Closed<T>> {
338        self.0.try_send(value).map_err(|error| match error {
339            TrySendError::Closed(value) | TrySendError::Full(value) => Closed(value),
340        })
341    }
342}
343
344/// Keeps a UI channel subscribed. Drop closes it and releases its local callback.
345pub struct UiSubscription {
346    owner: Weak<DispatcherInner>,
347    id: u64,
348}
349impl Drop for UiSubscription {
350    fn drop(&mut self) {
351        if let Some(owner) = self.owner.upgrade() {
352            owner.remove_endpoint(self.id);
353        }
354    }
355}
356
357struct PublisherLease {
358    count: Arc<AtomicUsize>,
359}
360impl Drop for PublisherLease {
361    fn drop(&mut self) {
362        self.count.fetch_sub(1, Ordering::Relaxed);
363    }
364}
365/// A shared property writer whose commit is atomic with lifetime revocation.
366/// Cloning this handle does not acquire another registration. Raw `Property`
367/// clones remain deliberately unguarded.
368#[derive(Clone)]
369pub struct GuardedPublisher<T> {
370    property: Property<T>,
371    authority: Authority,
372    _lease: Arc<PublisherLease>,
373}
374impl<T: SharedPropertyValue> GuardedPublisher<T> {
375    pub fn set(&self, value: T) -> Result<(), Closed<T>> {
376        self.property.publish_guarded(value, &self.authority)
377    }
378    pub fn is_closed(&self) -> bool {
379        !self.authority.live()
380    }
381}
382
383type CallbackContext = Rc<dyn Fn(&mut dyn FnMut())>;
384struct ScopeInner {
385    id: u64,
386    owner: Weak<DispatcherInner>,
387    authority: Authority,
388    callback_context: RefCell<Option<CallbackContext>>,
389    _parent: Option<AsyncScope>,
390}
391impl Drop for ScopeInner {
392    fn drop(&mut self) {
393        self.authority.revoke();
394        if let Some(owner) = self.owner.upgrade() {
395            owner.scopes.borrow_mut().remove(&self.id);
396        }
397    }
398}
399/// Local lifetime controller. Cancellation revokes descendants before requesting
400/// task abortion. Keep a component's scope from its mount context when handlers
401/// later receive contexts for child controls.
402///
403/// ```compile_fail
404/// use pax_runtime_api::{application::{ApplicationInstance, AppOptions}, TargetInfo};
405/// let app = ApplicationInstance::prepare(TargetInfo::default(), AppOptions::default()).unwrap();
406/// let scope = app.context().async_scope().unwrap();
407/// std::thread::spawn(move || scope.cancel());
408/// ```
409#[derive(Clone)]
410pub struct AsyncScope(Rc<ScopeInner>);
411impl AsyncScope {
412    fn owner(&self) -> Result<Rc<DispatcherInner>, AsyncError> {
413        if !self.0.authority.live() {
414            return Err(AsyncError::Closed);
415        }
416        self.0.owner.upgrade().ok_or(AsyncError::Closed)
417    }
418    /// Restores a mounted node's property ownership around local delivery.
419    #[doc(hidden)]
420    pub fn set_callback_context(&self, enter: impl Fn(&mut dyn FnMut()) + 'static) {
421        *self.0.callback_context.borrow_mut() = Some(Rc::new(enter));
422    }
423    pub fn id(&self) -> u64 {
424        self.0.id
425    }
426    pub fn is_closed(&self) -> bool {
427        !self.0.authority.live() || self.0.owner.strong_count() == 0
428    }
429    /// Create an independent operation underneath this lifetime.
430    pub fn child(&self) -> Result<Self, AsyncError> {
431        let owner = self.owner()?;
432        owner.scope(Some(self.clone()))
433    }
434    pub fn cancel(&self) {
435        self.0.authority.revoke();
436        if let Some(owner) = self.0.owner.upgrade() {
437            owner.sweep_cancelled();
438        }
439    }
440    /// Revoke the previous request before publishing replacement state and
441    /// creating its new operation. Registration failure leaves the old work closed.
442    pub fn replace<T: SharedPropertyValue>(
443        &self,
444        previous: &mut Option<AsyncScope>,
445        property: &Property<T>,
446        initial: T,
447    ) -> Result<AsyncScope, AsyncError> {
448        if let Some(old) = previous.take() {
449            old.cancel();
450        }
451        self.owner()?;
452        property.set(initial);
453        let next = self.child()?;
454        *previous = Some(next.clone());
455        Ok(next)
456    }
457    pub fn publisher<T: SharedPropertyValue>(
458        &self,
459        property: &Property<T>,
460    ) -> Result<GuardedPublisher<T>, AsyncError> {
461        let owner = self.owner()?;
462        owner
463            .publishers
464            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
465                (count < owner.limits.publishers).then_some(count + 1)
466            })
467            .map_err(|_| AsyncError::Limit("publisher"))?;
468        Ok(GuardedPublisher {
469            property: property.clone(),
470            authority: self.0.authority.clone(),
471            _lease: Arc::new(PublisherLease {
472                count: owner.publishers.clone(),
473            }),
474        })
475    }
476    pub fn completion<T: Send + 'static>(
477        &self,
478        callback: impl FnOnce(T) + 'static,
479    ) -> Result<Completion<T>, AsyncError> {
480        let mut callback = Some(callback);
481        let sender = self.endpoint(1, true, move |value| callback.take().unwrap()(value))?;
482        Ok(Completion(sender))
483    }
484    pub fn channel<T: Send + 'static>(
485        &self,
486        capacity: usize,
487        callback: impl FnMut(T) + 'static,
488    ) -> Result<(UiSender<T>, UiSubscription), AsyncError> {
489        let sender = self.endpoint(capacity, false, callback)?;
490        let subscription = UiSubscription {
491            owner: self.0.owner.clone(),
492            id: sender.queue.id,
493        };
494        Ok((sender, subscription))
495    }
496    pub fn default_channel<T: Send + 'static>(
497        &self,
498        callback: impl FnMut(T) + 'static,
499    ) -> Result<(UiSender<T>, UiSubscription), AsyncError> {
500        self.channel(self.owner()?.limits.channel_capacity, callback)
501    }
502    fn endpoint<T: Send + 'static>(
503        &self,
504        capacity: usize,
505        once: bool,
506        mut callback: impl FnMut(T) + 'static,
507    ) -> Result<UiSender<T>, AsyncError> {
508        if capacity == 0 {
509            return Err(AsyncError::ZeroCapacity);
510        }
511        let owner = self.owner()?;
512        if owner.endpoints.borrow().len() >= owner.limits.endpoints {
513            return Err(AsyncError::Limit("endpoint"));
514        }
515        if capacity > owner.limits.message_slots.saturating_sub(owner.slots.get()) {
516            return Err(AsyncError::Limit("message slot"));
517        }
518        let id = owner.next_id();
519        let queue = Arc::new(Queue {
520            id,
521            capacity,
522            authority: self.0.authority.clone(),
523            hub: Arc::downgrade(&owner.hub),
524            state: Mutex::new(QueueState {
525                values: VecDeque::new(),
526                closed: false,
527                producers: 1,
528                waiters: HashMap::new(),
529                next_waiter: 0,
530            }),
531        });
532        owner.slots.set(owner.slots.get() + capacity);
533        owner.endpoints.borrow_mut().insert(
534            id,
535            Rc::new(Endpoint {
536                scope: self.clone(),
537                queue: queue.clone(),
538                once,
539                callback: RefCell::new(Box::new(move |payload| {
540                    callback(*payload.downcast().unwrap())
541                })),
542            }),
543        );
544        Ok(UiSender {
545            queue,
546            _payload: PhantomData,
547        })
548    }
549    /// Adapter entry point. `start` runs only after activation and returns a
550    /// nonblocking cancellation request. Retaining the control is optional.
551    pub fn register_task(
552        &self,
553        start: impl FnOnce(TaskReporter) -> Box<dyn FnOnce()> + 'static,
554    ) -> Result<TaskControl, AsyncError> {
555        let owner = self.owner()?;
556        if owner.tasks.borrow().len() >= owner.limits.tasks {
557            return Err(AsyncError::Limit("task"));
558        }
559        let id = owner.next_id();
560        let status = Property::new(TaskStatus::Staged);
561        owner.tasks.borrow_mut().insert(
562            id,
563            TaskEntry {
564                scope: self.clone(),
565                status: status.clone(),
566                start: Some(Box::new(start)),
567                cancel: None,
568            },
569        );
570        if owner.active.get() {
571            owner.start_task(id);
572        }
573        Ok(TaskControl {
574            scope: self.clone(),
575            status,
576        })
577    }
578}
579
580/// Observable task state. A cancellation request is not proof that work stopped.
581#[derive(Clone, Debug, Default, PartialEq, Eq)]
582pub enum TaskStatus {
583    #[default]
584    Staged,
585    Running,
586    CancelRequested,
587    Completed,
588    Cancelled,
589    Panicked(String),
590}
591impl Interpolatable for TaskStatus {}
592/// The callback result keeps application `Result<T, E>` separate from a task panic.
593#[derive(Debug)]
594pub enum TaskOutcome<T> {
595    Completed(T),
596    Panicked(String),
597}
598/// UI-owned task control; dropping it does not detach work from its scope.
599#[derive(Clone)]
600pub struct TaskControl {
601    scope: AsyncScope,
602    status: Property<TaskStatus>,
603}
604impl TaskControl {
605    pub fn cancel(&self) {
606        self.scope.cancel();
607    }
608    pub fn status(&self) -> TaskStatus {
609        self.status.get()
610    }
611    /// A snapshot property for reactive status displays; applications should not
612    /// mutate this adapter-owned state.
613    pub fn status_property(&self) -> crate::properties::Published<TaskStatus> {
614        self.status.published()
615    }
616}
617/// Transferable completion signal owned by an executor adapter. Drop means the
618/// task stopped without normal completion, usually after an abort request.
619pub struct TaskReporter {
620    id: u64,
621    status: Property<TaskStatus>,
622    hub: std::sync::Weak<Hub>,
623    finished: bool,
624}
625impl TaskReporter {
626    pub fn finish(mut self, outcome: TaskStatus) {
627        if let TaskStatus::Panicked(error) = &outcome {
628            log::error!("async task panicked: {error}");
629        }
630        self.status.set(outcome);
631        self.finished = true;
632        if let Some(hub) = self.hub.upgrade() {
633            hub.notify(self.id);
634        }
635    }
636}
637impl Drop for TaskReporter {
638    fn drop(&mut self) {
639        if !self.finished {
640            self.status.set(TaskStatus::Cancelled);
641            if let Some(hub) = self.hub.upgrade() {
642                hub.notify(self.id);
643            }
644        }
645    }
646}
647
648type StartTask = Box<dyn FnOnce(TaskReporter) -> Box<dyn FnOnce()>>;
649struct TaskEntry {
650    scope: AsyncScope,
651    status: Property<TaskStatus>,
652    start: Option<StartTask>,
653    cancel: Option<Box<dyn FnOnce()>>,
654}
655struct Endpoint {
656    scope: AsyncScope,
657    queue: Arc<Queue>,
658    once: bool,
659    callback: RefCell<Box<dyn FnMut(Payload)>>,
660}
661struct DispatcherInner {
662    limits: AsyncLimits,
663    active: Cell<bool>,
664    root: Authority,
665    sequence: Cell<u64>,
666    hub: Arc<Hub>,
667    scopes: RefCell<HashMap<u64, Weak<ScopeInner>>>,
668    endpoints: RefCell<HashMap<u64, Rc<Endpoint>>>,
669    tasks: RefCell<HashMap<u64, TaskEntry>>,
670    slots: Cell<usize>,
671    publishers: Arc<AtomicUsize>,
672    error_sink: RefCell<Rc<dyn Fn(String)>>,
673    draining: Cell<bool>,
674}
675impl DispatcherInner {
676    fn next_id(&self) -> u64 {
677        let id = self
678            .sequence
679            .get()
680            .checked_add(1)
681            .expect("async identity exhausted");
682        self.sequence.set(id);
683        id
684    }
685    fn scope(self: &Rc<Self>, parent: Option<AsyncScope>) -> Result<AsyncScope, AsyncError> {
686        if !self.root.live() {
687            return Err(AsyncError::Closed);
688        }
689        if self.scopes.borrow().len() >= self.limits.scopes {
690            return Err(AsyncError::Limit("scope"));
691        }
692        let id = self.next_id();
693        let authority = parent
694            .as_ref()
695            .map_or_else(|| self.root.child(), |parent| parent.0.authority.child());
696        let callback_context = parent
697            .as_ref()
698            .and_then(|parent| parent.0.callback_context.borrow().clone());
699        let inner = Rc::new(ScopeInner {
700            id,
701            owner: Rc::downgrade(self),
702            authority,
703            callback_context: RefCell::new(callback_context),
704            _parent: parent,
705        });
706        self.scopes.borrow_mut().insert(id, Rc::downgrade(&inner));
707        Ok(AsyncScope(inner))
708    }
709    fn remove_endpoint(&self, id: u64) {
710        let endpoint = self.endpoints.borrow_mut().remove(&id);
711        if let Some(endpoint) = endpoint {
712            self.slots.set(self.slots.get() - endpoint.queue.capacity);
713            endpoint.queue.close();
714            self.hub.forget(id);
715            drop(endpoint);
716        }
717    }
718    fn start_task(&self, id: u64) {
719        let start = {
720            let mut tasks = self.tasks.borrow_mut();
721            let Some(task) = tasks.get_mut(&id) else {
722                return;
723            };
724            if task.scope.is_closed() {
725                return;
726            }
727            task.start.take().map(|start| (start, task.status.clone()))
728        };
729        if let Some((start, status)) = start {
730            status.set(TaskStatus::Running);
731            let reporter = TaskReporter {
732                id,
733                status: status.clone(),
734                hub: Arc::downgrade(&self.hub),
735                finished: false,
736            };
737            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| start(reporter))) {
738                Ok(cancel) => {
739                    let mut cancel = Some(cancel);
740                    if let Some(task) = self.tasks.borrow_mut().get_mut(&id) {
741                        if !task.scope.is_closed() {
742                            task.cancel = cancel.take();
743                        }
744                    }
745                    if let Some(cancel) = cancel {
746                        cancel();
747                    }
748                }
749                Err(panic) => {
750                    status.set(TaskStatus::Panicked(panic_message(panic)));
751                    self.hub.notify(id);
752                }
753            }
754        }
755    }
756    fn sweep_cancelled(&self) {
757        let endpoints: Vec<_> = self
758            .endpoints
759            .borrow()
760            .iter()
761            .filter(|(_, endpoint)| endpoint.scope.is_closed())
762            .map(|(id, _)| *id)
763            .collect();
764        for id in endpoints {
765            self.remove_endpoint(id);
766        }
767        let cancelled: Vec<_> = self
768            .tasks
769            .borrow()
770            .iter()
771            .filter(|(_, task)| task.scope.is_closed())
772            .map(|(id, _)| *id)
773            .collect();
774        for id in cancelled {
775            let (staged, cancel, status) = {
776                let mut tasks = self.tasks.borrow_mut();
777                let Some(task) = tasks.get_mut(&id) else {
778                    continue;
779                };
780                (task.start.take(), task.cancel.take(), task.status.clone())
781            };
782            if staged.is_some() {
783                status.set(TaskStatus::Cancelled);
784                drop(staged);
785                self.hub.notify(id);
786            } else if let Some(cancel) = cancel {
787                if matches!(status.get(), TaskStatus::Running) {
788                    status.set(TaskStatus::CancelRequested);
789                }
790                cancel();
791            }
792        }
793    }
794}
795impl Drop for DispatcherInner {
796    fn drop(&mut self) {
797        self.root.revoke();
798        for endpoint in self.endpoints.get_mut().values() {
799            endpoint.queue.close();
800        }
801        for task in self.tasks.get_mut().values_mut() {
802            if let Some(cancel) = task.cancel.take() {
803                cancel();
804            }
805        }
806    }
807}
808
809/// Chassis-owned delivery scheduler. Each turn snapshots eligible queue lengths,
810/// visits channels round-robin, and settles structural changes before every call.
811#[doc(hidden)]
812#[derive(Clone)]
813pub struct AsyncDispatcher(Rc<DispatcherInner>);
814impl AsyncDispatcher {
815    pub fn new(limits: AsyncLimits) -> Self {
816        Self(Rc::new(DispatcherInner {
817            limits,
818            active: Cell::new(false),
819            root: Authority::root(),
820            sequence: Cell::new(0),
821            hub: Arc::new(Hub {
822                ready: Mutex::new(Ready::default()),
823                wake: Mutex::new(Arc::new(|| {})),
824            }),
825            scopes: RefCell::new(HashMap::new()),
826            endpoints: RefCell::new(HashMap::new()),
827            tasks: RefCell::new(HashMap::new()),
828            slots: Cell::new(0),
829            publishers: Arc::new(AtomicUsize::new(0)),
830            error_sink: RefCell::new(Rc::new(|error| log::error!("async task failed: {error}"))),
831            draining: Cell::new(false),
832        }))
833    }
834    pub fn scope(&self) -> Result<AsyncScope, AsyncError> {
835        self.0.scope(None)
836    }
837    pub fn set_waker(&self, wake: impl Fn() + Send + Sync + 'static) {
838        *lock(&self.0.hub.wake) = Arc::new(wake);
839        if self.has_pending() {
840            self.0.hub.wake();
841        }
842    }
843    pub fn set_error_sink(&self, sink: impl Fn(String) + 'static) {
844        *self.0.error_sink.borrow_mut() = Rc::new(sink);
845    }
846    pub fn has_pending(&self) -> bool {
847        self.0.active.get() && !lock(&self.0.hub.ready).order.is_empty()
848    }
849    pub fn activate(&self) {
850        if self.0.active.replace(true) || !self.0.root.live() {
851            return;
852        }
853        let tasks: Vec<_> = self.0.tasks.borrow().keys().copied().collect();
854        for id in tasks {
855            self.0.start_task(id);
856        }
857        if self.has_pending() {
858            self.0.hub.wake();
859        }
860    }
861    pub fn close(&self) {
862        self.0.root.revoke();
863        self.0.sweep_cancelled();
864    }
865    pub fn task_count(&self) -> usize {
866        self.0.tasks.borrow().len()
867    }
868    pub fn counts(&self) -> (usize, usize, usize, usize) {
869        (
870            self.0.scopes.borrow().len(),
871            self.0.tasks.borrow().len(),
872            self.0.endpoints.borrow().len(),
873            self.0.publishers.load(Ordering::Relaxed),
874        )
875    }
876    pub fn drain(&self, mut settle: impl FnMut()) -> Result<usize, String> {
877        if !self.0.active.get() || self.0.draining.replace(true) {
878            return Ok(0);
879        }
880        struct Reset<'a>(&'a Cell<bool>);
881        impl Drop for Reset<'_> {
882            fn drop(&mut self) {
883                self.0.set(false);
884            }
885        }
886        let _reset = Reset(&self.0.draining);
887        let mut ready = VecDeque::new();
888        for id in self.0.hub.take() {
889            let task = {
890                let mut tasks = self.0.tasks.borrow_mut();
891                if tasks.get(&id).is_some_and(|task| {
892                    matches!(
893                        task.status.get(),
894                        TaskStatus::Completed | TaskStatus::Cancelled | TaskStatus::Panicked(_)
895                    )
896                }) {
897                    tasks.remove(&id)
898                } else {
899                    None
900                }
901            };
902            if let Some(task) = task {
903                if let TaskStatus::Panicked(error) = task.status.get() {
904                    let sink = self.0.error_sink.borrow().clone();
905                    if let Err(panic) =
906                        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink(error)))
907                    {
908                        self.close();
909                        return Err(panic_message(panic));
910                    }
911                }
912                drop(task);
913            }
914            let endpoint = self.0.endpoints.borrow().get(&id).cloned();
915            if let Some(endpoint) = endpoint {
916                let quota = lock(&endpoint.queue.state).values.len();
917                if quota == 0 && endpoint.queue.finished() {
918                    self.0.remove_endpoint(id);
919                } else if quota > 0 {
920                    ready.push_back((id, quota));
921                }
922            }
923        }
924        let started = monotonic_now();
925        let mut delivered = 0;
926        while !ready.is_empty()
927            && delivered < self.0.limits.callbacks_per_turn
928            && (delivered == 0
929                || monotonic_now().saturating_sub(started) < self.0.limits.callback_time)
930        {
931            settle();
932            let (id, quota) = ready.pop_front().unwrap();
933            let endpoint = self.0.endpoints.borrow().get(&id).cloned();
934            let Some(endpoint) = endpoint else {
935                continue;
936            };
937            if endpoint.scope.is_closed() {
938                self.0.remove_endpoint(id);
939                continue;
940            }
941            if let Some(value) = endpoint.queue.pop() {
942                let context = endpoint.scope.0.callback_context.borrow().clone();
943                let mut value = Some(value);
944                let mut deliver = || {
945                    (endpoint.callback.borrow_mut())(
946                        value.take().expect("callback context invoked twice"),
947                    )
948                };
949                let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
950                    if let Some(context) = context {
951                        context(&mut deliver);
952                    } else {
953                        deliver();
954                    }
955                }));
956                if let Err(panic) = result {
957                    self.close();
958                    return Err(panic_message(panic));
959                }
960                delivered += 1;
961                if endpoint.once || endpoint.queue.finished() {
962                    self.0.remove_endpoint(id);
963                } else if quota > 1 {
964                    ready.push_back((id, quota - 1));
965                }
966            }
967        }
968        // Preserve the next round across budgets. Enqueues during callbacks
969        // remain behind this round and never run reentrantly in the same drain.
970        self.0.hub.prepend(ready.into_iter().map(|(id, _)| id));
971        if self.has_pending() {
972            self.0.hub.wake();
973        }
974        Ok(delivered)
975    }
976}
977
978/// Converts an unwinding panic into an observable task/callback failure.
979pub fn panic_message(panic: Box<dyn Any + Send>) -> String {
980    panic
981        .downcast_ref::<String>()
982        .cloned()
983        .or_else(|| {
984            panic
985                .downcast_ref::<&str>()
986                .map(|value| (*value).to_owned())
987        })
988        .unwrap_or_else(|| "task panicked with a non-string payload".into())
989}
990
991/// A local owner that can bind work to its lifetime without exposing a node to
992/// an executor. Implemented by `AppContext` and runtime `NodeContext`.
993pub trait AsyncScopeSource {
994    fn work_scope(&self) -> Result<AsyncScope, AsyncError>;
995}
996
997#[cfg(test)]
998mod tests {
999    use super::*;
1000    use std::{cell::RefCell, sync::atomic::AtomicBool, task::Wake};
1001    fn dispatcher() -> (AsyncDispatcher, AsyncScope) {
1002        let dispatcher = AsyncDispatcher::new(AsyncLimits::default());
1003        let scope = dispatcher.scope().unwrap();
1004        dispatcher.activate();
1005        (dispatcher, scope)
1006    }
1007    struct Flag(AtomicBool);
1008    impl Wake for Flag {
1009        fn wake(self: Arc<Self>) {
1010            self.0.store(true, Ordering::SeqCst);
1011        }
1012    }
1013    #[test]
1014    fn worker_completion_runs_and_drops_its_local_capture_only_on_the_owner() {
1015        let (dispatcher, scope) = dispatcher();
1016        let owner = std::thread::current().id();
1017        let value = Rc::new(Cell::new(0));
1018        let captured = value.clone();
1019        let completion = scope
1020            .completion(move |result| {
1021                assert_eq!(std::thread::current().id(), owner);
1022                captured.set(result);
1023            })
1024            .unwrap();
1025        std::thread::spawn(move || completion.complete(42).unwrap())
1026            .join()
1027            .unwrap();
1028        assert_eq!(value.get(), 0);
1029        dispatcher.drain(|| {}).unwrap();
1030        assert_eq!(value.get(), 42);
1031        assert_eq!(Rc::strong_count(&value), 1);
1032        assert_eq!(dispatcher.counts().2, 0);
1033    }
1034    #[test]
1035    fn unused_producer_drop_releases_callback_on_next_owner_turn() {
1036        let (dispatcher, scope) = dispatcher();
1037        let local = Rc::new(());
1038        let capture = local.clone();
1039        let completion = scope.completion(move |_: ()| drop(capture)).unwrap();
1040        std::thread::spawn(move || drop(completion)).join().unwrap();
1041        assert_eq!(Rc::strong_count(&local), 2);
1042        dispatcher.drain(|| {}).unwrap();
1043        assert_eq!(Rc::strong_count(&local), 1);
1044    }
1045    #[test]
1046    fn structural_settlement_precedes_every_callback_and_revokes_queued_work() {
1047        let (dispatcher, scope) = dispatcher();
1048        let node = scope.child().unwrap();
1049        let removed = Rc::new(Cell::new(false));
1050        let remove = removed.clone();
1051        let first = scope.completion(move |_: ()| remove.set(true)).unwrap();
1052        let second = node
1053            .completion(|_: ()| panic!("removed node callback must not start"))
1054            .unwrap();
1055        first.complete(()).unwrap();
1056        second.complete(()).unwrap();
1057        assert_eq!(
1058            dispatcher
1059                .drain(|| {
1060                    if removed.get() {
1061                        node.cancel();
1062                    }
1063                })
1064                .unwrap(),
1065            1
1066        );
1067        assert_eq!(dispatcher.counts().2, 0);
1068    }
1069    #[test]
1070    fn callbacks_enqueue_for_a_later_drain_and_channels_rotate_under_budget() {
1071        let mut limits = AsyncLimits::default();
1072        limits.callbacks_per_turn = 2;
1073        let dispatcher = AsyncDispatcher::new(limits);
1074        let scope = dispatcher.scope().unwrap();
1075        dispatcher.activate();
1076        let history = Rc::new(RefCell::new(Vec::new()));
1077        let record = history.clone();
1078        let (a, _a) = scope
1079            .channel(4, move |value| record.borrow_mut().push(value))
1080            .unwrap();
1081        let record = history.clone();
1082        let a2 = a.clone();
1083        let (b, _b) = scope
1084            .channel(2, move |value| {
1085                record.borrow_mut().push(value);
1086                a2.try_send(9).unwrap();
1087            })
1088            .unwrap();
1089        a.try_send(1).unwrap();
1090        a.try_send(2).unwrap();
1091        b.try_send(3).unwrap();
1092        dispatcher.drain(|| {}).unwrap();
1093        assert_eq!(*history.borrow(), [1, 3]);
1094        dispatcher.drain(|| {}).unwrap();
1095        assert_eq!(*history.borrow(), [1, 3, 2, 9]);
1096    }
1097    #[test]
1098    fn closure_wakes_capacity_waiters_and_returns_their_values() {
1099        let (dispatcher, scope) = dispatcher();
1100        let (sender, subscription) = scope.channel(1, |_: usize| {}).unwrap();
1101        sender.try_send(1).unwrap();
1102        assert_eq!(sender.try_send(2), Err(TrySendError::Full(2)));
1103        let wake = Arc::new(Flag(AtomicBool::new(false)));
1104        let waker = Waker::from(wake.clone());
1105        let mut cx = Context::from_waker(&waker);
1106        let mut future = Box::pin(sender.send(3));
1107        assert!(future.as_mut().poll(&mut cx).is_pending());
1108        drop(subscription);
1109        assert!(wake.0.load(Ordering::SeqCst));
1110        assert_eq!(future.as_mut().poll(&mut cx), Poll::Ready(Err(Closed(3))));
1111        dispatcher.drain(|| {}).unwrap();
1112        assert_eq!(dispatcher.counts().2, 0);
1113    }
1114    #[test]
1115    fn late_guarded_writes_cannot_overwrite_replacement_even_from_retained_workers() {
1116        let (_dispatcher, scope) = dispatcher();
1117        let target = Property::new(0usize);
1118        let mut previous = None;
1119        let old = scope.replace(&mut previous, &target, 1).unwrap();
1120        let publisher = old.publisher(&target).unwrap();
1121        publisher.set(2).unwrap();
1122        let next = scope.replace(&mut previous, &target, 3).unwrap();
1123        assert_eq!(
1124            std::thread::spawn(move || publisher.set(99))
1125                .join()
1126                .unwrap(),
1127            Err(Closed(99))
1128        );
1129        assert_eq!(target.get(), 3);
1130        next.publisher(&target).unwrap().set(4).unwrap();
1131        scope.cancel();
1132        assert_eq!(target.get(), 4);
1133    }
1134    #[test]
1135    fn revocation_racing_a_worker_commit_cannot_overwrite_replacement_state() {
1136        let (_dispatcher, scope) = dispatcher();
1137        let target = Property::new(0usize);
1138        for _ in 0..128 {
1139            let operation = scope.child().unwrap();
1140            let publisher = operation.publisher(&target).unwrap();
1141            let start = Arc::new(std::sync::Barrier::new(2));
1142            let worker_start = start.clone();
1143            let worker = std::thread::spawn(move || {
1144                worker_start.wait();
1145                let _ = publisher.set(99);
1146            });
1147            start.wait();
1148            operation.cancel();
1149            target.set(7);
1150            worker.join().unwrap();
1151            assert_eq!(target.get(), 7);
1152        }
1153    }
1154    #[test]
1155    fn guarded_commit_releases_authority_before_waking_arbitrary_host_code() {
1156        let (_dispatcher, scope) = dispatcher();
1157        let authority = scope.0.authority.clone();
1158        let graph = crate::PropertyGraph::new(move || authority.revoke());
1159        let _entered = graph.enter();
1160        let target = Property::new(0usize);
1161        let _view = target.local();
1162        let publisher = scope.publisher(&target).unwrap();
1163        publisher.set(1).unwrap();
1164        assert!(publisher.is_closed());
1165        assert_eq!(target.get(), 1);
1166    }
1167    #[test]
1168    fn prepared_tasks_are_staged_and_discarded_without_starting() {
1169        let dispatcher = AsyncDispatcher::new(AsyncLimits::default());
1170        let scope = dispatcher.scope().unwrap();
1171        let task = scope
1172            .register_task(|_| panic!("discarded candidate executed a task"))
1173            .unwrap();
1174        assert_eq!(task.status(), TaskStatus::Staged);
1175        dispatcher.close();
1176        dispatcher.activate();
1177        assert_eq!(task.status(), TaskStatus::Cancelled);
1178    }
1179    #[test]
1180    fn dropping_controls_does_not_detach_tasks_and_panics_reach_the_sink() {
1181        let (dispatcher, scope) = dispatcher();
1182        let errors = Rc::new(RefCell::new(Vec::new()));
1183        let captured = errors.clone();
1184        dispatcher.set_error_sink(move |error| captured.borrow_mut().push(error));
1185        let reporter = Rc::new(RefCell::new(None));
1186        let captured = reporter.clone();
1187        let control = scope
1188            .child()
1189            .unwrap()
1190            .register_task(move |signal| {
1191                *captured.borrow_mut() = Some(signal);
1192                Box::new(|| {})
1193            })
1194            .unwrap();
1195        drop(control);
1196        assert_eq!(dispatcher.task_count(), 1);
1197        reporter
1198            .borrow_mut()
1199            .take()
1200            .unwrap()
1201            .finish(TaskStatus::Panicked("expected failure".into()));
1202        dispatcher.drain(|| {}).unwrap();
1203        assert_eq!(*errors.borrow(), ["expected failure"]);
1204        assert_eq!(dispatcher.task_count(), 0);
1205        assert_eq!(dispatcher.counts().0, 1);
1206    }
1207    #[test]
1208    fn resources_are_bounded_and_reusable_after_completion_or_drop() {
1209        let mut limits = AsyncLimits::default();
1210        limits.endpoints = 1;
1211        limits.message_slots = 2;
1212        limits.publishers = 1;
1213        limits.tasks = 1;
1214        let dispatcher = AsyncDispatcher::new(limits);
1215        let scope = dispatcher.scope().unwrap();
1216        dispatcher.activate();
1217        assert!(matches!(
1218            scope.channel(3, |_: ()| {}),
1219            Err(AsyncError::Limit("message slot"))
1220        ));
1221        let completion = scope.completion(|_: ()| {}).unwrap();
1222        assert!(matches!(
1223            scope.completion(|_: ()| {}),
1224            Err(AsyncError::Limit("endpoint"))
1225        ));
1226        drop(completion);
1227        dispatcher.drain(|| {}).unwrap();
1228        let completion = scope.completion(|_: ()| {}).unwrap();
1229        drop(completion);
1230        dispatcher.drain(|| {}).unwrap();
1231        let value = Property::new(0usize);
1232        let publisher = scope.publisher(&value).unwrap();
1233        assert!(matches!(
1234            scope.publisher(&value),
1235            Err(AsyncError::Limit("publisher"))
1236        ));
1237        drop(publisher);
1238        assert!(scope.publisher(&value).is_ok());
1239    }
1240    #[test]
1241    fn cancelling_parent_closes_descendants_and_waiters_before_returning() {
1242        let (_dispatcher, root) = dispatcher();
1243        let child = root.child().unwrap();
1244        let (sender, _subscription) = child.channel(1, |_: usize| {}).unwrap();
1245        sender.try_send(1).unwrap();
1246        let wake = Arc::new(Flag(AtomicBool::new(false)));
1247        let waker = Waker::from(wake.clone());
1248        let mut cx = Context::from_waker(&waker);
1249        let mut send = Box::pin(sender.send(2));
1250        assert!(send.as_mut().poll(&mut cx).is_pending());
1251        root.cancel();
1252        assert!(wake.0.load(Ordering::SeqCst));
1253        assert_eq!(send.as_mut().poll(&mut cx), Poll::Ready(Err(Closed(2))));
1254        assert!(child.is_closed());
1255    }
1256}
1257
1258/// Browser and native monotonic time for dispatch budgets and shutdown deadlines.
1259pub(crate) fn monotonic_now() -> Duration {
1260    #[cfg(not(target_arch = "wasm32"))]
1261    {
1262        static ORIGIN: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
1263        ORIGIN.get_or_init(std::time::Instant::now).elapsed()
1264    }
1265    #[cfg(target_arch = "wasm32")]
1266    {
1267        use wasm_bindgen::prelude::*;
1268        #[wasm_bindgen]
1269        extern "C" {
1270            #[wasm_bindgen(js_namespace = performance, js_name = now)]
1271            fn performance_now() -> f64;
1272        }
1273        Duration::from_secs_f64(performance_now() / 1000.0)
1274    }
1275}