Skip to main content

euv_core/reactive/signal/
impl.rs

1use crate::*;
2
3/// Implementation of reactive signal operations.
4impl<T> Signal<T>
5where
6    T: Clone + PartialEq + 'static,
7{
8    /// Creates a new `Signal` with the given initial value.
9    ///
10    /// Allocates `SignalInner<T>` on the heap via `Box`, stores the raw pointer
11    /// address, and registers it in the global registry for lifecycle tracking.
12    ///
13    /// # Arguments
14    ///
15    /// - `T` - The initial value of the signal.
16    ///
17    /// # Returns
18    ///
19    /// - `Self` - A handle to the newly created reactive signal.
20    pub fn create(value: T) -> Self {
21        let mut inner: SignalInner<T> = SignalInner::new(value, Vec::new(), true);
22        inner.set_listeners_replaced(false);
23        let boxed: Box<SignalInner<T>> = Box::new(inner);
24        let ptr: *mut SignalInner<T> = Box::into_raw(boxed);
25        let addr: usize = ptr as usize;
26        Self::registry_mut().insert(addr);
27        let mut signal: Self = Self::new(0, std::marker::PhantomData);
28        signal.set_inner(addr);
29        signal
30    }
31
32    /// Returns the current value of the signal.
33    ///
34    /// Directly reads the value from the heap-allocated inner state via raw
35    /// pointer dereference. No runtime borrow checking overhead.
36    ///
37    /// If the signal has been marked inactive (`alive == false`), returns the
38    /// last stored value without registering tracking dependencies. This
39    /// ensures that stale async callbacks (e.g., orphaned `setInterval`)
40    /// holding a `Signal` copy can still call `.get()` safely without
41    /// triggering side effects or panics.
42    ///
43    /// If a tracking context is active (i.e., a DynamicNode is being rendered),
44    /// automatically registers the current dynamic node as a dependent of
45    /// this signal for precise reactive updates.
46    ///
47    /// # Returns
48    ///
49    /// - `T` - The current value of the signal.
50    pub fn get(&self) -> T {
51        let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
52        if !inner.get_alive() {
53            return inner.get_value().clone();
54        }
55        let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
56        if tracking_id != usize::MAX {
57            self.add_dependent(tracking_id);
58        }
59        inner.get_value().clone()
60    }
61
62    /// Subscribes a callback to be invoked when the signal changes.
63    ///
64    /// # Arguments
65    ///
66    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
67    pub fn subscribe<F>(&self, callback: F)
68    where
69        F: FnMut() + 'static,
70    {
71        Self::inner_mut(self.get_inner())
72            .get_mut_listeners()
73            .push(Box::new(callback));
74    }
75
76    /// Replaces all listeners with a single new callback.
77    ///
78    /// Unlike `subscribe`, which appends a listener, this method clears any
79    /// existing listeners first and then adds the new one.
80    ///
81    /// # Arguments
82    ///
83    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
84    pub(crate) fn replace_listener<F>(&self, callback: F)
85    where
86        F: FnMut() + 'static,
87    {
88        let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
89        inner.get_mut_listeners().clear();
90        inner.get_mut_listeners().push(Box::new(callback));
91        inner.set_listeners_replaced(true);
92    }
93
94    /// Detaches this signal from the reactive system without freeing memory.
95    ///
96    /// Marks the signal inactive and clears its listeners and dependents, but
97    /// intentionally keeps the heap allocation alive.
98    ///
99    /// This is the only supported teardown path for a signal, and is used by
100    /// both DOM-bound subscribe closures (when their node is removed) and the
101    /// `use_signal` hook cleanup (when a component unmounts or a `match` arm
102    /// switches). Freeing the allocation is deliberately never done at these
103    /// points because `Signal<T>` is `Copy` (just a `usize` address): async
104    /// callbacks (`spawn_local` futures, `setTimeout` / `setInterval`
105    /// closures, Promise continuations) may still hold copies of the signal,
106    /// and freeing would turn their later `.get()` / `.set()` calls into a
107    /// use-after-free. Deactivating instead makes those stale calls safe
108    /// no-ops.
109    ///
110    /// The allocation remains valid until the page unloads. For SPAs this is
111    /// acceptable; a long-lived app could add a periodic sweep that frees
112    /// `alive == false` entries once no async references remain. This mirrors
113    /// the contract documented on `clear_signal_listeners`.
114    pub(crate) fn deactivate(&self) {
115        let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
116        inner.set_alive(false);
117        inner.get_mut_listeners().clear();
118        inner.get_mut_dependents().clear();
119    }
120
121    /// Core implementation of value update and listener notification.
122    ///
123    /// Returns `true` if the value was updated and listeners were notified.
124    /// Returns `false` if the signal is inactive or the value is unchanged.
125    ///
126    /// Uses a swap-out pattern for listeners: moves all listeners into a local
127    /// `Vec`, drops the mutable reference to inner state, then invokes each
128    /// listener. After invocation, listeners are moved back. This prevents
129    /// issues with re-entrant access during listener callbacks.
130    fn update(&self, value: T) -> bool {
131        let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
132        if !inner.get_alive() {
133            return false;
134        }
135        if *inner.get_value() == value {
136            return false;
137        }
138        inner.set_value(value);
139        inner.set_listeners_replaced(false);
140        let mut listeners: Vec<Box<dyn FnMut()>> = Vec::new();
141        swap(inner.get_mut_listeners(), &mut listeners);
142        for listener in listeners.iter_mut() {
143            listener();
144        }
145        if !Self::is_alive(self.get_inner()) {
146            return true;
147        }
148        let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
149        if inner.get_alive() {
150            if inner.get_listeners_replaced() {
151                inner.set_listeners_replaced(false);
152            } else {
153                let new_listeners: &mut Vec<Box<dyn FnMut()>> = inner.get_mut_listeners();
154                if new_listeners.is_empty() {
155                    swap(new_listeners, &mut listeners);
156                } else {
157                    listeners.append(new_listeners);
158                    swap(new_listeners, &mut listeners);
159                }
160            }
161        }
162        true
163    }
164
165    /// Registers a dynamic node ID as a dependent of this signal.
166    ///
167    /// When this signal changes, only its registered dependents will be
168    /// marked dirty for re-rendering, enabling precise updates instead
169    /// of broadcasting to all dynamic nodes.
170    ///
171    /// # Arguments
172    ///
173    /// - `usize` - The dynamic node ID to register as a dependent.
174    pub(crate) fn add_dependent(&self, dynamic_id: usize) {
175        let deps: &mut Vec<usize> = Self::inner_mut(self.get_inner()).get_mut_dependents();
176        if !deps.contains(&dynamic_id) {
177            deps.push(dynamic_id);
178        }
179    }
180
181    /// Removes a dynamic node ID from the dependents list of this signal.
182    ///
183    /// Called during cleanup when a dynamic node is removed from the DOM
184    /// and its dependency relationships need to be severed.
185    ///
186    /// # Arguments
187    ///
188    /// - `usize` - The dynamic node ID to remove.
189    #[allow(dead_code)]
190    pub(crate) fn remove_dependent(&self, dynamic_id: usize) {
191        Self::inner_mut(self.get_inner())
192            .get_mut_dependents()
193            .retain(|id: &usize| *id != dynamic_id);
194    }
195
196    /// Returns the list of dependent dynamic node IDs for this signal.
197    ///
198    /// # Returns
199    ///
200    /// - `Vec<usize>` - Clone of the dependents list.
201    pub(crate) fn get_dependents(&self) -> Vec<usize> {
202        Self::inner_mut(self.get_inner()).get_dependents().clone()
203    }
204
205    /// Sets the value of the signal and notifies listeners.
206    ///
207    /// Uses precise dirty marking: only dynamic nodes that depend on
208    /// this signal are marked dirty, avoiding full broadcast.
209    ///
210    /// When called inside `batch`, the dispatch is
211    /// deferred (dirty slots are still marked precisely), and the
212    /// outermost `set()` call outside the suppressed scope will
213    /// trigger the actual dispatch cycle.
214    ///
215    /// # Arguments
216    ///
217    /// - `T` - The new value to assign to the signal.
218    pub fn set(&self, value: T) {
219        if self.update(value) {
220            let dependents: Vec<usize> = self.get_dependents();
221            App::schedule_update(&dependents);
222        }
223    }
224
225    /// Retrieves a mutable pointer to `SignalInner<T>` directly from the
226    /// signal's stored address.
227    ///
228    /// SAFETY: The address stored in `Signal::inner` is always a valid pointer
229    /// to a `SignalInner<T>` that is kept alive by the global registry. Since
230    /// WASM is single-threaded, the pointer is always valid as long as the
231    /// signal has not been explicitly freed.
232    fn inner_mut(addr: usize) -> &'static mut SignalInner<T> {
233        unsafe { &mut *(addr as *mut SignalInner<T>) }
234    }
235
236    /// Returns whether the signal allocation at `addr` is still present in
237    /// the global registry (i.e. has not been freed).
238    fn is_alive(addr: usize) -> bool {
239        Self::registry_mut().contains(&addr)
240    }
241
242    /// Ensures the signal inner registry is initialized and returns a mutable
243    /// reference to the global signal address registry.
244    #[allow(static_mut_refs)]
245    fn registry_mut() -> &'static mut HashSet<usize> {
246        unsafe {
247            if (*SIGNAL_INNER_REGISTRY.get_0().get()).is_none() {
248                (*SIGNAL_INNER_REGISTRY.get_0().get()) = Some(HashSet::new());
249            }
250            (*SIGNAL_INNER_REGISTRY.get_0().get())
251                .as_mut()
252                .unwrap_unchecked()
253        }
254    }
255}
256
257/// Provides a safe default for `Signal<T>` by creating a valid signal
258/// initialized with `T::default()`.
259///
260/// This prevents the creation of invalid signals with `inner = 0` (null
261/// pointer), which would cause a panic when `.get()` is called.
262///
263/// # Returns
264///
265/// - `Self` - A valid signal initialized with `T::default()`.
266impl<T> Default for Signal<T>
267where
268    T: Clone + Default + PartialEq + 'static,
269{
270    fn default() -> Self {
271        Self::create(T::default())
272    }
273}
274
275/// Clones the signal, sharing the same inner state.
276///
277/// Since `Signal` is `Copy`, this simply returns `*self`.
278///
279/// # Returns
280///
281/// - `Self` - A copy of the signal handle sharing the same inner state.
282impl<T> Clone for Signal<T>
283where
284    T: Clone + PartialEq + 'static,
285{
286    fn clone(&self) -> Self {
287        *self
288    }
289}
290
291/// Copies the signal, sharing the same inner state.
292///
293/// Safe because only the inner address (a `usize`) is copied;
294/// the actual heap allocation is owned by the global signal registry.
295impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
296
297/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
298///
299/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
300/// Concurrent access from multiple threads would be undefined behavior.
301unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
302
303/// Implementation of SignalCell construction and access.
304impl<T> SignalCell<T>
305where
306    T: Clone + PartialEq + 'static,
307{
308    /// Creates a new `SignalCell` with no signal stored.
309    ///
310    /// # Returns
311    ///
312    /// - `Self` - An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
313    pub const fn none() -> Self {
314        Self {
315            inner: UnsafeCell::new(None),
316        }
317    }
318
319    /// Stores a signal into the cell.
320    ///
321    /// # Arguments
322    ///
323    /// - `Signal<T>` - The signal to store.
324    ///
325    /// # Panics
326    ///
327    /// Panics if a signal has already been stored.
328    pub fn set(&self, signal: Signal<T>) {
329        unsafe {
330            let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
331            if ptr.is_some() {
332                panic!("SignalCell::set called on an already-initialized cell");
333            }
334            *ptr = Some(signal);
335        }
336    }
337
338    /// Returns the signal stored in the cell.
339    ///
340    /// # Returns
341    ///
342    /// - `Signal<T>` - The stored signal.
343    ///
344    /// # Panics
345    ///
346    /// Panics if no signal has been stored via `set`.
347    pub fn get(&self) -> Signal<T> {
348        unsafe {
349            let ptr: &Option<Signal<T>> = &*self.get_inner().get();
350            match ptr {
351                Some(signal) => *signal,
352                None => panic!("SignalCell::get called on an uninitialized cell"),
353            }
354        }
355    }
356}
357
358/// Provides a default empty `SignalCell`.
359///
360/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
361///
362/// # Returns
363///
364/// - `Self` - An empty `SignalCell` with no signal stored.
365impl<T> Default for SignalCell<T>
366where
367    T: Clone + PartialEq + 'static,
368{
369    fn default() -> Self {
370        Self::new(UnsafeCell::new(None))
371    }
372}
373
374/// Marks `SignalInnerRegistryCell` as `Sync` for single-threaded WASM contexts.
375///
376/// SAFETY: `SignalInnerRegistryCell` is only used in single-threaded WASM contexts.
377/// Concurrent access from multiple threads would be undefined behavior.
378unsafe impl Sync for SignalInnerRegistryCell {}
379
380/// String-specific signal operations.
381impl Signal<String> {
382    /// Clears DOM-binding listeners on a bridge signal identified by its inner
383    /// pointer address, deactivates the bridge signal, and releases its value
384    /// memory.
385    ///
386    /// This function is used during DOM cleanup (`cleanup_dom_subtree`) to
387    /// release bridge `Signal<String>` instances that are no longer needed.
388    ///
389    /// Bridge signals are internal `Signal<String>` instances created by
390    /// `as_reactive_text` and `AttributeValue::Signal` for DOM binding.
391    /// They have exactly one consumer (the DOM element), so deactivating them
392    /// is safe when the element is removed. User-created source signals are
393    /// never passed to this function — they are tracked by `SignalInner.dependents`
394    /// and cleaned up by `use_signal`'s `deactivate()` on hook context teardown.
395    ///
396    /// The bridge signal's value is replaced with `String::new()` to release
397    /// the original string data, and `alive` is set to `false` so that any
398    /// stale async references become safe no-ops.
399    ///
400    /// # Arguments
401    ///
402    /// - `usize` - The inner pointer address of the bridge signal.
403    pub(crate) fn clear_listeners(addr: usize) {
404        let inner: &mut SignalInner<String> = Self::inner_mut(addr);
405        inner.get_mut_listeners().clear();
406        inner.set_alive(false);
407        inner.set_value(String::new());
408        Registry::cleanup_attr_slot(addr);
409    }
410}