Skip to main content

euv_core/reactive/signal/
impl.rs

1use super::*;
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        // Remove this signal as a subscriber from every bridge it currently
140        // depends on. Any bridge whose dependency set becomes empty AND has
141        // already been detached (no longer in `SIGNAL_INNER_REGISTRY`) is
142        // fully reclaimed by freeing its `SignalInner<T>` heap allocation.
143        // Bridges still in the registry are kept alive because their bound
144        // DOM element still references them via `data-euv-signal-addrs`.
145        let self_addr: usize = self.get_inner();
146        let mut ready_to_free: Vec<usize> = Vec::new();
147        for (bridge_addr, sources) in BridgeRefsCell::map_mut().iter_mut() {
148            if sources.remove(&self_addr) && sources.is_empty() {
149                // The bridge has no remaining source subscribers; it can
150                // be freed if it has already been deactivated (i.e. its
151                // element was detached and `clear_listeners` ran).
152                if !Self::registry().contains(bridge_addr) {
153                    ready_to_free.push(*bridge_addr);
154                }
155            }
156        }
157        for bridge_addr in ready_to_free {
158            BridgeRefsCell::map_mut().remove(&bridge_addr);
159            unsafe {
160                let _: Box<SignalInner<T>> = Box::from_raw(bridge_addr as *mut SignalInner<T>);
161            }
162        }
163    }
164
165    /// Core implementation of value update and listener notification.
166    ///
167    /// Returns `true` if the value was updated and listeners were notified.
168    /// Returns `false` if the signal is inactive or the value is unchanged.
169    ///
170    /// Uses a swap-out pattern for listeners: moves all listeners into a local
171    /// `Vec`, drops the mutable reference to inner state, then invokes each
172    /// listener. After invocation, listeners are moved back. This prevents
173    /// issues with re-entrant access during listener callbacks.
174    fn update(&self, value: T) -> bool {
175        let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
176        if !inner.get_alive() {
177            return false;
178        }
179        if *inner.get_value() == value {
180            return false;
181        }
182        inner.set_value(value);
183        inner.set_listeners_replaced(false);
184        let mut listeners: Vec<Box<dyn FnMut()>> = Vec::new();
185        swap(inner.get_mut_listeners(), &mut listeners);
186        for listener in listeners.iter_mut() {
187            listener();
188        }
189        if !Self::is_alive(self.get_inner()) {
190            return true;
191        }
192        let inner: &mut SignalInner<T> = Self::inner_mut(self.get_inner());
193        if inner.get_alive() {
194            if inner.get_listeners_replaced() {
195                inner.set_listeners_replaced(false);
196            } else {
197                let new_listeners: &mut Vec<Box<dyn FnMut()>> = inner.get_mut_listeners();
198                if new_listeners.is_empty() {
199                    swap(new_listeners, &mut listeners);
200                } else {
201                    listeners.append(new_listeners);
202                    swap(new_listeners, &mut listeners);
203                }
204            }
205        }
206        true
207    }
208
209    /// Registers a dynamic node ID as a dependent of this signal.
210    ///
211    /// When this signal changes, only its registered dependents will be
212    /// marked dirty for re-rendering, enabling precise updates instead
213    /// of broadcasting to all dynamic nodes.
214    ///
215    /// # Arguments
216    ///
217    /// - `usize` - The dynamic node ID to register as a dependent.
218    pub(crate) fn add_dependent(&self, dynamic_id: usize) {
219        let deps: &mut Vec<usize> = Self::inner_mut(self.get_inner()).get_mut_dependents();
220        if !deps.contains(&dynamic_id) {
221            deps.push(dynamic_id);
222        }
223    }
224
225    /// Returns the list of dependent dynamic node IDs for this signal.
226    ///
227    /// # Returns
228    ///
229    /// - `Vec<usize>` - Clone of the dependents list.
230    pub(crate) fn get_dependents(&self) -> Vec<usize> {
231        Self::inner_mut(self.get_inner()).get_dependents().clone()
232    }
233
234    /// Sets the value of the signal and notifies listeners.
235    ///
236    /// Uses precise dirty marking: only dynamic nodes that depend on
237    /// this signal are marked dirty, avoiding full broadcast.
238    ///
239    /// When called inside `batch`, the dispatch is
240    /// deferred (dirty slots are still marked precisely), and the
241    /// outermost `set()` call outside the suppressed scope will
242    /// trigger the actual dispatch cycle.
243    ///
244    /// # Arguments
245    ///
246    /// - `T: Clone + PartialEq + 'static` - The new value to assign to the signal.
247    pub fn set(&self, value: T) {
248        if self.update(value) {
249            let dependents: Vec<usize> = self.get_dependents();
250            App::schedule_update(&dependents);
251        }
252    }
253
254    /// Retrieves a mutable pointer to `SignalInner<T>` directly from the
255    /// signal's stored address.
256    ///
257    /// SAFETY: The address stored in `Signal::inner` is always a valid pointer
258    /// to a `SignalInner<T>` that is kept alive by the global registry. Since
259    /// WASM is single-threaded, the pointer is always valid as long as the
260    /// signal has not been explicitly freed.
261    fn inner_mut(addr: usize) -> &'static mut SignalInner<T> {
262        unsafe { &mut *(addr as *mut SignalInner<T>) }
263    }
264
265    /// Returns whether the signal allocation at `addr` is still present
266    /// in the global registry (i.e. has not been freed).
267    pub(crate) fn is_alive(addr: usize) -> bool {
268        Self::registry().contains(&addr)
269    }
270}
271
272/// Provides a safe default for `Signal<T>` by creating a valid signal
273/// initialized with `T::default()`.
274///
275/// This prevents the creation of invalid signals with `inner = 0` (null
276/// pointer), which would cause a panic when `.get()` is called.
277///
278/// # Returns
279///
280/// - `Self` - A valid signal initialized with `T::default()`.
281impl<T> Default for Signal<T>
282where
283    T: Clone + Default + PartialEq + 'static,
284{
285    fn default() -> Self {
286        Self::create(T::default())
287    }
288}
289
290/// Clones the signal, sharing the same inner state.
291///
292/// Since `Signal` is `Copy`, this simply returns `*self`.
293///
294/// # Returns
295///
296/// - `Self` - A copy of the signal handle sharing the same inner state.
297impl<T> Clone for Signal<T>
298where
299    T: Clone + PartialEq + 'static,
300{
301    fn clone(&self) -> Self {
302        *self
303    }
304}
305
306/// Copies the signal, sharing the same inner state.
307///
308/// Safe because only the inner address (a `usize`) is copied;
309/// the actual heap allocation is owned by the global signal registry.
310impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
311
312/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
313///
314/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
315/// Concurrent access from multiple threads would be undefined behavior.
316unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
317
318/// Implementation of SignalCell construction and access.
319impl<T> SignalCell<T>
320where
321    T: Clone + PartialEq + 'static,
322{
323    /// Creates a new `SignalCell` with no signal stored.
324    ///
325    /// # Returns
326    ///
327    /// - `Self` - An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
328    pub const fn none() -> Self {
329        Self {
330            inner: UnsafeCell::new(None),
331        }
332    }
333
334    /// Stores a signal into the cell.
335    ///
336    /// # Arguments
337    ///
338    /// - `Signal<T>` - The signal to store.
339    ///
340    /// # Panics
341    ///
342    /// Panics if a signal has already been stored.
343    pub fn set(&self, signal: Signal<T>) {
344        unsafe {
345            let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
346            if ptr.is_some() {
347                panic!("SignalCell::set called on an already-initialized cell");
348            }
349            *ptr = Some(signal);
350        }
351    }
352
353    /// Returns the signal stored in the cell.
354    ///
355    /// # Returns
356    ///
357    /// - `Signal<T>` - The stored signal.
358    ///
359    /// # Panics
360    ///
361    /// Panics if no signal has been stored via `set`.
362    pub fn get(&self) -> Signal<T> {
363        unsafe {
364            let ptr: &Option<Signal<T>> = &*self.get_inner().get();
365            match ptr {
366                Some(signal) => *signal,
367                None => panic!("SignalCell::get called on an uninitialized cell"),
368            }
369        }
370    }
371}
372
373/// Provides a default empty `SignalCell`.
374///
375/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
376///
377/// # Returns
378///
379/// - `Self` - An empty `SignalCell` with no signal stored.
380impl<T> Default for SignalCell<T>
381where
382    T: Clone + PartialEq + 'static,
383{
384    fn default() -> Self {
385        Self::new(UnsafeCell::new(None))
386    }
387}
388
389/// Marks `SignalInnerRegistryCell` as `Sync` for single-threaded WASM contexts.
390///
391/// SAFETY: `SignalInnerRegistryCell` is only used in single-threaded WASM contexts.
392/// Concurrent access from multiple threads would be undefined behavior.
393unsafe impl Sync for SignalInnerRegistryCell {}
394
395/// Marks `BridgeRefsCell` as `Sync` for single-threaded WASM contexts.
396///
397/// SAFETY: `BridgeRefsCell` is only used in single-threaded WASM contexts.
398/// Concurrent access from multiple threads would be undefined behavior.
399unsafe impl Sync for BridgeRefsCell {}
400
401/// Static methods for the bridge dependency reverse-index.
402impl BridgeRefsCell {
403    /// Returns a mutable reference to the underlying `HashMap`. Bypasses
404    /// `Lombok`'s auto-generated `get_mut` so call sites can mutate the map
405    /// directly via the `&mut` borrow lifetime.
406    ///
407    /// # Returns
408    ///
409    /// - `&'static mut HashMap<usize, HashSet<usize>>` - A mutable reference
410    ///   to the global bridge dependency reverse-index.
411    #[allow(static_mut_refs)]
412    pub(crate) fn map_mut() -> &'static mut HashMap<usize, HashSet<usize>> {
413        unsafe { &mut *BRIDGE_REFS.deref().get_0().get() }
414    }
415
416    /// Records that `source_addr` has registered a `subscribe` closure which
417    /// captures `bridge_addr`. Used by bridge-signal creation sites so the
418    /// framework can safely reclaim the bridge's heap allocation once
419    /// `source` is deactivated.
420    ///
421    /// Bridge signals live inside framework-internal code paths only
422    /// (`create_dom_with_doc`, `as_reactive_text`, `bool_to_attr`); user code
423    /// never needs to call this directly. The companion lookup happens in
424    /// `Signal::deactivate` (removes `source_addr` from every bridge's
425    /// dependency set) and `Signal::<String>::clear_listeners` (marks the
426    /// bridge as eligible for reclamation once its dependency set is empty).
427    ///
428    /// # Arguments
429    ///
430    /// - `usize` - The bridge signal's heap address (must currently be in
431    ///   `SIGNAL_INNER_REGISTRY`).
432    /// - `usize` - The source signal's heap address.
433    pub(crate) fn track(bridge_addr: usize, source_addr: usize) {
434        Self::map_mut()
435            .entry(bridge_addr)
436            .or_default()
437            .insert(source_addr);
438    }
439}
440
441/// String-specific signal operations.
442impl Signal<String> {
443    /// Clears DOM-binding listeners on a bridge signal identified by its inner
444    /// pointer address, deactivates the bridge signal, and releases its value
445    /// memory.
446    ///
447    /// This function is used during DOM cleanup (`cleanup_dom_subtree`) to
448    /// release bridge `Signal<String>` instances that are no longer needed.
449    ///
450    /// Bridge signals are internal `Signal<String>` instances created by
451    /// `as_reactive_text` and `AttributeValue::Signal` for DOM binding.
452    /// They have exactly one consumer (the DOM element), so deactivating them
453    /// is safe when the element is removed. User-created source signals are
454    /// never passed to this function — they are tracked by `SignalInner.dependents`
455    /// and cleaned up by `use_signal`'s `deactivate()` on hook context teardown.
456    ///
457    /// The bridge signal's value is replaced with `String::new()` to release
458    /// the original string data, and `alive` is set to `false` so that any
459    /// stale async references become safe no-ops.
460    ///
461    /// The `Box<SignalInner<String>>` heap allocation is intentionally NOT
462    /// freed here. `Signal<T>` is `Copy` and a closure registered on the
463    /// backing source signal via `subscribe` captures the bridge address by
464    /// `move`; if that source signal is still alive when the bound element
465    /// is detached (e.g., a `use_window_event` / `use_interval` callback, or
466    /// any source signal whose hook context hasn't been torn down yet), the
467    /// closure may still fire and call `bridge.get()` / `bridge.set()` on a
468    /// freed pointer — undefined behaviour. Mirrors the contract documented
469    /// on `Signal::deactivate`; see the SPA-sweep note there for a future
470    /// safe reclamation path.
471    ///
472    /// This function is idempotent: calling it a second time on the same
473    /// address is a safe no-op because `is_alive` returns `false` after the
474    /// first call.
475    ///
476    /// # Arguments
477    ///
478    /// - `usize` - The inner pointer address of the bridge signal.
479    pub(crate) fn clear_listeners(addr: usize) {
480        if !Self::is_alive(addr) {
481            return;
482        }
483        let inner: &mut SignalInner<String> = Self::inner_mut(addr);
484        inner.get_mut_listeners().clear();
485        inner.set_alive(false);
486        inner.set_value(String::new());
487        Registry::cleanup_attr_slot(addr);
488        // The bridge's element is gone; remove it from the global registry
489        // so subsequent reads via `is_alive` return false. The heap
490        // allocation itself is NOT freed here — that happens in
491        // `Signal::deactivate` once every source signal still subscribed to
492        // this bridge has been deactivated (so no stale closure can fire).
493        // See `BridgeRefsCell::track`.
494        Self::registry_mut().remove(&addr);
495    }
496}
497
498/// Implementation of `FireHandle` construction, invocation, and conversions.
499impl FireHandle {
500    /// Leaks the given closure and returns a handle pointing to its heap address.
501    ///
502    /// The closure is double-boxed (`Box<Box<dyn FnMut()>>`) and leaked so the
503    /// inner box's address remains stable for the lifetime of the program.
504    /// The address is captured as a `usize` and wrapped in a `FireHandle`.
505    ///
506    /// # Arguments
507    ///
508    /// - `F: FnMut() + 'static` - The fire closure to leak.
509    ///
510    /// # Returns
511    ///
512    /// - `FireHandle` - A handle holding the leaked closure's address.
513    pub fn new<F>(fire: F) -> Self
514    where
515        F: FnMut() + 'static,
516    {
517        let leaked: &'static mut Box<dyn FnMut()> =
518            Box::leak(Box::new(Box::new(fire) as Box<dyn FnMut()>));
519        let addr: usize = leaked as *mut Box<dyn FnMut()> as usize;
520        let mut handle: Self = Self { inner: 0 };
521        handle.set_inner(addr);
522        handle
523    }
524
525    /// Invokes the closure pointed to by this handle.
526    ///
527    /// Takes `self` by value because `FireHandle: Copy` — repeated invocations
528    /// on a single captured handle each copy the address and operate on the
529    /// same underlying closure.
530    ///
531    /// # Safety
532    ///
533    /// The handle must come from `FireHandle::new` (or `From`) and the
534    /// underlying boxed closure must still be live.
535    pub unsafe fn fire(self) {
536        unsafe { Self::fire_at(self.get_inner()) };
537    }
538
539    /// Invokes the closure stored at the given address.
540    ///
541    /// This is the static counterpart of `fire` for call sites that have
542    /// only the raw `usize` address (e.g., macro-generated code that
543    /// captures the address by `move` into a subscribe closure).
544    ///
545    /// # Arguments
546    ///
547    /// - `usize` - The address of a leaked `Box<dyn FnMut()>`.
548    ///
549    /// # Safety
550    ///
551    /// `addr` must come from a valid `FireHandle` produced by `new` (or
552    /// `From`) and the underlying boxed closure must still be live.
553    pub unsafe fn fire_at(addr: usize) {
554        let ptr: *mut Box<dyn FnMut()> = addr as *mut Box<dyn FnMut()>;
555        unsafe { (&mut *ptr)() };
556    }
557}
558
559/// Leaks a fire closure into a `FireHandle`.
560///
561/// This is the canonical `Into` path used by `watch!`/`computed!` macros
562/// and the virtual list component to obtain a `FireHandle` from a closure.
563impl<F> From<F> for FireHandle
564where
565    F: FnMut() + 'static,
566{
567    /// Leaks this closure and stores its address in the returned handle.
568    ///
569    /// # Returns
570    ///
571    /// - `FireHandle` - A handle holding the leaked closure's address.
572    fn from(fire: F) -> Self {
573        Self::new(fire)
574    }
575}
576
577/// Extracts the raw address from a `FireHandle`.
578///
579/// This is used by macro-generated code that needs to capture the address
580/// (a `Copy` type) into `FnMut() + 'static` subscribe closures.
581impl From<FireHandle> for usize {
582    /// Returns the leaked closure's heap address.
583    ///
584    /// # Returns
585    ///
586    /// - `usize` - The address held by this handle.
587    fn from(handle: FireHandle) -> Self {
588        handle.get_inner()
589    }
590}