use core::fmt;
use core::marker::PhantomData;
#[cfg(target_arch = "wasm32")]
use core::cell::RefCell;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::RwLock;
use super::runtime::{NodeId, with_runtime};
use super::scope::{
NodeKey, NodeKind, ReactiveScopeError, allocate_node, require_active_scope, with_node,
with_node_mut,
};
#[cfg(not(target_arch = "wasm32"))]
type SignalValue<T> = RwLock<T>;
#[cfg(target_arch = "wasm32")]
type SignalValue<T> = RefCell<T>;
struct SignalSlot<T> {
value: SignalValue<T>,
}
impl<T> SignalSlot<T> {
fn new(value: T) -> Self {
Self {
value: SignalValue::new(value),
}
}
}
pub struct Signal<T: 'static> {
key: NodeKey,
_marker: PhantomData<fn() -> T>,
}
impl<T: 'static> Clone for Signal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: 'static> Copy for Signal<T> {}
pub trait IntoSignalHandle<T: 'static> {
fn into_signal_handle(self) -> Signal<T>;
}
impl<T: 'static> IntoSignalHandle<T> for Signal<T> {
fn into_signal_handle(self) -> Signal<T> {
self
}
}
impl<T: 'static> IntoSignalHandle<T> for &Signal<T> {
fn into_signal_handle(self) -> Signal<T> {
*self
}
}
impl<T: 'static> IntoSignalHandle<T> for &mut Signal<T> {
fn into_signal_handle(self) -> Signal<T> {
*self
}
}
pub fn copy_signal_handle<T: 'static>(signal: impl IntoSignalHandle<T>) -> Signal<T> {
signal.into_signal_handle()
}
impl<T: 'static> Signal<T> {
pub fn new(value: T) -> Self {
require_active_scope("Signal::new");
Self {
key: allocate_node(NodeKind::Signal, SignalSlot::new(value)),
_marker: PhantomData,
}
}
pub fn get(&self) -> T
where
T: Clone,
{
with_runtime(|rt| rt.track_dependency(self.id()));
self.get_untracked()
}
pub fn get_untracked(&self) -> T
where
T: Clone,
{
self.try_get_untracked()
.unwrap_or_else(|err| panic!("{err}"))
}
pub fn try_get_untracked(&self) -> Result<T, ReactiveScopeError>
where
T: Clone,
{
with_node::<SignalSlot<T>, _>(self.key, |slot| {
#[cfg(not(target_arch = "wasm32"))]
{
slot.value.read().expect("Signal lock poisoned").clone()
}
#[cfg(target_arch = "wasm32")]
{
slot.value.borrow().clone()
}
})
}
pub fn with_untracked<R>(&self, f: impl FnOnce(&T) -> R) -> R {
with_node::<SignalSlot<T>, _>(self.key, |slot| {
#[cfg(not(target_arch = "wasm32"))]
{
f(&slot.value.read().expect("Signal lock poisoned"))
}
#[cfg(target_arch = "wasm32")]
{
f(&slot.value.borrow())
}
})
.unwrap_or_else(|err| panic!("{err}"))
}
pub fn set(&self, value: T) {
self.try_set(value).unwrap_or_else(|err| panic!("{err}"));
}
pub(crate) fn set_without_notify(&self, value: T) {
with_node_mut::<SignalSlot<T>, _>(self.key, |slot| {
#[cfg(not(target_arch = "wasm32"))]
{
*slot.value.write().expect("Signal lock poisoned") = value;
}
#[cfg(target_arch = "wasm32")]
{
*slot.value.borrow_mut() = value;
}
})
.unwrap_or_else(|err| panic!("{err}"));
}
pub(crate) fn notify_subscribers(&self) {
with_runtime(|rt| rt.notify_signal_change(self.id()));
}
pub fn try_set(&self, value: T) -> Result<(), ReactiveScopeError> {
with_node_mut::<SignalSlot<T>, _>(self.key, |slot| {
#[cfg(not(target_arch = "wasm32"))]
{
*slot.value.write().expect("Signal lock poisoned") = value;
}
#[cfg(target_arch = "wasm32")]
{
*slot.value.borrow_mut() = value;
}
})?;
with_runtime(|rt| rt.notify_signal_change(self.id()));
Ok(())
}
pub fn update<F>(&self, f: F)
where
F: FnOnce(&mut T),
{
with_node_mut::<SignalSlot<T>, _>(self.key, |slot| {
#[cfg(not(target_arch = "wasm32"))]
{
f(&mut slot.value.write().expect("Signal lock poisoned"));
}
#[cfg(target_arch = "wasm32")]
{
f(&mut slot.value.borrow_mut());
}
})
.unwrap_or_else(|err| panic!("{err}"));
with_runtime(|rt| rt.notify_signal_change(self.id()));
}
pub fn id(&self) -> NodeId {
self.key.node_id()
}
}
impl<T: fmt::Debug + Clone + 'static> fmt::Debug for Signal<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Signal")
.field("id", &self.id())
.field("value", &self.get_untracked())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::runtime::NodeType;
use crate::reactive::{Effect, batch};
use rstest::rstest;
use serial_test::serial;
use std::cell::Cell;
use std::rc::Rc;
#[rstest]
#[serial(reactive_runtime)]
fn signal_is_copy() {
fn assert_copy<T: Copy>() {}
assert_copy::<Signal<i32>>();
}
#[rstest]
#[serial(reactive_runtime)]
fn mutable_signal_borrow_converts_to_copied_handle() {
crate::reactive::ReactiveScope::run(|| {
let mut signal = Signal::new(41_i32);
let copied = copy_signal_handle(&mut signal);
copied.set(42);
assert_eq!(signal.get(), 42);
});
}
#[rstest]
#[serial(reactive_runtime)]
#[should_panic(expected = "Signal::new requires an active ReactiveScope")]
fn signal_new_requires_scope() {
let _ = Signal::new(1_i32);
}
#[rstest]
#[serial(reactive_runtime)]
#[should_panic(expected = "disposed reactive node access")]
fn signal_panics_after_scope_dispose() {
let signal = crate::reactive::ReactiveScope::run(|| Signal::new(1_i32));
let _ = signal.get();
}
#[rstest]
#[serial(reactive_runtime)]
fn signal_try_set_discards_completion_after_scope_dispose() {
let signal = crate::reactive::ReactiveScope::run(|| Signal::new(1_i32));
let result = signal.try_set(2);
assert!(result.is_err());
}
#[rstest]
#[serial(reactive_runtime)]
fn signal_update_can_read_a_different_signal() {
crate::reactive::ReactiveScope::run(|| {
let source = Signal::new(41_i32);
let target = Signal::new(0_i32);
target.update(|value| *value = source.get() + 1);
assert_eq!(target.get(), 42);
});
}
#[rstest]
#[serial(reactive_runtime)]
fn signal_with_untracked_can_reenter_a_different_signal() {
crate::reactive::ReactiveScope::run(|| {
let source = Signal::new(1_i32);
let target = Signal::new(0_i32);
source.with_untracked(|_| target.set(42));
assert_eq!(target.get(), 42);
});
}
#[rstest]
#[serial(reactive_runtime)]
fn signal_tracks_and_updates_inside_scope() {
crate::reactive::ReactiveScope::run(|| {
let signal = Signal::new(1_i32);
assert_eq!(signal.get(), 1);
signal.set(2);
assert_eq!(signal.get_untracked(), 2);
signal.update(|value| *value += 3);
assert_eq!(signal.get(), 5);
});
}
#[test]
#[serial]
fn test_signal_creation() {
crate::reactive::ReactiveScope::run(|| {
let signal = Signal::new(42);
assert_eq!(signal.get_untracked(), 42);
});
}
#[test]
#[serial]
fn test_signal_set() {
crate::reactive::ReactiveScope::run(|| {
let signal = Signal::new(0);
assert_eq!(signal.get_untracked(), 0);
signal.set(100);
assert_eq!(signal.get_untracked(), 100);
});
}
#[test]
#[serial]
fn test_signal_update() {
crate::reactive::ReactiveScope::run(|| {
let signal = Signal::new(0);
signal.update(|n| *n += 1);
assert_eq!(signal.get_untracked(), 1);
signal.update(|n| *n *= 2);
assert_eq!(signal.get_untracked(), 2);
});
}
#[test]
#[serial]
fn test_signal_clone() {
crate::reactive::ReactiveScope::run(|| {
let signal1 = Signal::new(42);
let signal2 = signal1;
assert_eq!(signal1.get_untracked(), 42);
assert_eq!(signal2.get_untracked(), 42);
signal1.set(100);
assert_eq!(signal1.get_untracked(), 100);
assert_eq!(signal2.get_untracked(), 100);
});
}
#[test]
#[serial]
fn test_multiple_signals() {
crate::reactive::ReactiveScope::run(|| {
let signal1 = Signal::new(10);
let signal2 = Signal::new(20);
let signal3 = Signal::new("hello");
assert_eq!(signal1.get_untracked(), 10);
assert_eq!(signal2.get_untracked(), 20);
assert_eq!(signal3.get_untracked(), "hello");
signal1.set(30);
signal2.set(40);
signal3.set("world");
assert_eq!(signal1.get_untracked(), 30);
assert_eq!(signal2.get_untracked(), 40);
assert_eq!(signal3.get_untracked(), "world");
});
}
#[test]
#[serial]
fn test_signal_dependency_tracking() {
crate::reactive::ReactiveScope::run(|| {
let signal = Signal::new(42);
assert_eq!(signal.get(), 42);
with_runtime(|rt| {
let observer_id = NodeId::new();
rt.push_observer(crate::reactive::runtime::Observer {
id: observer_id,
node_type: NodeType::Effect,
timing: crate::reactive::runtime::EffectTiming::default(),
cleanup: None,
});
let _ = signal.get();
rt.pop_observer();
let graph = rt.dependency_graph.borrow();
let signal_node = graph.get(&signal.id()).unwrap();
assert!(signal_node.subscribers.contains(&observer_id));
});
});
}
#[test]
#[serial]
fn test_signal_change_notification() {
crate::reactive::ReactiveScope::run(|| {
let signal = Signal::new(0);
let signal_for_effect = signal;
let effect = crate::reactive::Effect::new(move || {
let _ = signal_for_effect.get();
});
signal.set(42);
with_runtime(|rt| {
let pending = rt.pending_updates.borrow();
assert!(pending.contains(&effect.id()));
});
});
}
#[test]
#[serial]
fn test_batched_signal_change_notifies_effect() {
crate::reactive::ReactiveScope::run(|| {
let signal = Signal::new(0);
let runs = Rc::new(Cell::new(0));
let runs_for_effect = Rc::clone(&runs);
let _effect = Effect::new(move || {
let _ = signal.get();
runs_for_effect.set(runs_for_effect.get() + 1);
});
assert_eq!(runs.get(), 1);
batch(|| signal.set(42));
assert_eq!(runs.get(), 2);
});
}
}