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. Registered activation callbacks wait for
117    /// [`Self::commit`]. Ordinary [`crate::effect`] calls still run immediately;
118    /// explicitly use [`crate::coherence::prepare_state`] to defer candidate
119    /// constructor effects.
120    pub fn new() -> Self {
121        Self::create(None)
122    }
123
124    /// Prepare a child. It activates only after both it and its ancestors commit.
125    /// An expired parent produces a disposed child.
126    pub fn child(parent: &OwnerHandle) -> Self {
127        Self::create(Some(parent.0.clone()))
128    }
129
130    fn create(parent: Option<Weak<Inner>>) -> Self {
131        let owner = Self(Rc::new(Inner {
132            status: Cell::new(Status::Prepared),
133            committed: Cell::new(false),
134            activated: Cell::new(false),
135            #[cfg(feature = "dom")]
136            mount_ready: Cell::new(false),
137            parent,
138            children: RefCell::new(Vec::new()),
139            dead_children: Cell::new(0),
140            next: Cell::new(0),
141            activate: RefCell::new(BTreeMap::new()),
142            cleanup: RefCell::new(BTreeMap::new()),
143            contexts: RefCell::new(BTreeMap::new()),
144        }));
145        if let Some(parent) = &owner.0.parent {
146            if let Some(parent) = parent
147                .upgrade()
148                .filter(|p| p.status.get() != Status::Disposed)
149            {
150                let mut children = parent.children.borrow_mut();
151                // Reclaim weak tombstones only when they occupy at least half
152                // the registry. Scanning on every insertion makes a wide tree
153                // quadratic; this threshold amortizes scans over dead children.
154                if parent.dead_children.get() >= 32
155                    && parent.dead_children.get() >= children.len() / 2
156                {
157                    children.retain(|child| child.strong_count() != 0);
158                    parent.dead_children.set(0);
159                }
160                children.push(Rc::downgrade(&owner.0));
161            } else {
162                owner.0.status.set(Status::Disposed);
163            }
164        }
165        owner
166    }
167
168    pub fn handle(&self) -> OwnerHandle {
169        OwnerHandle(Rc::downgrade(&self.0))
170    }
171
172    /// Commit once preparation succeeds. Idempotent; cannot revive disposal.
173    /// This activates registered work; it does not validate a renderer's nodes,
174    /// publish a scene, or roll back already executed application side effects.
175    pub fn commit(&self) {
176        self.0.committed.set(true);
177        activate(&self.0);
178    }
179
180    /// The status of an owner this caller holds, without a weak handle.
181    #[cfg(feature = "dom")]
182    pub(crate) fn is_active(&self) -> bool {
183        self.0.status.get() == Status::Active
184    }
185
186    #[cfg(feature = "dom")]
187    pub(crate) fn is_disposed(&self) -> bool {
188        self.0.status.get() == Status::Disposed
189    }
190
191    #[cfg(feature = "dom")]
192    pub(crate) fn mark_mount_ready(&self) {
193        self.0.mount_ready.set(true);
194    }
195
196    #[cfg(any(feature = "dom", test))]
197    pub(crate) fn was_activated(&self) -> bool {
198        self.0.activated.get()
199    }
200
201    /// Invalidate the whole subtree before calling any cleanup. Reentrant and
202    /// idempotent. Cleanup is synchronous; it cannot await task termination.
203    pub fn dispose(&self) {
204        let mut callbacks = Vec::new();
205        invalidate(&self.0, &mut callbacks);
206        for callback in callbacks {
207            callback();
208        }
209    }
210}
211
212impl Drop for Owner {
213    fn drop(&mut self) {
214        self.dispose();
215    }
216}
217
218fn activate(inner: &Rc<Inner>) {
219    if inner.status.get() != Status::Prepared || !inner.committed.get() {
220        return;
221    }
222    if let Some(parent) = &inner.parent {
223        if !parent
224            .upgrade()
225            .is_some_and(|p| p.status.get() == Status::Active)
226        {
227            return;
228        }
229    }
230    inner.status.set(Status::Active);
231    inner.activated.set(true);
232    let callbacks = inner.activate.take();
233    for callback in callbacks.into_values() {
234        if inner.status.get() == Status::Active {
235            callback();
236        }
237    }
238    let children = inner.children.borrow().clone();
239    for child in children.into_iter().filter_map(|child| child.upgrade()) {
240        activate(&child);
241    }
242}
243
244fn invalidate(inner: &Rc<Inner>, callbacks: &mut Vec<Callback>) {
245    if inner.status.replace(Status::Disposed) == Status::Disposed {
246        return;
247    }
248    // Most owners register nothing; skip their empty registries.
249    // Hold discarded activation callbacks until the entire tree is invalid.
250    if !inner.activate.borrow().is_empty() {
251        let activations = inner.activate.take();
252        callbacks.push(Box::new(move || drop(activations)));
253    }
254    if !inner.cleanup.borrow().is_empty() {
255        callbacks.extend(inner.cleanup.take().into_values());
256    }
257    if !inner.children.borrow().is_empty() {
258        for child in inner
259            .children
260            .take()
261            .into_iter()
262            .filter_map(|c| c.upgrade())
263        {
264            invalidate(&child, callbacks);
265        }
266    }
267    // Defer destructors until the whole tree is invalid. Release a provider
268    // after its children's cleanup callbacks have run.
269    if !inner.contexts.borrow().is_empty() {
270        let contexts = inner.contexts.take();
271        callbacks.push(Box::new(move || drop(contexts)));
272    }
273}
274
275impl OwnerHandle {
276    /// Wrap a callback with a weak lifetime check. Late external callbacks return
277    /// `None` after disposal (or before activation) instead of publishing state.
278    /// The callback's own captures still obey ordinary Rust ownership rules.
279    pub fn guarded<A, R, F: FnMut(A) -> R>(
280        &self,
281        mut callback: F,
282    ) -> impl FnMut(A) -> Option<R> + use<A, R, F> {
283        let owner = self.clone();
284        move |argument| {
285            owner
286                .is_active()
287                .then(|| crate::untrack(|| callback(argument)))
288        }
289    }
290    /// Supply a value once on this owner. Descendants resolve the nearest key.
291    /// This is not reactive registration: supply a signal for changing state.
292    pub fn provide<K: ContextKey>(&self, value: K::Value) -> Result<(), ContextError> {
293        let Some(inner) = self
294            .0
295            .upgrade()
296            .filter(|inner| inner.status.get() != Status::Disposed)
297        else {
298            return Err(ContextError::Disposed);
299        };
300        // A rejected value is dropped after this local registry borrow.
301        let mut contexts = inner.contexts.borrow_mut();
302        if contexts.contains_key(&TypeId::of::<K>()) {
303            return Err(ContextError::AlreadyProvided);
304        }
305        contexts.insert(TypeId::of::<K>(), Rc::new(value));
306        Ok(())
307    }
308
309    /// Find the nearest live provider, including this owner. Missing/disposed
310    /// context returns `None`. Lookup is untracked and does not keep owners alive.
311    /// An obtained `Rc` can outlive the provider like any ordinary Rust value;
312    /// disposal prevents further lookup and releases the owner's reference.
313    pub fn context<K: ContextKey>(&self) -> Option<Rc<K::Value>> {
314        let mut current = self.0.upgrade();
315        while let Some(inner) = current {
316            if inner.status.get() == Status::Disposed {
317                return None;
318            }
319            let value = inner.contexts.borrow().get(&TypeId::of::<K>()).cloned();
320            if let Some(value) = value {
321                return Some(
322                    value
323                        .downcast::<K::Value>()
324                        .expect("context key and value type agree"),
325                );
326            }
327            current = inner.parent.as_ref().and_then(Weak::upgrade);
328        }
329        None
330    }
331
332    /// Whether this is an immediate child of the given lifetime.
333    pub fn is_child_of(&self, parent: &Self) -> bool {
334        self.0
335            .upgrade()
336            .is_some_and(|inner| inner.parent.as_ref().is_some_and(|p| p.ptr_eq(&parent.0)))
337    }
338    #[cfg(feature = "dom")]
339    pub(crate) fn is_within(&self, ancestor: &Self) -> bool {
340        let mut cursor = Some(self.0.clone());
341        while let Some(owner) = cursor {
342            if owner.ptr_eq(&ancestor.0) {
343                return true;
344            }
345            cursor = owner.upgrade().and_then(|owner| owner.parent.clone());
346        }
347        false
348    }
349    pub fn is_active(&self) -> bool {
350        self.0
351            .upgrade()
352            .is_some_and(|p| p.status.get() == Status::Active)
353    }
354    pub fn is_disposed(&self) -> bool {
355        self.0
356            .upgrade()
357            .is_none_or(|p| p.status.get() == Status::Disposed)
358    }
359    #[cfg(feature = "dom")]
360    pub(crate) fn is_mount_ready(&self) -> bool {
361        self.0.upgrade().is_some_and(|p| p.mount_ready.get())
362    }
363    /// Run on activation (immediately if active). Does not run after disposal.
364    pub fn on_activate(&self, callback: impl FnOnce() + 'static) -> Registration {
365        self.register(Box::new(callback), true)
366    }
367    /// Run once on disposal (immediately if already disposed).
368    pub fn on_cleanup(&self, callback: impl FnOnce() + 'static) -> Registration {
369        self.register(Box::new(callback), false)
370    }
371    fn register(&self, callback: Callback, activation: bool) -> Registration {
372        let mut registration = Registration {
373            owner: self.0.clone(),
374            id: 0,
375            activation,
376        };
377        let Some(inner) = self.0.upgrade() else {
378            if !activation {
379                callback();
380            }
381            return registration;
382        };
383        let id = inner
384            .next
385            .get()
386            .checked_add(1)
387            .expect("owner registration overflow");
388        inner.next.set(id);
389        match (activation, inner.status.get()) {
390            (true, Status::Active) | (false, Status::Disposed) => callback(),
391            (true, Status::Disposed) => {}
392            (true, Status::Prepared) => {
393                inner.activate.borrow_mut().insert(id, callback);
394            }
395            (false, _) => {
396                inner.cleanup.borrow_mut().insert(id, callback);
397            }
398        }
399        registration.id = id;
400        registration
401    }
402}
403
404#[cfg(test)]
405mod registry_tests {
406    use super::*;
407
408    #[test]
409    fn transient_children_are_compacted_without_losing_survivors_or_order() {
410        let parent = Owner::new();
411        let first = Owner::child(&parent.handle());
412        for _ in 0..10_000 {
413            drop(Owner::child(&parent.handle()));
414        }
415        let last = Owner::child(&parent.handle());
416        assert!(parent.0.children.borrow().len() <= 34);
417        let calls = Rc::new(RefCell::new(Vec::new()));
418        let registrations: Vec<_> = [&first, &last]
419            .into_iter()
420            .enumerate()
421            .map(|(id, owner)| {
422                let calls = calls.clone();
423                owner.commit();
424                owner
425                    .handle()
426                    .on_activate(move || calls.borrow_mut().push(id))
427            })
428            .collect();
429        parent.commit();
430        assert_eq!(*calls.borrow(), [0, 1]);
431        parent.dispose();
432        assert!(first.handle().is_disposed());
433        assert!(last.handle().is_disposed());
434        drop(registrations);
435    }
436}
437
438#[cfg(test)]
439mod activation_history_tests {
440    use super::*;
441
442    #[test]
443    fn activation_history_survives_disposal_and_does_not_confuse_commit_with_activation() {
444        let parent = Owner::new();
445        let child = Owner::child(&parent.handle());
446        assert!(!parent.was_activated());
447        child.commit();
448        assert!(!child.was_activated());
449        parent.commit();
450        assert!(parent.was_activated());
451        assert!(child.was_activated());
452        parent.dispose();
453        assert!(parent.was_activated());
454        assert!(child.was_activated());
455        child.commit();
456        assert!(child.was_activated());
457        assert!(child.handle().is_disposed());
458    }
459
460    #[test]
461    fn failed_preparation_and_disposed_ancestors_never_claim_activation() {
462        let parent = Owner::new();
463        let child = Owner::child(&parent.handle());
464        child.commit();
465        parent.dispose();
466        assert!(!parent.was_activated());
467        assert!(!child.was_activated());
468        let late = Owner::child(&parent.handle());
469        late.commit();
470        assert!(!late.was_activated());
471        let orphan = Owner::child(&Owner::new().handle());
472        orphan.commit();
473        assert!(!orphan.was_activated());
474    }
475
476    #[test]
477    fn parent_disposal_during_activation_does_not_claim_its_waiting_child() {
478        let parent = Rc::new(Owner::new());
479        let child = Owner::child(&parent.handle());
480        child.commit();
481        let weak = Rc::downgrade(&parent);
482        let _registration = parent.handle().on_activate(move || {
483            weak.upgrade().unwrap().dispose();
484        });
485        parent.commit();
486        assert!(parent.was_activated());
487        assert!(!child.was_activated());
488        assert!(child.handle().is_disposed());
489    }
490
491    #[test]
492    fn history_is_set_before_self_disposal_in_an_activation_callback() {
493        let owner = Rc::new(Owner::new());
494        let weak = Rc::downgrade(&owner);
495        let observed = Rc::new(Cell::new(false));
496        let observed_in_callback = observed.clone();
497        let _registration = owner.handle().on_activate(move || {
498            let owner = weak.upgrade().unwrap();
499            observed_in_callback.set(owner.was_activated());
500            owner.dispose();
501        });
502        owner.commit();
503        assert!(observed.get());
504        assert!(owner.was_activated());
505        assert!(owner.handle().is_disposed());
506    }
507}