//! The reactive graph: a thread-local arena of nodes (signals, memos, //! effects) with auto-tracked dependencies and mark-dirty / //! evaluate-on-demand invalidation. //! //! Invalidation uses the three-state algorithm (Clean / Check / Dirty): //! writing a signal marks direct subscribers Dirty and transitive ones Check; //! when an effect or memo is pulled, a Check node first asks its sources to //! update and only recomputes if one of them actually changed value. This //! gives glitch-freedom on diamond dependencies without running anything //! twice, and equality gating stops propagation where recomputed values are //! unchanged. //! //! User closures (memo bodies, effect bodies) are moved out of the arena //! while they run, so they can freely re-enter the runtime (read signals, //! create nodes, set signals). use std::any::Any; use std::cell::RefCell; use slotmap::{SlotMap, new_key_type}; new_key_type! { pub(crate) struct NodeKey; pub(crate) struct ScopeKey; } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] enum State { Clean, /// Possibly stale: a transitive source changed; sources must be checked /// before deciding whether to recompute. Check, /// Definitely stale: a direct source changed value. Dirty, } #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub(crate) enum Kind { Signal, Memo, Effect, } /// Body of a memo or effect. Memos compute their new value, write it into /// the slot (`None` on the very first run), and report whether it changed. /// Effects ignore the slot and the return value is meaningless. pub(crate) type RunFn = Box>) -> bool>; struct Node { kind: Kind, state: State, /// Present for signals and memos. value: Option>, /// Present for memos and effects; `None` while the body is executing. run: Option, sources: Vec, subscribers: Vec, /// Effects only: already in the flush queue. queued: bool, scope: ScopeKey, } #[derive(Default)] struct Scope { nodes: Vec, children: Vec, parent: Option, } pub(crate) struct Runtime { nodes: SlotMap, scopes: SlotMap, root_scope: ScopeKey, current_scope: ScopeKey, /// The node currently executing, for dependency auto-tracking. observer: Option, /// Effects waiting for the next flush. queue: Vec, flushing: bool, } thread_local! { static RUNTIME: RefCell = RefCell::new(Runtime::new()); } /// Rounds of effect execution per flush before assuming effects are feeding /// each other in a cycle. const MAX_FLUSH_ROUNDS: usize = 1000; impl Runtime { fn new() -> Self { let mut scopes = SlotMap::with_key(); let root_scope = scopes.insert(Scope::default()); Self { nodes: SlotMap::with_key(), scopes, root_scope, current_scope: root_scope, observer: None, queue: Vec::new(), flushing: false, } } fn insert_node( &mut self, kind: Kind, value: Option>, run: Option, ) -> NodeKey { let scope = self.current_scope; let key = self.nodes.insert(Node { kind, state: State::Clean, value, run, sources: Vec::new(), subscribers: Vec::new(), queued: false, scope, }); self.scopes[scope].nodes.push(key); key } /// Record that the running observer depends on `key`. fn track(&mut self, key: NodeKey) { let Some(observer) = self.observer else { return; }; if observer == key { return; } let Some(node) = self.nodes.get_mut(key) else { return; }; if !node.subscribers.contains(&observer) { node.subscribers.push(observer); if let Some(obs) = self.nodes.get_mut(observer) { obs.sources.push(key); } } } /// Mark `key` at least `level` stale, propagating Check to transitive /// subscribers and queueing any effects encountered. fn stale(&mut self, key: NodeKey, level: State) { let Some(node) = self.nodes.get_mut(key) else { return; }; if node.state >= level { return; } let was_clean = node.state == State::Clean; node.state = level; if node.kind == Kind::Effect && !node.queued { node.queued = true; self.queue.push(key); } if was_clean { let subscribers = node.subscribers.clone(); for sub in subscribers { self.stale(sub, State::Check); } } } /// Detach `key` from everything it reads, ahead of a re-run. fn clear_sources(&mut self, key: NodeKey) { let sources = match self.nodes.get_mut(key) { Some(node) => std::mem::take(&mut node.sources), None => return, }; for source in sources { if let Some(node) = self.nodes.get_mut(source) { node.subscribers.retain(|s| *s != key); } } } fn remove_node(&mut self, key: NodeKey) { self.clear_sources(key); let Some(node) = self.nodes.remove(key) else { return; }; // Dependents lose this source but stay functional; their next re-run // simply won't re-establish the edge. for sub in node.subscribers { if let Some(sub_node) = self.nodes.get_mut(sub) { sub_node.sources.retain(|s| *s != key); } } } fn dispose_scope(&mut self, scope: ScopeKey) { let Some(data) = self.scopes.remove(scope) else { return; }; for child in data.children { self.dispose_scope(child); } for node in data.nodes { self.remove_node(node); } if let Some(parent) = data.parent && let Some(parent_scope) = self.scopes.get_mut(parent) { parent_scope.children.retain(|c| *c != scope); } if self.current_scope == scope { self.current_scope = self.root_scope; } } } fn with_runtime(f: impl FnOnce(&mut Runtime) -> R) -> R { RUNTIME.with(|rt| f(&mut rt.borrow_mut())) } /// Bring `key` up to date, recomputing it (and any stale sources) as needed. /// May run user code; never holds the runtime borrow across it. fn update_if_necessary(key: NodeKey) { let state = match with_runtime(|rt| rt.nodes.get(key).map(|n| n.state)) { Some(state) => state, None => return, }; if state == State::Check { let sources = with_runtime(|rt| { rt.nodes .get(key) .map(|n| n.sources.clone()) .unwrap_or_default() }); for source in sources { update_if_necessary(source); // A recomputed source that changed value has promoted us to // Dirty; no need to check the rest. let now = with_runtime(|rt| rt.nodes.get(key).map(|n| n.state)); match now { Some(State::Dirty) => break, Some(_) => {} None => return, } } } let state = match with_runtime(|rt| rt.nodes.get(key).map(|n| n.state)) { Some(state) => state, None => return, }; if state == State::Dirty { recompute(key); } with_runtime(|rt| { if let Some(node) = rt.nodes.get_mut(key) { node.state = State::Clean; } }); } /// Re-run a memo or effect body with dependency tracking. fn recompute(key: NodeKey) { let taken = with_runtime(|rt| { rt.clear_sources(key); let node = rt.nodes.get_mut(key)?; let run = node.run.take()?; let value = node.value.take(); let prev_observer = rt.observer.replace(key); Some((run, value, node.kind, prev_observer)) }); let Some((mut run, mut value, kind, prev_observer)) = taken else { return; }; let changed = run(&mut value); with_runtime(|rt| { rt.observer = prev_observer; if let Some(node) = rt.nodes.get_mut(key) { node.run = Some(run); node.value = value; if changed && kind == Kind::Memo { let subscribers = node.subscribers.clone(); for sub in subscribers { rt.stale(sub, State::Dirty); } } } // If the node was disposed while running, its body and value are // simply dropped here. }); } // --- crate-internal API used by the typed wrappers in lib.rs --- pub(crate) fn create_node(kind: Kind, value: Option>, run: Option) -> NodeKey { with_runtime(|rt| rt.insert_node(kind, value, run)) } /// Create and immediately execute a memo or effect, establishing its initial /// dependencies. pub(crate) fn create_and_run(kind: Kind, run: RunFn) -> NodeKey { let key = create_node(kind, None, Some(run)); with_runtime(|rt| { if let Some(node) = rt.nodes.get_mut(key) { node.state = State::Dirty; } }); recompute(key); with_runtime(|rt| { if let Some(node) = rt.nodes.get_mut(key) { node.state = State::Clean; } }); key } /// Read a node's value. Tracks the read if `track` and the node is up to /// date first if it is a memo. pub(crate) fn read(key: NodeKey, track: bool, f: impl FnOnce(Option<&dyn Any>) -> R) -> R { let is_memo = with_runtime(|rt| rt.nodes.get(key).map(|n| n.kind == Kind::Memo)); if is_memo == Some(true) { update_if_necessary(key); } with_runtime(|rt| { if track { rt.track(key); } f(rt.nodes.get(key).and_then(|n| n.value.as_deref())) }) } /// Write a signal's value (already compared by the typed wrapper) and mark /// dependents stale. pub(crate) fn write(key: NodeKey, value: Box) { with_runtime(|rt| { let Some(node) = rt.nodes.get_mut(key) else { return; }; debug_assert_eq!(node.kind, Kind::Signal, "write is only for plain signals"); node.value = Some(value); let subscribers = node.subscribers.clone(); for sub in subscribers { rt.stale(sub, State::Dirty); } }); } /// Mutate a signal's value in place and mark dependents stale. pub(crate) fn write_with(key: NodeKey, f: impl FnOnce(&mut dyn Any)) { let changed = with_runtime(|rt| { let Some(node) = rt.nodes.get_mut(key) else { return false; }; if let Some(value) = node.value.as_deref_mut() { f(value); } true }); if changed { with_runtime(|rt| { let subscribers = rt .nodes .get(key) .map(|n| n.subscribers.clone()) .unwrap_or_default(); for sub in subscribers { rt.stale(sub, State::Dirty); } }); } } pub(crate) fn node_exists(key: NodeKey) -> bool { with_runtime(|rt| rt.nodes.contains_key(key)) } pub(crate) fn dispose_node(key: NodeKey) { with_runtime(|rt| { if let Some(node) = rt.nodes.get(key) { let scope = node.scope; if let Some(scope) = rt.scopes.get_mut(scope) { scope.nodes.retain(|n| *n != key); } } rt.remove_node(key); }); } /// Run every queued effect (and any they transitively queue) to quiescence. pub fn flush_effects() { let already_flushing = with_runtime(|rt| { if rt.flushing { true } else { rt.flushing = true; false } }); if already_flushing { return; } let mut rounds = 0; loop { let batch = with_runtime(|rt| std::mem::take(&mut rt.queue)); if batch.is_empty() { break; } rounds += 1; assert!( rounds <= MAX_FLUSH_ROUNDS, "effect flush did not converge after {MAX_FLUSH_ROUNDS} rounds; \ effects are likely writing signals they depend on" ); for key in batch { let runnable = with_runtime(|rt| match rt.nodes.get_mut(key) { Some(node) if node.queued => { node.queued = false; true } _ => false, }); if runnable { update_if_necessary(key); } } } with_runtime(|rt| rt.flushing = false); } /// Whether any effects are waiting for [`flush_effects`]. pub fn has_pending_effects() -> bool { with_runtime(|rt| !rt.queue.is_empty()) } // --- scopes --- pub(crate) fn create_scope() -> ScopeKey { with_runtime(|rt| { let parent = rt.current_scope; let key = rt.scopes.insert(Scope { parent: Some(parent), ..Default::default() }); rt.scopes[parent].children.push(key); key }) } pub(crate) fn enter_scope(scope: ScopeKey) -> ScopeKey { with_runtime(|rt| { let prev = rt.current_scope; if rt.scopes.contains_key(scope) { rt.current_scope = scope; } prev }) } pub(crate) fn restore_scope(scope: ScopeKey) { with_runtime(|rt| { if rt.scopes.contains_key(scope) { rt.current_scope = scope; } else { rt.current_scope = rt.root_scope; } }); } pub(crate) fn dispose_scope(scope: ScopeKey) { with_runtime(|rt| rt.dispose_scope(scope)); }