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