Skip to main content

whisker_runtime/reactive/
runtime.rs

1//! Core data structures for the reactive runtime.
2//!
3//! The runtime is a single thread-local `ReactiveRuntime` holding two
4//! generational slot maps (`owners` and `nodes`) plus a bit of
5//! transient bookkeeping for the currently-running effect and the
6//! pending-effects queue.
7//!
8//! All public reactive primitives (`ReadSignal`, `WriteSignal`,
9//! `RwSignal`) are `Copy` newtypes around a `NodeId`. They look their
10//! value up through the runtime on every operation. Cloning a handle
11//! is just an integer copy; the lifetime of the underlying state is
12//! bounded by the owning [`Scope`] (looked up via its [`Owner`] handle),
13//! not by the handle. `computed()` returns a `ReadSignal<T>` that happens
14//! to be backed by a `NodeData::Computed` node — externally
15//! indistinguishable from a primitive signal.
16//!
17//! This module defines the types only. The thread-local instance and
18//! the orchestration logic live in `mod.rs` and the sibling files.
19
20use std::any::{Any, TypeId};
21use std::cell::RefCell;
22use std::collections::{HashMap, HashSet};
23use std::rc::Rc;
24
25use slotmap::{new_key_type, SlotMap};
26
27new_key_type! {
28    /// Identifier for an [`Owner`] slot in the runtime's owner map.
29    /// Generational — disposing an owner invalidates outstanding
30    /// `Owner`s pointing at the same slot index.
31    pub struct Owner;
32
33    /// Identifier for a [`ReactiveNode`] slot. Generational like
34    /// [`Owner`].
35    pub struct NodeId;
36}
37
38/// Kind discriminator for [`ReactiveNode`]. Carried separately from
39/// [`NodeData`] so dependency-graph walks can branch on kind without
40/// matching the data variant (the variants carry mutable state that we
41/// generally don't want to touch during graph walks).
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum NodeKind {
44    /// Mutable reactive value. Subscribers re-run when it changes.
45    Signal,
46    /// Side-effecting reactive computation. Has no return value
47    /// observable to other nodes; runs once on registration and again
48    /// whenever a tracked source changes.
49    Effect,
50    /// Derived value. Like an effect, but caches its return so
51    /// downstream readers can observe it through the same dependency
52    /// mechanism as a signal.
53    Computed,
54}
55
56/// The mutable payload of a [`ReactiveNode`]. Signals carry a value,
57/// effects carry a compute closure, computed values carry both.
58///
59/// The compute closure is wrapped in `Rc<RefCell<…>>` so the
60/// scheduler can grab a clone of the handle before invoking it,
61/// keeping the runtime borrow short-lived. User code inside the
62/// closure can then re-borrow the runtime freely.
63pub enum NodeData {
64    Signal {
65        value: Rc<RefCell<dyn Any>>,
66    },
67    Effect {
68        compute: Rc<RefCell<dyn FnMut()>>,
69    },
70    Computed {
71        value: Rc<RefCell<dyn Any>>,
72        compute: Rc<RefCell<dyn FnMut()>>,
73    },
74}
75
76impl NodeData {
77    pub fn kind(&self) -> NodeKind {
78        match self {
79            NodeData::Signal { .. } => NodeKind::Signal,
80            NodeData::Effect { .. } => NodeKind::Effect,
81            NodeData::Computed { .. } => NodeKind::Computed,
82        }
83    }
84
85    /// Borrow the stored value if this node carries one. `None` for
86    /// pure effects (which have no observable value).
87    pub fn value(&self) -> Option<&Rc<RefCell<dyn Any>>> {
88        match self {
89            NodeData::Signal { value } => Some(value),
90            NodeData::Computed { value, .. } => Some(value),
91            NodeData::Effect { .. } => None,
92        }
93    }
94}
95
96/// One node in the reactive graph.
97///
98/// `sources` records what this node read in its last run (downstream
99/// dependencies); `subscribers` records who reads us. Both sets are
100/// kept in sync by the effect/computed runner — on each re-run, the runner
101/// re-derives `sources` by tracking signal reads during the closure,
102/// then sets `subscribers` on the new sources symmetrically.
103///
104/// `arc_sources` is the equivalent for `ArcSignal`-family reads: Arc-
105/// backed signals don't live in the arena, so they can't be referenced
106/// by `NodeId`. Instead, each Arc signal whose value this node read
107/// hands the node a `Rc<dyn ArcSubscription>` clone of its inner; on
108/// re-run / disposal, the node iterates this list to tell each signal
109/// "drop me from your subscriber list" via [`ArcSubscription::unsubscribe`].
110/// The signal itself stays alive (Arc refcount) regardless.
111pub struct ReactiveNode {
112    pub owner: Owner,
113    pub data: NodeData,
114    pub sources: HashSet<NodeId>,
115    pub subscribers: HashSet<NodeId>,
116    pub arc_sources: Vec<Rc<dyn ArcSubscription>>,
117}
118
119impl ReactiveNode {
120    pub fn kind(&self) -> NodeKind {
121        self.data.kind()
122    }
123}
124
125/// Cleanup interface that Arc-backed signals expose so the scheduler
126/// and owner-disposal code can detach a node from a signal's subscriber
127/// list without knowing the signal's concrete type.
128///
129/// The signal owns a `Vec<NodeId>` of subscribers; when a node either
130/// (a) re-runs and rebuilds its source set or (b) gets disposed with
131/// its owner, the runtime walks the node's `arc_sources` and calls
132/// `unsubscribe(node_id)` on each so the signal's bookkeeping stays in
133/// sync. The signal's value lives on as long as someone holds an Arc
134/// to it — disposal here only severs the back-reference.
135pub trait ArcSubscription {
136    /// Drop `subscriber` from this signal's subscriber list. No-op if
137    /// it was never present (e.g. already pruned by an earlier
138    /// `notify`).
139    fn unsubscribe(&self, subscriber: NodeId);
140}
141
142/// A scope record in the reactive tree. Created when a component
143/// mounts, disposed when the component unmounts. Tracks the reactive
144/// nodes allocated inside it (so they can be freed on disposal) and
145/// the child scopes (so disposal cascades).
146///
147/// The public-facing API surface is the [`super::owner::Owner`]
148/// handle (a `Copy` slotmap key); `Scope` is the data record that
149/// handle dereferences to via the runtime's `owners` slotmap. Users
150/// (and even framework extension authors) never name `Scope`
151/// directly.
152///
153/// `contexts` is the per-scope context bag for `provide_context` /
154/// `use_context`. `cleanups` is the LIFO callback queue from
155/// `on_cleanup`.
156pub struct Scope {
157    pub parent: Option<Owner>,
158    pub children: Vec<Owner>,
159    pub nodes: Vec<NodeId>,
160    // `Rc` (not `Box`) so `with_context` can clone the handle out in a
161    // short runtime borrow, drop the borrow, and only then invoke the
162    // user closure — letting the closure safely re-enter the runtime
163    // (read signals, nested `use_context`, etc.) without a double
164    // borrow, and keeping the value alive even if the closure
165    // re-provides the same type mid-call.
166    pub contexts: HashMap<TypeId, Rc<dyn Any>>,
167    pub cleanups: Vec<Box<dyn FnOnce()>>,
168    /// Function-pointer fingerprint of the component fn that created
169    /// this scope. Used by Strategy C hot reload (A6) to map
170    /// subsecond-patched fn pointers back to live owners. `None` for
171    /// non-component scopes (e.g. the root, or manually-created
172    /// scopes in tests).
173    pub mount_fn: Option<*const ()>,
174    /// Element handles created via `view::create_element` while this
175    /// scope was at the top of the owner stack. Released through
176    /// `view::release_element` when the scope is disposed (or its
177    /// ancestor disposes via cascade), preventing the renderer-side
178    /// `BridgeRenderer::elements` map from accumulating dangling
179    /// `WhiskerElement*` pointers across `<Show>` flips, `<For>`
180    /// item removals, and per-component remounts.
181    pub elements: Vec<crate::view::Element>,
182    /// When `true`, effects / computeds owned by this scope skip
183    /// flush — they're deferred onto [`ReactiveRuntime::deferred`]
184    /// until the owner is resumed.
185    ///
186    /// Cascades down the owner tree: `Owner::pause` / `Owner::resume`
187    /// walk descendants and mirror the flag; new scopes inherit the
188    /// parent's flag at `Owner::new` time. Used by `StackLayout`
189    /// to freeze back-stack entries that are mounted-but-off-screen.
190    pub paused: bool,
191}
192
193impl Scope {
194    pub fn new(parent: Option<Owner>) -> Self {
195        Self {
196            parent,
197            children: Vec::new(),
198            nodes: Vec::new(),
199            contexts: HashMap::new(),
200            cleanups: Vec::new(),
201            mount_fn: None,
202            elements: Vec::new(),
203            paused: false,
204        }
205    }
206}
207
208/// The reactive runtime itself. One per thread (held in a
209/// `thread_local!` slot in `mod.rs`).
210///
211/// All public reactive operations route through here. The pattern is
212/// always:
213///
214/// 1. Open a short `with_borrow_mut` to read or mutate `owners` /
215///    `nodes` / `current_*`.
216/// 2. If user code needs to run (effect / computed closure), drop the
217///    borrow first by cloning the necessary handles out, then call
218///    the closure.
219/// 3. Re-open a short borrow to restore book-keeping.
220///
221/// This keeps the `RefCell` borrow window narrow enough that user code
222/// running inside a closure can re-enter the runtime (read signals,
223/// write signals, register new effects) without panicking.
224pub struct ReactiveRuntime {
225    pub owners: SlotMap<Owner, Scope>,
226    pub nodes: SlotMap<NodeId, ReactiveNode>,
227    /// Owner stack: the topmost is the "current" owner — new signals,
228    /// effects, computed values, and lifecycle hooks register against it. Push
229    /// when entering a `Owner::with` (or `#[component]`) scope, pop on
230    /// exit.
231    pub owner_stack: Vec<Owner>,
232    /// The effect/computed currently being computed, if any. Signal reads
233    /// inside this effect register a `sources`/`subscribers` link
234    /// against it.
235    pub current_tracker: Option<NodeId>,
236    /// Queue of effect/computed nodes scheduled to re-run on the next flush.
237    /// Populated by signal writes; drained by [`flush_pending`].
238    pub pending: Vec<NodeId>,
239    /// Nodes that were scheduled to run but whose owner is `paused`.
240    /// Sit here until their owner is resumed; on resume, drain back
241    /// into [`Self::pending`] so the deferred work fires. See
242    /// `Owner::pause` / `Owner::resume` for the lifecycle.
243    pub deferred: Vec<NodeId>,
244    /// True while [`flush_pending`] is actively draining `pending`.
245    /// Used to avoid recursive flushes (signal writes inside a running
246    /// effect just enqueue; we keep draining the queue until empty
247    /// rather than recursing).
248    pub flushing: bool,
249    /// Component-fn-pointer → list of live owners that ran that fn.
250    /// Populated by `register_component`; consulted by the A6 hot-
251    /// reload path to find which owners to dispose when a fn body
252    /// gets subsecond-patched.
253    pub component_owners: HashMap<*const (), Vec<Owner>>,
254    /// Side table of remountable component mount sites
255    /// (`#[component]` with all-`Clone` props), keyed by a stable
256    /// `MountId`. Hot-reload remount walks this table on every
257    /// patch; ordinary `mount_component` (FnOnce body) does not
258    /// register here.
259    pub(crate) mount_sites: HashMap<super::component::MountId, super::component::MountSite>,
260    /// Component-fn-pointer → list of remountable mount sites that
261    /// ran that fn. Mirror of `component_owners` indexed by
262    /// `MountId` instead of `Owner` so it survives the dispose +
263    /// re-create cycle on each hot-reload remount (the owner is
264    /// fresh every time, the mount id is stable).
265    pub fn_ptr_mounts: HashMap<*const (), Vec<super::component::MountId>>,
266    /// Monotonic counter for fresh `MountId`s.
267    pub mount_id_counter: u64,
268    /// Pending on_mount callbacks, in the order they were registered.
269    /// Drained by [`super::flush_mounts`] — which the renderer (A3)
270    /// will call after appending a component's view to its parent.
271    pub pending_mounts: Vec<Box<dyn FnOnce()>>,
272}
273
274impl ReactiveRuntime {
275    pub fn new() -> Self {
276        Self {
277            owners: SlotMap::with_key(),
278            nodes: SlotMap::with_key(),
279            owner_stack: Vec::new(),
280            current_tracker: None,
281            pending: Vec::new(),
282            deferred: Vec::new(),
283            flushing: false,
284            component_owners: HashMap::new(),
285            pending_mounts: Vec::new(),
286            mount_sites: HashMap::new(),
287            fn_ptr_mounts: HashMap::new(),
288            mount_id_counter: 0,
289        }
290    }
291
292    /// Current top-of-stack owner. `None` outside any owner scope (the
293    /// pre-mount state, basically only relevant for tests).
294    pub fn current_owner(&self) -> Option<Owner> {
295        self.owner_stack.last().copied()
296    }
297}
298
299impl Default for ReactiveRuntime {
300    fn default() -> Self {
301        Self::new()
302    }
303}