Skip to main content

whisker_runtime/reactive/
owner.rs

1//! Owner / scope API surface.
2//!
3//! [`Owner`] is the public-facing handle for a reactive scope —
4//! the lifetime unit that ties together signals, effects, computed
5//! values, view element handles, and cleanup callbacks. Disposing
6//! an `Owner` cascades into its children, frees every node it
7//! allocated, releases the element handles it owned, and runs its
8//! cleanup callbacks in LIFO order.
9//!
10//! ## When to reach for these methods
11//!
12//! - Application code: **almost never**. `#[component]`,
13//!   `provide_context`, `on_cleanup` etc. set up and tear down
14//!   owners for you automatically.
15//! - Framework extension code (custom control-flow primitives, a
16//!   router, a custom list virtualizer): when you need to mount
17//!   sub-trees whose lifetime is shorter than the surrounding
18//!   component — that's where `Owner::new` / `owner.with` /
19//!   `owner.dispose` come in.
20//! - Tests: hand-driving owner lifecycle is convenient for
21//!   reactive unit tests.
22//!
23//! See the crate-level docs for the conceptual model.
24//!
25//! The underlying [`Owner`] type is a `Copy` slotmap key
26//! defined in [`super::runtime`]; the methods on this page are
27//! attached to that type via an `impl` block.
28
29use std::rc::Rc;
30
31use super::runtime::{NodeId, Owner, Scope};
32use super::with_runtime;
33
34impl Owner {
35    /// Create a new owner. If `parent` is `None` the current
36    /// top-of-stack owner is used (or the owner becomes a root if
37    /// the stack is empty).
38    ///
39    /// The new owner inherits its parent's `paused` flag — so a
40    /// sub-component mounted while its containing route is
41    /// suspended starts paused, and its effects won't fire until
42    /// the route resumes.
43    pub fn new(parent: Option<Owner>) -> Owner {
44        with_runtime(|rt| {
45            let parent = parent.or_else(|| rt.current_owner());
46            let parent_paused = parent
47                .and_then(|p| rt.owners.get(p))
48                .map(|o| o.paused)
49                .unwrap_or(false);
50            let mut scope = Scope::new(parent);
51            scope.paused = parent_paused;
52            let id = rt.owners.insert(scope);
53            if let Some(p) = parent {
54                if let Some(parent_scope) = rt.owners.get_mut(p) {
55                    parent_scope.children.push(id);
56                }
57            }
58            id
59        })
60    }
61
62    /// Create a parentless **root** owner, ignoring whatever owner is
63    /// currently on the stack.
64    ///
65    /// Unlike [`Owner::new(None)`](Owner::new) — which adopts the
66    /// current top-of-stack owner as parent — this always produces a
67    /// detached root. Use it for **process-global singletons** whose
68    /// lifetime must not be tied to the (possibly short-lived) owner
69    /// that happens to be active when the singleton is first touched.
70    ///
71    /// The canonical case is a module that lazily mints an
72    /// arena-backed handle on first access (e.g.
73    /// `whisker-safe-area`): if that first access lands inside a
74    /// per-route / per-component owner, minting under `new(None)` would
75    /// free the handle when that scope disposes, and a later read would
76    /// hit a disposed node. Minting under a `detached_root()` (then
77    /// never disposing it) keeps the handle alive for the whole
78    /// process — the intended semantics for a singleton.
79    ///
80    /// The returned owner is never auto-disposed; the caller is
81    /// expected to leak it (i.e. drop the handle without calling
82    /// [`dispose`](Owner::dispose)) for genuine process-lifetime data.
83    pub fn detached_root() -> Owner {
84        with_runtime(|rt| rt.owners.insert(Scope::new(None)))
85    }
86
87    /// Push `self` as the current scope, run `f`, pop back.
88    /// Reactive primitives (`signal()`, `effect()`, `computed()`,
89    /// view elements created via `render!`) allocated inside `f`
90    /// will belong to this owner.
91    pub fn with<R>(self, f: impl FnOnce() -> R) -> R {
92        with_runtime(|rt| rt.owner_stack.push(self));
93        let result = f();
94        with_runtime(|rt| {
95            let popped = rt.owner_stack.pop();
96            debug_assert_eq!(
97                popped,
98                Some(self),
99                "Owner::with: stack imbalance — owner pop didn't match push"
100            );
101        });
102        result
103    }
104
105    /// Dispose `self`, freeing all its descendants, nodes, and
106    /// running its cleanup callbacks.
107    ///
108    /// Recursive — disposes children first, then this owner. Safe
109    /// to call even if the owner has already been disposed (no-op).
110    pub fn dispose(self) {
111        // Step 1: collect what needs cleaning. We pull data out of
112        // the runtime in a short borrow rather than holding it
113        // through the recursion, because each level may itself need
114        // to mutate the runtime (running cleanup callbacks does not,
115        // but symmetrically we keep the pattern simple by avoiding
116        // nested borrows).
117        let children;
118        let nodes;
119        let cleanups;
120        let parent;
121        let mount_fn;
122        let elements;
123        {
124            let removed = with_runtime(|rt| rt.owners.remove(self));
125            let Some(o) = removed else { return };
126            children = o.children;
127            nodes = o.nodes;
128            cleanups = o.cleanups;
129            parent = o.parent;
130            mount_fn = o.mount_fn;
131            elements = o.elements;
132        }
133
134        // Step 1b: if this was a component owner, scrub the hot-
135        // reload registry so the fn pointer doesn't list a freed
136        // slot. Without this, A6's `owners_for_fn` would return a
137        // dangling Owner and remount logic would fault.
138        if let Some(fp) = mount_fn {
139            with_runtime(|rt| {
140                if let Some(list) = rt.component_owners.get_mut(&fp) {
141                    list.retain(|o| *o != self);
142                    if list.is_empty() {
143                        rt.component_owners.remove(&fp);
144                    }
145                }
146            });
147
148            // Step 1c: also clean up any remountable MountSite whose
149            // owner is this one. Without this scrub, cascading
150            // disposal (e.g. parent component re-mounts and discards
151            // its sub-tree) leaves orphan MountSites behind, and
152            // the next `remount_components_for` call processes
153            // them — operating on freed parent / body_root handles,
154            // with visible corruption (issue #17 follow-up).
155            //
156            // `site.owner` is `None` *during* a remount (the
157            // takes-then-reinstalls window in `remount_one`), so
158            // this scan won't accidentally evict the site that's
159            // mid-flight. It only matches MountSites whose
160            // component owner is the one actually being disposed.
161            with_runtime(|rt| {
162                let stale: Vec<super::component::MountId> = rt
163                    .mount_sites
164                    .iter()
165                    .filter_map(|(id, site)| {
166                        if site.owner == Some(self) {
167                            Some(*id)
168                        } else {
169                            None
170                        }
171                    })
172                    .collect();
173                for id in stale {
174                    rt.mount_sites.remove(&id);
175                    if let Some(list) = rt.fn_ptr_mounts.get_mut(&fp) {
176                        list.retain(|m| *m != id);
177                        if list.is_empty() {
178                            rt.fn_ptr_mounts.remove(&fp);
179                        }
180                    }
181                }
182            });
183        }
184
185        // Step 2: detach from parent's children list.
186        if let Some(p) = parent {
187            with_runtime(|rt| {
188                if let Some(parent_scope) = rt.owners.get_mut(p) {
189                    parent_scope.children.retain(|&c| c != self);
190                }
191            });
192        }
193
194        // Step 3: dispose descendants (post-order — bottom up).
195        for child in children {
196            child.dispose();
197        }
198
199        // Step 4: free every node this owner allocated. For effects
200        // / computed values, also detach them from any subscriber
201        // list they were on, so other live nodes don't try to
202        // notify a freed slot later.
203        //
204        // Arc-signal back-references (`arc_sources`) get collected
205        // here and unsubscribed below, outside the runtime borrow —
206        // the unsubscribe callees may re-enter the runtime.
207        let arc_unsubscribes: Vec<(Rc<dyn super::runtime::ArcSubscription>, NodeId)> =
208            with_runtime(|rt| {
209                let mut out: Vec<(Rc<dyn super::runtime::ArcSubscription>, NodeId)> = Vec::new();
210                for node_id in &nodes {
211                    let Some(node) = rt.nodes.remove(*node_id) else {
212                        continue;
213                    };
214                    // Remove ourselves from every source's subscriber list.
215                    for source in node.sources {
216                        if let Some(src_node) = rt.nodes.get_mut(source) {
217                            src_node.subscribers.remove(node_id);
218                        }
219                    }
220                    // Remove ourselves from every subscriber's source list —
221                    // a signal we owned may have been read by an outer effect.
222                    for sub in node.subscribers {
223                        if let Some(sub_node) = rt.nodes.get_mut(sub) {
224                            sub_node.sources.remove(node_id);
225                        }
226                    }
227                    // Collect arc-signal back-refs so we can call
228                    // `unsubscribe` outside the runtime borrow.
229                    for arc_src in node.arc_sources {
230                        out.push((arc_src, *node_id));
231                    }
232                }
233                // Strip these nodes from the pending and deferred queues
234                // if any were scheduled — otherwise a later flush /
235                // resume would try to re-run a freed slot.
236                rt.pending.retain(|n| !nodes.contains(n));
237                rt.deferred.retain(|n| !nodes.contains(n));
238                out
239            });
240
241        // Tell every Arc-backed signal that one of our disposed
242        // nodes used to be on its subscriber list. The signal itself
243        // stays alive (Arc refcount), but pruning here keeps its
244        // list bounded so a long-lived signal doesn't accumulate
245        // dead `NodeId`s from every transient subscriber that came
246        // and went.
247        for (arc_src, subscriber) in arc_unsubscribes {
248            arc_src.unsubscribe(subscriber);
249        }
250
251        // Step 5: release every element handle the disposed owner
252        // created. We do this AFTER recursing into children so that
253        // bottom-up disposal order matches what the renderer
254        // expects (a child element's release before its parent's
255        // is fine; the bridge only complains if a parent is missing
256        // when a child reaches up). Done with the runtime borrow
257        // released so a future renderer that wants to call back
258        // into the reactive system (e.g. to notify "element
259        // released") can do so.
260        for handle in elements {
261            crate::view::release_element(handle);
262        }
263
264        // Step 6: run cleanups in LIFO order, with no runtime
265        // borrow held (cleanups may legitimately touch other parts
266        // of the runtime).
267        for cleanup in cleanups.into_iter().rev() {
268            cleanup();
269        }
270    }
271
272    /// Pause `self` (and its descendants): effects and computeds
273    /// whose scope is the paused subtree skip flush. Their
274    /// scheduled re-runs land on the runtime's `deferred` list
275    /// until [`Owner::resume`] drains them back.
276    ///
277    /// Idempotent — pausing an already-paused owner is a no-op.
278    /// The cascade walks the children tree breadth-first; new
279    /// descendants created while paused inherit the flag via
280    /// [`Owner::new`].
281    ///
282    /// Used by `StackLayout` to freeze back-stack entries that are
283    /// mounted-but-off-screen, matching iOS
284    /// `UINavigationController` / Android Fragment back-stack
285    /// semantics: state survives but no CPU is spent on
286    /// signal-driven re-renders behind the top route.
287    pub fn pause(self) {
288        with_runtime(|rt| {
289            let mut stack = vec![self];
290            while let Some(id) = stack.pop() {
291                let Some(o) = rt.owners.get_mut(id) else {
292                    continue;
293                };
294                if o.paused {
295                    continue;
296                }
297                o.paused = true;
298                stack.extend(o.children.iter().copied());
299            }
300        });
301    }
302
303    /// Resume `self` (and its descendants): clear the paused flag
304    /// and move any of its deferred effects back onto the pending
305    /// queue so they fire on the next flush.
306    ///
307    /// Idempotent. Iterates [`super::runtime::ReactiveRuntime::deferred`]
308    /// and re-queues every node whose owner is no longer paused —
309    /// including descendants resumed by this cascade, and any
310    /// deferred node whose owner happens to have been unpaused by
311    /// an earlier call.
312    pub fn resume(self) {
313        let any_resumed = with_runtime(|rt| {
314            let mut stack = vec![self];
315            let mut any = false;
316            while let Some(id) = stack.pop() {
317                let Some(o) = rt.owners.get_mut(id) else {
318                    continue;
319                };
320                if !o.paused {
321                    continue;
322                }
323                o.paused = false;
324                any = true;
325                stack.extend(o.children.iter().copied());
326            }
327            if !any {
328                return false;
329            }
330            // Drain deferred → pending for every node whose owner
331            // is no longer paused. Stale entries (node disposed
332            // under a paused owner) are dropped here.
333            let deferred = std::mem::take(&mut rt.deferred);
334            for node in deferred {
335                let still_paused = rt
336                    .nodes
337                    .get(node)
338                    .and_then(|n| rt.owners.get(n.owner))
339                    .map(|o| o.paused);
340                match still_paused {
341                    Some(false) => {
342                        if !rt.pending.contains(&node) {
343                            rt.pending.push(node);
344                        }
345                    }
346                    Some(true) => rt.deferred.push(node),
347                    None => {} // node or owner is gone; drop silently
348                }
349            }
350            true
351        });
352        if any_resumed {
353            crate::host_wake::wake_runtime();
354        }
355    }
356
357    /// Whether `self` is currently paused. Mainly for tests;
358    /// production code should drive pause / resume from the
359    /// lifecycle layer and not branch on the flag directly.
360    pub fn is_paused(self) -> bool {
361        with_runtime(|rt| rt.owners.get(self).map(|o| o.paused).unwrap_or(false))
362    }
363}
364
365/// Register a callback to run when the current owner is disposed.
366/// Calls accumulate in LIFO order, mirroring Solid / Leptos
367/// `onCleanup` semantics.
368///
369/// No-op (with a warning in `debug`) if there is no current owner.
370///
371/// Kept as a free function (not a method on [`Owner`]) because it
372/// operates on whatever owner happens to be at the top of the
373/// runtime's owner stack — the caller can't sensibly name it.
374pub fn on_cleanup(f: impl FnOnce() + 'static) {
375    let registered = with_runtime(|rt| {
376        let Some(owner_id) = rt.current_owner() else {
377            return false;
378        };
379        if let Some(scope) = rt.owners.get_mut(owner_id) {
380            scope.cleanups.push(Box::new(f));
381            return true;
382        }
383        false
384    });
385    if !registered {
386        debug_assert!(
387            false,
388            "on_cleanup called outside any owner — registration ignored"
389        );
390    }
391}