saorsa_core/reactive/
effect.rs1use std::cell::{Cell, RefCell};
9use std::rc::Rc;
10
11use super::context::{self, SubscriberId};
12use super::signal::Subscriber;
13
14pub struct Effect(Rc<EffectInner>);
37
38struct EffectInner {
39 effect_fn: RefCell<Box<dyn FnMut()>>,
41 sub_id: SubscriberId,
43 active: Cell<bool>,
45}
46
47impl Effect {
48 #[must_use]
54 pub fn new(f: impl FnMut() + 'static) -> Self {
55 let sub_id = context::next_subscriber_id();
56
57 let inner = Rc::new(EffectInner {
58 effect_fn: RefCell::new(Box::new(f)),
59 sub_id,
60 active: Cell::new(true),
61 });
62
63 inner.run();
65
66 Effect(inner)
67 }
68
69 pub fn is_active(&self) -> bool {
71 self.0.active.get()
72 }
73
74 pub fn dispose(&self) {
79 self.0.active.set(false);
80 }
81
82 pub fn subscriber_id(&self) -> SubscriberId {
84 self.0.sub_id
85 }
86
87 pub fn as_subscriber(&self) -> std::rc::Weak<dyn Subscriber> {
89 Rc::downgrade(&self.0) as std::rc::Weak<dyn Subscriber>
90 }
91}
92
93impl Clone for Effect {
94 fn clone(&self) -> Self {
95 Effect(Rc::clone(&self.0))
96 }
97}
98
99impl EffectInner {
100 fn run(&self) {
102 if !self.active.get() {
103 return;
104 }
105
106 context::start_tracking(self.sub_id);
107
108 {
109 let mut f = self.effect_fn.borrow_mut();
110 f();
111 }
112
113 let _deps = context::stop_tracking();
114 }
115}
116
117impl Subscriber for EffectInner {
118 fn notify(&self) {
119 self.run();
120 }
121
122 fn id(&self) -> SubscriberId {
123 self.sub_id
124 }
125}
126
127#[cfg(test)]
128#[allow(clippy::unwrap_used)]
129mod tests {
130 use super::*;
131 use crate::reactive::signal::Signal;
132 use std::cell::Cell;
133
134 #[test]
135 fn effect_runs_immediately() {
136 let ran = Rc::new(Cell::new(false));
137 let _effect = Effect::new({
138 let ran = Rc::clone(&ran);
139 move || {
140 ran.set(true);
141 }
142 });
143 assert!(ran.get());
144 }
145
146 #[test]
147 fn effect_reruns_on_signal_change() {
148 let sig = Signal::new(0);
149 let log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
150
151 let effect = Effect::new({
152 let sig = sig.clone();
153 let log = Rc::clone(&log);
154 move || {
155 log.borrow_mut().push(sig.get());
156 }
157 });
158
159 sig.subscribe(effect.as_subscriber());
161
162 sig.set(1);
163 sig.set(2);
164
165 assert_eq!(*log.borrow(), vec![0, 1, 2]);
166 }
167
168 #[test]
169 fn effect_tracks_multiple_signals() {
170 let a = Signal::new(1);
171 let b = Signal::new(10);
172 let sum_log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
173
174 let effect = Effect::new({
175 let a = a.clone();
176 let b = b.clone();
177 let log = Rc::clone(&sum_log);
178 move || {
179 log.borrow_mut().push(a.get() + b.get());
180 }
181 });
182
183 a.subscribe(effect.as_subscriber());
184 b.subscribe(effect.as_subscriber());
185
186 a.set(2);
187 b.set(20);
188
189 assert_eq!(*sum_log.borrow(), vec![11, 12, 22]);
191 }
192
193 #[test]
194 fn disposed_effect_does_not_run() {
195 let sig = Signal::new(0);
196 let count = Rc::new(Cell::new(0u32));
197
198 let effect = Effect::new({
199 let sig = sig.clone();
200 let count = Rc::clone(&count);
201 move || {
202 let _ = sig.get();
203 count.set(count.get() + 1);
204 }
205 });
206
207 sig.subscribe(effect.as_subscriber());
208
209 assert_eq!(count.get(), 1); effect.dispose();
212 assert!(!effect.is_active());
213
214 sig.set(1);
215 assert_eq!(count.get(), 1); }
217
218 #[test]
219 fn effect_with_no_deps_runs_once() {
220 let count = Rc::new(Cell::new(0u32));
221 let _effect = Effect::new({
222 let count = Rc::clone(&count);
223 move || {
224 count.set(count.get() + 1);
225 }
226 });
227 assert_eq!(count.get(), 1);
228 }
229
230 #[test]
231 fn effect_reads_computed() {
232 use crate::reactive::Computed;
233
234 let sig = Signal::new(5);
235 let doubled = Computed::new({
236 let sig = sig.clone();
237 move || sig.get() * 2
238 });
239
240 sig.subscribe(doubled.as_subscriber());
241
242 let log: Rc<RefCell<Vec<i32>>> = Rc::new(RefCell::new(Vec::new()));
243 let effect = Effect::new({
244 let doubled = doubled.clone();
245 let log = Rc::clone(&log);
246 move || {
247 log.borrow_mut().push(doubled.get());
248 }
249 });
250
251 doubled.subscribe(effect.as_subscriber());
253
254 assert_eq!(*log.borrow(), vec![10]);
255
256 sig.set(7);
257 assert_eq!(*log.borrow(), vec![10, 14]);
258 }
259
260 #[test]
261 fn multiple_effects_on_same_signal() {
262 let sig = Signal::new(0);
263 let count_a = Rc::new(Cell::new(0u32));
264 let count_b = Rc::new(Cell::new(0u32));
265
266 let effect_a = Effect::new({
267 let sig = sig.clone();
268 let count = Rc::clone(&count_a);
269 move || {
270 let _ = sig.get();
271 count.set(count.get() + 1);
272 }
273 });
274
275 let effect_b = Effect::new({
276 let sig = sig.clone();
277 let count = Rc::clone(&count_b);
278 move || {
279 let _ = sig.get();
280 count.set(count.get() + 1);
281 }
282 });
283
284 sig.subscribe(effect_a.as_subscriber());
285 sig.subscribe(effect_b.as_subscriber());
286
287 sig.set(1);
288 assert_eq!(count_a.get(), 2); assert_eq!(count_b.get(), 2);
290 }
291
292 #[test]
293 fn effect_is_active_initially() {
294 let effect = Effect::new(|| {});
295 assert!(effect.is_active());
296 }
297
298 #[test]
299 fn effect_clone_shares_state() {
300 let effect = Effect::new(|| {});
301 let clone = effect.clone();
302 assert_eq!(effect.subscriber_id(), clone.subscriber_id());
303 effect.dispose();
304 assert!(!clone.is_active());
305 }
306}