Skip to main content

euv_core/reactive/signal/
struct.rs

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