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