Skip to main content

fusor/
reactive.rs

1use std::{
2    cell::{Cell, RefCell},
3    collections::VecDeque,
4    rc::{Rc, Weak},
5};
6
7mod graph;
8mod memo;
9pub mod versions;
10use graph::{Observer, ObserverKind, Source, track};
11pub use memo::{Memo, memo, memo_with_eq};
12
13thread_local! {
14    static CURRENT: RefCell<Option<Weak<Observer>>> = const { RefCell::new(None) };
15    static QUEUE: RefCell<VecDeque<Weak<EffectInner>>> = const { RefCell::new(VecDeque::new()) };
16    static BATCH_DEPTH: Cell<usize> = const { Cell::new(0) };
17    static FLUSHING: Cell<bool> = const { Cell::new(false) };
18    static NEXT_ID: Cell<u64> = const { Cell::new(0) };
19    static NEXT_FLUSH: Cell<u64> = const { Cell::new(0) };
20    static NEXT_WAVE: Cell<u64> = const { Cell::new(0) };
21    static COMPUTING: Cell<usize> = const { Cell::new(0) };
22    static RENDERING: Cell<bool> = const { Cell::new(false) };
23    #[cfg(feature = "javascript")]
24    static AFTER_FLUSH: RefCell<VecDeque<Box<dyn FnOnce()>>> = const { RefCell::new(VecDeque::new()) };
25}
26
27struct RenderGuard(bool);
28
29impl RenderGuard {
30    fn replace(rendering: bool) -> Self {
31        Self(RENDERING.with(|current| current.replace(rendering)))
32    }
33}
34
35impl Drop for RenderGuard {
36    fn drop(&mut self) {
37        RENDERING.with(|current| current.set(self.0));
38    }
39}
40
41fn assert_not_computing() {
42    assert_eq!(
43        COMPUTING.with(Cell::get),
44        0,
45        "memo computations and equality functions must not write signals or create effects"
46    );
47}
48
49struct SignalInner<T> {
50    value: RefCell<T>,
51    source: Rc<Source>,
52    render_values: RefCell<Vec<(Rc<T>, versions::Versions)>>,
53}
54
55/// Shared, single-threaded reactive state. Cloning shares the same value.
56pub struct Signal<T>(Rc<SignalInner<T>>);
57
58impl<T> Clone for Signal<T> {
59    fn clone(&self) -> Self {
60        Self(self.0.clone())
61    }
62}
63
64/// Create reactive state; reads inside an effect automatically subscribe to it.
65pub fn signal<T>(value: T) -> Signal<T> {
66    Signal(Rc::new(SignalInner {
67        value: RefCell::new(value),
68        source: Rc::new(Source::new(None)),
69        render_values: RefCell::new(Vec::new()),
70    }))
71}
72
73impl<T> Signal<T> {
74    /// Read without cloning the value, tracking this dependency.
75    /// Do not write this signal while the read closure holds its borrow.
76    pub fn with<R>(&self, read: impl FnOnce(&T) -> R) -> R {
77        if let Some((value, inputs)) = self.render_value() {
78            versions::Versions::exclude(|| track(&self.0.source));
79            inputs.include();
80            return read(&value);
81        }
82        track(&self.0.source);
83        read(&self.0.value.borrow())
84    }
85
86    /// Read without subscribing the current effect.
87    pub fn with_untracked<R>(&self, read: impl FnOnce(&T) -> R) -> R {
88        if let Some((value, _)) = self.render_value() {
89            return read(&value);
90        }
91        read(&self.0.value.borrow())
92    }
93
94    /// A generated keyed row's candidate input. The override is synchronous,
95    /// never published to observers, and validates against the collection's
96    /// actual source versions. This is not historical storage for signals.
97    ///
98    /// [`Memo`] reads recompute against candidate inputs without changing their
99    /// committed cache or subscriptions. The consuming evaluation tracks those
100    /// inputs directly. Nested overrides restore the preceding value, including
101    /// during unwinding. Do not mutate signals in `render`.
102    /// Part of the contract versioned by [`crate::coherence::VERSION`].
103    #[doc(hidden)]
104    pub fn with_render_value<R>(
105        &self,
106        value: Rc<T>,
107        inputs: versions::Versions,
108        render: impl FnOnce() -> R,
109    ) -> R {
110        struct Pop<'a, T>(&'a Signal<T>);
111        impl<T> Drop for Pop<'_, T> {
112            fn drop(&mut self) {
113                let value = self.0.0.render_values.borrow_mut().pop();
114                drop(value);
115            }
116        }
117        self.0.render_values.borrow_mut().push((value, inputs));
118        let _rendering = RenderGuard::replace(true);
119        let _pop = Pop(self);
120        render()
121    }
122
123    /// Mutate state, then notify subscribers after releasing the mutable borrow.
124    /// Always notifies; use `set` to skip unchanged values.
125    /// The mutation closure must not read or write this same signal.
126    pub fn update<R>(&self, update: impl FnOnce(&mut T) -> R) -> R {
127        assert_not_computing();
128        crate::coherence::mutation("signal write");
129        let result = update(&mut self.0.value.borrow_mut());
130        notify(&self.0.source);
131        result
132    }
133
134    /// Replace the value and notify subscribers, returning the previous value.
135    /// Unlike `set`, this needs no `PartialEq` and always notifies. Dropping
136    /// the result retires the old value after notification.
137    pub fn replace(&self, value: T) -> T {
138        self.update(|current| std::mem::replace(current, value))
139    }
140
141    fn render_value(&self) -> Option<(Rc<T>, versions::Versions)> {
142        RENDERING
143            .with(Cell::get)
144            .then(|| self.0.render_values.borrow().last().cloned())
145            .flatten()
146    }
147}
148
149impl<T: Clone> Signal<T> {
150    /// Clone the current value and automatically track the read.
151    pub fn get(&self) -> T {
152        self.with(Clone::clone)
153    }
154
155    /// Clone the current value without dependency tracking.
156    pub fn get_untracked(&self) -> T {
157        self.with_untracked(Clone::clone)
158    }
159}
160
161impl<T: PartialEq> Signal<T> {
162    /// Replace the value, notifying only when it actually changes.
163    /// Retire the old value after notification, outside the internal borrow.
164    /// Inside a batch, notification queues observers until the batch completes.
165    pub fn set(&self, value: T) {
166        assert_not_computing();
167        crate::coherence::mutation("signal write");
168        let retired = {
169            let mut current = self.0.value.borrow_mut();
170            if *current == value {
171                None
172            } else {
173                Some(std::mem::replace(&mut *current, value))
174            }
175        };
176        if retired.is_some() {
177            notify(&self.0.source);
178        }
179        drop(retired);
180    }
181}
182
183/// A lazy computation. Reads track its underlying signals in the consuming effect.
184/// It is deliberately not a cache: each `get` evaluates the function once.
185pub struct Derived<T>(Rc<dyn Fn() -> T>);
186
187impl<T> Clone for Derived<T> {
188    fn clone(&self) -> Self {
189        Self(self.0.clone())
190    }
191}
192
193pub fn derived<T>(compute: impl Fn() -> T + 'static) -> Derived<T> {
194    Derived(Rc::new(compute))
195}
196
197impl<T> Derived<T> {
198    pub fn get(&self) -> T {
199        (self.0)()
200    }
201}
202
203struct EffectInner {
204    observer: Rc<Observer>,
205    active: Cell<bool>,
206    queued: Cell<bool>,
207    last_flush: Cell<u64>,
208    flush_runs: Cell<u32>,
209    callback: RefCell<Box<dyn FnMut()>>,
210    lifecycle: RefCell<Vec<crate::Registration>>,
211}
212
213impl EffectInner {
214    fn unsubscribe(&self) {
215        self.observer.unsubscribe();
216    }
217
218    fn run(self: &Rc<Self>, initial: bool) {
219        if !self.active.get() {
220            return;
221        }
222        // Invalidated memos are checked lazily before deciding whether the
223        // effect's observed values actually changed.
224        if !initial && !untrack(|| self.observer.changed()) {
225            return;
226        }
227        // Recollect on every run, so conditional reads shed stale dependencies.
228        let _run = self.observer.begin();
229        let _tracking = TrackingGuard::replace(Some(Rc::downgrade(&self.observer)));
230        (self.callback.borrow_mut())();
231    }
232}
233
234/// Owns a subscription. Dropping it detaches every dependency.
235#[must_use = "retain the effect handle for as long as the subscription should live"]
236pub struct Effect(Rc<EffectInner>);
237
238impl Effect {
239    #[cfg(feature = "dom")]
240    pub(crate) fn initializer(&self) -> impl FnOnce() + 'static {
241        let weak = Rc::downgrade(&self.0);
242        move || {
243            if let Some(inner) = weak.upgrade() {
244                batch(|| inner.run(true));
245            }
246        }
247    }
248
249    /// Stop reacting. Safe to call more than once.
250    pub fn dispose(&self) {
251        self.0.active.set(false);
252        self.0.unsubscribe();
253    }
254}
255
256impl Drop for Effect {
257    fn drop(&mut self) {
258        self.dispose();
259    }
260}
261
262/// Run immediately, then rerun when any signal read by the callback changes.
263/// Effects are synchronous outside a batch; queued reruns are deduplicated.
264fn allocate_effect(callback: impl FnMut() + 'static) -> Effect {
265    assert_not_computing();
266    Effect(Rc::new_cyclic(|weak| EffectInner {
267        observer: Observer::new(ObserverKind::Effect(weak.clone())),
268        active: Cell::new(true),
269        queued: Cell::new(false),
270        last_flush: Cell::new(0),
271        flush_runs: Cell::new(0),
272        callback: RefCell::new(Box::new(callback)),
273        lifecycle: RefCell::new(Vec::new()),
274    }))
275}
276
277#[cfg(feature = "dom")]
278pub(crate) fn prepared_effect(callback: impl FnMut() + 'static) -> Effect {
279    allocate_effect(callback)
280}
281
282pub fn effect(callback: impl FnMut() + 'static) -> Effect {
283    let subscription = allocate_effect(callback);
284    // Initial effects still run immediately inside a batch. Their writes flush
285    // after the initial callback returns, avoiding recursive RefCell borrows.
286    if let Some(owner) = crate::coherence::preparing_owner().filter(|owner| !owner.is_active()) {
287        let weak = Rc::downgrade(&subscription.0);
288        let activation = owner.on_activate(move || {
289            if let Some(inner) = weak.upgrade() {
290                batch(|| inner.run(true));
291            }
292        });
293        let weak = Rc::downgrade(&subscription.0);
294        let cleanup = owner.on_cleanup(move || {
295            if let Some(inner) = weak.upgrade() {
296                inner.active.set(false);
297                inner.unsubscribe();
298            }
299        });
300        subscription
301            .0
302            .lifecycle
303            .borrow_mut()
304            .extend([activation, cleanup]);
305    } else if !crate::coherence::mutation("effect creation") {
306        batch(|| subscription.0.run(true));
307    }
308    subscription
309}
310
311fn notify(source: &Source) {
312    source.advance();
313    // Invalidate the entire reachable graph before executing any effects.
314    source.notify();
315    flush();
316}
317
318fn clear_queue() {
319    #[cfg(feature = "javascript")]
320    AFTER_FLUSH.with(|queue| queue.borrow_mut().clear());
321    QUEUE.with(|queue| {
322        for pending in queue
323            .borrow_mut()
324            .drain(..)
325            .filter_map(|item| item.upgrade())
326        {
327            pending.queued.set(false);
328        }
329    });
330}
331
332struct FlushGuard;
333
334impl Drop for FlushGuard {
335    fn drop(&mut self) {
336        FLUSHING.with(|flushing| flushing.set(false));
337        if std::thread::panicking() {
338            clear_queue();
339        }
340    }
341}
342
343fn flush() {
344    if BATCH_DEPTH.with(Cell::get) > 0 || FLUSHING.with(|flushing| flushing.replace(true)) {
345        return;
346    }
347    let _guard = FlushGuard;
348    let epoch = NEXT_FLUSH.with(|next| {
349        let epoch = next
350            .get()
351            .checked_add(1)
352            .expect("reactive flush ID exhausted");
353        next.set(epoch);
354        epoch
355    });
356    loop {
357        let next = QUEUE.with(|queue| queue.borrow_mut().pop_front());
358        let Some(next) = next else {
359            #[cfg(feature = "javascript")]
360            {
361                let callback = AFTER_FLUSH.with(|queue| queue.borrow_mut().pop_front());
362                if let Some(callback) = callback {
363                    untrack(callback);
364                    continue;
365                }
366            }
367            break;
368        };
369        if let Some(next) = next.upgrade() {
370            next.queued.set(false);
371            // Width and acyclic propagation depth are not feedback loops.
372            // Limit repeated execution of each effect, without a per-flush map.
373            let runs = if next.last_flush.replace(epoch) == epoch {
374                next.flush_runs.get() + 1
375            } else {
376                1
377            };
378            next.flush_runs.set(runs);
379            assert!(
380                runs <= 10_000,
381                "reactive cycle: one effect exceeded 10,000 runs in one flush"
382            );
383            next.run(false);
384        }
385    }
386}
387
388/// Schedule foreign notifications after ordinary reactive work has settled.
389/// The queue never lends a borrow across callbacks; their writes join the next
390/// wave of the current flush rather than recursively running an observer.
391#[cfg(feature = "javascript")]
392pub(crate) fn after_flush(callback: impl FnOnce() + 'static) {
393    AFTER_FLUSH.with(|queue| queue.borrow_mut().push_back(Box::new(callback)));
394}
395
396struct TrackingGuard(Option<Weak<Observer>>);
397
398impl TrackingGuard {
399    fn replace(next: Option<Weak<Observer>>) -> Self {
400        Self(CURRENT.with(|current| current.replace(next)))
401    }
402}
403
404impl Drop for TrackingGuard {
405    fn drop(&mut self) {
406        CURRENT.with(|current| current.replace(self.0.take()));
407    }
408}
409
410/// Evaluate a closure without collecting dependencies in the current effect.
411pub fn untrack<R>(read: impl FnOnce() -> R) -> R {
412    let _guard = TrackingGuard::replace(None);
413    read()
414}
415
416struct BatchGuard;
417
418impl Drop for BatchGuard {
419    fn drop(&mut self) {
420        BATCH_DEPTH.with(|depth| depth.set(depth.get() - 1));
421        if std::thread::panicking() {
422            clear_queue();
423        } else {
424            flush();
425        }
426    }
427}
428
429/// Group synchronous writes into one notification per affected effect.
430/// Nested batches flush at the outer boundary. This is not a rollback transaction.
431pub fn batch<R>(update: impl FnOnce() -> R) -> R {
432    BATCH_DEPTH.with(|depth| depth.set(depth.get() + 1));
433    let _guard = BatchGuard;
434    update()
435}