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    /// Push `self` as the current scope, run `f`, pop back.
63    /// Reactive primitives (`signal()`, `effect()`, `computed()`,
64    /// view elements created via `render!`) allocated inside `f`
65    /// will belong to this owner.
66    pub fn with<R>(self, f: impl FnOnce() -> R) -> R {
67        with_runtime(|rt| rt.owner_stack.push(self));
68        let result = f();
69        with_runtime(|rt| {
70            let popped = rt.owner_stack.pop();
71            debug_assert_eq!(
72                popped,
73                Some(self),
74                "Owner::with: stack imbalance — owner pop didn't match push"
75            );
76        });
77        result
78    }
79
80    /// Dispose `self`, freeing all its descendants, nodes, and
81    /// running its cleanup callbacks.
82    ///
83    /// Recursive — disposes children first, then this owner. Safe
84    /// to call even if the owner has already been disposed (no-op).
85    pub fn dispose(self) {
86        // Step 1: collect what needs cleaning. We pull data out of
87        // the runtime in a short borrow rather than holding it
88        // through the recursion, because each level may itself need
89        // to mutate the runtime (running cleanup callbacks does not,
90        // but symmetrically we keep the pattern simple by avoiding
91        // nested borrows).
92        let children;
93        let nodes;
94        let cleanups;
95        let parent;
96        let mount_fn;
97        let elements;
98        {
99            let removed = with_runtime(|rt| rt.owners.remove(self));
100            let Some(o) = removed else { return };
101            children = o.children;
102            nodes = o.nodes;
103            cleanups = o.cleanups;
104            parent = o.parent;
105            mount_fn = o.mount_fn;
106            elements = o.elements;
107        }
108
109        // Step 1b: if this was a component owner, scrub the hot-
110        // reload registry so the fn pointer doesn't list a freed
111        // slot. Without this, A6's `owners_for_fn` would return a
112        // dangling Owner and remount logic would fault.
113        if let Some(fp) = mount_fn {
114            with_runtime(|rt| {
115                if let Some(list) = rt.component_owners.get_mut(&fp) {
116                    list.retain(|o| *o != self);
117                    if list.is_empty() {
118                        rt.component_owners.remove(&fp);
119                    }
120                }
121            });
122
123            // Step 1c: also clean up any remountable MountSite whose
124            // owner is this one. Without this scrub, cascading
125            // disposal (e.g. parent component re-mounts and discards
126            // its sub-tree) leaves orphan MountSites behind, and
127            // the next `remount_components_for` call processes
128            // them — operating on freed parent / body_root handles,
129            // with visible corruption (issue #17 follow-up).
130            //
131            // `site.owner` is `None` *during* a remount (the
132            // takes-then-reinstalls window in `remount_one`), so
133            // this scan won't accidentally evict the site that's
134            // mid-flight. It only matches MountSites whose
135            // component owner is the one actually being disposed.
136            with_runtime(|rt| {
137                let stale: Vec<super::component::MountId> = rt
138                    .mount_sites
139                    .iter()
140                    .filter_map(|(id, site)| {
141                        if site.owner == Some(self) {
142                            Some(*id)
143                        } else {
144                            None
145                        }
146                    })
147                    .collect();
148                for id in stale {
149                    rt.mount_sites.remove(&id);
150                    if let Some(list) = rt.fn_ptr_mounts.get_mut(&fp) {
151                        list.retain(|m| *m != id);
152                        if list.is_empty() {
153                            rt.fn_ptr_mounts.remove(&fp);
154                        }
155                    }
156                }
157            });
158        }
159
160        // Step 2: detach from parent's children list.
161        if let Some(p) = parent {
162            with_runtime(|rt| {
163                if let Some(parent_scope) = rt.owners.get_mut(p) {
164                    parent_scope.children.retain(|&c| c != self);
165                }
166            });
167        }
168
169        // Step 3: dispose descendants (post-order — bottom up).
170        for child in children {
171            child.dispose();
172        }
173
174        // Step 4: free every node this owner allocated. For effects
175        // / computed values, also detach them from any subscriber
176        // list they were on, so other live nodes don't try to
177        // notify a freed slot later.
178        //
179        // Arc-signal back-references (`arc_sources`) get collected
180        // here and unsubscribed below, outside the runtime borrow —
181        // the unsubscribe callees may re-enter the runtime.
182        let arc_unsubscribes: Vec<(Rc<dyn super::runtime::ArcSubscription>, NodeId)> =
183            with_runtime(|rt| {
184                let mut out: Vec<(Rc<dyn super::runtime::ArcSubscription>, NodeId)> = Vec::new();
185                for node_id in &nodes {
186                    let Some(node) = rt.nodes.remove(*node_id) else {
187                        continue;
188                    };
189                    // Remove ourselves from every source's subscriber list.
190                    for source in node.sources {
191                        if let Some(src_node) = rt.nodes.get_mut(source) {
192                            src_node.subscribers.remove(node_id);
193                        }
194                    }
195                    // Remove ourselves from every subscriber's source list —
196                    // a signal we owned may have been read by an outer effect.
197                    for sub in node.subscribers {
198                        if let Some(sub_node) = rt.nodes.get_mut(sub) {
199                            sub_node.sources.remove(node_id);
200                        }
201                    }
202                    // Collect arc-signal back-refs so we can call
203                    // `unsubscribe` outside the runtime borrow.
204                    for arc_src in node.arc_sources {
205                        out.push((arc_src, *node_id));
206                    }
207                }
208                // Strip these nodes from the pending and deferred queues
209                // if any were scheduled — otherwise a later flush /
210                // resume would try to re-run a freed slot.
211                rt.pending.retain(|n| !nodes.contains(n));
212                rt.deferred.retain(|n| !nodes.contains(n));
213                out
214            });
215
216        // Tell every Arc-backed signal that one of our disposed
217        // nodes used to be on its subscriber list. The signal itself
218        // stays alive (Arc refcount), but pruning here keeps its
219        // list bounded so a long-lived signal doesn't accumulate
220        // dead `NodeId`s from every transient subscriber that came
221        // and went.
222        for (arc_src, subscriber) in arc_unsubscribes {
223            arc_src.unsubscribe(subscriber);
224        }
225
226        // Step 5: release every element handle the disposed owner
227        // created. We do this AFTER recursing into children so that
228        // bottom-up disposal order matches what the renderer
229        // expects (a child element's release before its parent's
230        // is fine; the bridge only complains if a parent is missing
231        // when a child reaches up). Done with the runtime borrow
232        // released so a future renderer that wants to call back
233        // into the reactive system (e.g. to notify "element
234        // released") can do so.
235        for handle in elements {
236            crate::view::release_element(handle);
237        }
238
239        // Step 6: run cleanups in LIFO order, with no runtime
240        // borrow held (cleanups may legitimately touch other parts
241        // of the runtime).
242        for cleanup in cleanups.into_iter().rev() {
243            cleanup();
244        }
245    }
246
247    /// Pause `self` (and its descendants): effects and computeds
248    /// whose scope is the paused subtree skip flush. Their
249    /// scheduled re-runs land on the runtime's `deferred` list
250    /// until [`Owner::resume`] drains them back.
251    ///
252    /// Idempotent — pausing an already-paused owner is a no-op.
253    /// The cascade walks the children tree breadth-first; new
254    /// descendants created while paused inherit the flag via
255    /// [`Owner::new`].
256    ///
257    /// Used by `StackLayout` to freeze back-stack entries that are
258    /// mounted-but-off-screen, matching iOS
259    /// `UINavigationController` / Android Fragment back-stack
260    /// semantics: state survives but no CPU is spent on
261    /// signal-driven re-renders behind the top route.
262    pub fn pause(self) {
263        with_runtime(|rt| {
264            let mut stack = vec![self];
265            while let Some(id) = stack.pop() {
266                let Some(o) = rt.owners.get_mut(id) else {
267                    continue;
268                };
269                if o.paused {
270                    continue;
271                }
272                o.paused = true;
273                stack.extend(o.children.iter().copied());
274            }
275        });
276    }
277
278    /// Resume `self` (and its descendants): clear the paused flag
279    /// and move any of its deferred effects back onto the pending
280    /// queue so they fire on the next flush.
281    ///
282    /// Idempotent. Iterates [`super::runtime::ReactiveRuntime::deferred`]
283    /// and re-queues every node whose owner is no longer paused —
284    /// including descendants resumed by this cascade, and any
285    /// deferred node whose owner happens to have been unpaused by
286    /// an earlier call.
287    pub fn resume(self) {
288        let any_resumed = with_runtime(|rt| {
289            let mut stack = vec![self];
290            let mut any = false;
291            while let Some(id) = stack.pop() {
292                let Some(o) = rt.owners.get_mut(id) else {
293                    continue;
294                };
295                if !o.paused {
296                    continue;
297                }
298                o.paused = false;
299                any = true;
300                stack.extend(o.children.iter().copied());
301            }
302            if !any {
303                return false;
304            }
305            // Drain deferred → pending for every node whose owner
306            // is no longer paused. Stale entries (node disposed
307            // under a paused owner) are dropped here.
308            let deferred = std::mem::take(&mut rt.deferred);
309            for node in deferred {
310                let still_paused = rt
311                    .nodes
312                    .get(node)
313                    .and_then(|n| rt.owners.get(n.owner))
314                    .map(|o| o.paused);
315                match still_paused {
316                    Some(false) => {
317                        if !rt.pending.contains(&node) {
318                            rt.pending.push(node);
319                        }
320                    }
321                    Some(true) => rt.deferred.push(node),
322                    None => {} // node or owner is gone; drop silently
323                }
324            }
325            true
326        });
327        if any_resumed {
328            crate::host_wake::wake_runtime();
329        }
330    }
331
332    /// Whether `self` is currently paused. Mainly for tests;
333    /// production code should drive pause / resume from the
334    /// lifecycle layer and not branch on the flag directly.
335    pub fn is_paused(self) -> bool {
336        with_runtime(|rt| rt.owners.get(self).map(|o| o.paused).unwrap_or(false))
337    }
338}
339
340/// Register a callback to run when the current owner is disposed.
341/// Calls accumulate in LIFO order, mirroring Solid / Leptos
342/// `onCleanup` semantics.
343///
344/// No-op (with a warning in `debug`) if there is no current owner.
345///
346/// Kept as a free function (not a method on [`Owner`]) because it
347/// operates on whatever owner happens to be at the top of the
348/// runtime's owner stack — the caller can't sensibly name it.
349pub fn on_cleanup(f: impl FnOnce() + 'static) {
350    let registered = with_runtime(|rt| {
351        let Some(owner_id) = rt.current_owner() else {
352            return false;
353        };
354        if let Some(scope) = rt.owners.get_mut(owner_id) {
355            scope.cleanups.push(Box::new(f));
356            return true;
357        }
358        false
359    });
360    if !registered {
361        debug_assert!(
362            false,
363            "on_cleanup called outside any owner — registration ignored"
364        );
365    }
366}