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