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}