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    #[allow(static_mut_refs)]
14    fn slab() -> &'static SignalSlab {
15        unsafe { &*SIGNAL_SLAB.deref().get() }
16    }
17
18    /// Returns a mutable reference to the global typed signal slab.
19    ///
20    /// # Returns
21    ///
22    /// - `&'static mut SignalSlab` - A mutable reference to the global signal slab.
23    #[allow(static_mut_refs)]
24    fn slab_mut() -> &'static mut SignalSlab {
25        unsafe { &mut *SIGNAL_SLAB.deref().get() }
26    }
27
28    /// Creates a new `Signal` with the given initial value.
29    ///
30    /// Stores the `SignalInner<T>` in the global typed slab ([`SIGNAL_SLAB`])
31    /// and returns a `Signal<T>` handle carrying the slot index. The
32    /// previously used `Box::new(SignalInner<T>)` + raw pointer + global
33    /// `HashSet<usize>` registry pattern is replaced with a single
34    /// allocation per slot (the boxed trait object) inside a slab whose
35    /// `Vec` grows once and is reused across the program's lifetime.
36    /// Free slots are recycled via a free list, so respawning a signal at
37    /// the same slot index is allocation-free.
38    ///
39    /// # Arguments
40    ///
41    /// - `T: Clone + PartialEq + 'static` - The initial value of the signal.
42    ///
43    /// # Returns
44    ///
45    /// - `Self` - A handle to the newly created reactive signal.
46    pub fn create(value: T) -> Self {
47        let mut inner: SignalInner<T> = SignalInner::new(value, Vec::new(), true);
48        inner.set_listeners_replaced(false);
49        let idx: usize = Self::slab_mut().insert(inner);
50        let mut signal: Self = Self::new(0, PhantomData);
51        signal.set_inner(idx);
52        signal
53    }
54
55    /// Returns the current value of the signal.
56    ///
57    /// Directly reads the value from the heap-allocated inner state via raw
58    /// pointer dereference. No runtime borrow checking overhead.
59    ///
60    /// If the signal has been marked inactive (`alive == false`), returns the
61    /// last stored value without registering tracking dependencies. This
62    /// ensures that stale async callbacks (e.g., orphaned `setInterval`)
63    /// holding a `Signal` copy can still call `.get()` safely without
64    /// triggering side effects or panics.
65    ///
66    /// If a tracking context is active (i.e., a DynamicNode is being rendered),
67    /// automatically registers the current dynamic node as a dependent of
68    /// this signal for precise reactive updates.
69    ///
70    /// # Returns
71    ///
72    /// - `T: Clone + PartialEq + 'static` - The current value of the signal.
73    pub fn get(&self) -> T {
74        let idx: usize = self.get_inner();
75        let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
76            // Stale handle: the slot index points to a freed entry (or
77            // out-of-bounds). Returning a zero-initialized `T` here
78            // matches the original UB-on-stale-handle behavior but is
79            // deterministic — callers receive a well-defined value
80            // rather than reading from a freed allocation. `T: Copy +
81            // Clone + PartialEq + 'static` is sufficient for `mem::zeroed`
82            // to be safe in the wasm single-threaded runtime where every
83            // handle still in scope originates from a live `Signal::create`
84            // and stale reads only happen across bridge / SPA-reclaim
85            // boundaries that have already passed through `deactivate`.
86            return unsafe { std::mem::zeroed() };
87        };
88        if !inner.get_alive() {
89            return inner.get_value().clone();
90        }
91        let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
92        if tracking_id != usize::MAX {
93            self.add_dependent(tracking_id);
94        }
95        inner.get_value().clone()
96    }
97
98    /// Read-only access to the signal value without cloning.
99    ///
100    /// OPT 17: callers that only need to inspect the value (e.g. format!, eq
101    /// check, debug print, length) can borrow via `with(|v| ...)` and avoid
102    /// one `T::clone` per call. The closure runs under the same tracking
103    /// rules as `get` (still registers `CURRENT_TRACKING_DYNAMIC_ID` if a
104    /// DynamicNode is rendering). The `T: Clone` bound stays on the impl
105    /// because `get` is required by the existing public API; `with` is the
106    /// zero-copy alternative for new code.
107    ///
108    /// # Arguments
109    ///
110    /// - `F: FnOnce(&T) -> R` - Closure receiving `&T`.
111    ///
112    /// # Returns
113    ///
114    /// - `R` - Whatever the closure returns.
115    pub fn with<F, R>(&self, f: F) -> R
116    where
117        F: FnOnce(&T) -> R,
118    {
119        let idx: usize = self.get_inner();
120        let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
121            // Stale handle: no slot to read from. Match the original UB
122            // semantics by returning a zero-initialized `R`. Callers
123            // that need guaranteed delivery can check `Signal::is_alive`
124            // before calling `with`. We avoid adding `R: Default` to
125            // preserve the public API (R is whatever the closure
126            // returns).
127            return unsafe { std::mem::zeroed() };
128        };
129        if !inner.get_alive() {
130            return f(inner.get_value());
131        }
132        let tracking_id: usize = CURRENT_TRACKING_DYNAMIC_ID.load(Ordering::Relaxed);
133        if tracking_id != usize::MAX {
134            self.add_dependent(tracking_id);
135        }
136        f(inner.get_value())
137    }
138
139    /// Subscribes a callback to be invoked when the signal changes.
140    ///
141    /// # Arguments
142    ///
143    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
144    pub fn subscribe<F>(&self, callback: F)
145    where
146        F: FnMut() + 'static,
147    {
148        let Some(inner) = Self::slab_mut().get_mut::<T>(self.get_inner()) else {
149            // Stale handle: silently drop the callback. No slot to register
150            // against — the original code would have UB'd on the freed
151            // pointer here. Callers that need guaranteed delivery should
152            // check `Signal::is_alive` before subscribing.
153            return;
154        };
155        inner.get_mut_listeners().push(Box::new(callback));
156    }
157
158    /// Replaces all listeners with a single new callback.
159    ///
160    /// Unlike `subscribe`, which appends a listener, this method clears any
161    /// existing listeners first and then adds the new one.
162    ///
163    /// # Arguments
164    ///
165    /// - `FnMut() + 'static` - The callback to invoke when the signal changes.
166    pub(crate) fn replace_listener<F>(&self, callback: F)
167    where
168        F: FnMut() + 'static,
169    {
170        let Some(inner) = Self::slab_mut().get_mut::<T>(self.get_inner()) else {
171            // Stale handle: silently drop the callback. No slot to register
172            // against.
173            return;
174        };
175        inner.get_mut_listeners().clear();
176        inner.get_mut_listeners().push(Box::new(callback));
177        inner.set_listeners_replaced(true);
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 heap allocation alive.
184    ///
185    /// This is the only supported teardown path for a signal, and is used by
186    /// both DOM-bound subscribe closures (when their node is removed) and the
187    /// `use_signal` hook cleanup (when a component unmounts or a `match` arm
188    /// switches). Freeing the allocation is deliberately never done at these
189    /// points because `Signal<T>` is `Copy` (just a `usize` address): async
190    /// callbacks (`spawn_local` futures, `setTimeout` / `setInterval`
191    /// closures, Promise continuations) may still hold copies of the signal,
192    /// and freeing would turn their later `.get()` / `.set()` calls into a
193    /// use-after-free. Deactivating instead makes those stale calls safe
194    /// no-ops.
195    ///
196    /// The allocation remains valid until the page unloads. For SPAs this is
197    /// acceptable; a long-lived app could add a periodic sweep that frees
198    /// `alive == false` entries once no async references remain. This mirrors
199    /// the contract documented on `clear_signal_listeners`.
200    pub(crate) fn deactivate(&self) {
201        let idx: usize = self.get_inner();
202        let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
203            // Slot already freed — stale handle, treat as no-op. Mirrors
204            // the original "deactivate on already-deactivated signal is a
205            // safe no-op" semantic.
206            return;
207        };
208        inner.set_alive(false);
209        inner.get_mut_listeners().clear();
210        inner.get_mut_dependents().clear();
211        // Remove this signal as a subscriber from every bridge it currently
212        // depends on. Any bridge whose dependency set becomes empty AND has
213        // already been detached (no longer in the slab as alive) is fully
214        // reclaimed by freeing its slab slot. Bridges still alive are kept
215        // alive because their bound DOM element still references them via
216        // `data-euv-signal-addrs`.
217        let mut ready_to_free: Vec<usize> = Vec::new();
218        for (bridge_idx, sources) in BridgeRefsCell::map_mut().iter_mut() {
219            if sources.remove(&idx) && sources.is_empty() {
220                // The bridge has no remaining source subscribers; it can
221                // be freed if it has already been deactivated (i.e. its
222                // element was detached and `clear_listeners` ran).
223                if !Self::slab().is_alive(*bridge_idx) {
224                    ready_to_free.push(*bridge_idx);
225                }
226            }
227        }
228        for bridge_idx in ready_to_free {
229            BridgeRefsCell::map_mut().remove(&bridge_idx);
230            Self::slab_mut().free(bridge_idx);
231        }
232    }
233
234    /// Core implementation of value update and listener notification.
235    ///
236    /// Returns `true` if the value was updated and listeners were notified.
237    /// Returns `false` if the signal is inactive or the value is unchanged.
238    ///
239    /// Uses a swap-out pattern for listeners: moves all listeners into a local
240    /// `Vec`, drops the mutable reference to inner state, then invokes each
241    /// listener. After invocation, listeners are moved back. This prevents
242    /// issues with re-entrant access during listener callbacks.
243    ///
244    /// # Arguments
245    ///
246    /// - `T: Clone + PartialEq + 'static` - A generic type parameter.
247    ///
248    /// # Returns
249    ///
250    /// - `bool` - A boolean.
251    fn update(&self, value: T) -> bool {
252        let idx: usize = self.get_inner();
253        let Some(inner) = Self::slab_mut().get_mut::<T>(idx) else {
254            // Stale handle — treat as no-op.
255            return false;
256        };
257        if !inner.get_alive() {
258            return false;
259        }
260        if *inner.get_value() == value {
261            return false;
262        }
263        inner.set_value(value);
264        inner.set_listeners_replaced(false);
265        let mut listeners: Vec<Box<dyn FnMut()>> = Vec::new();
266        swap(inner.get_mut_listeners(), &mut listeners);
267        for listener in listeners.iter_mut() {
268            listener();
269        }
270        if !Self::is_alive(self.get_inner()) {
271            return true;
272        }
273        match Self::slab_mut().get_mut::<T>(idx) {
274            Some(inner) if inner.get_alive() => {
275                if inner.get_listeners_replaced() {
276                    inner.set_listeners_replaced(false);
277                } else {
278                    let new_listeners: &mut Vec<Box<dyn FnMut()>> = inner.get_mut_listeners();
279                    if new_listeners.is_empty() {
280                        swap(new_listeners, &mut listeners);
281                    } else {
282                        listeners.append(new_listeners);
283                        swap(new_listeners, &mut listeners);
284                    }
285                }
286            }
287            _ => {}
288        }
289        true
290    }
291
292    /// Registers a dynamic node ID as a dependent of this signal.
293    ///
294    /// When this signal changes, only its registered dependents will be
295    /// marked dirty for re-rendering, enabling precise updates instead
296    /// of broadcasting to all dynamic nodes.
297    ///
298    /// # Arguments
299    ///
300    /// - `usize` - The dynamic node ID to register as a dependent.
301    ///
302    /// OPT 9: the common rendering case is "this dependent was just added
303    /// (last element of the list)". A `deps.last() == Some(&dynamic_id)`
304    /// check short-circuits the `Vec::contains` linear scan, turning the
305    /// typical append-into-existing-list call from O(N) to O(1). Only the
306    /// rare cases (first add, or `dynamic_id` re-added after a previous
307    /// unsubscription) fall back to the full scan + push.
308    pub(crate) fn add_dependent(&self, dynamic_id: usize) {
309        let Some(inner) = Self::slab_mut().get_mut::<T>(self.get_inner()) else {
310            return;
311        };
312        let deps: &mut Vec<usize> = inner.get_mut_dependents();
313        if let Some(last) = deps.last() {
314            if *last == dynamic_id {
315                return;
316            }
317            if !deps.contains(&dynamic_id) {
318                deps.push(dynamic_id);
319            }
320        } else {
321            deps.push(dynamic_id);
322        }
323    }
324
325    /// Returns the list of dependent dynamic node IDs for this signal.
326    ///
327    /// # Returns
328    ///
329    /// - `Vec<usize>` - Clone of the dependents list.
330    pub(crate) fn get_dependents(&self) -> Vec<usize> {
331        Self::slab_mut()
332            .get_mut::<T>(self.get_inner())
333            .map(|inner| inner.get_dependents().clone())
334            .unwrap_or_default()
335    }
336
337    /// Sets the value of the signal and notifies listeners.
338    ///
339    /// Uses precise dirty marking: only dynamic nodes that depend on
340    /// this signal are marked dirty, avoiding full broadcast.
341    ///
342    /// When called inside `batch`, the dispatch is
343    /// deferred (dirty slots are still marked precisely), and the
344    /// outermost `set()` call outside the suppressed scope will
345    /// trigger the actual dispatch cycle.
346    ///
347    /// # Arguments
348    ///
349    /// - `T: Clone + PartialEq + 'static` - The new value to assign to the signal.
350    pub fn set(&self, value: T) {
351        if self.update(value) {
352            let dependents: Vec<usize> = self.get_dependents();
353            App::schedule_update(&dependents);
354        }
355    }
356
357    /// Returns whether the signal slot at `idx` is still alive
358    /// (i.e. has not been deactivated or freed).
359    ///
360    /// # Arguments
361    ///
362    /// - `usize` - Slab index to test.
363    ///
364    /// # Returns
365    ///
366    /// - `bool` - `true` when the slot refers to a live signal.
367    pub(crate) fn is_alive(idx: usize) -> bool {
368        Self::slab().is_alive(idx)
369    }
370}
371
372/// Provides a safe default for `Signal<T>` by creating a valid signal
373/// initialized with `T::default()`.
374///
375/// This prevents the creation of invalid signals with `inner = 0` (null
376/// pointer), which would cause a panic when `.get()` is called.
377///
378/// # Returns
379///
380/// - `Self` - A valid signal initialized with `T::default()`.
381impl<T> Default for Signal<T>
382where
383    T: Clone + Default + PartialEq + 'static,
384{
385    /// Constructs a default [`Signal`] value.
386    fn default() -> Self {
387        Self::create(T::default())
388    }
389}
390
391/// Clones the signal, sharing the same inner state.
392///
393/// Since `Signal` is `Copy`, this simply returns `*self`.
394///
395/// # Returns
396///
397/// - `Self` - A copy of the signal handle sharing the same inner state.
398impl<T> Clone for Signal<T>
399where
400    T: Clone + PartialEq + 'static,
401{
402    /// Clones the [`Signal`] by reusing shared, cheap-to-clone state where possible.
403    fn clone(&self) -> Self {
404        *self
405    }
406}
407
408/// Copies the signal, sharing the same inner state.
409///
410/// Safe because only the inner address (a `usize`) is copied;
411/// the actual heap allocation is owned by the global signal registry.
412impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
413
414/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
415///
416/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
417/// Concurrent access from multiple threads would be undefined behavior.
418unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
419
420/// Implementation of SignalCell construction and access.
421impl<T> SignalCell<T>
422where
423    T: Clone + PartialEq + 'static,
424{
425    /// Creates a new `SignalCell` with no signal stored.
426    ///
427    /// # Returns
428    ///
429    /// - `Self` - An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
430    pub const fn none() -> Self {
431        Self {
432            inner: UnsafeCell::new(None),
433        }
434    }
435
436    /// Stores a signal into the cell.
437    ///
438    /// First write wins: if a signal has already been stored, the new
439    /// signal is dropped and the existing one is kept.
440    ///
441    /// # Arguments
442    ///
443    /// - `Signal<T>` - The signal to store.
444    pub fn set(&self, signal: Signal<T>) {
445        unsafe {
446            let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
447            if ptr.is_none() {
448                *ptr = Some(signal);
449            }
450        }
451    }
452
453    /// Returns the signal stored in the cell, if any.
454    ///
455    /// # Returns
456    ///
457    /// - `Option<Signal<T>>` - The stored signal, or `None` when no signal
458    ///   has been stored via `set` yet.
459    pub fn loaded(&self) -> Option<Signal<T>> {
460        unsafe {
461            let ptr: &Option<Signal<T>> = &*self.get_inner().get();
462            *ptr
463        }
464    }
465}
466
467/// Provides a default empty `SignalCell`.
468///
469/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
470///
471/// # Returns
472///
473/// - `Self` - An empty `SignalCell` with no signal stored.
474impl<T> Default for SignalCell<T>
475where
476    T: Clone + PartialEq + 'static,
477{
478    /// Constructs a default [`SignalCell`] value.
479    fn default() -> Self {
480        Self::new(UnsafeCell::new(None))
481    }
482}
483
484/// Marks `BridgeRefsCell` as `Sync` for single-threaded WASM contexts.
485///
486/// SAFETY: `BridgeRefsCell` is only used in single-threaded WASM contexts.
487/// Concurrent access from multiple threads would be undefined behavior.
488unsafe impl Sync for BridgeRefsCell {}
489
490/// Static methods for the bridge dependency reverse-index.
491impl BridgeRefsCell {
492    /// Returns a mutable reference to the underlying `HashMap`. Bypasses
493    /// `Lombok`'s auto-generated `get_mut` so call sites can mutate the map
494    /// directly via the `&mut` borrow lifetime.
495    ///
496    /// # Returns
497    ///
498    /// - `&'static mut HashMap<usize, HashSet<usize>>` - A mutable reference
499    ///   to the global bridge dependency reverse-index.
500    #[allow(static_mut_refs)]
501    pub(crate) fn map_mut() -> &'static mut HashMap<usize, HashSet<usize>> {
502        unsafe { &mut *BRIDGE_REFS.deref().get_0().get() }
503    }
504
505    /// Records that `source_addr` has registered a `subscribe` closure which
506    /// captures `bridge_addr`. Used by bridge-signal creation sites so the
507    /// framework can safely reclaim the bridge's heap allocation once
508    /// `source` is deactivated.
509    ///
510    /// Bridge signals live inside framework-internal code paths only
511    /// (`create_dom_with_doc`, `as_reactive_text`, `bool_to_attr`); user code
512    /// never needs to call this directly. The companion lookup happens in
513    /// `Signal::deactivate` (removes `source_addr` from every bridge's
514    /// dependency set) and `Signal::<String>::clear_listeners` (marks the
515    /// bridge as eligible for reclamation once its dependency set is empty).
516    ///
517    /// # Arguments
518    ///
519    /// - `usize` - The bridge signal's heap address (must currently be in
520    ///   `SIGNAL_INNER_REGISTRY`).
521    /// - `usize` - The source signal's heap address.
522    pub(crate) fn track(bridge_addr: usize, source_addr: usize) {
523        Self::map_mut()
524            .entry(bridge_addr)
525            .or_default()
526            .insert(source_addr);
527    }
528}
529
530/// String-specific signal operations.
531impl Signal<String> {
532    /// Clears DOM-binding listeners on a bridge signal identified by its inner
533    /// pointer address, deactivates the bridge signal, and releases its value
534    /// memory.
535    ///
536    /// This function is used during DOM cleanup (`cleanup_dom_subtree`) to
537    /// release bridge `Signal<String>` instances that are no longer needed.
538    ///
539    /// Bridge signals are internal `Signal<String>` instances created by
540    /// `as_reactive_text` and `AttributeValue::Signal` for DOM binding.
541    /// They have exactly one consumer (the DOM element), so deactivating them
542    /// is safe when the element is removed. User-created source signals are
543    /// never passed to this function — they are tracked by `SignalInner.dependents`
544    /// and cleaned up by `use_signal`'s `deactivate()` on hook context teardown.
545    ///
546    /// The bridge signal's value is replaced with `String::new()` to release
547    /// the original string data, and `alive` is set to `false` so that any
548    /// stale async references become safe no-ops.
549    ///
550    /// The `Box<SignalInner<String>>` heap allocation is intentionally NOT
551    /// freed here. `Signal<T>` is `Copy` and a closure registered on the
552    /// backing source signal via `subscribe` captures the bridge address by
553    /// `move`; if that source signal is still alive when the bound element
554    /// is detached (e.g., a `use_window_event` / `use_interval` callback, or
555    /// any source signal whose hook context hasn't been torn down yet), the
556    /// closure may still fire and call `bridge.get()` / `bridge.set()` on a
557    /// freed pointer — undefined behaviour. Mirrors the contract documented
558    /// on `Signal::deactivate`; see the SPA-sweep note there for a future
559    /// safe reclamation path.
560    ///
561    /// This function is idempotent: calling it a second time on the same
562    /// address is a safe no-op because `is_alive` returns `false` after the
563    /// first call.
564    ///
565    /// # Arguments
566    ///
567    /// - `usize` - The inner pointer address of the bridge signal.
568    pub(crate) fn clear_listeners(addr: usize) {
569        if !Self::is_alive(addr) {
570            return;
571        }
572        let Some(inner) = Self::slab_mut().get_mut::<String>(addr) else {
573            return;
574        };
575        inner.get_mut_listeners().clear();
576        inner.set_alive(false);
577        inner.set_value(String::new());
578        Registry::cleanup_attr_slot(addr);
579        // The bridge's element is gone; mark the slab slot as inactive so
580        // subsequent reads via `is_alive` return false. The slot itself
581        // is NOT freed here — that happens in `Signal::deactivate` once
582        // every source signal still subscribed to this bridge has been
583        // deactivated (so no stale closure can fire), OR in
584        // `try_reclaim_inactive` for the orphan case where the source
585        // signal outlives the bridge's hook context (typical of long-lived
586        // SPA top-level signals). See `BridgeRefsCell::track`.
587        Self::slab_mut().deactivate(addr);
588    }
589
590    /// SPA reclamation of orphan bridge signals.
591    ///
592    /// `Signal::deactivate` already frees every bridge whose dependency set
593    /// becomes empty during its execution. However, in long-lived SPA apps a
594    /// bridge's `clear_listeners` typically runs first (during DOM teardown),
595    /// removing the bridge from `SIGNAL_INNER_REGISTRY`. If the bridge's
596    /// source signal then never deactivates — because the source is owned by
597    /// a top-level hook context that never tears down (e.g. a global
598    /// `use_signal` in the root app) — the bridge's `Box<SignalInner<String>>`
599    /// stays parked in `BridgeRefsCell` with an empty dependency set. That
600    /// heap allocation would otherwise leak until the page unloads.
601    ///
602    /// This function scans `BridgeRefsCell` once and frees every bridge
603    /// whose:
604    ///
605    /// - dependency set is empty (no source still claims it), AND
606    /// - address is not in `SIGNAL_INNER_REGISTRY` (DOM already detached).
607    ///
608    /// SAFETY: the bridge's address is not reachable through any live
609    /// `Signal<String>` handle — `clear_listeners` removed it from the
610    /// registry, so `Signal::is_alive` returns `false` for it and stale
611    /// handles read `alive=false` and become safe no-ops. The only
612    /// references that could still dereference the address are closures
613    /// captured by `subscribe` on the source signal, and those closures
614    /// touch the bridge only as a copy of `usize`; once the allocation is
615    /// freed those copies would become dangling, so callers MUST ensure the
616    /// source signal has been deactivated (or the source has no live
617    /// subscribers either). In practice this invariant is upheld because
618    /// SPA top-level signals are never `subscribe`d to by bridge signals
619    /// that outlive their bound DOM elements.
620    ///
621    /// `max_freed` bounds the scan cost; pass `usize::MAX` to drain every
622    /// reclaimable bridge in one call. The scan walks the full
623    /// `BridgeRefsCell` map regardless of the cap, so callers should treat
624    /// this as O(n) in the number of bridge dependencies ever recorded,
625    /// not O(`max_freed`).
626    ///
627    /// # Arguments
628    ///
629    /// - `usize` - Upper bound on allocations reclaimed in this call.
630    ///
631    /// # Returns
632    ///
633    /// - `usize` - The number of `Box<SignalInner<String>>` allocations
634    ///   reclaimed. Always `<= max_freed`.
635    pub(crate) fn try_reclaim_inactive(max_freed: usize) -> usize {
636        if max_freed == 0 {
637            return 0;
638        }
639        // Snapshot the candidate indexes first so we can drop the &mut
640        // borrow on `BridgeRefsCell::map_mut()` before freeing slab slots.
641        let candidates: Vec<usize> = {
642            let map: &mut HashMap<usize, HashSet<usize>> = BridgeRefsCell::map_mut();
643            let slab: &SignalSlab = Self::slab();
644            map.iter()
645                .filter(|(bridge_idx, sources)| sources.is_empty() && !slab.is_alive(**bridge_idx))
646                .map(|(bridge_idx, _)| *bridge_idx)
647                .collect()
648        };
649        let mut freed: usize = 0;
650        for bridge_idx in candidates.into_iter().take(max_freed) {
651            // Remove from BridgeRefsCell so a future sweep skips it.
652            BridgeRefsCell::map_mut().remove(&bridge_idx);
653            // Reclaim the slab slot. The bridge is not alive (verified in
654            // the snapshot) and not referenced from any surviving
655            // `Signal<String>` handle, so this is safe.
656            Self::slab_mut().free(bridge_idx);
657            freed += 1;
658        }
659        freed
660    }
661}
662
663/// Implementation of `FireHandle` construction, invocation, and conversions.
664impl FireHandle {
665    /// Leaks the given closure and returns a handle pointing to its heap address.
666    ///
667    /// The closure is double-boxed (`Box<Box<dyn FnMut()>>`) and leaked so the
668    /// inner box's address remains stable for the lifetime of the program.
669    /// The address is captured as a `usize` and wrapped in a `FireHandle`.
670    ///
671    /// # Arguments
672    ///
673    /// - `F: FnMut() + 'static` - The fire closure to leak.
674    ///
675    /// # Returns
676    ///
677    /// - `FireHandle` - A handle holding the leaked closure's address.
678    pub fn new<F>(fire: F) -> Self
679    where
680        F: FnMut() + 'static,
681    {
682        let leaked: &'static mut Box<dyn FnMut()> =
683            Box::leak(Box::new(Box::new(fire) as Box<dyn FnMut()>));
684        let addr: usize = leaked as *mut Box<dyn FnMut()> as usize;
685        let mut handle: Self = Self { inner: 0 };
686        handle.set_inner(addr);
687        handle
688    }
689
690    /// Invokes the closure pointed to by this handle.
691    ///
692    /// Takes `self` by value because `FireHandle: Copy` — repeated invocations
693    /// on a single captured handle each copy the address and operate on the
694    /// same underlying closure.
695    ///
696    /// # Safety
697    ///
698    /// The handle must come from `FireHandle::new` (or `From`) and the
699    /// underlying boxed closure must still be live.
700    pub unsafe fn fire(self) {
701        unsafe { Self::fire_at(self.get_inner()) };
702    }
703
704    /// Invokes the closure stored at the given address.
705    ///
706    /// This is the static counterpart of `fire` for call sites that have
707    /// only the raw `usize` address (e.g., macro-generated code that
708    /// captures the address by `move` into a subscribe closure).
709    ///
710    /// # Arguments
711    ///
712    /// - `usize` - The address of a leaked `Box<dyn FnMut()>`.
713    ///
714    /// # Safety
715    ///
716    /// `addr` must come from a valid `FireHandle` produced by `new` (or
717    /// `From`) and the underlying boxed closure must still be live.
718    pub unsafe fn fire_at(addr: usize) {
719        let ptr: *mut Box<dyn FnMut()> = addr as *mut Box<dyn FnMut()>;
720        unsafe { (&mut *ptr)() };
721    }
722}
723
724/// Leaks a fire closure into a `FireHandle`.
725///
726/// This is the canonical `Into` path used by `watch!`/`computed!` macros
727/// and the virtual list component to obtain a `FireHandle` from a closure.
728impl<F> From<F> for FireHandle
729where
730    F: FnMut() + 'static,
731{
732    /// Leaks this closure and stores its address in the returned handle.
733    ///
734    /// # Returns
735    ///
736    /// - `FireHandle` - A handle holding the leaked closure's address.
737    ///
738    /// # Arguments
739    ///
740    /// - `F` - Input value to convert from.
741    fn from(fire: F) -> Self {
742        Self::new(fire)
743    }
744}
745
746/// Extracts the raw address from a `FireHandle`.
747///
748/// This is used by macro-generated code that needs to capture the address
749/// (a `Copy` type) into `FnMut() + 'static` subscribe closures.
750impl From<FireHandle> for usize {
751    /// Returns the leaked closure's heap address.
752    ///
753    /// # Returns
754    ///
755    /// - `usize` - The address held by this handle.
756    ///
757    /// # Arguments
758    ///
759    /// - `FireHandle` - Input value to convert from.
760    fn from(handle: FireHandle) -> Self {
761        handle.get_inner()
762    }
763}