saorsa_core/reactive/
scope.rs1use super::computed::Computed;
8use super::effect::Effect;
9use super::signal::Signal;
10
11pub struct ReactiveScope {
35 effects: Vec<Effect>,
37 children: Vec<ReactiveScope>,
39 cleanups: Vec<Box<dyn FnOnce()>>,
41}
42
43impl ReactiveScope {
44 #[must_use]
46 pub fn new() -> Self {
47 Self {
48 effects: Vec::new(),
49 children: Vec::new(),
50 cleanups: Vec::new(),
51 }
52 }
53
54 pub fn create_signal<T>(&self, value: T) -> Signal<T> {
57 Signal::new(value)
58 }
59
60 pub fn create_computed<T: Clone + 'static>(&self, f: impl Fn() -> T + 'static) -> Computed<T> {
63 Computed::new(f)
64 }
65
66 pub fn create_effect(&mut self, f: impl FnMut() + 'static) -> Effect {
70 let effect = Effect::new(f);
71 self.effects.push(effect.clone());
72 effect
73 }
74
75 pub fn on_cleanup(&mut self, f: impl FnOnce() + 'static) {
79 self.cleanups.push(Box::new(f));
80 }
81
82 pub fn child(&mut self) -> &mut ReactiveScope {
86 self.children.push(ReactiveScope::new());
87 let last = self.children.len() - 1;
88 &mut self.children[last]
89 }
90
91 pub fn effect_count(&self) -> usize {
93 self.effects.len()
94 }
95
96 pub fn child_count(&self) -> usize {
98 self.children.len()
99 }
100}
101
102impl Default for ReactiveScope {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl Drop for ReactiveScope {
109 fn drop(&mut self) {
110 self.children.clear();
112
113 for effect in &self.effects {
115 effect.dispose();
116 }
117
118 while let Some(cleanup) = self.cleanups.pop() {
120 cleanup();
121 }
122 }
123}
124
125#[cfg(test)]
126#[allow(clippy::unwrap_used)]
127mod tests {
128 use super::*;
129 use std::cell::Cell;
130 use std::rc::Rc;
131
132 #[test]
133 fn scope_disposes_effects_on_drop() {
134 let sig = Signal::new(0);
135 let count = Rc::new(Cell::new(0u32));
136
137 let effect;
138 {
139 let mut scope = ReactiveScope::new();
140 effect = scope.create_effect({
141 let sig = sig.clone();
142 let count = Rc::clone(&count);
143 move || {
144 let _ = sig.get();
145 count.set(count.get() + 1);
146 }
147 });
148
149 sig.subscribe(effect.as_subscriber());
150 assert_eq!(count.get(), 1);
151
152 sig.set(1);
153 assert_eq!(count.get(), 2);
154 }
155 assert!(!effect.is_active());
157
158 sig.set(2);
159 assert_eq!(count.get(), 2); }
161
162 #[test]
163 fn scope_runs_cleanups_on_drop() {
164 let order = Rc::new(RefCell::new(Vec::new()));
165
166 {
167 let mut scope = ReactiveScope::new();
168 scope.on_cleanup({
169 let order = Rc::clone(&order);
170 move || order.borrow_mut().push(1)
171 });
172 scope.on_cleanup({
173 let order = Rc::clone(&order);
174 move || order.borrow_mut().push(2)
175 });
176 }
177
178 assert_eq!(*order.borrow(), vec![2, 1]);
180 }
181
182 #[test]
183 fn nested_scope_dropped_before_parent_cleanup() {
184 let order = Rc::new(RefCell::new(Vec::new()));
185
186 {
187 let mut scope = ReactiveScope::new();
188 scope.on_cleanup({
189 let order = Rc::clone(&order);
190 move || order.borrow_mut().push("parent")
191 });
192
193 let child = scope.child();
194 child.on_cleanup({
195 let order = Rc::clone(&order);
196 move || order.borrow_mut().push("child")
197 });
198 }
199
200 assert_eq!(*order.borrow(), vec!["child", "parent"]);
202 }
203
204 #[test]
205 fn effect_in_dropped_scope_does_not_fire() {
206 let sig = Signal::new(0);
207 let count = Rc::new(Cell::new(0u32));
208
209 {
210 let mut scope = ReactiveScope::new();
211 let effect = scope.create_effect({
212 let sig = sig.clone();
213 let count = Rc::clone(&count);
214 move || {
215 let _ = sig.get();
216 count.set(count.get() + 1);
217 }
218 });
219 sig.subscribe(effect.as_subscriber());
220 }
221
222 sig.set(1);
223 assert_eq!(count.get(), 1);
225 }
226
227 #[test]
228 fn create_signal_through_scope() {
229 let scope = ReactiveScope::new();
230 let sig = scope.create_signal(42);
231 assert_eq!(sig.get(), 42);
232 }
233
234 #[test]
235 fn create_computed_through_scope() {
236 let scope = ReactiveScope::new();
237 let sig = Signal::new(3);
238 let doubled = scope.create_computed({
239 let sig = sig.clone();
240 move || sig.get() * 2
241 });
242 assert_eq!(doubled.get(), 6);
243 }
244
245 #[test]
246 fn scope_counts() {
247 let mut scope = ReactiveScope::new();
248 assert_eq!(scope.effect_count(), 0);
249 assert_eq!(scope.child_count(), 0);
250
251 scope.create_effect(|| {});
252 assert_eq!(scope.effect_count(), 1);
253
254 scope.child();
255 assert_eq!(scope.child_count(), 1);
256 }
257
258 #[test]
259 fn scope_can_be_moved() {
260 let mut scope = ReactiveScope::new();
261 scope.create_effect(|| {});
262
263 let scope2 = scope;
264 assert_eq!(scope2.effect_count(), 1);
265 }
266
267 #[test]
268 fn multiple_nested_scopes() {
269 let order = Rc::new(RefCell::new(Vec::new()));
270
271 {
272 let mut scope = ReactiveScope::new();
273 scope.on_cleanup({
274 let order = Rc::clone(&order);
275 move || order.borrow_mut().push("root")
276 });
277
278 let child1 = scope.child();
279 child1.on_cleanup({
280 let order = Rc::clone(&order);
281 move || order.borrow_mut().push("child1")
282 });
283
284 let child2 = scope.child();
285 child2.on_cleanup({
286 let order = Rc::clone(&order);
287 move || order.borrow_mut().push("child2")
288 });
289 }
290
291 let v = order.borrow().clone();
293 assert_eq!(v.len(), 3);
294 assert_eq!(v[0], "child1");
296 assert_eq!(v[1], "child2");
297 assert_eq!(v[2], "root");
298 }
299
300 use std::cell::RefCell;
301}