use std::any::Any;
use std::cell::RefCell;
use std::rc::Rc;
use super::owner::{OwnerId, under_scope};
use super::runtime::SignalId;
use super::state::with_reactive;
type SignalValue = Rc<dyn Any>;
struct Slot {
value: Option<SignalValue>,
generation: u32,
}
#[derive(Default)]
pub(crate) struct SignalStorage {
slots: Vec<Slot>,
free_indices: Vec<u32>,
}
impl SignalStorage {
pub(crate) fn successor(&self) -> Self {
Self {
slots: self
.slots
.iter()
.map(|slot| Slot {
value: None,
generation: slot.generation,
})
.collect(),
free_indices: (0..self.slots.len() as u32).rev().collect(),
}
}
}
fn clone_slot_rc(id: SignalId, operation: &str) -> Rc<dyn Any> {
with_reactive(|reactive| {
let storage = reactive.storage.borrow();
let slot = storage.slots.get(id.index()).unwrap_or_else(|| {
panic!(
"Invalid signal ID {}: out of bounds (storage has {} slots)",
id,
storage.slots.len()
)
});
if slot.generation != id.generation() {
panic!(
"Signal {} was disposed - cannot {} after owner cleanup. \
This usually means the signal's owner was disposed while you still hold a reference to the signal.",
id, operation
);
}
let value = slot.value.as_ref().unwrap_or_else(|| {
panic!(
"Signal {} was disposed - cannot {} after owner cleanup. \
This usually means the signal's owner was disposed while you still hold a reference to the signal.",
id, operation
)
});
Rc::clone(value)
})
}
fn try_clone_slot_rc(id: SignalId) -> Option<Rc<dyn Any>> {
with_reactive(|reactive| {
let storage = reactive.storage.borrow();
let slot = storage.slots.get(id.index())?;
if slot.generation != id.generation() {
return None;
}
slot.value.as_ref().map(Rc::clone)
})
}
fn with_signal_cell<T: 'static, R>(
id: SignalId,
operation: &str,
f: impl FnOnce(&RefCell<T>) -> R,
) -> R {
let rc = clone_slot_rc(id, operation);
let cell = rc.downcast_ref::<RefCell<T>>().unwrap_or_else(|| {
panic!(
"Signal {} type mismatch: stored type does not match requested type {}",
id,
std::any::type_name::<T>()
)
});
f(cell)
}
fn alloc_slot(value: Rc<dyn Any>) -> SignalId {
with_reactive(|reactive| {
let mut storage = reactive.storage.borrow_mut();
if let Some(index) = storage.free_indices.pop() {
let slot = &mut storage.slots[index as usize];
slot.generation = slot.generation.wrapping_add(1);
slot.value = Some(value);
return SignalId::new(index, slot.generation);
}
let index = storage.slots.len() as u32;
storage.slots.push(Slot {
value: Some(value),
generation: 0,
});
SignalId::new(index, 0)
})
}
pub fn create_signal_value<T: 'static>(value: T) -> SignalId {
alloc_slot(Rc::new(RefCell::new(value)))
}
pub fn create_stored_value<T: 'static>(value: T) -> SignalId {
alloc_slot(Rc::new(value))
}
struct Derived<T> {
scope: Option<OwnerId>,
call: Box<dyn Fn() -> T>,
}
pub fn create_derived_value<T: 'static>(closure: impl Fn() -> T + 'static) -> SignalId {
let scope = with_reactive(|reactive| reactive.current_owner.get());
alloc_slot(Rc::new(Derived {
scope,
call: Box::new(closure),
}))
}
pub fn call_derived<T: 'static>(id: SignalId) -> T {
with_stored_ref(id, |derived: &Derived<T>| {
under_scope(derived.scope, || (derived.call)())
})
}
pub fn dispose_signal(id: SignalId) {
with_reactive(|reactive| {
let mut storage = reactive.storage.borrow_mut();
let Some(slot) = storage
.slots
.get_mut(id.index())
.filter(|slot| slot.generation == id.generation())
else {
return; };
if slot.value.take().is_some() {
storage.free_indices.push(id.index() as u32);
}
});
}
pub fn get_stored_value<T: Clone + 'static>(id: SignalId) -> T {
with_stored_ref(id, |v: &T| v.clone())
}
pub fn with_stored_value<T: 'static, R>(id: SignalId, f: impl FnOnce(&T) -> R) -> R {
with_stored_ref(id, f)
}
fn with_stored_ref<T: 'static, R>(id: SignalId, f: impl FnOnce(&T) -> R) -> R {
let rc = clone_slot_rc(id, "read");
let val = rc.downcast_ref::<T>().unwrap_or_else(|| {
panic!(
"Signal {} type mismatch: stored type does not match requested type {}",
id,
std::any::type_name::<T>()
)
});
f(val)
}
pub fn get_signal_value<T: Clone + 'static>(id: SignalId) -> T {
with_signal_cell(id, "read", |cell: &RefCell<T>| cell.borrow().clone())
}
pub fn try_get_signal_value<T: Clone + 'static>(id: SignalId) -> Option<T> {
let rc = try_clone_slot_rc(id)?;
Some(downcast_cell::<T>(&rc, id).borrow().clone())
}
pub fn set_signal_value<T: 'static>(id: SignalId, value: T) {
with_signal_cell(id, "write", |cell: &RefCell<T>| {
*cell.borrow_mut() = value;
});
}
pub fn update_signal_value<T: 'static, R>(id: SignalId, f: impl FnOnce(&mut T) -> R) -> R {
with_signal_cell(id, "update", |cell: &RefCell<T>| f(&mut cell.borrow_mut()))
}
pub fn with_signal_value<T: 'static, R>(id: SignalId, f: impl FnOnce(&T) -> R) -> R {
with_signal_cell(id, "borrow", |cell: &RefCell<T>| f(&cell.borrow()))
}
pub fn compare_and_set_signal_value<T: PartialEq + 'static>(id: SignalId, value: T) -> bool {
let Some(rc) = try_clone_slot_rc(id) else {
log::warn!("ignoring write to disposed signal {id}");
return false;
};
let cell = downcast_cell::<T>(&rc, id);
let mut current = cell.borrow_mut();
if *current != value {
*current = value;
true
} else {
false
}
}
pub fn compare_and_update_signal_value<T: Clone + PartialEq + 'static>(
id: SignalId,
f: impl FnOnce(&mut T),
) -> bool {
let Some(rc) = try_clone_slot_rc(id) else {
log::warn!("ignoring update of disposed signal {id}");
return false;
};
let cell = downcast_cell::<T>(&rc, id);
let mut current = cell.borrow_mut();
let old = current.clone();
f(&mut current);
old != *current
}
pub fn set_signal_value_always<T: 'static>(id: SignalId, value: T) -> bool {
let Some(rc) = try_clone_slot_rc(id) else {
log::warn!("ignoring write to disposed signal {id}");
return false;
};
let cell = downcast_cell::<T>(&rc, id);
*cell.borrow_mut() = value;
true
}
pub fn update_signal_value_always<T: 'static>(id: SignalId, f: impl FnOnce(&mut T)) -> bool {
let Some(rc) = try_clone_slot_rc(id) else {
log::warn!("ignoring update of disposed signal {id}");
return false;
};
let cell = downcast_cell::<T>(&rc, id);
f(&mut cell.borrow_mut());
true
}
fn downcast_cell<T: 'static>(rc: &Rc<dyn Any>, id: SignalId) -> &RefCell<T> {
rc.downcast_ref::<RefCell<T>>().unwrap_or_else(|| {
panic!(
"Signal {} type mismatch: stored type does not match requested type {}",
id,
std::any::type_name::<T>()
)
})
}
#[cfg(test)]
pub(crate) fn slot_count() -> usize {
with_reactive(|reactive| reactive.storage.borrow().slots.len())
}
#[cfg(test)]
pub(crate) fn discard_all_signals() {
with_reactive(|reactive| reactive.storage.take());
}
pub fn live_signal_count() -> usize {
with_reactive(|reactive| {
reactive
.storage
.borrow()
.slots
.iter()
.filter(|slot| slot.value.is_some())
.count()
})
}
pub fn has_signal(id: SignalId) -> bool {
with_reactive(|reactive| {
let storage = reactive.storage.borrow();
storage
.slots
.get(id.index())
.filter(|slot| slot.generation == id.generation())
.and_then(|slot| slot.value.as_ref())
.is_some()
})
}
#[cfg(test)]
mod dispose_write_tests {
use crate::reactive::create_signal;
use crate::reactive::owner::{dispose_owner_now, with_owner};
#[test]
fn write_to_disposed_signal_is_a_noop() {
let (sig, owner) = with_owner(|| create_signal(1u32));
dispose_owner_now(owner);
sig.set(2); sig.update(|v| *v += 1); }
}
#[cfg(test)]
mod derived_scope_tests {
use super::live_signal_count;
use crate::reactive::owner::{dispose_owner_now, with_owner};
use crate::reactive::{create_derived, create_signal};
#[test]
fn what_a_derived_body_makes_belongs_to_the_scope_the_closure_was_written_in() {
let baseline = live_signal_count();
let (derived, scope) = with_owner(|| create_derived(|| create_signal(7u32).get()));
assert_eq!(derived.get(), 7);
assert_eq!(derived.get(), 7);
assert!(
live_signal_count() > baseline,
"the reads made signals, which is what this is about"
);
dispose_owner_now(scope);
assert_eq!(
live_signal_count(),
baseline,
"everything the closure made went with the scope it was written in"
);
}
}