use std::marker::PhantomData;
use super::diagnostics::{check_reactive_scope, snapshot_zone};
use super::invalidation::{notify_signal_change, record_signal_read, suspend_widget_tracking};
use super::owner::register_signal;
use super::runtime::{
SignalId, current_write_epoch, notify_write, queue_bg_write, record_effect_read,
suspend_effect_tracking, with_runtime,
};
use super::storage::{
call_derived, compare_and_set_signal_value, compare_and_update_signal_value,
create_derived_value, create_signal_value, create_stored_value, get_signal_value,
get_stored_value, has_signal, try_get_signal_value, with_signal_value, with_stored_value,
};
macro_rules! impl_signal_id_traits {
($ty:ident) => {
impl<T> Clone for $ty<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for $ty<T> {}
impl<T> PartialEq for $ty<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<T> Eq for $ty<T> {}
};
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub(crate) enum SignalKind {
Stored = 0,
Mutable = 1,
Derived = 2,
}
#[inline]
#[cfg_attr(debug_assertions, track_caller)]
fn tracked_get<T: Clone + 'static>(id: SignalId, kind: SignalKind) -> T {
match kind {
SignalKind::Stored => get_stored_value(id),
SignalKind::Derived => {
check_reactive_scope();
snapshot_zone(|| call_derived::<T>(id))
}
SignalKind::Mutable => {
check_reactive_scope();
record_effect_read(id);
record_signal_read(id);
get_signal_value(id)
}
}
}
#[inline]
#[cfg_attr(debug_assertions, track_caller)]
fn tracked_with<T: Clone + 'static, R>(
id: SignalId,
kind: SignalKind,
f: impl FnOnce(&T) -> R,
) -> R {
match kind {
SignalKind::Stored => with_stored_value(id, f),
SignalKind::Derived => {
check_reactive_scope();
let val = snapshot_zone(|| call_derived::<T>(id));
f(&val)
}
SignalKind::Mutable => {
check_reactive_scope();
record_effect_read(id);
record_signal_read(id);
with_signal_value(id, f)
}
}
}
fn write_and_notify<T: Clone + PartialEq + 'static>(id: SignalId, value: T) {
if compare_and_set_signal_value(id, value) {
notify_signal_change(id);
notify_write(id);
}
}
fn update_and_notify<T: Clone + PartialEq + 'static>(id: SignalId, f: impl FnOnce(&mut T)) {
if compare_and_update_signal_value(id, f) {
notify_signal_change(id);
notify_write(id);
}
}
fn write_and_notify_always<T: Clone + 'static>(id: SignalId, value: T) {
if crate::reactive::storage::set_signal_value_always(id, value) {
notify_signal_change(id);
notify_write(id);
}
}
fn update_and_notify_always<T: 'static>(id: SignalId, f: impl FnOnce(&mut T)) {
if crate::reactive::storage::update_signal_value_always(id, f) {
notify_signal_change(id);
notify_write(id);
}
}
pub struct Signal<T> {
id: SignalId,
kind: SignalKind,
_marker: PhantomData<T>,
_not_send: PhantomData<*const ()>,
}
fn untracked_derived<T: Clone + 'static>(id: SignalId) -> T {
snapshot_zone(|| suspend_widget_tracking(|| suspend_effect_tracking(|| call_derived::<T>(id))))
}
impl_signal_id_traits!(Signal);
impl<T> Signal<T> {
pub(crate) fn is_live(&self) -> bool {
has_signal(self.id)
}
}
impl<T: Clone + 'static> Signal<T> {
#[cfg_attr(debug_assertions, track_caller)]
pub fn get(&self) -> T {
tracked_get(self.id, self.kind)
}
pub fn get_untracked(&self) -> T {
match self.kind {
SignalKind::Stored => get_stored_value(self.id),
SignalKind::Derived => untracked_derived::<T>(self.id),
SignalKind::Mutable => get_signal_value(self.id),
}
}
#[cfg_attr(debug_assertions, track_caller)]
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
tracked_with(self.id, self.kind, f)
}
pub fn with_untracked<R>(&self, f: impl FnOnce(&T) -> R) -> R {
match self.kind {
SignalKind::Stored => with_stored_value(self.id, f),
SignalKind::Derived => {
let val = untracked_derived::<T>(self.id);
f(&val)
}
SignalKind::Mutable => with_signal_value(self.id, f),
}
}
}
impl<T: Clone + Send + Sync + 'static> Signal<T> {
pub fn watch(&self) -> tokio::sync::watch::Receiver<T> {
let this = *self;
watch_value(this.get_untracked(), move || this.get())
}
}
pub struct RwSignal<T> {
id: SignalId,
_marker: PhantomData<T>,
_not_send: PhantomData<*const ()>,
}
impl_signal_id_traits!(RwSignal);
impl<T> RwSignal<T> {
pub(crate) const fn from_id(id: SignalId) -> Self {
Self {
id,
_marker: PhantomData,
_not_send: PhantomData,
}
}
pub(crate) fn id(&self) -> SignalId {
self.id
}
}
impl<T: 'static> RwSignal<T> {
#[inline]
#[cfg_attr(debug_assertions, track_caller)]
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
check_reactive_scope();
record_effect_read(self.id);
record_signal_read(self.id);
with_signal_value(self.id, f)
}
#[inline]
pub fn with_untracked<R>(&self, f: impl FnOnce(&T) -> R) -> R {
with_signal_value(self.id, f)
}
pub fn update_always<F: FnOnce(&mut T)>(&self, f: F) {
update_and_notify_always(self.id, f);
}
}
impl<T: Clone + 'static> RwSignal<T> {
#[inline]
#[cfg_attr(debug_assertions, track_caller)]
pub fn get(&self) -> T {
check_reactive_scope();
record_effect_read(self.id);
record_signal_read(self.id);
get_signal_value(self.id)
}
#[inline]
pub fn get_untracked(&self) -> T {
get_signal_value(self.id)
}
#[inline]
pub(crate) fn try_get_untracked(&self) -> Option<T> {
try_get_signal_value(self.id)
}
pub fn read_only(self) -> Signal<T> {
Signal {
id: self.id,
kind: SignalKind::Mutable,
_marker: PhantomData,
_not_send: PhantomData,
}
}
}
impl<T: Clone + PartialEq + 'static> RwSignal<T> {
pub fn set(&self, value: T) {
write_and_notify(self.id, value);
}
pub fn update<F: FnOnce(&mut T)>(&self, f: F) {
update_and_notify(self.id, f);
}
}
impl<T: Clone + 'static> RwSignal<T> {
pub fn set_always(&self, value: T) {
write_and_notify_always(self.id, value);
}
}
impl<T: Clone + Send + Sync + 'static> RwSignal<T> {
pub fn watch(&self) -> tokio::sync::watch::Receiver<T> {
let this = *self;
watch_value(this.get_untracked(), move || this.get())
}
}
impl<T: Clone + Send + 'static> RwSignal<T> {
pub fn writer(&self) -> WriteSignal<T> {
WriteSignal {
id: self.id,
epoch: current_write_epoch(),
_marker: PhantomData,
}
}
}
impl<T: Clone + 'static> From<RwSignal<T>> for Signal<T> {
fn from(rw: RwSignal<T>) -> Self {
rw.read_only()
}
}
pub struct WriteSignal<T> {
id: SignalId,
epoch: u64,
_marker: PhantomData<T>,
}
impl<T> Clone for WriteSignal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for WriteSignal<T> {}
impl<T: Clone + PartialEq + Send + 'static> WriteSignal<T> {
pub fn set(&self, value: T) {
if has_signal(self.id) {
write_and_notify(self.id, value);
} else {
let id = self.id;
let epoch = self.epoch;
queue_bg_write(epoch, move || {
if has_signal(id) {
write_and_notify(id, value);
} else {
log::debug!("dropping background write to disposed signal {id}");
}
});
}
}
pub fn update<F>(&self, f: F)
where
F: FnOnce(&mut T) + Send + 'static,
{
if has_signal(self.id) {
update_and_notify(self.id, f);
} else {
let id = self.id;
let epoch = self.epoch;
queue_bg_write(epoch, move || {
if has_signal(id) {
update_and_notify(id, f);
} else {
log::debug!("dropping background update to disposed signal {id}");
}
});
}
}
}
impl<T: Clone + Send + 'static> WriteSignal<T> {
pub fn set_always(&self, value: T) {
if has_signal(self.id) {
write_and_notify_always(self.id, value);
} else {
let id = self.id;
let epoch = self.epoch;
queue_bg_write(epoch, move || {
if has_signal(id) {
write_and_notify_always(id, value);
} else {
log::debug!("dropping background write to disposed signal {id}");
}
});
}
}
pub fn update_always<F>(&self, f: F)
where
F: FnOnce(&mut T) + Send + 'static,
{
if has_signal(self.id) {
update_and_notify_always(self.id, f);
} else {
let id = self.id;
let epoch = self.epoch;
queue_bg_write(epoch, move || {
if has_signal(id) {
update_and_notify_always(id, f);
} else {
log::debug!("dropping background update to disposed signal {id}");
}
});
}
}
}
pub fn create_signal<T: Clone + Send + 'static>(value: T) -> RwSignal<T> {
new_rw_signal(value)
}
pub(crate) fn new_rw_signal<T: 'static>(value: T) -> RwSignal<T> {
let id = create_signal_value(value);
with_runtime(|rt| rt.register_signal(id));
register_signal(id);
RwSignal {
id,
_marker: PhantomData,
_not_send: PhantomData,
}
}
pub fn create_stored<T: Clone + 'static>(value: T) -> Signal<T> {
let id = create_stored_value(value);
register_signal(id);
Signal {
id,
kind: SignalKind::Stored,
_marker: PhantomData,
_not_send: PhantomData,
}
}
pub fn create_derived<T: Clone + 'static>(f: impl Fn() -> T + 'static) -> Signal<T> {
let id = create_derived_value(f);
register_signal(id);
Signal {
id,
kind: SignalKind::Derived,
_marker: PhantomData,
_not_send: PhantomData,
}
}
fn watch_value<T: Clone + Send + Sync + 'static>(
initial: T,
read: impl Fn() -> T + 'static,
) -> tokio::sync::watch::Receiver<T> {
let (tx, rx) = tokio::sync::watch::channel(initial);
super::effect::create_effect(move || {
let _ = tx.send(read());
});
rx
}
#[cfg(test)]
mod watch_tests {
use super::*;
#[test]
fn a_watcher_sees_the_current_value_and_every_change() {
let flag = create_signal(false);
let rx = flag.watch();
assert!(!*rx.borrow());
flag.set(true);
assert!(*rx.borrow(), "the watcher follows the signal");
assert!(rx.has_changed().unwrap(), "and is notified, not polled");
}
#[test]
fn watching_a_memo_reports_only_real_changes() {
let count = create_signal(0);
let is_high = crate::reactive::create_memo(move || count.get() > 10);
let mut rx = is_high.watch();
assert!(!*rx.borrow_and_update());
count.set(5);
assert!(
!rx.has_changed().unwrap(),
"still false: nothing to wake for"
);
count.set(50);
assert!(rx.has_changed().unwrap());
assert!(*rx.borrow_and_update());
}
#[test]
fn disposing_the_scope_closes_the_channel() {
let (rx, owner) = crate::reactive::owner::with_owner(|| {
let flag = create_signal(false);
flag.watch()
});
assert!(!rx.borrow().to_owned());
crate::reactive::__internal::dispose_owner(owner);
assert!(
rx.has_changed().is_err(),
"a disposed scope must close the channel"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_signal_and_get() {
let signal = create_signal(42);
assert_eq!(signal.get(), 42);
}
#[test]
fn test_set_updates_value() {
let signal = create_signal(10);
signal.set(20);
assert_eq!(signal.get(), 20);
}
#[test]
fn test_update_with_closure() {
let signal = create_signal(5);
signal.update(|v| *v += 10);
assert_eq!(signal.get(), 15);
}
#[test]
fn test_with_for_borrowing() {
let signal = create_signal(String::from("hello"));
let length = signal.with(|s| s.len());
assert_eq!(length, 5);
}
#[test]
fn test_get_untracked() {
let signal = create_signal(100);
let value = signal.get_untracked();
assert_eq!(value, 100);
}
#[test]
fn test_clone_shares_underlying_value() {
let signal1 = create_signal(50);
let signal2 = signal1;
signal1.set(75);
assert_eq!(signal2.get(), 75);
signal2.set(100);
assert_eq!(signal1.get(), 100);
}
#[test]
fn test_with_untracked() {
let signal = create_signal(String::from("test"));
let result = signal.with_untracked(|s| format!("{}ing", s));
assert_eq!(result, "testing");
}
#[test]
fn test_update_only_triggers_on_change() {
let signal = create_signal(10);
signal.update(|v| *v = 10); assert_eq!(signal.get(), 10);
}
#[test]
fn test_set_only_triggers_on_change() {
let signal = create_signal(5);
signal.set(5); assert_eq!(signal.get(), 5);
signal.set(10); assert_eq!(signal.get(), 10);
}
#[test]
fn test_rw_signal_is_copy() {
let signal = create_signal(42);
let _copy1 = signal;
let _copy2 = signal;
assert_eq!(signal.get(), 42);
}
#[test]
fn test_rw_signal_to_signal() {
let rw = create_signal(42);
let read_only: Signal<i32> = rw.into();
assert_eq!(read_only.get(), 42);
rw.set(100);
assert_eq!(read_only.get(), 100);
}
#[test]
fn test_rw_signal_size() {
assert_eq!(std::mem::size_of::<RwSignal<i32>>(), 8);
assert_eq!(std::mem::size_of::<Signal<i32>>(), 12);
}
#[test]
fn test_stale_handle_does_not_alias_recycled_slot() {
use super::super::owner::{dispose_owner_now, with_owner};
let (stale, owner_id) = with_owner(|| create_signal(1_i32));
dispose_owner_now(owner_id);
let fresh = create_signal(2_i32);
assert_eq!(
stale.id.index(),
fresh.id.index(),
"slot should be recycled"
);
assert_ne!(stale.id.generation(), fresh.id.generation());
assert!(!has_signal(stale.id));
assert!(has_signal(fresh.id));
assert_eq!(fresh.get(), 2);
}
#[test]
fn test_writer_set_on_main_thread() {
let signal = create_signal(42);
let writer = signal.writer();
writer.set(100);
assert_eq!(signal.get(), 100);
}
#[test]
fn test_writer_update_on_main_thread() {
let signal = create_signal(10);
let writer = signal.writer();
writer.update(|v| *v += 5);
assert_eq!(signal.get(), 15);
}
#[test]
fn set_dedups_but_set_always_notifies() {
use crate::reactive::create_effect;
use std::cell::Cell;
use std::rc::Rc;
let sig = create_signal(7);
let runs = Rc::new(Cell::new(0));
let counter = runs.clone();
create_effect(move || {
let _ = sig.get();
counter.set(counter.get() + 1);
});
assert_eq!(runs.get(), 1);
sig.set(7); assert_eq!(runs.get(), 1, "set of an equal value must not notify");
sig.set_always(7); assert_eq!(runs.get(), 2, "set_always must notify unconditionally");
}
#[test]
fn non_partialeq_type_uses_always_writes() {
#[derive(Clone)]
struct Snapshot {
value: u32,
}
let sig = create_signal(Snapshot { value: 1 });
sig.set_always(Snapshot { value: 2 });
assert_eq!(sig.with(|s| s.value), 2);
sig.update_always(|s| s.value += 1);
assert_eq!(sig.with(|s| s.value), 3);
let w = sig.writer();
w.set_always(Snapshot { value: 10 });
assert_eq!(sig.with(|s| s.value), 10);
}
}