Skip to main content

fusor/
owner.rs

1//! Explicit lifetimes shared by DOM scopes and optional runtime integrations.
2use std::{
3    any::{Any, TypeId},
4    cell::{Cell, RefCell},
5    collections::BTreeMap,
6    rc::{Rc, Weak},
7};
8
9type Callback = Box<dyn FnOnce()>;
10
11#[derive(Clone, Copy, PartialEq, Eq)]
12enum Status {
13    Prepared,
14    Active,
15    Disposed,
16}
17
18struct Inner {
19    status: Cell<Status>,
20    committed: Cell<bool>,
21    // Monotone lifecycle history; disposal must not erase adopted-DOM ownership.
22    activated: Cell<bool>,
23    // Fallible DOM setup can finish while this owner still awaits activation.
24    #[cfg(feature = "dom")]
25    mount_ready: Cell<bool>,
26    parent: Option<Weak<Inner>>,
27    children: RefCell<Vec<Weak<Inner>>>,
28    dead_children: Cell<usize>,
29    next: Cell<u64>,
30    activate: RefCell<BTreeMap<u64, Callback>>,
31    cleanup: RefCell<BTreeMap<u64, Callback>>,
32    contexts: RefCell<BTreeMap<TypeId, Rc<dyn Any>>>,
33}
34
35// Inner, rather than Owner, accounts for expiry: callbacks can temporarily
36// retain an Inner while a unique Owner is being dropped.
37impl Drop for Inner {
38    fn drop(&mut self) {
39        if let Some(parent) = self.parent.as_ref().and_then(Weak::upgrade) {
40            parent.dead_children.set(parent.dead_children.get() + 1);
41        }
42    }
43}
44
45/// A typed, namespaced key for a value supplied by a component or application.
46/// Distinct key types can provide the same value type without colliding.
47///
48/// ```
49/// use fusor::{ContextKey, Owner, Signal, signal};
50/// struct Locale;
51/// impl ContextKey for Locale { type Value = Signal<String>; }
52/// let app = Owner::new();
53/// app.handle().provide::<Locale>(signal("en".to_owned())).unwrap();
54/// let child = Owner::child(&app.handle());
55/// assert_eq!(child.handle().context::<Locale>().unwrap().get(), "en");
56/// ```
57pub trait ContextKey: 'static {
58    type Value: 'static;
59}
60
61/// A context provider cannot be replaced or registered after disposal.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ContextError {
64    Disposed,
65    AlreadyProvided,
66}
67
68impl std::fmt::Display for ContextError {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.write_str(match self {
71            Self::Disposed => "cannot provide context on a disposed owner",
72            Self::AlreadyProvided => "this owner already provides the context key",
73        })
74    }
75}
76
77impl std::error::Error for ContextError {}
78
79/// A unique lifetime owner. Commit starts registered work; drop disposes it.
80/// Handles are weak and cannot extend the lifetime. This is single-threaded.
81#[must_use = "dropping the owner disposes its work"]
82pub struct Owner(Rc<Inner>);
83
84/// Weak access to an owner's lifecycle. Safe to retain after disposal.
85#[derive(Clone)]
86pub struct OwnerHandle(Weak<Inner>);
87
88/// Removing this registration unregisters the callback without invoking it.
89#[must_use = "retain the registration until the callback is no longer needed"]
90pub struct Registration {
91    owner: Weak<Inner>,
92    id: u64,
93    activation: bool,
94}
95
96impl Drop for Registration {
97    fn drop(&mut self) {
98        if let Some(owner) = self.owner.upgrade() {
99            let removed = if self.activation {
100                owner.activate.borrow_mut().remove(&self.id)
101            } else {
102                owner.cleanup.borrow_mut().remove(&self.id)
103            };
104            drop(removed);
105        }
106    }
107}
108
109impl Default for Owner {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl Owner {
116    /// Prepare a root lifetime. Work waits for [`Self::commit`].
117    pub fn new() -> Self {
118        Self::create(None)
119    }
120
121    /// Prepare a child. It activates only after both it and its ancestors commit.
122    /// An expired parent produces a disposed child.
123    pub fn child(parent: &OwnerHandle) -> Self {
124        Self::create(Some(parent.0.clone()))
125    }
126
127    fn create(parent: Option<Weak<Inner>>) -> Self {
128        let owner = Self(Rc::new(Inner {
129            status: Cell::new(Status::Prepared),
130            committed: Cell::new(false),
131            activated: Cell::new(false),
132            #[cfg(feature = "dom")]
133            mount_ready: Cell::new(false),
134            parent,
135            children: RefCell::new(Vec::new()),
136            dead_children: Cell::new(0),
137            next: Cell::new(0),
138            activate: RefCell::new(BTreeMap::new()),
139            cleanup: RefCell::new(BTreeMap::new()),
140            contexts: RefCell::new(BTreeMap::new()),
141        }));
142        if let Some(parent) = &owner.0.parent {
143            if let Some(parent) = parent
144                .upgrade()
145                .filter(|p| p.status.get() != Status::Disposed)
146            {
147                let mut children = parent.children.borrow_mut();
148                // Reclaim weak tombstones only when they occupy at least half
149                // the registry. Scanning on every insertion makes a wide tree
150                // quadratic; this threshold amortizes scans over dead children.
151                if parent.dead_children.get() >= 32
152                    && parent.dead_children.get() >= children.len() / 2
153                {
154                    children.retain(|child| child.strong_count() != 0);
155                    parent.dead_children.set(0);
156                }
157                children.push(Rc::downgrade(&owner.0));
158            } else {
159                owner.0.status.set(Status::Disposed);
160            }
161        }
162        owner
163    }
164
165    pub fn handle(&self) -> OwnerHandle {
166        OwnerHandle(Rc::downgrade(&self.0))
167    }
168
169    /// Commit once preparation succeeds. Idempotent; cannot revive disposal.
170    pub fn commit(&self) {
171        self.0.committed.set(true);
172        activate(&self.0);
173    }
174
175    /// The status of an owner this caller holds, without a weak handle.
176    #[cfg(feature = "dom")]
177    pub(crate) fn is_active(&self) -> bool {
178        self.0.status.get() == Status::Active
179    }
180
181    #[cfg(feature = "dom")]
182    pub(crate) fn is_disposed(&self) -> bool {
183        self.0.status.get() == Status::Disposed
184    }
185
186    #[cfg(feature = "dom")]
187    pub(crate) fn mark_mount_ready(&self) {
188        self.0.mount_ready.set(true);
189    }
190
191    #[cfg(any(feature = "dom", test))]
192    pub(crate) fn was_activated(&self) -> bool {
193        self.0.activated.get()
194    }
195
196    /// Invalidate the whole subtree before calling any cleanup. Reentrant and
197    /// idempotent. Cleanup is synchronous; it cannot await task termination.
198    pub fn dispose(&self) {
199        let mut callbacks = Vec::new();
200        invalidate(&self.0, &mut callbacks);
201        for callback in callbacks {
202            callback();
203        }
204    }
205}
206
207impl Drop for Owner {
208    fn drop(&mut self) {
209        self.dispose();
210    }
211}
212
213fn activate(inner: &Rc<Inner>) {
214    if inner.status.get() != Status::Prepared || !inner.committed.get() {
215        return;
216    }
217    if let Some(parent) = &inner.parent {
218        if !parent
219            .upgrade()
220            .is_some_and(|p| p.status.get() == Status::Active)
221        {
222            return;
223        }
224    }
225    inner.status.set(Status::Active);
226    inner.activated.set(true);
227    let callbacks = inner.activate.take();
228    for callback in callbacks.into_values() {
229        if inner.status.get() == Status::Active {
230            callback();
231        }
232    }
233    let children = inner.children.borrow().clone();
234    for child in children.into_iter().filter_map(|child| child.upgrade()) {
235        activate(&child);
236    }
237}
238
239fn invalidate(inner: &Rc<Inner>, callbacks: &mut Vec<Callback>) {
240    if inner.status.replace(Status::Disposed) == Status::Disposed {
241        return;
242    }
243    // Most owners register nothing; skip their empty registries.
244    // Hold discarded activation callbacks until the entire tree is invalid.
245    if !inner.activate.borrow().is_empty() {
246        let activations = inner.activate.take();
247        callbacks.push(Box::new(move || drop(activations)));
248    }
249    if !inner.cleanup.borrow().is_empty() {
250        callbacks.extend(inner.cleanup.take().into_values());
251    }
252    if !inner.children.borrow().is_empty() {
253        for child in inner
254            .children
255            .take()
256            .into_iter()
257            .filter_map(|c| c.upgrade())
258        {
259            invalidate(&child, callbacks);
260        }
261    }
262    // Defer destructors until the whole tree is invalid. Release a provider
263    // after its children's cleanup callbacks have run.
264    if !inner.contexts.borrow().is_empty() {
265        let contexts = inner.contexts.take();
266        callbacks.push(Box::new(move || drop(contexts)));
267    }
268}
269
270impl OwnerHandle {
271    /// Wrap a callback with a weak lifetime check. Late external callbacks return
272    /// `None` after disposal (or before activation) instead of publishing state.
273    /// The callback's own captures still obey ordinary Rust ownership rules.
274    pub fn guarded<A, R, F: FnMut(A) -> R>(
275        &self,
276        mut callback: F,
277    ) -> impl FnMut(A) -> Option<R> + use<A, R, F> {
278        let owner = self.clone();
279        move |argument| {
280            owner
281                .is_active()
282                .then(|| crate::untrack(|| callback(argument)))
283        }
284    }
285    /// Supply a value once on this owner. Descendants resolve the nearest key.
286    /// This is not reactive registration: supply a signal for changing state.
287    pub fn provide<K: ContextKey>(&self, value: K::Value) -> Result<(), ContextError> {
288        let Some(inner) = self
289            .0
290            .upgrade()
291            .filter(|inner| inner.status.get() != Status::Disposed)
292        else {
293            return Err(ContextError::Disposed);
294        };
295        // A rejected value is dropped after this local registry borrow.
296        let mut contexts = inner.contexts.borrow_mut();
297        if contexts.contains_key(&TypeId::of::<K>()) {
298            return Err(ContextError::AlreadyProvided);
299        }
300        contexts.insert(TypeId::of::<K>(), Rc::new(value));
301        Ok(())
302    }
303
304    /// Find the nearest live provider, including this owner. Missing/disposed
305    /// context returns `None`. Lookup is untracked and does not keep owners alive.
306    /// An obtained `Rc` can outlive the provider like any ordinary Rust value;
307    /// disposal prevents further lookup and releases the owner's reference.
308    pub fn context<K: ContextKey>(&self) -> Option<Rc<K::Value>> {
309        let mut current = self.0.upgrade();
310        while let Some(inner) = current {
311            if inner.status.get() == Status::Disposed {
312                return None;
313            }
314            let value = inner.contexts.borrow().get(&TypeId::of::<K>()).cloned();
315            if let Some(value) = value {
316                return Some(
317                    value
318                        .downcast::<K::Value>()
319                        .expect("context key and value type agree"),
320                );
321            }
322            current = inner.parent.as_ref().and_then(Weak::upgrade);
323        }
324        None
325    }
326
327    /// Whether this is an immediate child of the given lifetime.
328    pub fn is_child_of(&self, parent: &Self) -> bool {
329        self.0
330            .upgrade()
331            .is_some_and(|inner| inner.parent.as_ref().is_some_and(|p| p.ptr_eq(&parent.0)))
332    }
333    #[cfg(feature = "dom")]
334    pub(crate) fn is_within(&self, ancestor: &Self) -> bool {
335        let mut cursor = Some(self.0.clone());
336        while let Some(owner) = cursor {
337            if owner.ptr_eq(&ancestor.0) {
338                return true;
339            }
340            cursor = owner.upgrade().and_then(|owner| owner.parent.clone());
341        }
342        false
343    }
344    pub fn is_active(&self) -> bool {
345        self.0
346            .upgrade()
347            .is_some_and(|p| p.status.get() == Status::Active)
348    }
349    pub fn is_disposed(&self) -> bool {
350        self.0
351            .upgrade()
352            .is_none_or(|p| p.status.get() == Status::Disposed)
353    }
354    #[cfg(feature = "dom")]
355    pub(crate) fn is_mount_ready(&self) -> bool {
356        self.0.upgrade().is_some_and(|p| p.mount_ready.get())
357    }
358    /// Run on activation (immediately if active). Does not run after disposal.
359    pub fn on_activate(&self, callback: impl FnOnce() + 'static) -> Registration {
360        self.register(Box::new(callback), true)
361    }
362    /// Run once on disposal (immediately if already disposed).
363    pub fn on_cleanup(&self, callback: impl FnOnce() + 'static) -> Registration {
364        self.register(Box::new(callback), false)
365    }
366    fn register(&self, callback: Callback, activation: bool) -> Registration {
367        let mut registration = Registration {
368            owner: self.0.clone(),
369            id: 0,
370            activation,
371        };
372        let Some(inner) = self.0.upgrade() else {
373            if !activation {
374                callback();
375            }
376            return registration;
377        };
378        let id = inner
379            .next
380            .get()
381            .checked_add(1)
382            .expect("owner registration overflow");
383        inner.next.set(id);
384        match (activation, inner.status.get()) {
385            (true, Status::Active) | (false, Status::Disposed) => callback(),
386            (true, Status::Disposed) => {}
387            (true, Status::Prepared) => {
388                inner.activate.borrow_mut().insert(id, callback);
389            }
390            (false, _) => {
391                inner.cleanup.borrow_mut().insert(id, callback);
392            }
393        }
394        registration.id = id;
395        registration
396    }
397}
398
399#[cfg(test)]
400mod registry_tests {
401    use super::*;
402
403    #[test]
404    fn transient_children_are_compacted_without_losing_survivors_or_order() {
405        let parent = Owner::new();
406        let first = Owner::child(&parent.handle());
407        for _ in 0..10_000 {
408            drop(Owner::child(&parent.handle()));
409        }
410        let last = Owner::child(&parent.handle());
411        assert!(parent.0.children.borrow().len() <= 34);
412        let calls = Rc::new(RefCell::new(Vec::new()));
413        let registrations: Vec<_> = [&first, &last]
414            .into_iter()
415            .enumerate()
416            .map(|(id, owner)| {
417                let calls = calls.clone();
418                owner.commit();
419                owner
420                    .handle()
421                    .on_activate(move || calls.borrow_mut().push(id))
422            })
423            .collect();
424        parent.commit();
425        assert_eq!(*calls.borrow(), [0, 1]);
426        parent.dispose();
427        assert!(first.handle().is_disposed());
428        assert!(last.handle().is_disposed());
429        drop(registrations);
430    }
431}
432
433#[cfg(test)]
434mod activation_history_tests {
435    use super::*;
436
437    #[test]
438    fn activation_history_survives_disposal_and_does_not_confuse_commit_with_activation() {
439        let parent = Owner::new();
440        let child = Owner::child(&parent.handle());
441        assert!(!parent.was_activated());
442        child.commit();
443        assert!(!child.was_activated());
444        parent.commit();
445        assert!(parent.was_activated());
446        assert!(child.was_activated());
447        parent.dispose();
448        assert!(parent.was_activated());
449        assert!(child.was_activated());
450        child.commit();
451        assert!(child.was_activated());
452        assert!(child.handle().is_disposed());
453    }
454
455    #[test]
456    fn failed_preparation_and_disposed_ancestors_never_claim_activation() {
457        let parent = Owner::new();
458        let child = Owner::child(&parent.handle());
459        child.commit();
460        parent.dispose();
461        assert!(!parent.was_activated());
462        assert!(!child.was_activated());
463        let late = Owner::child(&parent.handle());
464        late.commit();
465        assert!(!late.was_activated());
466        let orphan = Owner::child(&Owner::new().handle());
467        orphan.commit();
468        assert!(!orphan.was_activated());
469    }
470
471    #[test]
472    fn parent_disposal_during_activation_does_not_claim_its_waiting_child() {
473        let parent = Rc::new(Owner::new());
474        let child = Owner::child(&parent.handle());
475        child.commit();
476        let weak = Rc::downgrade(&parent);
477        let _registration = parent.handle().on_activate(move || {
478            weak.upgrade().unwrap().dispose();
479        });
480        parent.commit();
481        assert!(parent.was_activated());
482        assert!(!child.was_activated());
483        assert!(child.handle().is_disposed());
484    }
485
486    #[test]
487    fn history_is_set_before_self_disposal_in_an_activation_callback() {
488        let owner = Rc::new(Owner::new());
489        let weak = Rc::downgrade(&owner);
490        let observed = Rc::new(Cell::new(false));
491        let observed_in_callback = observed.clone();
492        let _registration = owner.handle().on_activate(move || {
493            let owner = weak.upgrade().unwrap();
494            observed_in_callback.set(owner.was_activated());
495            owner.dispose();
496        });
497        owner.commit();
498        assert!(observed.get());
499        assert!(owner.was_activated());
500        assert!(owner.handle().is_disposed());
501    }
502}