Skip to main content

euv_core/reactive/signal/
struct.rs

1use super::*;
2
3/// A `(subscription_id, callback)` pair stored in a signal's listener list.
4pub(crate) type ListenerEntry = (usize, Box<dyn FnMut()>);
5
6/// Inner state of a signal, holding the value and subscribed listeners.
7///
8/// This struct is not exposed directly; use `Signal` instead.
9#[derive(CustomDebug, Data, New)]
10pub(crate) struct SignalInner<T>
11where
12    T: Clone,
13{
14    /// The current value of the signal.
15    #[debug(skip)]
16    #[get(pub(crate))]
17    #[get_mut(pub(crate))]
18    #[set(pub(crate))]
19    pub(crate) value: T,
20    /// Callbacks to invoke when the value changes, each paired with its
21    /// subscription id so [`Signal::unsubscribe`] can detach a single
22    /// listener without disturbing the rest.
23    #[debug(skip)]
24    #[get(pub(crate))]
25    #[get_mut(pub(crate))]
26    #[set(pub(crate))]
27    pub(crate) listeners: Vec<ListenerEntry>,
28    /// Monotonic counter backing subscription ids for `listeners`.
29    #[get(pub, type(copy))]
30    #[get_mut(pub(crate))]
31    #[set(pub(crate))]
32    #[new(skip)]
33    pub(crate) next_listener_id: usize,
34    /// Ids detached via [`Signal::unsubscribe`] while `update` had the
35    /// listener list swapped out. Drained by `update`'s merge-back pass so
36    /// a listener detached mid-notification is not resurrected.
37    #[debug(skip)]
38    #[get(pub(crate))]
39    #[get_mut(pub(crate))]
40    #[set(pub(crate))]
41    #[new(skip)]
42    pub(crate) removed_listener_ids: Vec<usize>,
43    /// `true` while `update` has the listener list swapped out for
44    /// notification. `unsubscribe` consults this flag to decide between
45    /// direct removal and deferred removal via `removed_listener_ids`.
46    #[get(pub, type(copy))]
47    #[get_mut(pub(crate))]
48    #[set(pub(crate))]
49    #[new(skip)]
50    pub(crate) notifying: bool,
51    /// Whether this signal is still active. Set to `false` by `deactivate()`
52    /// (and `clear_signal_listeners`) to make subsequent `set()` calls
53    /// complete no-ops (no value update, no listener invocation, no
54    /// dispatch scheduling), ensuring stale closures like orphaned
55    /// `setInterval` handlers or pending `spawn_local` futures become harmless.
56    #[get(pub, type(copy))]
57    #[get_mut(pub(crate))]
58    #[set(pub(crate))]
59    pub(crate) alive: bool,
60    /// IDs of dynamic nodes that depend on this signal for precise dirty marking.
61    /// When this signal changes, only these dynamic nodes are marked dirty
62    /// instead of broadcasting to all registered dynamic nodes.
63    #[debug(skip)]
64    #[get_mut(pub(crate))]
65    #[new(skip)]
66    pub(crate) dependents: Vec<usize>,
67}
68
69/// A reactive signal handle.
70///
71/// Allows reading, writing, and subscribing to changes.
72/// Implements `Clone` and `Copy` for ergonomic use; all copies share the same
73/// underlying state. The inner state lives in the global append-only signal
74/// slab and the handle carries its slot index as a `usize`. The `Copy`
75/// semantics are safe because only the slot index is copied — the actual
76/// slot is owned by the slab and is never freed or recycled.
77#[derive(CustomDebug, Data, Eq, Hash, New, Ord, PartialEq, PartialOrd)]
78pub struct Signal<T>
79where
80    T: Clone + PartialEq + 'static,
81{
82    /// Slot index of the inner state in the global signal slab.
83    #[debug(skip)]
84    #[get(pub, type(copy))]
85    #[get_mut(pub(crate))]
86    #[set(pub(crate))]
87    pub(crate) inner: usize,
88    /// Marker for the generic type parameter (uses fn pointer to be `Copy`
89    /// regardless of `T`).
90    #[debug(skip)]
91    #[get(pub, type(copy))]
92    #[get_mut(pub(crate))]
93    #[set(pub(crate))]
94    pub(crate) _marker: PhantomData<fn() -> T>,
95}
96
97/// A `Sync` wrapper for single-threaded global `Signal` access.
98///
99/// SAFETY: This type is only safe to use in single-threaded contexts
100/// (e.g., WASM). It implements `Sync` to allow usage as a `static`
101/// variable, but concurrent access from multiple threads would be
102/// undefined behavior.
103#[derive(CustomDebug, Data, New)]
104pub struct SignalCell<T>
105where
106    T: Clone + PartialEq + 'static,
107{
108    /// Interior-mutable storage for an optional signal handle.
109    #[debug(skip)]
110    #[get(pub(crate))]
111    #[get_mut(pub(crate))]
112    #[set(pub(crate))]
113    pub(crate) inner: UnsafeCell<Option<Signal<T>>>,
114}
115
116/// Typed slab allocator for `SignalInner<T>`.
117///
118/// The slab owns all `SignalInner<T>` allocations for the program's
119/// lifetime. `Signal<T>` carries only a `usize` slot index, so signals are
120/// trivially `Copy` and stay cheap to clone. Slots are append-only and
121/// never recycled: a stale `Signal<T>` handle therefore always finds its
122/// original slot (marked `alive == false` after `Signal::deactivate`), so
123/// stale reads return the last stored value instead of hitting a reused
124/// slot of a different type or — worse — a `mem::zeroed()` fallback for
125/// a freed slot.
126pub(crate) struct SignalSlab {
127    /// Slot storage. Index 0..len.
128    pub(crate) entries: Vec<Box<dyn AnySignalInner>>,
129}
130
131/// A handle to a leaked `FnMut()` closure, stored as the closure's heap address.
132///
133/// The closure is double-boxed (`Box<Box<dyn FnMut()>>`) and leaked, so its
134/// memory outlives any `FireHandle` copy and can be safely invoked from any
135/// context via the raw pointer. The handle is `Copy` because it only holds
136/// the address — repeated invocations on captured copies all resolve to the
137/// same underlying closure.
138///
139/// This type replaces the inline `Box::leak(... as *mut Box<dyn FnMut()> as usize)`
140/// pattern that was used by `watch!`/`computed!` macros and the virtual list
141/// component, encapsulating the unsized coercion, double-boxing, and raw
142/// pointer arithmetic behind `From`/`Into` conversions and a dedicated
143/// `fire` method.
144#[derive(Clone, Copy, CustomDebug, Data, Eq, Hash, Ord, PartialEq, PartialOrd)]
145pub struct FireHandle {
146    /// Address of the leaked `Box<dyn FnMut()>` allocation.
147    #[get(pub, type(copy))]
148    #[get_mut(pub(crate))]
149    #[set(pub(crate))]
150    pub(crate) inner: usize,
151}