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            runtime
43                .children
44                .borrow_mut()
45                .entry(scope)
46                .or_default()
47                .insert(*self);
48            runtime.parents.borrow_mut().insert(*self, scope);
49        });
50    }
51
52    /// Dispose only the children of this Id without removing resources tied to the Id itself.
53    pub(crate) fn dispose_children(&self) {
54        if let Ok(Some(children)) =
55            RUNTIME.try_with(|runtime| runtime.children.borrow_mut().remove(self))
56        {
57            for child in children {
58                child.dispose();
59            }
60        }
61    }
62
63    /// Dispose the relevant resources that's linking to this Id, and the all the children
64    /// and grandchildren.
65    pub(crate) fn dispose(&self) {
66        if !Runtime::is_ui_thread() {
67            // Bounce disposal work to the UI thread so we clean up the correct runtime.
68            SYNC_RUNTIME.enqueue_disposals([*self]);
69            return;
70        }
71
72        if let Ok((children, signal, effect)) = RUNTIME.try_with(|runtime| {
73            // Clean up scope-specific data
74            runtime.scope_contexts.borrow_mut().remove(self);
75            runtime.parents.borrow_mut().remove(self);
76
77            (
78                runtime.children.borrow_mut().remove(self),
79                runtime.signals.borrow_mut().remove(self),
80                runtime.effects.borrow_mut().remove(self),
81            )
82        }) {
83            if let Some(children) = children {
84                for child in children {
85                    child.dispose();
86                }
87            }
88
89            if let Some(effect) = effect {
90                observer_clean_up(&effect);
91            }
92
93            let mut signal = signal;
94            if signal.is_none() {
95                signal = SYNC_RUNTIME.remove_signal(self).map(Into::into);
96            }
97            Self::cleanup_signal(signal);
98        } else if let Some(signal) = SYNC_RUNTIME.remove_signal(self) {
99            Self::cleanup_signal(Some(signal.into()));
100        }
101    }
102
103    fn cleanup_signal(signal: Option<SignalState>) {
104        if let Some(signal) = signal {
105            for effect_id in signal.subscriber_ids() {
106                // Drop any effect that was subscribed to this signal so it can't linger
107                // with dangling dependencies.
108                effect_id.dispose();
109            }
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use std::{cell::Cell, rc::Rc};
117
118    use crate::{
119        create_effect, create_rw_signal,
120        runtime::{Runtime, RUNTIME},
121        scope::Scope,
122        SignalTrack, SignalUpdate,
123    };
124
125    #[test]
126    fn effect_disposed_when_dependency_signal_disposed() {
127        let parent = Scope::new();
128        let signal_scope = parent.create_child();
129        let (signal, setter) = signal_scope.create_signal(0);
130
131        let count = Rc::new(Cell::new(0));
132        parent.enter(|| {
133            let count = count.clone();
134            create_effect(move |_| {
135                signal.track();
136                count.set(count.get() + 1);
137            });
138        });
139
140        assert_eq!(count.get(), 1);
141
142        // Disposing the signal's scope should clean up the subscribing effect.
143        signal_scope.dispose();
144
145        // Mutations after disposal should not rerun the effect.
146        setter.set(1);
147        Runtime::drain_pending_work();
148        assert_eq!(count.get(), 1);
149
150        // The effect should be removed from the runtime.
151        RUNTIME.with(|runtime| assert!(runtime.effects.borrow().is_empty()));
152    }
153
154    #[test]
155    fn signals_created_by_effect_are_disposed_with_effect() {
156        let parent = Scope::new();
157        let dep_scope = parent.create_child();
158        let (dep_signal, dep_setter) = dep_scope.create_signal(0);
159
160        let created_signal = Rc::new(std::cell::RefCell::new(None));
161        let run_count = Rc::new(Cell::new(0));
162
163        parent.enter(|| {
164            let created_signal = created_signal.clone();
165            let run_count = run_count.clone();
166            create_effect(move |_| {
167                dep_signal.track();
168                run_count.set(run_count.get() + 1);
169                if created_signal.borrow().is_none() {
170                    created_signal.replace(Some(create_rw_signal(0)));
171                }
172            });
173        });
174
175        assert_eq!(run_count.get(), 1);
176        let inner_signal = created_signal.borrow().expect("signal created");
177        assert!(inner_signal.id().signal().is_some());
178
179        // Dispose the dependency scope; the effect should be disposed and clean up its children.
180        dep_scope.dispose();
181        Runtime::drain_pending_work();
182
183        // Mutating the dependency after disposal should do nothing.
184        dep_setter.set(1);
185        Runtime::drain_pending_work();
186        assert_eq!(run_count.get(), 1);
187
188        assert!(inner_signal.id().signal().is_none());
189        RUNTIME.with(|runtime| assert!(runtime.effects.borrow().is_empty()));
190    }
191
192    #[test]
193    fn disposing_scope_drops_signals_and_effects() {
194        let scope = Scope::new();
195        let (signal, setter) = scope.create_signal(0);
196        let signal_id = signal.id();
197
198        let run_count = Rc::new(Cell::new(0));
199        scope.enter(|| {
200            let run_count = run_count.clone();
201            create_effect(move |_| {
202                signal.track();
203                run_count.set(run_count.get() + 1);
204            });
205        });
206
207        // Sanity: effect ran and runtime holds signal/effect.
208        assert_eq!(run_count.get(), 1);
209        RUNTIME.with(|runtime| {
210            assert!(runtime.signals.borrow().contains_key(&signal_id));
211            assert_eq!(runtime.effects.borrow().len(), 1);
212            assert!(runtime.children.borrow().get(&scope.0).is_some());
213        });
214
215        // Dispose the scope; both signal and effect should be cleaned up.
216        scope.dispose();
217        Runtime::drain_pending_work();
218
219        setter.set(1);
220        Runtime::drain_pending_work();
221        assert_eq!(run_count.get(), 1);
222
223        RUNTIME.with(|runtime| {
224            assert!(runtime.signals.borrow().get(&signal_id).is_none());
225            assert!(runtime.effects.borrow().is_empty());
226            assert!(runtime.children.borrow().get(&scope.0).is_none());
227        });
228    }
229}