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        SignalTrack, SignalUpdate, create_effect, create_rw_signal,
120        runtime::{RUNTIME, Runtime},
121        scope::Scope,
122    };
123
124    #[test]
125    fn effect_disposed_when_dependency_signal_disposed() {
126        let parent = Scope::new();
127        let signal_scope = parent.create_child();
128        let (signal, setter) = signal_scope.create_signal(0);
129
130        let count = Rc::new(Cell::new(0));
131        parent.enter(|| {
132            let count = count.clone();
133            create_effect(move |_| {
134                signal.track();
135                count.set(count.get() + 1);
136            });
137        });
138
139        assert_eq!(count.get(), 1);
140
141        // Disposing the signal's scope should clean up the subscribing effect.
142        signal_scope.dispose();
143
144        // Mutations after disposal should not rerun the effect.
145        setter.set(1);
146        Runtime::drain_pending_work();
147        assert_eq!(count.get(), 1);
148
149        // The effect should be removed from the runtime.
150        RUNTIME.with(|runtime| assert!(runtime.effects.borrow().is_empty()));
151    }
152
153    #[test]
154    fn signals_created_by_effect_are_disposed_with_effect() {
155        let parent = Scope::new();
156        let dep_scope = parent.create_child();
157        let (dep_signal, dep_setter) = dep_scope.create_signal(0);
158
159        let created_signal = Rc::new(std::cell::RefCell::new(None));
160        let run_count = Rc::new(Cell::new(0));
161
162        parent.enter(|| {
163            let created_signal = created_signal.clone();
164            let run_count = run_count.clone();
165            create_effect(move |_| {
166                dep_signal.track();
167                run_count.set(run_count.get() + 1);
168                if created_signal.borrow().is_none() {
169                    created_signal.replace(Some(create_rw_signal(0)));
170                }
171            });
172        });
173
174        assert_eq!(run_count.get(), 1);
175        let inner_signal = created_signal.borrow().expect("signal created");
176        assert!(inner_signal.id().signal().is_some());
177
178        // Dispose the dependency scope; the effect should be disposed and clean up its children.
179        dep_scope.dispose();
180        Runtime::drain_pending_work();
181
182        // Mutating the dependency after disposal should do nothing.
183        dep_setter.set(1);
184        Runtime::drain_pending_work();
185        assert_eq!(run_count.get(), 1);
186
187        assert!(inner_signal.id().signal().is_none());
188        RUNTIME.with(|runtime| assert!(runtime.effects.borrow().is_empty()));
189    }
190
191    #[test]
192    fn disposing_scope_drops_signals_and_effects() {
193        let scope = Scope::new();
194        let (signal, setter) = scope.create_signal(0);
195        let signal_id = signal.id();
196
197        let run_count = Rc::new(Cell::new(0));
198        scope.enter(|| {
199            let run_count = run_count.clone();
200            create_effect(move |_| {
201                signal.track();
202                run_count.set(run_count.get() + 1);
203            });
204        });
205
206        // Sanity: effect ran and runtime holds signal/effect.
207        assert_eq!(run_count.get(), 1);
208        RUNTIME.with(|runtime| {
209            assert!(runtime.signals.borrow().contains_key(&signal_id));
210            assert_eq!(runtime.effects.borrow().len(), 1);
211            assert!(runtime.children.borrow().get(&scope.0).is_some());
212        });
213
214        // Dispose the scope; both signal and effect should be cleaned up.
215        scope.dispose();
216        Runtime::drain_pending_work();
217
218        setter.set(1);
219        Runtime::drain_pending_work();
220        assert_eq!(run_count.get(), 1);
221
222        RUNTIME.with(|runtime| {
223            assert!(runtime.signals.borrow().get(&signal_id).is_none());
224            assert!(runtime.effects.borrow().is_empty());
225            assert!(runtime.children.borrow().get(&scope.0).is_none());
226        });
227    }
228
229    #[test]
230    fn set_parent_reparents_scope() {
231        // Create two independent scopes (simulating eager construction)
232        let parent = Scope::new();
233        let child = Scope::new();
234
235        // Initially child has no parent
236        assert!(child.parent().is_none());
237
238        // Re-parent child under parent
239        child.set_parent(parent);
240
241        // Verify parent relationship
242        assert_eq!(child.parent().map(|s| s.0), Some(parent.0));
243
244        // Verify child is in parent's children set
245        RUNTIME.with(|runtime| {
246            let children = runtime.children.borrow();
247            let parent_children = children
248                .get(&parent.0)
249                .expect("parent should have children");
250            assert!(parent_children.contains(&child.0));
251        });
252    }
253
254    #[test]
255    fn set_parent_disposes_with_new_parent() {
256        let parent = Scope::new();
257        let child = Scope::new();
258
259        // Create a signal in the child scope
260        let signal = child.create_rw_signal(42);
261        let signal_id = signal.id();
262
263        // Re-parent child under parent
264        child.set_parent(parent);
265
266        // Signal should still exist
267        assert!(signal_id.signal().is_some());
268
269        // Dispose parent - should also dispose child and its signal
270        parent.dispose();
271        Runtime::drain_pending_work();
272
273        // Signal should be cleaned up
274        assert!(signal_id.signal().is_none());
275    }
276
277    #[test]
278    fn set_parent_removes_from_old_parent() {
279        let old_parent = Scope::new();
280        let new_parent = Scope::new();
281        let child = old_parent.create_child();
282
283        // Verify initial parent
284        assert_eq!(child.parent().map(|s| s.0), Some(old_parent.0));
285
286        // Re-parent to new parent
287        child.set_parent(new_parent);
288
289        // Verify new parent
290        assert_eq!(child.parent().map(|s| s.0), Some(new_parent.0));
291
292        // Verify removed from old parent's children
293        RUNTIME.with(|runtime| {
294            let children = runtime.children.borrow();
295            if let Some(old_children) = children.get(&old_parent.0) {
296                assert!(!old_children.contains(&child.0));
297            }
298        });
299
300        // Verify added to new parent's children
301        RUNTIME.with(|runtime| {
302            let children = runtime.children.borrow();
303            let new_children = children
304                .get(&new_parent.0)
305                .expect("new parent should have children");
306            assert!(new_children.contains(&child.0));
307        });
308    }
309}