Skip to main content

ansiq_core/
reactivity.rs

1use std::any::Any;
2use std::cell::RefCell;
3use std::collections::{BTreeSet, HashMap, VecDeque};
4use std::marker::PhantomData;
5use std::rc::Rc;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct SignalId(usize);
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct ScopeId(usize);
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14enum DependencyId {
15    Signal(SignalId),
16    Computed(ScopeId),
17}
18
19#[derive(Clone)]
20pub struct Signal<T> {
21    id: SignalId,
22    marker: PhantomData<(T, Rc<()>)>,
23}
24
25#[derive(Clone)]
26pub struct Computed<T> {
27    scope: ScopeId,
28    marker: PhantomData<(T, Rc<()>)>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct EffectHandle {
33    scope: ScopeId,
34    marker: PhantomData<Rc<()>>,
35}
36
37pub fn signal<T: Clone + 'static>(value: T) -> Signal<T> {
38    RUNTIME.with(|runtime| runtime.borrow_mut().create_signal(value))
39}
40
41pub fn computed<T, F>(compute: F) -> Computed<T>
42where
43    T: Clone + 'static,
44    F: Fn() -> T + 'static,
45{
46    RUNTIME.with(|runtime| runtime.borrow_mut().create_computed(compute))
47}
48
49pub fn effect<F>(callback: F) -> EffectHandle
50where
51    F: FnMut() + 'static,
52{
53    let scope = RUNTIME.with(|runtime| runtime.borrow_mut().create_effect(callback));
54    run_effect(scope);
55    EffectHandle {
56        scope,
57        marker: PhantomData,
58    }
59}
60
61pub fn flush_reactivity() {
62    while let Some(scope) = RUNTIME.with(|runtime| runtime.borrow_mut().next_dirty_effect()) {
63        run_effect(scope);
64    }
65}
66
67pub fn reset_reactivity_for_testing() {
68    RUNTIME.with(|runtime| *runtime.borrow_mut() = ReactiveRuntime::default());
69}
70
71pub fn render_in_component_scope<T, F>(existing: Option<ScopeId>, render: F) -> (ScopeId, T)
72where
73    F: FnOnce(ScopeId) -> T,
74{
75    let scope = RUNTIME.with(|runtime| runtime.borrow_mut().prepare_component_scope(existing));
76    let value = render(scope);
77    RUNTIME.with(|runtime| runtime.borrow_mut().finish_component_scope(scope));
78    (scope, value)
79}
80
81pub fn take_dirty_component_scopes() -> Vec<ScopeId> {
82    RUNTIME.with(|runtime| runtime.borrow_mut().take_dirty_components())
83}
84
85pub fn dispose_component_scope(scope: ScopeId) {
86    RUNTIME.with(|runtime| runtime.borrow_mut().dispose_component(scope));
87}
88
89pub(crate) fn dispose_computed_scope(scope: ScopeId) {
90    RUNTIME.with(|runtime| runtime.borrow_mut().dispose_scope(scope));
91}
92
93pub fn current_reactive_scope() -> Option<ScopeId> {
94    RUNTIME.with(|runtime| runtime.borrow().active_scopes.last().copied())
95}
96
97impl<T: Clone + 'static> Signal<T> {
98    pub fn get(&self) -> T {
99        RUNTIME.with(|runtime| runtime.borrow_mut().read_signal(self.id))
100    }
101
102    pub fn set(&self, value: T) {
103        RUNTIME.with(|runtime| runtime.borrow_mut().write_signal(self.id, value));
104    }
105
106    pub fn set_if_changed(&self, value: T)
107    where
108        T: PartialEq,
109    {
110        RUNTIME.with(|runtime| {
111            runtime.borrow_mut().write_signal_if_changed(self.id, value);
112        });
113    }
114
115    pub fn update<F>(&self, update: F)
116    where
117        F: FnOnce(&mut T),
118    {
119        RUNTIME.with(|runtime| runtime.borrow_mut().update_signal(self.id, update));
120    }
121}
122
123impl<T: Clone + 'static> Computed<T> {
124    pub fn get(&self) -> T {
125        RUNTIME.with(|runtime| {
126            runtime
127                .borrow_mut()
128                .record_dependency(DependencyId::Computed(self.scope));
129        });
130
131        let evaluator =
132            RUNTIME.with(|runtime| runtime.borrow_mut().prepare_computed_evaluation(self.scope));
133
134        if let Some(evaluator) = evaluator {
135            // Evaluate outside the runtime borrow so nested `get()` calls can
136            // register dependencies without tripping RefCell reentrancy.
137            let value = evaluator();
138            RUNTIME.with(|runtime| {
139                runtime
140                    .borrow_mut()
141                    .finish_computed_evaluation(self.scope, value);
142            });
143        }
144
145        RUNTIME.with(|runtime| runtime.borrow().read_cached_computed(self.scope))
146    }
147
148    pub(crate) fn scope_id(&self) -> ScopeId {
149        self.scope
150    }
151}
152
153impl EffectHandle {
154    pub fn stop(&self) {
155        RUNTIME.with(|runtime| runtime.borrow_mut().dispose_effect(self.scope));
156    }
157}
158
159thread_local! {
160    static RUNTIME: RefCell<ReactiveRuntime> = RefCell::new(ReactiveRuntime::default());
161}
162
163fn run_effect(scope: ScopeId) {
164    let effect = RUNTIME.with(|runtime| runtime.borrow_mut().prepare_effect_run(scope));
165    if let Some(effect) = effect {
166        // Like computeds, effects run outside the graph borrow so reads can
167        // rebuild their dependency edges during execution.
168        (effect.borrow_mut())();
169        RUNTIME.with(|runtime| runtime.borrow_mut().finish_effect_run(scope));
170    }
171}
172
173#[derive(Default)]
174struct ReactiveRuntime {
175    next_signal_id: usize,
176    next_scope_id: usize,
177    active_scopes: Vec<ScopeId>,
178    signals: HashMap<SignalId, Box<dyn Any>>,
179    signal_dependents: HashMap<SignalId, BTreeSet<ScopeId>>,
180    computed_dependents: HashMap<ScopeId, BTreeSet<ScopeId>>,
181    scope_dependencies: HashMap<ScopeId, BTreeSet<DependencyId>>,
182    scopes: HashMap<ScopeId, ScopeState>,
183    dirty_effects: VecDeque<ScopeId>,
184    dirty_components: VecDeque<ScopeId>,
185}
186
187enum ScopeState {
188    Computed(ComputedState),
189    Effect(EffectState),
190    Component(ComponentState),
191}
192
193struct ComputedState {
194    evaluator: Rc<dyn Fn() -> Box<dyn Any>>,
195    cached: Option<Box<dyn Any>>,
196    dirty: bool,
197    evaluating: bool,
198}
199
200struct EffectState {
201    effect: Rc<RefCell<Box<dyn FnMut()>>>,
202    dirty: bool,
203    queued: bool,
204    disposed: bool,
205}
206
207struct ComponentState {
208    dirty: bool,
209    queued: bool,
210}
211
212impl ReactiveRuntime {
213    fn create_signal<T: Clone + 'static>(&mut self, value: T) -> Signal<T> {
214        let id = SignalId(self.next_signal_id);
215        self.next_signal_id += 1;
216        self.signals.insert(id, Box::new(value));
217        Signal {
218            id,
219            marker: PhantomData,
220        }
221    }
222
223    fn create_computed<T, F>(&mut self, compute: F) -> Computed<T>
224    where
225        T: Clone + 'static,
226        F: Fn() -> T + 'static,
227    {
228        let scope = ScopeId(self.next_scope_id);
229        self.next_scope_id += 1;
230        let evaluator: Rc<dyn Fn() -> Box<dyn Any>> = Rc::new(move || Box::new(compute()));
231        self.scopes.insert(
232            scope,
233            ScopeState::Computed(ComputedState {
234                evaluator,
235                cached: None,
236                dirty: true,
237                evaluating: false,
238            }),
239        );
240        Computed {
241            scope,
242            marker: PhantomData,
243        }
244    }
245
246    fn create_effect<F>(&mut self, effect: F) -> ScopeId
247    where
248        F: FnMut() + 'static,
249    {
250        let scope = ScopeId(self.next_scope_id);
251        self.next_scope_id += 1;
252        self.scopes.insert(
253            scope,
254            ScopeState::Effect(EffectState {
255                effect: Rc::new(RefCell::new(Box::new(effect))),
256                dirty: true,
257                queued: false,
258                disposed: false,
259            }),
260        );
261        scope
262    }
263
264    fn prepare_component_scope(&mut self, existing: Option<ScopeId>) -> ScopeId {
265        let scope = existing.unwrap_or_else(|| {
266            let scope = ScopeId(self.next_scope_id);
267            self.next_scope_id += 1;
268            self.scopes.insert(
269                scope,
270                ScopeState::Component(ComponentState {
271                    dirty: false,
272                    queued: false,
273                }),
274            );
275            scope
276        });
277
278        self.clear_scope_dependencies(scope);
279        if let Some(ScopeState::Component(state)) = self.scopes.get_mut(&scope) {
280            state.dirty = false;
281            state.queued = false;
282        }
283        self.active_scopes.push(scope);
284        scope
285    }
286
287    fn finish_component_scope(&mut self, scope: ScopeId) {
288        let popped = self.active_scopes.pop();
289        assert_eq!(popped, Some(scope), "component scope stack out of sync");
290    }
291
292    fn read_signal<T: Clone + 'static>(&mut self, id: SignalId) -> T {
293        self.record_dependency(DependencyId::Signal(id));
294        self.signals
295            .get(&id)
296            .expect("signal should exist")
297            .downcast_ref::<T>()
298            .expect("signal type mismatch")
299            .clone()
300    }
301
302    fn write_signal<T: Clone + 'static>(&mut self, id: SignalId, value: T) {
303        let signal = self
304            .signals
305            .get_mut(&id)
306            .expect("signal should exist")
307            .downcast_mut::<T>()
308            .expect("signal type mismatch");
309        *signal = value;
310        self.propagate_dirty(DependencyId::Signal(id));
311    }
312
313    fn write_signal_if_changed<T>(&mut self, id: SignalId, value: T)
314    where
315        T: Clone + PartialEq + 'static,
316    {
317        let signal = self
318            .signals
319            .get_mut(&id)
320            .expect("signal should exist")
321            .downcast_mut::<T>()
322            .expect("signal type mismatch");
323
324        if *signal == value {
325            return;
326        }
327
328        *signal = value;
329        self.propagate_dirty(DependencyId::Signal(id));
330    }
331
332    fn update_signal<T: Clone + 'static, F>(&mut self, id: SignalId, update: F)
333    where
334        F: FnOnce(&mut T),
335    {
336        let signal = self
337            .signals
338            .get_mut(&id)
339            .expect("signal should exist")
340            .downcast_mut::<T>()
341            .expect("signal type mismatch");
342        update(signal);
343        self.propagate_dirty(DependencyId::Signal(id));
344    }
345
346    fn record_dependency(&mut self, dependency: DependencyId) {
347        let Some(&scope) = self.active_scopes.last() else {
348            return;
349        };
350
351        if matches!(dependency, DependencyId::Computed(source) if source == scope) {
352            return;
353        }
354
355        self.scope_dependencies
356            .entry(scope)
357            .or_default()
358            .insert(dependency);
359
360        match dependency {
361            DependencyId::Signal(signal) => {
362                self.signal_dependents
363                    .entry(signal)
364                    .or_default()
365                    .insert(scope);
366            }
367            DependencyId::Computed(computed) => {
368                self.computed_dependents
369                    .entry(computed)
370                    .or_default()
371                    .insert(scope);
372            }
373        }
374    }
375
376    fn prepare_computed_evaluation(
377        &mut self,
378        scope: ScopeId,
379    ) -> Option<Rc<dyn Fn() -> Box<dyn Any>>> {
380        let should_evaluate = match self.scopes.get(&scope) {
381            Some(ScopeState::Computed(state)) => state.dirty || state.cached.is_none(),
382            _ => panic!("scope should be a computed"),
383        };
384
385        if !should_evaluate {
386            return None;
387        }
388
389        let evaluating = match self.scopes.get(&scope) {
390            Some(ScopeState::Computed(state)) => state.evaluating,
391            _ => unreachable!(),
392        };
393        assert!(!evaluating, "computed cycle detected");
394
395        self.clear_scope_dependencies(scope);
396
397        let evaluator = match self.scopes.get_mut(&scope) {
398            Some(ScopeState::Computed(state)) => {
399                state.evaluating = true;
400                state.evaluator.clone()
401            }
402            _ => unreachable!(),
403        };
404
405        self.active_scopes.push(scope);
406        Some(evaluator)
407    }
408
409    fn finish_computed_evaluation(&mut self, scope: ScopeId, value: Box<dyn Any>) {
410        match self.scopes.get_mut(&scope) {
411            Some(ScopeState::Computed(state)) => {
412                state.cached = Some(value);
413                state.dirty = false;
414                state.evaluating = false;
415            }
416            _ => panic!("scope should be a computed"),
417        }
418
419        let popped = self.active_scopes.pop();
420        assert_eq!(popped, Some(scope), "computed scope stack out of sync");
421    }
422
423    fn read_cached_computed<T: Clone + 'static>(&self, scope: ScopeId) -> T {
424        match self.scopes.get(&scope) {
425            Some(ScopeState::Computed(state)) => state
426                .cached
427                .as_ref()
428                .expect("computed should have a cached value")
429                .downcast_ref::<T>()
430                .expect("computed type mismatch")
431                .clone(),
432            _ => panic!("scope should be a computed"),
433        }
434    }
435
436    fn prepare_effect_run(&mut self, scope: ScopeId) -> Option<Rc<RefCell<Box<dyn FnMut()>>>> {
437        let should_run = match self.scopes.get(&scope) {
438            Some(ScopeState::Effect(state)) => state.dirty && !state.disposed,
439            _ => false,
440        };
441
442        if !should_run {
443            return None;
444        }
445
446        self.clear_scope_dependencies(scope);
447
448        let effect = match self.scopes.get_mut(&scope) {
449            Some(ScopeState::Effect(state)) => {
450                state.dirty = false;
451                state.queued = false;
452                state.effect.clone()
453            }
454            _ => unreachable!(),
455        };
456
457        self.active_scopes.push(scope);
458        Some(effect)
459    }
460
461    fn finish_effect_run(&mut self, scope: ScopeId) {
462        let popped = self.active_scopes.pop();
463        assert_eq!(popped, Some(scope), "effect scope stack out of sync");
464    }
465
466    fn next_dirty_effect(&mut self) -> Option<ScopeId> {
467        while let Some(scope) = self.dirty_effects.pop_front() {
468            let ready = match self.scopes.get_mut(&scope) {
469                Some(ScopeState::Effect(state)) => {
470                    state.queued = false;
471                    state.dirty && !state.disposed
472                }
473                _ => false,
474            };
475            if ready {
476                return Some(scope);
477            }
478        }
479        None
480    }
481
482    fn dispose_effect(&mut self, scope: ScopeId) {
483        self.clear_scope_dependencies(scope);
484        if let Some(ScopeState::Effect(state)) = self.scopes.get_mut(&scope) {
485            state.disposed = true;
486            state.dirty = false;
487            state.queued = false;
488        }
489    }
490
491    fn dispose_component(&mut self, scope: ScopeId) {
492        if matches!(self.scopes.get(&scope), Some(ScopeState::Component(_))) {
493            self.dispose_scope(scope);
494        }
495    }
496
497    fn dispose_scope(&mut self, scope: ScopeId) {
498        self.clear_scope_dependencies(scope);
499        self.computed_dependents.remove(&scope);
500        self.scope_dependencies.remove(&scope);
501        self.scopes.remove(&scope);
502    }
503
504    fn clear_scope_dependencies(&mut self, scope: ScopeId) {
505        let Some(dependencies) = self.scope_dependencies.remove(&scope) else {
506            return;
507        };
508
509        for dependency in dependencies {
510            match dependency {
511                DependencyId::Signal(signal) => {
512                    if let Some(dependents) = self.signal_dependents.get_mut(&signal) {
513                        dependents.remove(&scope);
514                    }
515                }
516                DependencyId::Computed(computed) => {
517                    if let Some(dependents) = self.computed_dependents.get_mut(&computed) {
518                        dependents.remove(&scope);
519                    }
520                }
521            }
522        }
523    }
524
525    fn take_dirty_components(&mut self) -> Vec<ScopeId> {
526        let mut dirty = Vec::new();
527        while let Some(scope) = self.dirty_components.pop_front() {
528            let ready = match self.scopes.get_mut(&scope) {
529                Some(ScopeState::Component(state)) => {
530                    state.queued = false;
531                    if state.dirty {
532                        state.dirty = false;
533                        true
534                    } else {
535                        false
536                    }
537                }
538                _ => false,
539            };
540
541            if ready {
542                dirty.push(scope);
543            }
544        }
545        dirty
546    }
547
548    fn propagate_dirty(&mut self, dependency: DependencyId) {
549        let mut queue = VecDeque::from([dependency]);
550        let mut visited_scopes = BTreeSet::new();
551
552        while let Some(current) = queue.pop_front() {
553            // Dirty state propagates through the dependency graph: signals
554            // invalidate computeds, and invalid computeds wake downstream
555            // computeds or effects without eagerly recomputing values.
556            let dependents = match current {
557                DependencyId::Signal(signal) => self
558                    .signal_dependents
559                    .get(&signal)
560                    .cloned()
561                    .unwrap_or_default(),
562                DependencyId::Computed(computed) => self
563                    .computed_dependents
564                    .get(&computed)
565                    .cloned()
566                    .unwrap_or_default(),
567            };
568
569            for scope in dependents {
570                if !visited_scopes.insert(scope) {
571                    continue;
572                }
573
574                match self.scopes.get_mut(&scope) {
575                    Some(ScopeState::Computed(state)) => {
576                        state.dirty = true;
577                        queue.push_back(DependencyId::Computed(scope));
578                    }
579                    Some(ScopeState::Effect(state)) => {
580                        if state.disposed {
581                            continue;
582                        }
583                        state.dirty = true;
584                        if !state.queued {
585                            state.queued = true;
586                            self.dirty_effects.push_back(scope);
587                        }
588                    }
589                    Some(ScopeState::Component(state)) => {
590                        state.dirty = true;
591                        if !state.queued {
592                            state.queued = true;
593                            self.dirty_components.push_back(scope);
594                        }
595                    }
596                    None => {}
597                }
598            }
599        }
600    }
601}