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