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(Rc::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().as_ref().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().as_ref().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: retain a shared value snapshot, 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<Rc<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(Rc::clone(inner.get_value()))
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.as_ref())
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().as_ref() == value {
318 return false;
319 }
320 if let Some(current) = Rc::get_mut(inner.get_mut_value()) {
321 *current = value;
322 } else {
323 inner.set_value(Rc::new(value));
324 }
325 inner.set_notifying(true);
326 swap(inner.get_mut_listeners(), &mut listeners);
327 true
328 },
329 false,
330 );
331 if !started {
332 return false;
333 }
334 // Phase 2: listeners run with no slab borrow held.
335 for (_id, listener) in listeners.iter_mut() {
336 listener();
337 }
338 // Phase 3: merge the surviving listeners back into the slot.
339 Self::with_slab(
340 |slab: &mut SignalSlab| {
341 let Some(inner) = slab.get_mut::<T>(idx) else {
342 return;
343 };
344 if !inner.get_alive() {
345 // The signal was deactivated by a listener mid-notification.
346 // Nothing should be merged back into a dead slot; clear the
347 // notification state so a later `unsubscribe` cannot pile up
348 // deferred removals that will never be drained.
349 inner.set_notifying(false);
350 inner.get_mut_removed_listener_ids().clear();
351 return;
352 }
353 let removed: Vec<usize> = take(inner.get_mut_removed_listener_ids());
354 if !removed.is_empty() {
355 listeners
356 .retain(|(listener_id, _): &ListenerEntry| !removed.contains(listener_id));
357 }
358 let new_listeners: &mut Vec<ListenerEntry> = inner.get_mut_listeners();
359 if new_listeners.is_empty() {
360 swap(new_listeners, &mut listeners);
361 } else {
362 listeners.append(new_listeners);
363 swap(new_listeners, &mut listeners);
364 }
365 inner.set_notifying(false);
366 },
367 (),
368 );
369 true
370 }
371
372 /// Registers a dynamic node ID as a dependent of the signal whose inner
373 /// state is already mutably borrowed by the caller.
374 ///
375 /// Fused form of the former `add_dependent` - `get` / `with` already hold
376 /// the slab borrow for the value read, so the dependent push happens on
377 /// the same borrow instead of resolving the slot a second time.
378 ///
379 /// OPT 9: the common rendering case is "this dependent was just added
380 /// (last element of the list)". A `deps.last() == Some(&dynamic_id)`
381 /// check short-circuits the `Vec::contains` linear scan, turning the
382 /// typical append-into-existing-list call from O(N) to O(1). Only the
383 /// rare cases (first add, or `dynamic_id` re-added after a previous
384 /// unsubscription) fall back to the full scan + push.
385 ///
386 /// # Arguments
387 ///
388 /// - `&mut SignalInner<T>` - The already-borrowed inner state owning
389 /// the dependent list.
390 /// - `usize` - The dynamic node ID to record as a dependent.
391 fn push_dependent(inner: &mut SignalInner<T>, dynamic_id: usize) {
392 let deps: &mut Vec<usize> = inner.get_mut_dependents();
393 if let Some(last) = deps.last() {
394 if *last == dynamic_id {
395 return;
396 }
397 if !deps.contains(&dynamic_id) {
398 deps.push(dynamic_id);
399 }
400 } else {
401 deps.push(dynamic_id);
402 }
403 }
404
405 /// Takes the dependent dynamic node ID list out of the slot, leaving an
406 /// empty list behind.
407 ///
408 /// Move semantics are sound here because every dependent re-registers
409 /// itself via `get` / `with` when its dynamic node re-renders, and the
410 /// dirty marking of the taken IDs has already happened by the time the
411 /// list is drained (see `set`). Stale IDs of unmounted nodes are dropped
412 /// instead of accumulating in the slot.
413 ///
414 /// # Returns
415 ///
416 /// - `Vec<usize>` - The drained dependents list.
417 pub(crate) fn take_dependents(&self) -> Vec<usize> {
418 let idx: usize = self.get_inner();
419 Self::with_slab(
420 |slab: &mut SignalSlab| {
421 slab.get_mut::<T>(idx)
422 .map(|inner: &mut SignalInner<T>| take(inner.get_mut_dependents()))
423 .unwrap_or_default()
424 },
425 Vec::new(),
426 )
427 }
428
429 /// Sets the value of the signal and notifies listeners.
430 ///
431 /// Uses precise dirty marking: only dynamic nodes that depend on
432 /// this signal are marked dirty, avoiding full broadcast.
433 ///
434 /// When called inside `batch`, the dispatch is
435 /// deferred (dirty slots are still marked precisely), and the
436 /// outermost `set()` call outside the suppressed scope will
437 /// trigger the actual dispatch cycle.
438 ///
439 /// # Arguments
440 ///
441 /// - `T` - The new value to assign to the signal.
442 pub fn set(&self, value: T) {
443 if self.update(value) {
444 let dependents: Vec<usize> = self.take_dependents();
445 App::schedule_update(&dependents);
446 }
447 }
448}
449
450/// Provides a safe default for `Signal<T>` by creating a valid signal
451/// initialized with `T::default()`.
452///
453/// This prevents the creation of invalid signals with `inner = 0` (null
454/// pointer), which would cause a panic when `.get()` is called.
455///
456/// # Returns
457///
458/// - `Self` - A valid signal initialized with `T::default()`.
459impl<T> Default for Signal<T>
460where
461 T: Clone + Default + PartialEq + 'static,
462{
463 /// Constructs a default [`Signal`] value.
464 fn default() -> Self {
465 Self::create(T::default())
466 }
467}
468
469/// Clones the signal, sharing the same inner state.
470///
471/// Since `Signal` is `Copy`, this simply returns `*self`.
472///
473/// # Returns
474///
475/// - `Self` - A copy of the signal handle sharing the same inner state.
476impl<T> Clone for Signal<T>
477where
478 T: Clone + PartialEq + 'static,
479{
480 /// Clones the [`Signal`] by reusing shared, cheap-to-clone state where possible.
481 fn clone(&self) -> Self {
482 *self
483 }
484}
485
486/// Copies the signal, sharing the same inner state.
487///
488/// Safe because only the inner address (a `usize`) is copied;
489/// the actual heap allocation is owned by the global signal registry.
490impl<T> Copy for Signal<T> where T: Clone + PartialEq + 'static {}
491
492/// Marks `SignalCell` as `Sync` for single-threaded WASM contexts.
493///
494/// SAFETY: `SignalCell` is only used in single-threaded WASM contexts.
495/// Concurrent access from multiple threads would be undefined behavior.
496unsafe impl<T> Sync for SignalCell<T> where T: Clone + PartialEq + 'static {}
497
498/// Implementation of SignalCell construction and access.
499impl<T> SignalCell<T>
500where
501 T: Clone + PartialEq + 'static,
502{
503 /// Creates a new `SignalCell` with no signal stored.
504 ///
505 /// # Returns
506 ///
507 /// - `Self` - An empty `SignalCell` with `None` stored in the inner `UnsafeCell`.
508 pub const fn none() -> Self {
509 Self {
510 inner: UnsafeCell::new(None),
511 }
512 }
513
514 /// Stores a signal into the cell.
515 ///
516 /// First write wins: if a signal has already been stored, the new
517 /// signal is dropped and the existing one is kept.
518 ///
519 /// # Arguments
520 ///
521 /// - `Signal<T>` - The signal to store.
522 pub fn set(&self, signal: Signal<T>) {
523 unsafe {
524 let ptr: &mut Option<Signal<T>> = &mut *self.get_inner().get();
525 if ptr.is_none() {
526 *ptr = Some(signal);
527 }
528 }
529 }
530
531 /// Returns the signal stored in the cell, if any.
532 ///
533 /// # Returns
534 ///
535 /// - `Option<Signal<T>>` - The stored signal, or `None` when no signal
536 /// has been stored via `set` yet.
537 pub fn loaded(&self) -> Option<Signal<T>> {
538 unsafe {
539 let ptr: &Option<Signal<T>> = &*self.get_inner().get();
540 *ptr
541 }
542 }
543}
544
545/// Provides a default empty `SignalCell`.
546///
547/// Creates a `SignalCell` with `None` stored in the inner `UnsafeCell`.
548///
549/// # Returns
550///
551/// - `Self` - An empty `SignalCell` with no signal stored.
552impl<T> Default for SignalCell<T>
553where
554 T: Clone + PartialEq + 'static,
555{
556 /// Constructs a default [`SignalCell`] value.
557 fn default() -> Self {
558 Self::new(UnsafeCell::new(None))
559 }
560}
561
562/// Implementation of `FireHandle` construction, invocation, and conversions.
563impl FireHandle {
564 /// Leaks the given closure and returns a handle pointing to its heap address.
565 ///
566 /// The closure is double-boxed (`Box<Box<dyn FnMut()>>`) and leaked so the
567 /// inner box's address remains stable for the lifetime of the program.
568 /// The address is captured as a `usize` and wrapped in a `FireHandle`.
569 ///
570 /// # Arguments
571 ///
572 /// - `F` - The fire closure to leak.
573 ///
574 /// # Returns
575 ///
576 /// - `Self` - A handle holding the leaked closure's address.
577 pub fn new<F>(fire: F) -> Self
578 where
579 F: FnMut() + 'static,
580 {
581 let leaked: &'static mut Box<dyn FnMut()> =
582 Box::leak(Box::new(Box::new(fire) as Box<dyn FnMut()>));
583 let addr: usize = leaked as *mut Box<dyn FnMut()> as usize;
584 let mut handle: Self = Self { inner: 0 };
585 handle.set_inner(addr);
586 handle
587 }
588
589 /// Invokes the closure pointed to by this handle.
590 ///
591 /// Takes `self` by value because `FireHandle: Copy` — repeated invocations
592 /// on a single captured handle each copy the address and operate on the
593 /// same underlying closure.
594 ///
595 /// # Safety
596 ///
597 /// The handle must come from `FireHandle::new` (or `From`) and the
598 /// underlying boxed closure must still be live.
599 pub unsafe fn fire(self) {
600 unsafe { Self::fire_at(self.get_inner()) };
601 }
602
603 /// Invokes the closure stored at the given address.
604 ///
605 /// This is the static counterpart of `fire` for call sites that have
606 /// only the raw `usize` address (e.g., macro-generated code that
607 /// captures the address by `move` into a subscribe closure).
608 ///
609 /// # Arguments
610 ///
611 /// - `usize` - The address of a leaked `Box<dyn FnMut()>`.
612 ///
613 /// # Safety
614 ///
615 /// `addr` must come from a valid `FireHandle` produced by `new` (or
616 /// `From`) and the underlying boxed closure must still be live.
617 pub unsafe fn fire_at(addr: usize) {
618 let ptr: *mut Box<dyn FnMut()> = addr as *mut Box<dyn FnMut()>;
619 unsafe { (&mut *ptr)() };
620 }
621}
622
623/// Leaks a fire closure into a `FireHandle`.
624///
625/// This is the canonical `Into` path used by `watch!`/`computed!` macros
626/// and the virtual list component to obtain a `FireHandle` from a closure.
627impl<F> From<F> for FireHandle
628where
629 F: FnMut() + 'static,
630{
631 /// Leaks this closure and stores its address in the returned handle.
632 ///
633 /// # Returns
634 ///
635 /// - `FireHandle` - A handle holding the leaked closure's address.
636 ///
637 /// # Arguments
638 ///
639 /// - `F` - Input value to convert from.
640 fn from(fire: F) -> Self {
641 Self::new(fire)
642 }
643}
644
645/// Extracts the raw address from a `FireHandle`.
646///
647/// This is used by macro-generated code that needs to capture the address
648/// (a `Copy` type) into `FnMut() + 'static` subscribe closures.
649impl From<FireHandle> for usize {
650 /// Returns the leaked closure's heap address.
651 ///
652 /// # Returns
653 ///
654 /// - `usize` - The address held by this handle.
655 ///
656 /// # Arguments
657 ///
658 /// - `FireHandle` - Input value to convert from.
659 fn from(handle: FireHandle) -> Self {
660 handle.get_inner()
661 }
662}
663
664/// Implementation of the typed signal slab allocator.
665impl SignalSlab {
666 /// Creates an empty slab.
667 pub(crate) fn new() -> Self {
668 Self {
669 entries: Vec::new(),
670 }
671 }
672
673 /// Inserts a new typed `SignalInner<T>` and returns its slot index.
674 ///
675 /// Append-only: the slot index issued here is never reused for another
676 /// signal, which is what makes stale-handle reads sound (they always
677 /// resolve to this slot's original, possibly deactivated, inner state).
678 ///
679 /// # Arguments
680 ///
681 /// - `SignalInner<T>` - The typed inner state to store in a new slot.
682 ///
683 /// # Returns
684 ///
685 /// - `usize` - The freshly issued slot index.
686 pub(crate) fn insert<T>(&mut self, inner: SignalInner<T>) -> usize
687 where
688 T: Clone + PartialEq + 'static,
689 {
690 let boxed: Box<dyn AnySignalInner> = Box::new(inner);
691 let idx: usize = self.get_entries().len();
692 self.get_mut_entries().push(boxed);
693 idx
694 }
695
696 /// Returns a typed `&mut SignalInner<T>` view of the slot at `idx`.
697 ///
698 /// Returns `None` when the index is out of bounds or was issued for a
699 /// different concrete `T` (defensive TypeId check). Slots are never
700 /// freed, so `None` means the caller is holding a corrupted handle —
701 /// surfaced as `None` rather than panicking so that stale handles
702 /// degrade into safe no-ops (matching the `alive == false` semantics).
703 ///
704 /// # Arguments
705 ///
706 /// - `usize` - The slot index previously issued by
707 /// [`SignalSlab::insert`].
708 ///
709 /// # Returns
710 ///
711 /// - `Option<&mut SignalInner<T>>` - The typed view of the slot, or
712 /// `None` when the index is out of bounds or belongs to another `T`.
713 pub(crate) fn get_mut<T>(&mut self, idx: usize) -> Option<&mut SignalInner<T>>
714 where
715 T: Clone + PartialEq + 'static,
716 {
717 self.get_mut_entries()
718 .get_mut(idx)?
719 .as_any_mut()
720 .downcast_mut::<SignalInner<T>>()
721 }
722}