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