Skip to main content

auralis_task/
scope.rs

1//! Explicit [`TaskScope`] tree with iterative cancellation, parent
2//! back-references, callback-handle lifecycle management, and a context
3//! store for dependency injection.
4
5use std::any::{Any, TypeId};
6use std::cell::{Cell, RefCell};
7use std::collections::{HashMap, VecDeque};
8use std::fmt;
9use std::future::Future;
10use std::rc::{Rc, Weak};
11
12use auralis_signal::{Memo, Signal};
13
14use crate::executor;
15use crate::Priority;
16
17type ScopeId = u64;
18type TaskId = u64;
19
20// ---------------------------------------------------------------------------
21// Scope-id allocator
22// ---------------------------------------------------------------------------
23
24thread_local! {
25    static NEXT_SCOPE_ID: Cell<ScopeId> = const { Cell::new(1) };
26}
27
28fn alloc_scope_id() -> ScopeId {
29    NEXT_SCOPE_ID.with(|c| {
30        let id = c.get();
31        c.set(id + 1);
32        id
33    })
34}
35
36// ---------------------------------------------------------------------------
37// CallbackHandle
38// ---------------------------------------------------------------------------
39
40/// Owns a resource that must be cleaned up when the owning [`TaskScope`]
41/// is dropped.
42///
43/// Currently used for signal subscriptions registered by the `bind_*`
44/// functions.  When the [`TaskScope`] drops, every registered
45/// [`CallbackHandle`] is dropped, which calls the stored cleanup closure
46/// to unsubscribe from the signal.
47pub struct CallbackHandle {
48    cleanup: Option<Box<dyn FnOnce() + 'static>>,
49}
50
51impl CallbackHandle {
52    /// Create a handle from a cleanup closure.
53    pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
54        Self {
55            cleanup: Some(Box::new(cleanup)),
56        }
57    }
58
59    /// Create a no-op handle that does nothing on drop.
60    ///
61    /// Useful as a placeholder when a [`CallbackHandle`] is required
62    /// but no cleanup is needed.
63    #[must_use]
64    pub fn noop() -> Self {
65        Self { cleanup: None }
66    }
67}
68
69impl Drop for CallbackHandle {
70    fn drop(&mut self) {
71        if let Some(f) = self.cleanup.take() {
72            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
73        }
74    }
75}
76
77// ---------------------------------------------------------------------------
78// Scope registry — maps ScopeId → live TaskScope for executor injection
79// ---------------------------------------------------------------------------
80//
81// # Why Weak references
82//
83// The registry stores `Weak<RefCell<TaskScopeInner>>` rather than
84// `Rc<...>`.  This prevents the registry from keeping scopes alive
85// after the application has dropped them — when the last strong
86// reference is gone, the Weak upgrade returns `None` and the executor
87// skips that scope.
88//
89// # Thread safety
90//
91// `SCOPE_REGISTRY` is a `thread_local!` because Auralis is
92// single-threaded by design (Wasm constraint).  For multi-task SSR
93// servers, each request uses an isolated [`Executor`] instance created
94// via [`Executor::new_instance`](crate::Executor::new_instance), and
95// the [`ScopeStore`] trait provides pluggable per-task storage.
96
97type ScopeRegistryEntry = (Weak<RefCell<TaskScopeInner>>, Weak<Cell<bool>>);
98
99thread_local! {
100    static SCOPE_REGISTRY: RefCell<HashMap<ScopeId, ScopeRegistryEntry>> =
101        RefCell::new(HashMap::new());
102}
103
104/// Register a scope in the global registry so the executor can look it
105/// up by id and inject it as the current scope when polling tasks.
106fn register_scope(id: ScopeId, inner: &Rc<RefCell<TaskScopeInner>>, suspended: &Rc<Cell<bool>>) {
107    let _ = SCOPE_REGISTRY.try_with(|reg| {
108        if let Ok(mut r) = reg.try_borrow_mut() {
109            r.insert(id, (Rc::downgrade(inner), Rc::downgrade(suspended)));
110        }
111    });
112}
113
114fn unregister_scope(id: ScopeId) {
115    let _ = SCOPE_REGISTRY.try_with(|reg| {
116        if let Ok(mut r) = reg.try_borrow_mut() {
117            r.remove(&id);
118        }
119    });
120}
121
122/// Find a live [`TaskScope`] by its id.
123///
124/// Returns `None` if the scope has been dropped or the id is unknown.
125#[must_use]
126pub fn find_scope(scope_id: ScopeId) -> Option<TaskScope> {
127    SCOPE_REGISTRY
128        .try_with(|reg| {
129            if let Ok(r) = reg.try_borrow() {
130                r.get(&scope_id).and_then(|(inner_weak, suspended_weak)| {
131                    let inner = inner_weak.upgrade()?;
132                    let suspended = suspended_weak.upgrade()?;
133                    let cancelled = inner.borrow().cancelled.clone();
134                    Some(TaskScope {
135                        inner,
136                        cancelled,
137                        suspended,
138                    })
139                })
140            } else {
141                None
142            }
143        })
144        .ok()
145        .flatten()
146}
147
148/// Return the debug label for the scope with the given id, if any.
149///
150/// Only available with the `debug` feature.
151#[cfg(feature = "debug")]
152#[doc(hidden)]
153#[must_use]
154pub fn scope_debug_label(scope_id: ScopeId) -> Option<String> {
155    find_scope(scope_id).and_then(|s| s.inner.borrow().label.clone())
156}
157
158/// Clear the scope registry.
159#[doc(hidden)]
160pub fn clear_scope_registry() {
161    let _ = SCOPE_REGISTRY.try_with(|reg| {
162        if let Ok(mut r) = reg.try_borrow_mut() {
163            r.clear();
164        }
165    });
166}
167
168// ---------------------------------------------------------------------------
169// Current-scope storage — injectable, defaults to thread-local
170// ---------------------------------------------------------------------------
171
172/// Function signatures for scope store operations.
173///
174/// Using function pointers keeps the store `Send + Sync` even though
175/// `TaskScope` itself is `!Send` — Rust function pointer types are
176/// always `Send + Sync` regardless of parameter/return types.
177type ScopeSetFn = fn(Option<TaskScope>);
178type ScopeGetFn = fn() -> Option<TaskScope>;
179
180/// A pluggable backend for per-task (or per-thread) scope storage.
181///
182/// The default implementation uses a thread-local cell, which is
183/// sufficient for single-threaded Wasm environments.  For multi-task
184/// SSR runtimes (e.g. tokio) the host application should inject a
185/// task-local implementation via [`set_scope_store`].
186pub struct ScopeStore {
187    /// Store a scope (or `None` to clear).
188    pub set_fn: ScopeSetFn,
189    /// Retrieve the current scope.
190    pub get_fn: ScopeGetFn,
191}
192
193use std::sync::OnceLock;
194static SCOPE_STORE: OnceLock<ScopeStore> = OnceLock::new();
195
196fn ensure_default_store() -> &'static ScopeStore {
197    SCOPE_STORE.get_or_init(|| ScopeStore {
198        set_fn: thread_local_set,
199        get_fn: thread_local_get,
200    })
201}
202
203/// Install a custom scope store.
204///
205/// Must be called before any scope operations.  On Wasm or in tests the
206/// default thread-local store is sufficient.
207///
208/// # Example (tokio SSR)
209///
210/// ```rust,ignore
211/// use auralis_task::ScopeStore;
212///
213/// auralis_task::set_scope_store(ScopeStore {
214///     set_fn: my_tokio_task_local_set,
215///     get_fn: my_tokio_task_local_get,
216/// });
217/// ```
218pub fn set_scope_store(store: ScopeStore) {
219    let _ = SCOPE_STORE.set(store);
220}
221
222// The `set_scope_store` API allows injecting a custom scope store.
223// For SSR in multi-threaded tokio runtimes, users should implement a
224// `ScopeStore` backed by `tokio::task::LocalKey` (available when the
225// `ssr-tokio` feature is enabled) or a similar per-task mechanism.
226//
227// Example with tokio (when `ssr-tokio` is enabled):
228//
229// ```rust,ignore
230// use auralis_task::{ScopeStore, set_scope_store};
231//
232// tokio::task::LocalKey! {
233//     static TK_SCOPE: std::cell::RefCell<Option<auralis_task::TaskScope>> =
234//         const { std::cell::RefCell::new(None) };
235// }
236//
237// set_scope_store(ScopeStore {
238//     set_fn: |s| TK_SCOPE.with(|c| *c.borrow_mut() = s),
239//     get_fn: || TK_SCOPE.with(|c| c.borrow().clone()),
240// });
241// ```
242//
243// For single-threaded tokio use (LocalSet / spawn_local), the default
244// thread-local store works correctly without any configuration.
245
246// ---- default thread-local implementation -------------------------------
247
248thread_local! {
249    static CURRENT_SCOPE: RefCell<Option<TaskScope>> = const { RefCell::new(None) };
250}
251
252fn thread_local_set(scope: Option<TaskScope>) {
253    CURRENT_SCOPE.with(|cell| {
254        cell.replace(scope);
255    });
256}
257
258fn thread_local_get() -> Option<TaskScope> {
259    CURRENT_SCOPE.with(|cell| cell.borrow().clone())
260}
261
262/// Directly set the current scope without save/restore.
263///
264/// Used by the executor to inject the owning scope before polling a
265/// task.  The caller must restore the previous scope after the poll.
266pub(crate) fn set_scope_direct(scope: Option<TaskScope>) {
267    let store = ensure_default_store();
268    (store.set_fn)(scope);
269}
270
271/// Directly get the current scope.
272pub(crate) fn get_scope_direct() -> Option<TaskScope> {
273    let store = ensure_default_store();
274    (store.get_fn)()
275}
276
277// ---- ssr-tokio integration ----------------------------------------------
278
279/// Initialise the scope store for tokio-based SSR runtimes.
280///
281/// Uses `tokio::task::LocalKey` to store the current [`TaskScope`] per
282/// tokio task, enabling true multi-request isolation.  Call this once
283/// at process startup, before any scope operations.
284///
285/// Only available with the **`ssr-tokio`** feature (non-wasm).
286///
287/// # Example
288///
289/// ```rust,ignore
290/// auralis_task::init_scope_store_tokio();
291/// ```
292#[cfg(feature = "ssr-tokio")]
293pub fn init_scope_store_tokio() {
294    tokio::task_local! {
295        static TK_SCOPE: std::cell::RefCell<Option<TaskScope>>;
296    }
297
298    // Initialise the key.
299    let _ = TK_SCOPE.try_with(|cell| {
300        cell.replace(None);
301    });
302
303    set_scope_store(ScopeStore {
304        set_fn: |s| {
305            let _ = TK_SCOPE.try_with(|cell| {
306                cell.replace(s);
307            });
308        },
309        get_fn: || {
310            TK_SCOPE
311                .try_with(|cell| cell.borrow().clone())
312                .ok()
313                .flatten()
314        },
315    });
316}
317
318// ---- public API --------------------------------------------------------
319
320/// Set the current [`TaskScope`] for the duration of `f`.
321///
322/// Set `scope` as the current scope for the duration of `f`,
323/// restoring the previous scope afterward.
324///
325/// Used by framework glue code so that bind functions can discover the
326/// owning scope via [`current_scope`].
327pub fn with_current_scope<R>(scope: &TaskScope, f: impl FnOnce() -> R) -> R {
328    let store = ensure_default_store();
329    let prev = (store.get_fn)();
330    (store.set_fn)(Some(scope.clone_inner()));
331    let result = f();
332    (store.set_fn)(prev);
333    result
334}
335
336/// Get the currently active [`TaskScope`], if any.
337#[must_use]
338pub fn current_scope() -> Option<TaskScope> {
339    let store = ensure_default_store();
340    (store.get_fn)()
341}
342
343// ---------------------------------------------------------------------------
344// TaskScopeInner
345// ---------------------------------------------------------------------------
346
347struct TaskScopeInner {
348    id: ScopeId,
349    task_ids: Vec<TaskId>,
350    children: Vec<TaskScope>,
351    /// Weak back-reference to parent (set for child scopes).
352    parent: Option<Weak<RefCell<TaskScopeInner>>>,
353    /// Typed context store for dependency injection.
354    context: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
355    /// Callback handles registered by bind_* functions.
356    callbacks: RefCell<Vec<CallbackHandle>>,
357    /// Whether this scope has been cancelled.  Stored as `Rc<Cell<bool>>`
358    /// so it can be read/set without borrowing the `RefCell`, avoiding
359    /// re-entrant borrow failures during drop.  `TaskScope` holds a clone
360    /// of the same `Rc` for direct access.
361    cancelled: Rc<Cell<bool>>,
362    /// Optional label for `dump_reactive_graph` output.
363    label: Option<String>,
364    /// The executor that owns tasks spawned in this scope.
365    /// Stored as `Rc` (strong reference) so the executor lives
366    /// at least as long as the scope — essential for safe
367    /// cancellation during drop.
368    executor: executor::ExecutorRef,
369}
370
371// ---------------------------------------------------------------------------
372// JoinHandle — per-task cancellation handle
373// ---------------------------------------------------------------------------
374
375/// A handle to a spawned task, allowing individual cancellation.
376///
377/// Created by [`TaskScope::spawn`] and [`TaskScope::spawn_with_priority`].
378/// Dropping the handle does **not** cancel the task — call [`cancel`](JoinHandle::cancel)
379/// explicitly, or drop the owning [`TaskScope`] to cancel all tasks at once.
380pub struct JoinHandle {
381    task_id: Option<TaskId>,
382    executor: executor::ExecutorRef,
383}
384
385impl JoinHandle {
386    /// Cancel this specific task.
387    ///
388    /// Cancellation drops the task's [`Future`] on the next executor
389    /// flush — it is cooperative (`.await`-bound), same as all async
390    /// cancellation in Rust.
391    ///
392    /// No-op if the task has already completed or was spawned into an
393    /// already-cancelled scope.
394    pub fn cancel(&self) {
395        if let Some(tid) = self.task_id {
396            executor::cancel_task(&self.executor, tid);
397        }
398    }
399
400    /// Return `true` if the task has completed (normally or via cancellation).
401    ///
402    /// Returns `true` for handles created by spawning into an already-cancelled
403    /// scope (they never had a real task).
404    #[must_use]
405    pub fn is_finished(&self) -> bool {
406        match self.task_id {
407            Some(tid) => executor::is_task_finished(&self.executor, tid),
408            None => true,
409        }
410    }
411
412    /// Return the id of the wrapped task, or `None` if the handle was
413    /// created by spawning into an already-cancelled scope.
414    #[must_use]
415    pub fn task_id(&self) -> Option<TaskId> {
416        self.task_id
417    }
418}
419
420impl fmt::Debug for JoinHandle {
421    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422        f.debug_struct("JoinHandle")
423            .field("task_id", &self.task_id)
424            .finish_non_exhaustive()
425    }
426}
427
428// ---------------------------------------------------------------------------
429// TaskScope
430// ---------------------------------------------------------------------------
431
432/// A node in the scope tree that owns spawned tasks and carries a typed
433/// context for dependency injection.
434///
435/// # Drop guarantee
436///
437/// When a [`TaskScope`] is dropped, all descendant scopes and their
438/// tasks are cancelled **iteratively** using a work queue — recursion
439/// is never used, so deeply nested UI trees (200+ levels) never
440/// overflow the stack.
441///
442/// # Cancellation is cooperative
443///
444/// Cancellation drops the task's [`Future`] and removes it from the
445/// executor.  Like all async Rust, this only takes effect at the next
446/// `.await` point — a task stuck in a synchronous compute loop cannot
447/// be interrupted mid-execution.  This is the same trade-off made by
448/// `tokio::task::JoinHandle::abort`.  For long synchronous work,
449/// insert [`yield_now`](crate::yield_now) at checkpoints.
450///
451/// # Context
452///
453/// Use [`provide`](TaskScope::provide) / [`consume`](TaskScope::consume)
454/// for lightweight dependency injection that walks up the scope tree.
455///
456/// # Callback lifecycle
457///
458/// [`CallbackHandle`]s registered via
459/// [`register_callback_handle`](Self::register_callback_handle) are
460/// dropped **before** spawned tasks are cancelled, ensuring that
461/// signal subscriptions are removed before any task cleanup.
462#[must_use]
463pub struct TaskScope {
464    inner: Rc<RefCell<TaskScopeInner>>,
465    /// Whether this scope has been cancelled (dropped).  Stored outside
466    /// the `RefCell` so that [`is_cancelled`](Self::is_cancelled) can be
467    /// checked and set without borrowing — avoids re-entrant borrow
468    /// panics and ensures the cancelled flag is always set even when
469    /// the inner `RefCell` is already borrowed during drop.
470    cancelled: Rc<Cell<bool>>,
471    /// Whether this scope is suspended.  Stored outside the `RefCell`
472    /// for the same reason as `cancelled`.
473    suspended: Rc<Cell<bool>>,
474}
475
476impl TaskScope {
477    /// Create a new root scope on the global thread-local executor.
478    ///
479    /// For explicit executor ownership use [`TaskScope::with_executor`].
480    pub fn new() -> Self {
481        Self::with_executor(&executor::current_executor_instance())
482    }
483
484    /// Create a new root scope on the given executor.
485    ///
486    /// All tasks spawned in this scope (and its descendants) run on
487    /// `ex`.  The scope holds a strong reference, keeping the executor
488    /// alive at least as long as the scope.
489    pub fn with_executor(ex: &executor::ExecutorRef) -> Self {
490        let cancelled = Rc::new(Cell::new(false));
491        let inner = Rc::new(RefCell::new(TaskScopeInner {
492            id: alloc_scope_id(),
493            task_ids: Vec::new(),
494            children: Vec::new(),
495            parent: None,
496            context: RefCell::new(HashMap::new()),
497            callbacks: RefCell::new(Vec::new()),
498            cancelled: Rc::clone(&cancelled),
499            label: None,
500            executor: Rc::clone(ex),
501        }));
502        let id = inner.borrow().id;
503        let suspended = Rc::new(Cell::new(false));
504        register_scope(id, &inner, &suspended);
505        Self {
506            inner,
507            cancelled,
508            suspended,
509        }
510    }
511
512    /// Create a child scope that inherits the parent's executor.
513    pub fn new_child(parent: &Self) -> Self {
514        let ex = parent.inner.borrow().executor.clone();
515        let cancelled = Rc::new(Cell::new(false));
516        let inner = Rc::new(RefCell::new(TaskScopeInner {
517            id: alloc_scope_id(),
518            task_ids: Vec::new(),
519            children: Vec::new(),
520            parent: Some(Rc::downgrade(&parent.inner)),
521            context: RefCell::new(HashMap::new()),
522            callbacks: RefCell::new(Vec::new()),
523            cancelled: Rc::clone(&cancelled),
524            label: None,
525            executor: ex,
526        }));
527        let id = inner.borrow().id;
528        let suspended = Rc::new(Cell::new(false));
529        register_scope(id, &inner, &suspended);
530        let child = Self {
531            inner,
532            cancelled,
533            suspended,
534        };
535        parent.inner.borrow_mut().children.push(child.clone_inner());
536        child
537    }
538
539    /// Spawn a future in this scope at low priority.
540    ///
541    /// Returns a [`JoinHandle`] that can cancel this individual task.
542    /// Drop the handle to detach (the task keeps running until the
543    /// scope is dropped).
544    pub fn spawn(&self, future: impl Future<Output = ()> + 'static) -> JoinHandle {
545        self.spawn_with_priority(Priority::Low, future)
546    }
547
548    /// Spawn a future in this scope at the given priority.
549    ///
550    /// The current scope is set to `self` during the spawn so that any
551    /// synchronous work inside the future constructor (e.g. `bind_text`)
552    /// can discover the owning scope via [`current_scope`].
553    ///
554    /// Returns a [`JoinHandle`] that can cancel this individual task.
555    pub fn spawn_with_priority(
556        &self,
557        priority: Priority,
558        future: impl Future<Output = ()> + 'static,
559    ) -> JoinHandle {
560        // Extract fields before spawning so the Ref borrow is released.
561        // If the scheduler fires synchronously (e.g. TestScheduleFlush),
562        // the spawned task's future is polled immediately, and a nested
563        // spawn on the current scope would panic if `inner` were still
564        // borrowed.
565        let (cancelled, ex, scope_id) = {
566            let inner = self.inner.borrow();
567            (inner.cancelled.get(), Rc::clone(&inner.executor), inner.id)
568        };
569        if cancelled {
570            return JoinHandle {
571                task_id: None,
572                executor: ex,
573            };
574        }
575        let task_id = executor::with_executor(&ex, || {
576            with_current_scope(self, || {
577                executor::spawn_scoped_on(&ex, priority, scope_id, future)
578            })
579        });
580        self.inner.borrow_mut().task_ids.push(task_id);
581        JoinHandle {
582            task_id: Some(task_id),
583            executor: ex,
584        }
585    }
586
587    /// Spawn a task that calls `f` with the new value whenever `sig` changes.
588    ///
589    /// This is a convenience wrapper around the common pattern:
590    ///
591    /// ```ignore
592    /// scope.spawn({
593    ///     let s = sig.clone();
594    ///     async move { loop { s.changed().await; f(&s.read()); } }
595    /// });
596    /// ```
597    ///
598    /// Returns a [`JoinHandle`] for individual cancellation.
599    pub fn watch<T: Clone + 'static>(
600        &self,
601        sig: &Signal<T>,
602        f: impl FnMut(&T) + 'static,
603    ) -> JoinHandle {
604        let s = sig.clone();
605        let mut f = f;
606        self.spawn(async move {
607            loop {
608                s.changed().await;
609                f(&s.read());
610            }
611        })
612    }
613
614    /// Spawn a task that re-runs `effect` whenever any [`Signal`] read
615    /// inside it changes — using a [`Memo`](auralis_signal::Memo) internally
616    /// to auto-track dependencies.
617    ///
618    /// The effect is run once immediately to discover its dependencies.
619    /// Subsequent runs happen on the executor when a dependency changes.
620    ///
621    /// Returns a [`JoinHandle`] for individual cancellation.
622    pub fn watch_effect(&self, effect: impl Fn() + 'static) -> JoinHandle {
623        let memo = Memo::new(effect);
624        self.spawn(async move {
625            loop {
626                memo.changed().await;
627                #[allow(clippy::let_unit_value, clippy::ignored_unit_patterns)]
628                let _ = memo.read();
629            }
630        })
631    }
632
633    // -- callback lifecycle ------------------------------------------------
634
635    /// Register a [`CallbackHandle`] that will be dropped when this scope
636    /// is dropped (or when `clear_callbacks` is called).
637    ///
638    /// Used by `bind_*` functions to ensure signal subscriptions are
639    /// cleaned up when the owning component is destroyed.
640    pub fn register_callback_handle(&self, handle: CallbackHandle) {
641        let inner = self.inner.borrow();
642        if inner.cancelled.get() {
643            return;
644        }
645        inner.callbacks.borrow_mut().push(handle);
646    }
647
648    /// Register a cleanup function that runs when this scope is dropped.
649    ///
650    /// Equivalent to `register_callback_handle(CallbackHandle::new(f))`.
651    ///
652    /// Cleanup functions run before spawned tasks are cancelled, so they
653    /// can safely interact with signals and other resources.
654    ///
655    /// If the scope is already cancelled, `f` is dropped immediately.
656    pub fn on_cleanup(&self, f: impl FnOnce() + 'static) {
657        self.register_callback_handle(CallbackHandle::new(f));
658    }
659
660    // -- context -----------------------------------------------------------
661
662    /// Store a value of type `T` in this scope.
663    ///
664    /// The value is wrapped in [`Rc`] so it can be shared.  A subsequent
665    /// call to [`consume`](TaskScope::consume) on this scope (or any
666    /// descendant) will discover it by walking up the parent chain.
667    pub fn provide<T: 'static>(&self, value: T) {
668        self.inner
669            .borrow()
670            .context
671            .borrow_mut()
672            .insert(TypeId::of::<T>(), Rc::new(value));
673    }
674
675    /// Look up a value of type `T` by walking up the scope tree.
676    ///
677    /// Returns `None` if no ancestor (including `self`) has provided a
678    /// value of this type.
679    #[must_use]
680    pub fn consume<T: 'static>(&self) -> Option<Rc<T>> {
681        let mut current = Some(Rc::clone(&self.inner));
682
683        while let Some(inner) = current {
684            // Check local context.
685            {
686                let inner_ref = inner.borrow();
687                let ctx = inner_ref.context.borrow();
688                if let Some(val) = ctx.get(&TypeId::of::<T>()) {
689                    if let Ok(downcast) = val.clone().downcast::<T>() {
690                        return Some(downcast);
691                    }
692                }
693            }
694
695            // Walk up to parent.
696            let parent = {
697                let inner_ref = inner.borrow();
698                inner_ref.parent.as_ref().and_then(Weak::upgrade)
699            };
700            current = parent;
701        }
702
703        None
704    }
705
706    /// Like [`consume`](TaskScope::consume) but panics if the value is
707    /// not found.
708    ///
709    /// # Panics
710    ///
711    /// Panics if no ancestor scope has provided a value of type `T`.
712    #[must_use]
713    #[track_caller]
714    pub fn expect_context<T: 'static>(&self) -> Rc<T> {
715        self.consume::<T>()
716            .unwrap_or_else(|| panic!("context not found: {}", std::any::type_name::<T>()))
717    }
718
719    /// Return `true` if this scope has been cancelled (dropped).
720    ///
721    /// A cancelled scope silently ignores [`spawn`](TaskScope::spawn) calls.
722    #[must_use]
723    pub fn is_cancelled(&self) -> bool {
724        self.cancelled.get()
725    }
726
727    // -- debugging ----------------------------------------------------------
728
729    /// Set a human-readable label for this scope.
730    ///
731    /// Labels appear in `dump_reactive_graph()` output and are useful
732    /// for debugging.
733    pub fn set_label(&self, label: impl Into<String>) {
734        self.inner.borrow_mut().label = Some(label.into());
735    }
736
737    /// Return the label set by [`set_label`](Self::set_label), if any.
738    #[must_use]
739    pub fn label(&self) -> Option<String> {
740        self.inner.borrow().label.clone()
741    }
742
743    /// Set a label for this scope, shown in [`dump_task_tree`] output.
744    ///
745    /// Only available with the `debug` feature.
746    #[cfg(feature = "debug")]
747    #[doc(hidden)]
748    #[deprecated(note = "use `set_label` instead")]
749    pub fn set_debug_label(&self, label: impl Into<String>) {
750        self.set_label(label);
751    }
752
753    // -- testing -----------------------------------------------------------
754
755    /// Return the number of spawned tasks in this scope (test-only).
756    #[cfg(test)]
757    #[must_use]
758    pub fn task_count(&self) -> usize {
759        self.inner.borrow().task_ids.len()
760    }
761
762    /// Return the number of child scopes (test-only).
763    #[cfg(test)]
764    #[must_use]
765    pub fn child_count(&self) -> usize {
766        self.inner.borrow().children.len()
767    }
768
769    // -- internals ---------------------------------------------------------
770
771    fn clone_inner(&self) -> Self {
772        Self {
773            inner: Rc::clone(&self.inner),
774            cancelled: Rc::clone(&self.cancelled),
775            suspended: Rc::clone(&self.suspended),
776        }
777    }
778
779    /// Run `f` with `self` set as the current scope for the thread.
780    ///
781    /// Used by framework glue code so bind functions can discover the
782    /// owning scope via [`current_scope`].
783    pub fn enter<R>(&self, f: impl FnOnce() -> R) -> R {
784        with_current_scope(self, f)
785    }
786
787    /// Suspend all tasks owned by this scope and its descendants.
788    ///
789    /// Suspended tasks are skipped during executor polling.  Signal
790    /// subscriptions remain registered but their callbacks are not
791    /// invoked while the scope is suspended.  Use [`resume`](Self::resume)
792    /// to restart execution.
793    ///
794    /// Used by `if_async_cached` and `match_async_cached` to pause
795    /// hidden branches.
796    pub fn suspend(&self) {
797        if self.suspended.get() {
798            return;
799        }
800        self.suspended.set(true);
801        // Cascading: suspend all descendants.
802        let children: Vec<TaskScope> = {
803            self.inner
804                .borrow()
805                .children
806                .iter()
807                .map(TaskScope::clone_inner)
808                .collect()
809        };
810        for child in &children {
811            child.suspend();
812        }
813    }
814
815    /// Resume all tasks owned by this scope and its descendants.
816    ///
817    /// This reverses the effect of [`suspend`](Self::suspend).  Tasks
818    /// become eligible for polling again on the next executor flush.
819    pub fn resume(&self) {
820        if !self.suspended.get() {
821            return;
822        }
823        self.suspended.set(false);
824
825        let (task_ids, children) = {
826            let inner = self.inner.borrow();
827            let tids = inner.task_ids.clone();
828            let children: Vec<TaskScope> =
829                inner.children.iter().map(TaskScope::clone_inner).collect();
830            (tids, children)
831        };
832
833        // Enqueue all tasks belonging to this scope.
834        let ex = Rc::clone(&self.inner.borrow().executor);
835        executor::enqueue_scope_tasks_on(&ex, &task_ids);
836
837        // Resume children (cascading).
838        for child in &children {
839            child.resume();
840        }
841    }
842
843    /// Return `true` if this scope is currently suspended.
844    #[must_use]
845    pub fn is_suspended(&self) -> bool {
846        self.suspended.get()
847    }
848}
849
850impl Default for TaskScope {
851    fn default() -> Self {
852        Self::new()
853    }
854}
855
856impl Clone for TaskScope {
857    fn clone(&self) -> Self {
858        self.clone_inner()
859    }
860}
861
862// Iterative cancellation: descendants are collected BFS, then
863// cancelled leaf→root, avoiding recursive drop that would overflow
864// the stack on deeply-nested trees (200+ levels).
865//
866// Callback handles are dropped BEFORE tasks, ensuring signal
867// subscriptions are removed before any task is cancelled.
868impl Drop for TaskScope {
869    fn drop(&mut self) {
870        // Only cancel when this is the last reference to the inner.
871        // Temporary clones (from find_scope during executor flush,
872        // from with_current_scope during spawn) share the same inner
873        // and must not cancel the scope when they go out of scope.
874        if Rc::strong_count(&self.inner) > 1 {
875            return;
876        }
877
878        // Always set cancelled first — this Cell is outside the RefCell
879        // and always writable, so the scope is marked cancelled even if
880        // we can't do full cleanup below.
881        self.cancelled.set(true);
882
883        let Ok(mut inner) = self.inner.try_borrow_mut() else {
884            // Already borrowed — re-entrant drop (e.g. a callback or
885            // spawned task dropped the last clone during executor flush).
886            // Cancelled flag is set, so future spawns are rejected and
887            // the executor will clean up tasks on the next flush.
888            eprintln!(
889                "[auralis-task] WARNING: TaskScope::drop cannot borrow inner \
890                 (already borrowed). Tasks and callbacks in this scope will \
891                 be cleaned up on the next executor flush. Avoid dropping \
892                 the last TaskScope clone inside a callback."
893            );
894            return;
895        };
896
897        // ---- drop callback handles first ---------------------------------
898        inner.callbacks.borrow_mut().clear();
899
900        // ---- collect descendants BFS ------------------------------------
901        let mut descendants: Vec<Rc<RefCell<TaskScopeInner>>> = Vec::new();
902        {
903            let mut queue: VecDeque<Rc<RefCell<TaskScopeInner>>> = VecDeque::new();
904            for child in &inner.children {
905                queue.push_back(Rc::clone(&child.inner));
906            }
907
908            while let Some(scope_rc) = queue.pop_front() {
909                let scope = scope_rc.borrow();
910                for child in &scope.children {
911                    queue.push_back(Rc::clone(&child.inner));
912                }
913                descendants.push(Rc::clone(&scope_rc));
914            }
915        }
916
917        // ---- cancel leaves → root ---------------------------------------
918        for scope_rc in descendants.iter().rev() {
919            let mut scope = scope_rc.borrow_mut();
920            if scope.cancelled.get() {
921                continue;
922            }
923            scope.cancelled.set(true);
924
925            // Drop callbacks before tasks.
926            scope.callbacks.borrow_mut().clear();
927
928            if !scope.task_ids.is_empty() {
929                let ex = Rc::clone(&scope.executor);
930                let task_ids = std::mem::take(&mut scope.task_ids);
931                let dropped_futures = executor::cancel_scope_tasks_on(&ex, &task_ids);
932                drop(dropped_futures);
933            }
934            scope.context.borrow_mut().clear();
935            unregister_scope(scope.id);
936        }
937
938        // ---- cancel own tasks -------------------------------------------
939        if !inner.task_ids.is_empty() {
940            let ex = Rc::clone(&inner.executor);
941            let task_ids = std::mem::take(&mut inner.task_ids);
942            let dropped_futures = executor::cancel_scope_tasks_on(&ex, &task_ids);
943            drop(dropped_futures);
944        }
945
946        inner.context.borrow_mut().clear();
947        inner.children.clear();
948
949        // Remove from the global registry so stale lookups return None.
950        unregister_scope(inner.id);
951    }
952}
953
954// ---------------------------------------------------------------------------
955// Convenience macros for context injection / retrieval
956// ---------------------------------------------------------------------------
957
958/// Shorthand for `scope.provide(value)`.
959///
960/// ```rust,ignore
961/// provide_context!(scope, 42i32);
962/// ```
963#[macro_export]
964macro_rules! provide_context {
965    ($scope:expr, $value:expr) => {
966        $scope.provide($value)
967    };
968}
969
970/// Shorthand for `scope.consume::<T>()`.
971///
972/// ```rust,ignore
973/// let theme: Option<Rc<Theme>> = consume_context!(scope, Theme);
974/// ```
975#[macro_export]
976macro_rules! consume_context {
977    ($scope:expr, $ty:ty) => {
978        $scope.consume::<$ty>()
979    };
980}
981
982// ---------------------------------------------------------------------------
983// Structured scope tree (debug feature)
984// ---------------------------------------------------------------------------
985
986/// A node in the scope tree, serializable for `DevTools`.
987#[cfg(feature = "debug")]
988#[derive(Debug, Clone, serde::Serialize)]
989pub struct ScopeTreeNode {
990    /// Unique scope id.
991    pub id: ScopeId,
992    /// Label set via `set_label()`.
993    pub label: Option<String>,
994    /// Spawned tasks in this scope.
995    pub tasks: Vec<TaskNode>,
996    /// Child scopes (recursive).
997    pub children: Vec<ScopeTreeNode>,
998}
999
1000/// A task entry within a scope.
1001#[cfg(feature = "debug")]
1002#[derive(Debug, Clone, serde::Serialize)]
1003pub struct TaskNode {
1004    /// Executor-assigned task id.
1005    pub id: TaskId,
1006    /// `"H"` or `"L"`.
1007    pub priority: &'static str,
1008    /// Whether the task is currently enqueued for polling.
1009    pub queued: bool,
1010    /// Total number of times this task has been polled.
1011    pub total_poll_count: u64,
1012    /// Microseconds spent in the most recent poll.
1013    pub last_poll_duration_us: u64,
1014}
1015
1016/// Recursively assemble a scope sub-tree.
1017#[cfg(feature = "debug")]
1018fn attach_children(
1019    id: u64,
1020    scope_map: &mut std::collections::HashMap<u64, ScopeTreeNode>,
1021    child_map: &std::collections::HashMap<u64, Vec<u64>>,
1022) -> ScopeTreeNode {
1023    let mut node = scope_map.remove(&id).unwrap_or(ScopeTreeNode {
1024        id,
1025        label: None,
1026        tasks: Vec::new(),
1027        children: Vec::new(),
1028    });
1029    if let Some(child_ids) = child_map.get(&id) {
1030        let mut child_ids = child_ids.clone();
1031        child_ids.sort_unstable();
1032        for cid in child_ids {
1033            node.children
1034                .push(attach_children(cid, scope_map, child_map));
1035        }
1036    }
1037    node
1038}
1039
1040/// Build the scope tree from the global scope registry.
1041///
1042/// Root scopes (those with no live parent) form the top-level list.
1043/// Tasks are annotated with their enqueued status.
1044#[cfg(feature = "debug")]
1045#[must_use]
1046pub fn scope_tree() -> Vec<ScopeTreeNode> {
1047    use crate::executor;
1048
1049    let task_snap = executor::debug_task_snapshot();
1050    let queued: std::collections::HashSet<u64> =
1051        executor::debug_queued_task_ids().into_iter().collect();
1052    let timing = executor::debug_task_timing();
1053
1054    // Group tasks by scope_id.
1055    let mut tasks_by_scope: std::collections::HashMap<u64, Vec<TaskNode>> =
1056        std::collections::HashMap::new();
1057    for (tid, pri, sid) in &task_snap {
1058        let (poll_count, last_us) = timing.get(tid).copied().unwrap_or((0, 0));
1059        tasks_by_scope.entry(*sid).or_default().push(TaskNode {
1060            id: *tid,
1061            priority: match pri {
1062                Priority::High => "H",
1063                Priority::Low => "L",
1064            },
1065            queued: queued.contains(tid),
1066            total_poll_count: poll_count,
1067            last_poll_duration_us: last_us,
1068        });
1069    }
1070
1071    // Collect live scopes.
1072    let mut scope_map: std::collections::HashMap<u64, ScopeTreeNode> =
1073        std::collections::HashMap::new();
1074
1075    let _ = SCOPE_REGISTRY.try_with(|reg| {
1076        if let Ok(r) = reg.try_borrow() {
1077            for (&id, (inner_weak, _suspended_weak)) in r.iter() {
1078                let Some(inner) = inner_weak.upgrade() else {
1079                    continue;
1080                };
1081                let b = inner.borrow();
1082                scope_map.insert(
1083                    id,
1084                    ScopeTreeNode {
1085                        id,
1086                        label: b.label.clone(),
1087                        tasks: tasks_by_scope.remove(&id).unwrap_or_default(),
1088                        children: Vec::new(), // filled below
1089                    },
1090                );
1091            }
1092        }
1093    });
1094
1095    // Build parent-child links as id→[child_id] maps.
1096    let mut roots: Vec<u64> = Vec::new();
1097    let mut child_map: std::collections::HashMap<u64, Vec<u64>> = std::collections::HashMap::new();
1098
1099    let _ = SCOPE_REGISTRY.try_with(|reg| {
1100        if let Ok(r) = reg.try_borrow() {
1101            for (&id, (inner_weak, _)) in r.iter() {
1102                let Some(inner) = inner_weak.upgrade() else {
1103                    continue;
1104                };
1105                let b = inner.borrow();
1106                let has_live_parent = b.parent.as_ref().and_then(Weak::upgrade).is_some();
1107                if !has_live_parent {
1108                    roots.push(id);
1109                } else if let Some(p) = b.parent.as_ref().and_then(Weak::upgrade) {
1110                    child_map.entry(p.borrow().id).or_default().push(id);
1111                }
1112            }
1113        }
1114    });
1115
1116    // Sort tasks within each scope by id for determinism.
1117    for node in scope_map.values_mut() {
1118        node.tasks.sort_by_key(|t| t.id);
1119    }
1120
1121    let mut tree = Vec::new();
1122    roots.sort_unstable();
1123    for rid in roots {
1124        tree.push(attach_children(rid, &mut scope_map, &child_map));
1125    }
1126    // Any remaining scopes not reachable from roots (shouldn't normally
1127    // happen, but be defensive).
1128    let remaining: Vec<u64> = {
1129        let mut ids: Vec<u64> = scope_map.keys().copied().collect();
1130        ids.sort_unstable();
1131        ids
1132    };
1133    for id in remaining {
1134        tree.push(attach_children(id, &mut scope_map, &child_map));
1135    }
1136
1137    tree
1138}
1139
1140// ---------------------------------------------------------------------------
1141// Tests
1142// ---------------------------------------------------------------------------
1143
1144#[cfg(test)]
1145#[allow(clippy::items_after_statements)]
1146#[path = "scope_tests.rs"]
1147mod tests;