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 global typed signal slab.
9    ///
10    /// # Returns
11    ///
12    /// - `&'static SignalSlab` - A shared reference to the global signal slab.
13    fn slab() -> &'static SignalSlab {
14        unsafe { &*(*std::ptr::addr_of!(SIGNAL_SLAB)).deref().get() }
15    }
16
17    /// Returns a mutable reference to the global typed signal slab.
18    ///
19    /// # Returns
20    ///
21    /// - `&'static mut SignalSlab` - A mutable reference to the global signal slab.
22    fn slab_mut() -> &'static mut SignalSlab {
23        unsafe { &mut *(*std::ptr::addr_of_mut!(SIGNAL_SLAB)).deref().get() }
24    }
25
26    /// Creates a new `Signal` with the given initial value.
27    ///
28    /// Stores the `SignalInner<T>` in the global slab ([`SIGNAL_SLAB`]) and
29    /// returns a `Signal<T>` handle carrying the slot index. The slab is
30    /// append-only: a slot always belongs to the `Signal` that created it,
31    /// so stale handles can never observe a recycled slot of a different
32    /// type.
33    ///
34    /// # Arguments
35    ///
36    /// - `T: Clone + PartialEq + 'static` - The initial value of the signal.
37    ///
38    /// # Returns
39    ///
40    /// - `Self` - A handle to the newly created reactive signal.
41    pub fn create(value: T) -> Self {
42        let inner: SignalInner<T> = SignalInner::new(value, Vec::new(), true);
43        let idx: usize = Self::slab_mut().insert(inner);
44        let mut signal: Self = Self::new(0, PhantomData);
45        signal.set_inner(idx);
46        signal
47    }
48
49    /// Returns the current value of the signal.
50    ///
51    /// Directly reads the value from the slot stored in the global slab.
52    ///
53    /// If the signal has been marked inactive (`alive == false`), returns the
54    /// last stored value without registering tracking dependencies. This
55    /// ensures that stale async callbacks (e.g., orphaned `setInterval`)
56    /// holding a `Signal` copy can still call `.get()` safely without
57    /// triggering side effects or panics.
58    ///
59    /// If a tracking context is active (i.e., a DynamicNode is being rendered),
60    /// automatically registers the current dynamic node as a dependent of
61    /// this signal for precise reactive updates.
62    ///
63    /// # Returns
64    ///
65    /// - `T: Clone + PartialEq + 'static` - The current value of the signal.
66    pub fn get(&self) -> T {
67        let idx: usize = self.get_inner();
68        let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
69            // Unresolvable handle: the slot index was never issued by this
70            // slab or belongs to a different concrete `T` (a corrupted or
71            // forged handle). Slots are never freed or recycled, so any
72            // handle produced by `Signal::create` always resolves; a `None`
73            // here is a program bug, and panicking is strictly better than
74            // vending a zero-initialized `T` (unsound for non-zeroable
75            // types such as `String` / `Vec`).
76            unreachable!("Signal handle does not resolve to a slab slot");
77        };
78        if !inner.get_alive() {
79            return inner.get_value().clone();
80        }
81        let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
82        if tracking_id != usize::MAX {
83            Self::push_dependent(inner, tracking_id);
84        }
85        inner.get_value().clone()
86    }
87
88    /// Read-only access to the signal value without cloning.
89    ///
90    /// OPT 17: callers that only need to inspect the value (e.g. format!, eq
91    /// check, debug print, length) can borrow via `with(|v| ...)` and avoid
92    /// one `T::clone` per call. The closure runs under the same tracking
93    /// rules as `get` (still registers `CURRENT_TRACKING_DYNAMIC_ID` if a
94    /// DynamicNode is rendering). The `T: Clone` bound stays on the impl
95    /// because `get` is required by the existing public API; `with` is the
96    /// zero-copy alternative for new code.
97    ///
98    /// # Arguments
99    ///
100    /// - `F: FnOnce(&T) -> R` - Closure receiving `&T`.
101    ///
102    /// # Returns
103    ///
104    /// - `R` - Whatever the closure returns.
105    pub fn with<F, R>(&self, f: F) -> R
106    where
107        F: FnOnce(&T) -> R,
108    {
109        let idx: usize = self.get_inner();
110        let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
111            // Unresolvable handle: unreachable for slab-issued handles (see
112            // `get`). Panic instead of vending a zero-initialized `R`, which
113            // would be unsound for non-zeroable return types.
114            unreachable!("Signal handle does not resolve to a slab slot");
115        };
116        if !inner.get_alive() {
117            return f(inner.get_value());
118        }
119        let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
120        if tracking_id != usize::MAX {
121            Self::push_dependent(inner, tracking_id);
122        }
123        f(inner.get_value())
124    }
125
126    /// Subscribes a callback to be invoked when the signal changes.
127    ///
128    /// Returns the subscription id, which can later be passed to
129    /// [`Signal::unsubscribe`] to detach exactly this listener. Framework
130    /// DOM bindings use the id to tear down a binding when its element is
131    /// removed; macro-generated `watch!` / `computed!` subscriptions keep
132    /// the id unused because their lifetime is the enclosing hook context.
133    ///
134    /// # Arguments
135    ///
136    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
137    ///
138    /// # Returns
139    ///
140    /// - `u64` - The subscription id. `u64::MAX` when the handle is stale
141    ///   (out-of-bounds slot); such an id is a safe no-op for `unsubscribe`.
142    pub fn subscribe<F>(&self, callback: F) -> u64
143    where
144        F: FnMut() + 'static,
145    {
146        let Some(inner) = Self::slab_mut().get_mut::<T>(self.get_inner()) else {
147            // Stale handle: no slot to register against — the subscription
148            // is silently dropped, matching the previous no-op semantics.
149            return u64::MAX;
150        };
151        let id: u64 = inner.get_next_listener_id();
152        inner.set_next_listener_id(id.wrapping_add(1));
153        inner.get_mut_listeners().push((id, Box::new(callback)));
154        id
155    }
156
157    /// Detaches a single listener previously registered by [`Signal::subscribe`].
158    ///
159    /// When called while the signal is mid-notification (a listener callback
160    /// triggered this call re-entrantly), the removal is deferred: the id is
161    /// recorded and filtered out during `update`'s merge-back pass, so the
162    /// detached listener cannot be resurrected into the live list.
163    ///
164    /// # Arguments
165    ///
166    /// - `u64` - The subscription id returned by `subscribe`.
167    pub fn unsubscribe(&self, id: u64) {
168        let Some(inner) = Self::slab_mut().get_mut::<T>(self.get_inner()) else {
169            return;
170        };
171        if inner.get_notifying() {
172            inner.get_mut_removed_listener_ids().push(id);
173            return;
174        }
175        inner
176            .get_mut_listeners()
177            .retain(|(listener_id, _): &(u64, Box<dyn FnMut()>)| *listener_id != id);
178    }
179
180    /// Detaches this signal from the reactive system without freeing memory.
181    ///
182    /// Marks the signal inactive and clears its listeners and dependents, but
183    /// intentionally keeps the slab slot alive.
184    ///
185    /// This is the only supported teardown path for a signal, and is used by
186    /// the `use_signal` hook cleanup (when a component unmounts or a `match`
187    /// arm switches). The slot is deliberately never freed or recycled because
188    /// `Signal<T>` is `Copy` (just a `usize` slot index): async callbacks
189    /// (`spawn_local` futures, `setTimeout` / `setInterval` closures, Promise
190    /// continuations) may still hold copies of the signal, and recycling would
191    /// turn their later `.get()` / `.set()` calls into reads of an unrelated
192    /// signal. Deactivating instead makes those stale calls safe no-ops.
193    pub(crate) fn deactivate(&self) {
194        let idx: usize = self.get_inner();
195        let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
196            // Out-of-bounds handle — treat as no-op. Mirrors the
197            // "deactivate on already-deactivated signal is a safe no-op"
198            // semantic.
199            return;
200        };
201        inner.set_alive(false);
202        inner.get_mut_listeners().clear();
203        inner.get_mut_dependents().clear();
204        inner.get_mut_removed_listener_ids().clear();
205    }
206
207    /// Core implementation of value update and listener notification.
208    ///
209    /// Returns `true` if the value was updated and listeners were notified.
210    /// Returns `false` if the signal is inactive or the value is unchanged.
211    ///
212    /// Uses a swap-out pattern for listeners: moves all listeners into a local
213    /// `Vec`, drops the mutable reference to inner state, then invokes each
214    /// listener. After invocation, listeners are moved back. This prevents
215    /// issues with re-entrant access during listener callbacks. Listeners
216    /// detached via `unsubscribe` mid-notification are filtered out during
217    /// the merge-back pass via `removed_listener_ids`.
218    ///
219    /// # Arguments
220    ///
221    /// - `T: Clone + PartialEq + 'static` - A generic type parameter.
222    ///
223    /// # Returns
224    ///
225    /// - `bool` - A boolean.
226    fn update(&self, value: T) -> bool {
227        let idx: usize = self.get_inner();
228        let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
229            // Stale handle — treat as no-op.
230            return false;
231        };
232        if !inner.get_alive() {
233            return false;
234        }
235        if *inner.get_value() == value {
236            return false;
237        }
238        inner.set_value(value);
239        inner.set_notifying(true);
240        let mut listeners: Vec<(u64, Box<dyn FnMut()>)> = Vec::new();
241        swap(inner.get_mut_listeners(), &mut listeners);
242        for (_id, listener) in listeners.iter_mut() {
243            listener();
244        }
245        if !Self::is_alive(self.get_inner()) {
246            // The signal was deactivated by a listener mid-notification.
247            // Nothing should be merged back into a dead slot; clear the
248            // notification state so a later `unsubscribe` cannot pile up
249            // deferred removals that will never be drained.
250            if let Some(inner) = Self::slab_mut().get_mut::<T>(idx) {
251                inner.set_notifying(false);
252                inner.get_mut_removed_listener_ids().clear();
253            }
254            return true;
255        }
256        if let Some(inner) = Self::slab_mut().get_mut::<T>(idx)
257            && inner.get_alive()
258        {
259            let removed: Vec<u64> = take(inner.get_mut_removed_listener_ids());
260            if !removed.is_empty() {
261                listeners.retain(|(listener_id, _): &(u64, Box<dyn FnMut()>)| {
262                    !removed.contains(listener_id)
263                });
264            }
265            let new_listeners: &mut Vec<(u64, Box<dyn FnMut()>)> = inner.get_mut_listeners();
266            if new_listeners.is_empty() {
267                swap(new_listeners, &mut listeners);
268            } else {
269                listeners.append(new_listeners);
270                swap(new_listeners, &mut listeners);
271            }
272            inner.set_notifying(false);
273        }
274        true
275    }
276
277    /// Registers a dynamic node ID as a dependent of the signal whose inner
278    /// state is already mutably borrowed by the caller.
279    ///
280    /// Fused form of the former `add_dependent`: `get` / `with` already hold
281    /// the slab borrow for the value read, so the dependent push happens on
282    /// the same borrow instead of resolving the slot a second time.
283    ///
284    /// OPT 9: the common rendering case is "this dependent was just added
285    /// (last element of the list)". A `deps.last() == Some(&dynamic_id)`
286    /// check short-circuits the `Vec::contains` linear scan, turning the
287    /// typical append-into-existing-list call from O(N) to O(1). Only the
288    /// rare cases (first add, or `dynamic_id` re-added after a previous
289    /// unsubscription) fall back to the full scan + push.
290    fn push_dependent(inner: &mut SignalInner<T>, dynamic_id: usize) {
291        let deps: &mut Vec<usize> = inner.get_mut_dependents();
292        if let Some(last) = deps.last() {
293            if *last == dynamic_id {
294                return;
295            }
296            if !deps.contains(&dynamic_id) {
297                deps.push(dynamic_id);
298            }
299        } else {
300            deps.push(dynamic_id);
301        }
302    }
303
304    /// Takes the dependent dynamic node ID list out of the slot, leaving an
305    /// empty list behind.
306    ///
307    /// Move semantics are sound here because every dependent re-registers
308    /// itself via `get` / `with` when its dynamic node re-renders, and the
309    /// dirty marking of the taken IDs has already happened by the time the
310    /// list is drained (see `set`). Stale IDs of unmounted nodes are dropped
311    /// instead of accumulating in the slot.
312    ///
313    /// # Returns
314    ///
315    /// - `Vec<usize>` - The drained dependents list.
316    pub(crate) fn take_dependents(&self) -> Vec<usize> {
317        Self::slab_mut()
318            .get_mut::<T>(self.get_inner())
319            .map(|inner: &mut SignalInner<T>| take(inner.get_mut_dependents()))
320            .unwrap_or_default()
321    }
322
323    /// Sets the value of the signal and notifies listeners.
324    ///
325    /// Uses precise dirty marking: only dynamic nodes that depend on
326    /// this signal are marked dirty, avoiding full broadcast.
327    ///
328    /// When called inside `batch`, the dispatch is
329    /// deferred (dirty slots are still marked precisely), and the
330    /// outermost `set()` call outside the suppressed scope will
331    /// trigger the actual dispatch cycle.
332    ///
333    /// # Arguments
334    ///
335    /// - `T: Clone + PartialEq + 'static` - The new value to assign to the signal.
336    pub fn set(&self, value: T) {
337        if self.update(value) {
338            let dependents: Vec<usize> = self.take_dependents();
339            App::schedule_update(&dependents);
340        }
341    }
342
343    /// Returns whether the signal slot at `idx` is still alive
344    /// (i.e. has not been deactivated).
345    ///
346    /// # Arguments
347    ///
348    /// - `usize` - Slab index to test.
349    ///
350    /// # Returns
351    ///
352    /// - `bool` - `true` when the slot refers to a live signal.
353    pub(crate) fn is_alive(idx: usize) -> bool {
354        Self::slab().is_alive(idx)
355    }
356}
357
358/// Provides a safe default for `Signal<T>` by creating a valid signal
359/// initialized with `T::default()`.
360///
361/// This prevents the creation of invalid signals with `inner = 0` (null
362/// pointer), which would cause a panic when `.get()` is called.
363///
364/// # Returns
365///
366/// - `Self` - A valid signal initialized with `T::default()`.
367impl<T> Default for Signal<T>
368where
369    T: Clone + Default + PartialEq + 'static,
370{
371    /// Constructs a default [`Signal`] value.
372    fn default() -> Self {
373        Self::create(T::default())
374    }
375}
376
377/// Clones the signal, sharing the same inner state.
378///
379/// Since `Signal` is `Copy`, this simply returns `*self`.
380///
381/// # Returns
382///
383/// - `Self` - A copy of the signal handle sharing the same inner state.
384impl<T> Clone for Signal<T>
385where
386    T: Clone + PartialEq + 'static,
387{
388    /// Clones the [`Signal`] by reusing shared, cheap-to-clone state where possible.
389    fn clone(&self) -> Self {
390        *self
391    }
392}
393
394/// Copies the signal, sharing the same inner state.
395///
396/// Safe because only the inner address (a `usize`) is copied;
397/// the actual heap allocation is owned by the global signal registry.
398impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
399
400/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
401///
402/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
403/// Concurrent access from multiple threads would be undefined behavior.
404unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
405
406/// Implementation of SignalCell construction and access.
407impl<T> SignalCell<T>
408where
409    T: Clone + PartialEq + 'static,
410{
411    /// Creates a new `SignalCell` with no signal stored.
412    ///
413    /// # Returns
414    ///
415    /// - `Self` - An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
416    pub const fn none() -> Self {
417        Self {
418            inner: UnsafeCell::new(None),
419        }
420    }
421
422    /// Stores a signal into the cell.
423    ///
424    /// First write wins: if a signal has already been stored, the new
425    /// signal is dropped and the existing one is kept.
426    ///
427    /// # Arguments
428    ///
429    /// - `Signal<T>` - The signal to store.
430    pub fn set(&self, signal: Signal<T>) {
431        unsafe {
432            let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
433            if ptr.is_none() {
434                *ptr = Some(signal);
435            }
436        }
437    }
438
439    /// Returns the signal stored in the cell, if any.
440    ///
441    /// # Returns
442    ///
443    /// - `Option<Signal<T>>` - The stored signal, or `None` when no signal
444    ///   has been stored via `set` yet.
445    pub fn loaded(&self) -> Option<Signal<T>> {
446        unsafe {
447            let ptr: &Option<Signal<T>> = &*self.get_inner().get();
448            *ptr
449        }
450    }
451}
452
453/// Provides a default empty `SignalCell`.
454///
455/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
456///
457/// # Returns
458///
459/// - `Self` - An empty `SignalCell` with no signal stored.
460impl<T> Default for SignalCell<T>
461where
462    T: Clone + PartialEq + 'static,
463{
464    /// Constructs a default [`SignalCell`] value.
465    fn default() -> Self {
466        Self::new(UnsafeCell::new(None))
467    }
468}
469
470/// Implementation of `FireHandle` construction, invocation, and conversions.
471impl FireHandle {
472    /// Leaks the given closure and returns a handle pointing to its heap address.
473    ///
474    /// The closure is double-boxed (`Box<Box<dyn FnMut()>>`) and leaked so the
475    /// inner box's address remains stable for the lifetime of the program.
476    /// The address is captured as a `usize` and wrapped in a `FireHandle`.
477    ///
478    /// # Arguments
479    ///
480    /// - `F: FnMut() + 'static` - The fire closure to leak.
481    ///
482    /// # Returns
483    ///
484    /// - `Self` - A handle holding the leaked closure's address.
485    pub fn new<F>(fire: F) -> Self
486    where
487        F: FnMut() + 'static,
488    {
489        let leaked: &'static mut Box<dyn FnMut()> =
490            Box::leak(Box::new(Box::new(fire) as Box<dyn FnMut()>));
491        let addr: usize = leaked as *mut Box<dyn FnMut()> as usize;
492        let mut handle: Self = Self { inner: 0 };
493        handle.set_inner(addr);
494        handle
495    }
496
497    /// Invokes the closure pointed to by this handle.
498    ///
499    /// Takes `self` by value because `FireHandle: Copy` — repeated invocations
500    /// on a single captured handle each copy the address and operate on the
501    /// same underlying closure.
502    ///
503    /// # Safety
504    ///
505    /// The handle must come from `FireHandle::new` (or `From`) and the
506    /// underlying boxed closure must still be live.
507    pub unsafe fn fire(self) {
508        unsafe { Self::fire_at(self.get_inner()) };
509    }
510
511    /// Invokes the closure stored at the given address.
512    ///
513    /// This is the static counterpart of `fire` for call sites that have
514    /// only the raw `usize` address (e.g., macro-generated code that
515    /// captures the address by `move` into a subscribe closure).
516    ///
517    /// # Arguments
518    ///
519    /// - `usize` - The address of a leaked `Box<dyn FnMut()>`.
520    ///
521    /// # Safety
522    ///
523    /// `addr` must come from a valid `FireHandle` produced by `new` (or
524    /// `From`) and the underlying boxed closure must still be live.
525    pub unsafe fn fire_at(addr: usize) {
526        let ptr: *mut Box<dyn FnMut()> = addr as *mut Box<dyn FnMut()>;
527        unsafe { (&mut *ptr)() };
528    }
529}
530
531/// Leaks a fire closure into a `FireHandle`.
532///
533/// This is the canonical `Into` path used by `watch!`/`computed!` macros
534/// and the virtual list component to obtain a `FireHandle` from a closure.
535impl<F> From<F> for FireHandle
536where
537    F: FnMut() + 'static,
538{
539    /// Leaks this closure and stores its address in the returned handle.
540    ///
541    /// # Returns
542    ///
543    /// - `FireHandle` - A handle holding the leaked closure's address.
544    ///
545    /// # Arguments
546    ///
547    /// - `F` - Input value to convert from.
548    fn from(fire: F) -> Self {
549        Self::new(fire)
550    }
551}
552
553/// Extracts the raw address from a `FireHandle`.
554///
555/// This is used by macro-generated code that needs to capture the address
556/// (a `Copy` type) into `FnMut() + 'static` subscribe closures.
557impl From<FireHandle> for usize {
558    /// Returns the leaked closure's heap address.
559    ///
560    /// # Returns
561    ///
562    /// - `usize` - The address held by this handle.
563    ///
564    /// # Arguments
565    ///
566    /// - `FireHandle` - Input value to convert from.
567    fn from(handle: FireHandle) -> Self {
568        handle.get_inner()
569    }
570}
571
572/// Implementation of the typed signal slab allocator.
573impl SignalSlab {
574    /// Creates an empty slab.
575    pub(crate) fn new() -> Self {
576        Self {
577            entries: Vec::new(),
578        }
579    }
580
581    /// Inserts a new typed `SignalInner<T>` and returns its slot index.
582    ///
583    /// Append-only: the slot index issued here is never reused for another
584    /// signal, which is what makes stale-handle reads sound (they always
585    /// resolve to this slot's original, possibly deactivated, inner state).
586    pub(crate) fn insert<T>(&mut self, inner: SignalInner<T>) -> usize
587    where
588        T: Clone + PartialEq + 'static,
589    {
590        let boxed: Box<dyn AnySignalInner> = Box::new(inner);
591        let idx: usize = self.entries.len();
592        self.entries.push(boxed);
593        idx
594    }
595
596    /// Returns a typed `&mut SignalInner<T>` view of the slot at `idx`.
597    ///
598    /// Returns `None` when the index is out of bounds or was issued for a
599    /// different concrete `T` (defensive TypeId check). Slots are never
600    /// freed, so `None` means the caller is holding a corrupted handle —
601    /// surfaced as `None` rather than panicking so that stale handles
602    /// degrade into safe no-ops (matching the `alive == false` semantics).
603    pub(crate) fn get_mut<T>(&mut self, idx: usize) -> Option<&mut SignalInner<T>>
604    where
605        T: Clone + PartialEq + 'static,
606    {
607        self.entries
608            .get_mut(idx)?
609            .as_any_mut()
610            .downcast_mut::<SignalInner<T>>()
611    }
612
613    /// Returns `true` when the slot at `idx` exists AND its inner signal is
614    /// still marked `alive`. Used by `Signal::is_alive`.
615    pub(crate) fn is_alive(&self, idx: usize) -> bool {
616        match self.entries.get(idx) {
617            Some(inner) => inner.alive(),
618            None => false,
619        }
620    }
621}