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}