Skip to main content

incular_core/
reactivity.rs

1//! Scheduler-independent dependency sources and owned subscriptions.
2//!
3//! Hosts supply stable metadata and invalidation callbacks. Core never knows
4//! about windows, elements, phases or executors. Dropping a subscription removes
5//! its edge immediately, including during notification.
6use std::{
7    any::Any,
8    cell::{Cell, Ref, RefCell, RefMut},
9    collections::BTreeMap,
10    marker::PhantomData,
11    rc::{Rc, Weak},
12    sync::atomic::{AtomicU64, Ordering},
13};
14
15static NEXT_ID: AtomicU64 = AtomicU64::new(1);
16fn next_id() -> u64 {
17    NEXT_ID
18        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
19        .expect("reactive identity exhausted")
20}
21
22struct Observer {
23    active: Cell<bool>,
24    metadata: Box<dyn Any>,
25    invalidate: Box<dyn Fn()>,
26}
27struct SourceInner {
28    id: u64,
29    observers: RefCell<BTreeMap<u64, Weak<Observer>>>,
30}
31
32/// A dependency identity with no scheduling policy of its own.
33#[derive(Clone)]
34pub struct DependencySource(Rc<SourceInner>);
35impl Default for DependencySource {
36    fn default() -> Self {
37        Self(Rc::new(SourceInner {
38            id: next_id(),
39            observers: RefCell::new(BTreeMap::new()),
40        }))
41    }
42}
43impl DependencySource {
44    #[must_use]
45    pub fn id(&self) -> u64 {
46        self.0.id
47    }
48
49    /// The returned token owns the edge. Metadata is diagnostic identity only.
50    pub fn subscribe(&self, metadata: impl Any, invalidate: impl Fn() + 'static) -> Subscription {
51        let id = next_id();
52        let observer = Rc::new(Observer {
53            active: Cell::new(true),
54            metadata: Box::new(metadata),
55            invalidate: Box::new(invalidate),
56        });
57        self.0
58            .observers
59            .borrow_mut()
60            .insert(id, Rc::downgrade(&observer));
61        Subscription {
62            id,
63            source: Rc::downgrade(&self.0),
64            observer,
65        }
66    }
67
68    /// Calls live observers outside registry borrows. New observers wait for
69    /// the next notification; observers removed during dispatch are skipped.
70    pub fn notify(&self) {
71        let observers: Vec<_> = self
72            .0
73            .observers
74            .borrow()
75            .values()
76            .filter_map(Weak::upgrade)
77            .collect();
78        for observer in observers {
79            if observer.active.get() {
80                (observer.invalidate)();
81            }
82        }
83    }
84
85    pub fn subscriber_count(&self) -> usize {
86        self.0.observers.borrow().len()
87    }
88
89    /// Returns only live subscriber metadata of the requested host-owned type.
90    pub fn subscribers<T: Any + Clone>(&self) -> Vec<T> {
91        let observers: Vec<_> = self
92            .0
93            .observers
94            .borrow()
95            .values()
96            .filter_map(Weak::upgrade)
97            .collect();
98        observers
99            .into_iter()
100            .filter(|observer| observer.active.get())
101            .filter_map(|observer| observer.metadata.downcast_ref::<T>().cloned())
102            .collect()
103    }
104
105    /// Records this source in the nearest active collection, if any.
106    pub fn track(&self) -> bool {
107        let callback = TRACKING.with(|stack| stack.borrow().last().cloned());
108        if let Some(callback) = callback {
109            callback(self.clone());
110            true
111        } else {
112            false
113        }
114    }
115}
116
117/// An owned invalidation edge. It must live as long as its consumer.
118#[must_use = "keep the subscription alive to observe changes"]
119pub struct Subscription {
120    id: u64,
121    source: Weak<SourceInner>,
122    observer: Rc<Observer>,
123}
124impl Drop for Subscription {
125    fn drop(&mut self) {
126        self.observer.active.set(false);
127        if let Some(source) = self.source.upgrade() {
128            source.observers.borrow_mut().remove(&self.id);
129        }
130    }
131}
132
133type Collector = Rc<dyn Fn(DependencySource)>;
134thread_local! { static TRACKING: RefCell<Vec<Collector>> = const { RefCell::new(Vec::new()) }; }
135thread_local! { static READ_ONLY: Cell<usize> = const { Cell::new(0) }; }
136
137/// Forbids reactive writes while a pure builder or memo is evaluating.
138pub struct ReadOnlyGuard(PhantomData<Rc<()>>);
139impl ReadOnlyGuard {
140    #[must_use]
141    pub fn enter() -> Self {
142        READ_ONLY.with(|depth| depth.set(depth.get() + 1));
143        Self(PhantomData)
144    }
145}
146impl Drop for ReadOnlyGuard {
147    fn drop(&mut self) {
148        READ_ONLY.with(|depth| depth.set(depth.get() - 1));
149    }
150}
151/// Panics before modifying state when a pure evaluation is active.
152pub fn assert_mutation_allowed() {
153    assert!(
154        READ_ONLY.with(Cell::get) == 0,
155        "reactive mutation during a pure build or memo evaluation"
156    );
157}
158
159/// Restores the previous collector on normal return or unwinding.
160pub struct TrackingGuard(PhantomData<Rc<()>>);
161impl TrackingGuard {
162    #[must_use]
163    pub fn enter(collect: impl Fn(DependencySource) + 'static) -> Self {
164        TRACKING.with(|stack| stack.borrow_mut().push(Rc::new(collect)));
165        Self(PhantomData)
166    }
167}
168impl Drop for TrackingGuard {
169    fn drop(&mut self) {
170        TRACKING.with(|stack| {
171            stack.borrow_mut().pop();
172        });
173    }
174}
175
176/// Storage for a reactive domain value. The domain API decides equality and
177/// validation and calls `source().notify()` after releasing mutable borrows.
178/// This is backend infrastructure, not another application signal API.
179pub struct ReactiveCell<T> {
180    value: RefCell<T>,
181    source: DependencySource,
182}
183impl<T> ReactiveCell<T> {
184    pub fn new(value: T) -> Self {
185        Self {
186            value: RefCell::new(value),
187            source: DependencySource::default(),
188        }
189    }
190    pub fn borrow(&self) -> Ref<'_, T> {
191        self.value.borrow()
192    }
193    pub fn borrow_mut(&self) -> RefMut<'_, T> {
194        self.value.borrow_mut()
195    }
196    pub fn source(&self) -> &DependencySource {
197        &self.source
198    }
199}