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