floem_reactive/
id.rs

1use std::sync::atomic::AtomicU64;
2
3use crate::{
4    effect::observer_clean_up,
5    runtime::{Runtime, RUNTIME},
6    signal::SignalState,
7    sync_runtime::SYNC_RUNTIME,
8};
9
10/// An internal id which can reference a Signal/Effect/Scope.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct Id(u64);
13
14impl Id {
15    /// Create a new Id that's next in order
16    pub(crate) fn next() -> Id {
17        static COUNTER: AtomicU64 = AtomicU64::new(0);
18        Id(COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
19    }
20
21    /// Try to get the Signal that links with this Id
22    pub(crate) fn signal(&self) -> Option<SignalState> {
23        if Runtime::is_ui_thread() {
24            if let Some(sig) = RUNTIME.with(|runtime| runtime.signals.borrow().get(self).cloned()) {
25                return Some(sig);
26            }
27            SYNC_RUNTIME.get_signal(self).map(Into::into)
28        } else {
29            SYNC_RUNTIME.get_signal(self).map(Into::into)
30        }
31    }
32
33    /// Try to set the Signal to be linking with this Id
34    pub(crate) fn add_signal(&self, signal: SignalState) {
35        RUNTIME.with(|runtime| runtime.signals.borrow_mut().insert(*self, signal));
36    }
37
38    /// Make this Id a child of the current Scope
39    pub(crate) fn set_scope(&self) {
40        RUNTIME.with(|runtime| {
41            let scope = runtime.current_scope.borrow();
42            let mut children = runtime.children.borrow_mut();
43            let children = children.entry(*scope).or_default();
44            children.insert(*self);
45        });
46    }
47
48    /// Dispose only the children of this Id without removing resources tied to the Id itself.
49    pub(crate) fn dispose_children(&self) {
50        if let Ok(Some(children)) =
51            RUNTIME.try_with(|runtime| runtime.children.borrow_mut().remove(self))
52        {
53            for child in children {
54                child.dispose();
55            }
56        }
57    }
58
59    /// Dispose the relevant resources that's linking to this Id, and the all the children
60    /// and grandchildren.
61    pub(crate) fn dispose(&self) {
62        if !Runtime::is_ui_thread() {
63            // Bounce disposal work to the UI thread so we clean up the correct runtime.
64            SYNC_RUNTIME.enqueue_disposals([*self]);
65            return;
66        }
67
68        if let Ok((children, signal, effect)) = RUNTIME.try_with(|runtime| {
69            (
70                runtime.children.borrow_mut().remove(self),
71                runtime.signals.borrow_mut().remove(self),
72                runtime.effects.borrow_mut().remove(self),
73            )
74        }) {
75            if let Some(children) = children {
76                for child in children {
77                    child.dispose();
78                }
79            }
80
81            if let Some(effect) = effect {
82                observer_clean_up(&effect);
83            }
84
85            let mut signal = signal;
86            if signal.is_none() {
87                signal = SYNC_RUNTIME.remove_signal(self).map(Into::into);
88            }
89            Self::cleanup_signal(signal);
90        } else if let Some(signal) = SYNC_RUNTIME.remove_signal(self) {
91            Self::cleanup_signal(Some(signal.into()));
92        }
93    }
94
95    fn cleanup_signal(signal: Option<SignalState>) {
96        if let Some(signal) = signal {
97            for effect_id in signal.subscriber_ids() {
98                // Drop any effect that was subscribed to this signal so it can't linger
99                // with dangling dependencies.
100                effect_id.dispose();
101            }
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use std::{cell::Cell, rc::Rc};
109
110    use crate::{
111        create_effect, create_rw_signal,
112        runtime::{Runtime, RUNTIME},
113        scope::Scope,
114        SignalTrack, SignalUpdate,
115    };
116
117    #[test]
118    fn effect_disposed_when_dependency_signal_disposed() {
119        let parent = Scope::new();
120        let signal_scope = parent.create_child();
121        let (signal, setter) = signal_scope.create_signal(0);
122
123        let count = Rc::new(Cell::new(0));
124        parent.enter(|| {
125            let count = count.clone();
126            create_effect(move |_| {
127                signal.track();
128                count.set(count.get() + 1);
129            });
130        });
131
132        assert_eq!(count.get(), 1);
133
134        // Disposing the signal's scope should clean up the subscribing effect.
135        signal_scope.dispose();
136
137        // Mutations after disposal should not rerun the effect.
138        setter.set(1);
139        Runtime::drain_pending_work();
140        assert_eq!(count.get(), 1);
141
142        // The effect should be removed from the runtime.
143        RUNTIME.with(|runtime| assert!(runtime.effects.borrow().is_empty()));
144    }
145
146    #[test]
147    fn signals_created_by_effect_are_disposed_with_effect() {
148        let parent = Scope::new();
149        let dep_scope = parent.create_child();
150        let (dep_signal, dep_setter) = dep_scope.create_signal(0);
151
152        let created_signal = Rc::new(std::cell::RefCell::new(None));
153        let run_count = Rc::new(Cell::new(0));
154
155        parent.enter(|| {
156            let created_signal = created_signal.clone();
157            let run_count = run_count.clone();
158            create_effect(move |_| {
159                dep_signal.track();
160                run_count.set(run_count.get() + 1);
161                if created_signal.borrow().is_none() {
162                    created_signal.replace(Some(create_rw_signal(0)));
163                }
164            });
165        });
166
167        assert_eq!(run_count.get(), 1);
168        let inner_signal = created_signal.borrow().clone().expect("signal created");
169        assert!(inner_signal.id().signal().is_some());
170
171        // Dispose the dependency scope; the effect should be disposed and clean up its children.
172        dep_scope.dispose();
173        Runtime::drain_pending_work();
174
175        // Mutating the dependency after disposal should do nothing.
176        dep_setter.set(1);
177        Runtime::drain_pending_work();
178        assert_eq!(run_count.get(), 1);
179
180        assert!(inner_signal.id().signal().is_none());
181        RUNTIME.with(|runtime| assert!(runtime.effects.borrow().is_empty()));
182    }
183
184    #[test]
185    fn disposing_scope_drops_signals_and_effects() {
186        let scope = Scope::new();
187        let (signal, setter) = scope.create_signal(0);
188        let signal_id = signal.id();
189
190        let run_count = Rc::new(Cell::new(0));
191        scope.enter(|| {
192            let run_count = run_count.clone();
193            create_effect(move |_| {
194                signal.track();
195                run_count.set(run_count.get() + 1);
196            });
197        });
198
199        // Sanity: effect ran and runtime holds signal/effect.
200        assert_eq!(run_count.get(), 1);
201        RUNTIME.with(|runtime| {
202            assert!(runtime.signals.borrow().contains_key(&signal_id));
203            assert_eq!(runtime.effects.borrow().len(), 1);
204            assert!(runtime.children.borrow().get(&scope.0).is_some());
205        });
206
207        // Dispose the scope; both signal and effect should be cleaned up.
208        scope.dispose();
209        Runtime::drain_pending_work();
210
211        setter.set(1);
212        Runtime::drain_pending_work();
213        assert_eq!(run_count.get(), 1);
214
215        RUNTIME.with(|runtime| {
216            assert!(runtime.signals.borrow().get(&signal_id).is_none());
217            assert!(runtime.effects.borrow().is_empty());
218            assert!(runtime.children.borrow().get(&scope.0).is_none());
219        });
220    }
221}