Skip to main content

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
230    #[test]
231    fn set_parent_reparents_scope() {
232        // Create two independent scopes (simulating eager construction)
233        let parent = Scope::new();
234        let child = Scope::new();
235
236        // Initially child has no parent
237        assert!(child.parent().is_none());
238
239        // Re-parent child under parent
240        child.set_parent(parent);
241
242        // Verify parent relationship
243        assert_eq!(child.parent().map(|s| s.0), Some(parent.0));
244
245        // Verify child is in parent's children set
246        RUNTIME.with(|runtime| {
247            let children = runtime.children.borrow();
248            let parent_children = children
249                .get(&parent.0)
250                .expect("parent should have children");
251            assert!(parent_children.contains(&child.0));
252        });
253    }
254
255    #[test]
256    fn set_parent_disposes_with_new_parent() {
257        let parent = Scope::new();
258        let child = Scope::new();
259
260        // Create a signal in the child scope
261        let signal = child.create_rw_signal(42);
262        let signal_id = signal.id();
263
264        // Re-parent child under parent
265        child.set_parent(parent);
266
267        // Signal should still exist
268        assert!(signal_id.signal().is_some());
269
270        // Dispose parent - should also dispose child and its signal
271        parent.dispose();
272        Runtime::drain_pending_work();
273
274        // Signal should be cleaned up
275        assert!(signal_id.signal().is_none());
276    }
277
278    #[test]
279    fn set_parent_removes_from_old_parent() {
280        let old_parent = Scope::new();
281        let new_parent = Scope::new();
282        let child = old_parent.create_child();
283
284        // Verify initial parent
285        assert_eq!(child.parent().map(|s| s.0), Some(old_parent.0));
286
287        // Re-parent to new parent
288        child.set_parent(new_parent);
289
290        // Verify new parent
291        assert_eq!(child.parent().map(|s| s.0), Some(new_parent.0));
292
293        // Verify removed from old parent's children
294        RUNTIME.with(|runtime| {
295            let children = runtime.children.borrow();
296            if let Some(old_children) = children.get(&old_parent.0) {
297                assert!(!old_children.contains(&child.0));
298            }
299        });
300
301        // Verify added to new parent's children
302        RUNTIME.with(|runtime| {
303            let children = runtime.children.borrow();
304            let new_children = children
305                .get(&new_parent.0)
306                .expect("new parent should have children");
307            assert!(new_children.contains(&child.0));
308        });
309    }
310}