use crate::{
console_warn, create_isomorphic_effect, diagnostics, diagnostics::*,
macros::debug_warn, node::NodeId, on_cleanup, runtime::with_runtime,
Runtime,
};
use futures::Stream;
use std::{
any::Any,
cell::RefCell,
fmt,
hash::{Hash, Hasher},
marker::PhantomData,
pin::Pin,
rc::Rc,
};
use thiserror::Error;
macro_rules! impl_get_fn_traits {
($($ty:ident $(($method_name:ident))?),*) => {
$(
#[cfg(feature = "nightly")]
impl<T: Clone> FnOnce<()> for $ty<T> {
type Output = T;
#[inline(always)]
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
impl_get_fn_traits!(@method_name self $($method_name)?)
}
}
#[cfg(feature = "nightly")]
impl<T: Clone> FnMut<()> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
impl_get_fn_traits!(@method_name self $($method_name)?)
}
}
#[cfg(feature = "nightly")]
impl<T: Clone> Fn<()> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
impl_get_fn_traits!(@method_name self $($method_name)?)
}
}
)*
};
(@method_name $self:ident) => {
$self.get()
};
(@method_name $self:ident $ident:ident) => {
$self.$ident()
};
}
macro_rules! impl_set_fn_traits {
($($ty:ident $($method_name:ident)?),*) => {
$(
#[cfg(feature = "nightly")]
impl<T> FnOnce<(T,)> for $ty<T> {
type Output = ();
#[inline(always)]
extern "rust-call" fn call_once(self, args: (T,)) -> Self::Output {
impl_set_fn_traits!(@method_name self $($method_name)? args)
}
}
#[cfg(feature = "nightly")]
impl<T> FnMut<(T,)> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, args: (T,)) -> Self::Output {
impl_set_fn_traits!(@method_name self $($method_name)? args)
}
}
#[cfg(feature = "nightly")]
impl<T> Fn<(T,)> for $ty<T> {
#[inline(always)]
extern "rust-call" fn call(&self, args: (T,)) -> Self::Output {
impl_set_fn_traits!(@method_name self $($method_name)? args)
}
}
)*
};
(@method_name $self:ident $args:ident) => {
$self.set($args.0)
};
(@method_name $self:ident $ident:ident $args:ident) => {
$self.$ident($args.0)
};
}
impl_get_fn_traits![ReadSignal, RwSignal];
impl_set_fn_traits![WriteSignal];
pub mod prelude {
pub use super::*;
pub use crate::{
memo::*, selector::*, signal_wrappers_read::*, signal_wrappers_write::*,
};
}
pub trait SignalGet {
type Value;
#[track_caller]
fn get(&self) -> Self::Value;
fn try_get(&self) -> Option<Self::Value>;
}
pub trait SignalWith {
type Value;
#[track_caller]
fn with<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> O;
fn try_with<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> Option<O>;
fn track(&self) {
_ = self.try_with(|_| {});
}
}
pub trait SignalSet {
type Value;
#[track_caller]
fn set(&self, new_value: Self::Value);
fn try_set(&self, new_value: Self::Value) -> Option<Self::Value>;
}
pub trait SignalUpdate {
type Value;
#[track_caller]
fn update(&self, f: impl FnOnce(&mut Self::Value));
fn try_update<O>(&self, f: impl FnOnce(&mut Self::Value) -> O)
-> Option<O>;
}
pub trait SignalGetUntracked {
type Value;
#[track_caller]
fn get_untracked(&self) -> Self::Value;
fn try_get_untracked(&self) -> Option<Self::Value>;
}
pub trait SignalWithUntracked {
type Value;
#[track_caller]
fn with_untracked<O>(&self, f: impl FnOnce(&Self::Value) -> O) -> O;
#[track_caller]
fn try_with_untracked<O>(
&self,
f: impl FnOnce(&Self::Value) -> O,
) -> Option<O>;
}
pub trait SignalSetUntracked<T> {
#[track_caller]
fn set_untracked(&self, new_value: T);
#[track_caller]
fn try_set_untracked(&self, new_value: T) -> Option<T>;
}
pub trait SignalUpdateUntracked<T> {
#[track_caller]
fn update_untracked(&self, f: impl FnOnce(&mut T));
fn try_update_untracked<O>(&self, f: impl FnOnce(&mut T) -> O)
-> Option<O>;
}
pub trait SignalStream<T> {
#[track_caller]
fn to_stream(&self) -> Pin<Box<dyn Stream<Item = T>>>;
}
pub trait SignalDispose {
#[track_caller]
fn dispose(self);
}
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
pub fn create_signal<T>(value: T) -> (ReadSignal<T>, WriteSignal<T>) {
Runtime::current().create_signal(value)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn create_signal_from_stream<T>(
#[allow(unused_mut)] mut stream: impl Stream<Item = T> + Unpin + 'static,
) -> ReadSignal<Option<T>> {
cfg_if::cfg_if! {
if #[cfg(feature = "ssr")] {
_ = stream;
let (read, _) = create_signal(None);
read
} else {
use crate::spawn_local;
use futures::StreamExt;
let (read, write) = create_signal(None);
spawn_local(async move {
while let Some(value) = stream.next().await {
write.set(Some(value));
}
});
read
}
}
}
pub struct ReadSignal<T>
where
T: 'static,
{
pub(crate) id: NodeId,
pub(crate) ty: PhantomData<T>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T: Clone> SignalGetUntracked for ReadSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::get_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn get_untracked(&self) -> T {
match with_runtime(|runtime| {
self.id.try_with_no_subscription(runtime, T::clone)
})
.expect("runtime to be alive")
{
Ok(t) => t,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::try_get_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn try_get_untracked(&self) -> Option<T> {
with_runtime(|runtime| {
self.id.try_with_no_subscription(runtime, Clone::clone).ok()
})
.ok()
.flatten()
}
}
impl<T> SignalWithUntracked for ReadSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::with_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
self.with_no_subscription(f)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::try_with_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
match with_runtime(|runtime| {
self.id.try_with_no_subscription(runtime, f)
}) {
Ok(Ok(o)) => Some(o),
_ => None,
}
}
}
impl<T> SignalWith for ReadSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::with()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
let diagnostics = diagnostics!(self);
match with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
.expect("runtime to be alive")
{
Ok(o) => o,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::try_with()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics).ok())
.ok()
.flatten()
}
}
impl<T: Clone> SignalGet for ReadSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn get(&self) -> T {
let diagnostics = diagnostics!(self);
match with_runtime(|runtime| {
self.id.try_with(runtime, T::clone, diagnostics)
})
.expect("runtime to be alive")
{
Ok(t) => t,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::try_get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_get(&self) -> Option<T> {
self.try_with(Clone::clone).ok()
}
}
impl<T: Clone> SignalStream<T> for ReadSignal<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "ReadSignal::to_stream()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn to_stream(&self) -> Pin<Box<dyn Stream<Item = T>>> {
let (tx, rx) = futures::channel::mpsc::unbounded();
let close_channel = tx.clone();
on_cleanup(move || close_channel.close_channel());
let this = *self;
create_isomorphic_effect(move |_| {
let _ = tx.unbounded_send(this.get());
});
Box::pin(rx)
}
}
impl<T> SignalDispose for ReadSignal<T> {
fn dispose(self) {
_ = with_runtime(|runtime| runtime.dispose_node(self.id));
}
}
impl<T> ReadSignal<T>
where
T: 'static,
{
#[track_caller]
#[inline(always)]
pub(crate) fn with_no_subscription<U>(&self, f: impl FnOnce(&T) -> U) -> U {
#[cfg(debug_assertions)]
let caller = std::panic::Location::caller();
self.id
.try_with_no_subscription_by_id(f)
.unwrap_or_else(|_| {
#[cfg(not(debug_assertions))]
{
panic!("tried to access ReadSignal that has been disposed")
}
#[cfg(debug_assertions)]
{
panic!(
"at {}, tried to access ReadSignal<{}> defined at {}, \
but it has already been disposed",
caller,
std::any::type_name::<T>(),
self.defined_at
)
}
})
}
#[track_caller]
#[inline(always)]
pub(crate) fn try_with<U>(
&self,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError> {
let diagnostics = diagnostics!(self);
match with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
{
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(e),
Err(_) => Err(SignalError::RuntimeDisposed),
}
}
}
impl<T> Clone for ReadSignal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ReadSignal<T> {}
impl<T> fmt::Debug for ReadSignal<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("ReadSignal");
s.field("id", &self.id);
s.field("ty", &self.ty);
#[cfg(any(debug_assertions, feature = "ssr"))]
s.field("defined_at", &self.defined_at);
s.finish()
}
}
impl<T> Eq for ReadSignal<T> {}
impl<T> PartialEq for ReadSignal<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<T> Hash for ReadSignal<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
Runtime::current().hash(state);
self.id.hash(state);
}
}
pub struct WriteSignal<T>
where
T: 'static,
{
pub(crate) id: NodeId,
pub(crate) ty: PhantomData<T>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T> SignalSetUntracked<T> for WriteSignal<T>
where
T: 'static,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "WriteSignal::set_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn set_untracked(&self, new_value: T) {
self.id.update_with_no_effect(
|v| *v = new_value,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "WriteSignal::try_set_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_set_untracked(&self, new_value: T) -> Option<T> {
let mut new_value = Some(new_value);
self.id.update(
|t| *t = new_value.take().unwrap(),
#[cfg(debug_assertions)]
None,
);
new_value
}
}
impl<T> SignalUpdateUntracked<T> for WriteSignal<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "WriteSignal::updated_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
self.id.update_with_no_effect(
f,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
}
#[inline(always)]
fn try_update_untracked<O>(
&self,
f: impl FnOnce(&mut T) -> O,
) -> Option<O> {
self.id.update_with_no_effect(
f,
#[cfg(debug_assertions)]
None,
)
}
}
impl<T> SignalUpdate for WriteSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
name = "WriteSignal::update()",
level = "trace",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn update(&self, f: impl FnOnce(&mut T)) {
if self
.id
.update(
f,
#[cfg(debug_assertions)]
Some(self.defined_at),
)
.is_none()
{
warn_updating_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
);
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
name = "WriteSignal::try_update()",
level = "trace",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
self.id.update(
f,
#[cfg(debug_assertions)]
None,
)
}
}
impl<T> SignalSet for WriteSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "WriteSignal::set()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn set(&self, new_value: T) {
self.id.update(
|n| *n = new_value,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "WriteSignal::try_set()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_set(&self, new_value: T) -> Option<T> {
let mut new_value = Some(new_value);
self.id.update(
|t| *t = new_value.take().unwrap(),
#[cfg(debug_assertions)]
None,
);
new_value
}
}
impl<T> SignalDispose for WriteSignal<T> {
fn dispose(self) {
_ = with_runtime(|runtime| runtime.dispose_node(self.id));
}
}
impl<T> Clone for WriteSignal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for WriteSignal<T> {}
impl<T> fmt::Debug for WriteSignal<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("WriteSignal");
s.field("id", &self.id);
s.field("ty", &self.ty);
#[cfg(any(debug_assertions, feature = "ssr"))]
s.field("defined_at", &self.defined_at);
s.finish()
}
}
impl<T> Eq for WriteSignal<T> {}
impl<T> PartialEq for WriteSignal<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<T> Hash for WriteSignal<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
Runtime::current().hash(state);
self.id.hash(state);
}
}
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
pub fn create_rw_signal<T>(value: T) -> RwSignal<T> {
Runtime::current().create_rw_signal(value)
}
pub struct RwSignal<T>
where
T: 'static,
{
pub(crate) id: NodeId,
pub(crate) ty: PhantomData<T>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T: Default> Default for RwSignal<T> {
fn default() -> Self {
Self::new(Default::default())
}
}
impl<T> Clone for RwSignal<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for RwSignal<T> {}
impl<T> fmt::Debug for RwSignal<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("RwSignal");
s.field("id", &self.id);
s.field("ty", &self.ty);
#[cfg(any(debug_assertions, feature = "ssr"))]
s.field("defined_at", &self.defined_at);
s.finish()
}
}
impl<T> Eq for RwSignal<T> {}
impl<T> PartialEq for RwSignal<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<T> Hash for RwSignal<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
Runtime::current().hash(state);
self.id.hash(state);
}
}
impl<T> From<T> for RwSignal<T> {
fn from(value: T) -> Self {
create_rw_signal(value)
}
}
impl<T: Clone> SignalGetUntracked for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::get_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn get_untracked(&self) -> T {
#[cfg(debug_assertions)]
let caller = std::panic::Location::caller();
self.id
.try_with_no_subscription_by_id(Clone::clone)
.unwrap_or_else(|_| {
#[cfg(not(debug_assertions))]
{
panic!("tried to access RwSignal that has been disposed")
}
#[cfg(debug_assertions)]
{
panic!(
"at {}, tried to access RwSignal<{}> defined at {}, \
but it has already been disposed",
caller,
std::any::type_name::<T>(),
self.defined_at
)
}
})
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::try_get_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn try_get_untracked(&self) -> Option<T> {
with_runtime(|runtime| {
self.id.try_with_no_subscription(runtime, Clone::clone).ok()
})
.ok()
.flatten()
}
}
impl<T> SignalWithUntracked for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::with_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
self.id
.try_with_no_subscription_by_id(f)
.unwrap_or_else(|_| {
#[cfg(not(debug_assertions))]
{
panic!("tried to access RwSignal that has been disposed")
}
#[cfg(debug_assertions)]
{
panic!(
"tried to access RwSignal<{}> defined at {}, but it \
has already been disposed",
std::any::type_name::<T>(),
self.defined_at
)
}
})
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::try_with_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
match with_runtime(|runtime| {
self.id.try_with_no_subscription(runtime, f)
}) {
Ok(Ok(o)) => Some(o),
_ => None,
}
}
}
impl<T> SignalSetUntracked<T> for RwSignal<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::set_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn set_untracked(&self, new_value: T) {
self.id.update_with_no_effect(
|v| *v = new_value,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::try_set_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_set_untracked(&self, new_value: T) -> Option<T> {
let mut new_value = Some(new_value);
self.id.update(
|t| *t = new_value.take().unwrap(),
#[cfg(debug_assertions)]
None,
);
new_value
}
}
impl<T> SignalUpdateUntracked<T> for RwSignal<T> {
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
name = "RwSignal::update_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn update_untracked(&self, f: impl FnOnce(&mut T)) {
self.id.update_with_no_effect(
f,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::try_update_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn try_update_untracked<O>(
&self,
f: impl FnOnce(&mut T) -> O,
) -> Option<O> {
self.id.update_with_no_effect(
f,
#[cfg(debug_assertions)]
None,
)
}
}
impl<T> SignalWith for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::with()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
let diagnostics = diagnostics!(self);
match with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics))
.expect("runtime to be alive")
{
Ok(o) => o,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::try_with()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| self.id.try_with(runtime, f, diagnostics).ok())
.ok()
.flatten()
}
}
impl<T: Clone> SignalGet for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn get(&self) -> T
where
T: Clone,
{
let diagnostics = diagnostics!(self);
match with_runtime(|runtime| {
self.id.try_with(runtime, T::clone, diagnostics)
})
.expect("runtime to be alive")
{
Ok(t) => t,
Err(_) => panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::try_get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn try_get(&self) -> Option<T> {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| {
self.id.try_with(runtime, Clone::clone, diagnostics).ok()
})
.ok()
.flatten()
}
}
impl<T> SignalUpdate for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::update()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn update(&self, f: impl FnOnce(&mut T)) {
if self
.id
.update(
f,
#[cfg(debug_assertions)]
Some(self.defined_at),
)
.is_none()
{
warn_updating_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
);
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::try_update()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn try_update<O>(&self, f: impl FnOnce(&mut T) -> O) -> Option<O> {
self.id.update(
f,
#[cfg(debug_assertions)]
None,
)
}
}
impl<T> SignalSet for RwSignal<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::set()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn set(&self, value: T) {
self.id.update(
|n| *n = value,
#[cfg(debug_assertions)]
Some(self.defined_at),
);
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::try_set()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn try_set(&self, new_value: T) -> Option<T> {
let mut new_value = Some(new_value);
self.id.update(
|t| *t = new_value.take().unwrap(),
#[cfg(debug_assertions)]
None,
);
new_value
}
}
impl<T: Clone> SignalStream<T> for RwSignal<T> {
fn to_stream(&self) -> Pin<Box<dyn Stream<Item = T>>> {
let (tx, rx) = futures::channel::mpsc::unbounded();
let close_channel = tx.clone();
on_cleanup(move || close_channel.close_channel());
let this = *self;
create_isomorphic_effect(move |_| {
let _ = tx.unbounded_send(this.get());
});
Box::pin(rx)
}
}
impl<T> SignalDispose for RwSignal<T> {
fn dispose(self) {
_ = with_runtime(|runtime| runtime.dispose_node(self.id));
}
}
impl<T> RwSignal<T> {
#[inline(always)]
#[track_caller]
pub fn new(value: T) -> Self {
create_rw_signal(value)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::read_only()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
pub fn read_only(&self) -> ReadSignal<T> {
ReadSignal {
id: self.id,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::write_only()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
pub fn write_only(&self) -> WriteSignal<T> {
WriteSignal {
id: self.id,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "RwSignal::split()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
pub fn split(&self) -> (ReadSignal<T>, WriteSignal<T>) {
(
ReadSignal {
id: self.id,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
},
WriteSignal {
id: self.id,
ty: PhantomData,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: std::panic::Location::caller(),
},
)
}
}
#[derive(Debug, Error)]
pub(crate) enum SignalError {
#[error("tried to access a signal in a runtime that had been disposed")]
RuntimeDisposed,
#[error("tried to access a signal that had been disposed")]
Disposed,
#[error("error casting signal to type {0}")]
Type(&'static str),
}
impl NodeId {
#[track_caller]
pub(crate) fn subscribe(
&self,
runtime: &Runtime,
#[allow(unused)] diagnostics: AccessDiagnostics,
) {
if let Some(observer) = runtime.observer.get() {
let mut subs = runtime.node_subscribers.borrow_mut();
if let Some(subs) = subs.entry(*self) {
subs.or_default().borrow_mut().insert(observer);
}
let mut sources = runtime.node_sources.borrow_mut();
if let Some(sources) = sources.entry(observer) {
let sources = sources.or_default();
sources.borrow_mut().insert(*self);
}
} else {
#[cfg(all(debug_assertions, not(feature = "ssr")))]
{
if !SpecialNonReactiveZone::is_inside() {
let AccessDiagnostics {
called_at,
defined_at,
} = diagnostics;
crate::macros::debug_warn!(
"At {called_at}, you access a signal or memo (defined \
at {defined_at}) outside a reactive tracking \
context. This might mean your app is not responding \
to changes in signal values in the way you \
expect.\n\nHere’s how to fix it:\n\n1. If this is \
inside a `view!` macro, make sure you are passing a \
function, not a value.\n ❌ NO <p>{{x.get() * \
2}}</p>\n âś… YES <p>{{move || x.get() * \
2}}</p>\n\n2. If it’s in the body of a component, \
try wrapping this access in a closure: \n ❌ NO \
let y = x.get() * 2\n âś… YES let y = move || \
x.get() * 2.\n\n3. If you’re *trying* to access the \
value without tracking, use `.get_untracked()` or \
`.with_untracked()` instead."
);
}
}
}
}
fn try_with_no_subscription_inner(
&self,
runtime: &Runtime,
) -> Result<Rc<RefCell<dyn Any>>, SignalError> {
runtime.update_if_necessary(*self);
let nodes = runtime.nodes.borrow();
let node = nodes.get(*self).ok_or(SignalError::Disposed)?;
Ok(node.value())
}
#[inline(always)]
pub(crate) fn try_with_no_subscription_by_id<T, U>(
&self,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError>
where
T: 'static,
{
with_runtime(|runtime| self.try_with_no_subscription(runtime, f))
.expect("runtime to be alive")
}
#[track_caller]
#[inline(always)]
pub(crate) fn try_with_no_subscription<T, U>(
&self,
runtime: &Runtime,
f: impl FnOnce(&T) -> U,
) -> Result<U, SignalError>
where
T: 'static,
{
let value = self.try_with_no_subscription_inner(runtime)?;
let value = value.borrow();
let value = value
.downcast_ref::<T>()
.ok_or_else(|| SignalError::Type(std::any::type_name::<T>()))
.expect("to downcast signal type");
Ok(f(value))
}
#[track_caller]
#[inline(always)]
pub(crate) fn try_with<T, U>(
&self,
runtime: &Runtime,
f: impl FnOnce(&T) -> U,
diagnostics: AccessDiagnostics,
) -> Result<U, SignalError>
where
T: 'static,
{
self.subscribe(runtime, diagnostics);
self.try_with_no_subscription(runtime, f)
}
#[inline(always)]
#[track_caller]
fn update_value<T, U>(
&self,
f: impl FnOnce(&mut T) -> U,
#[cfg(debug_assertions)] defined_at: Option<
&'static std::panic::Location<'static>,
>,
) -> Option<U>
where
T: 'static,
{
#[cfg(debug_assertions)]
let location = std::panic::Location::caller();
with_runtime(|runtime| {
if let Some(value) = runtime.get_value(*self) {
let mut value = value.borrow_mut();
if let Some(value) = value.downcast_mut::<T>() {
Some(f(value))
} else {
debug_warn!(
"[Signal::update] failed when downcasting to \
Signal<{}>",
std::any::type_name::<T>()
);
None
}
} else {
#[cfg(debug_assertions)]
{
if let Some(defined_at) = defined_at {
debug_warn!(
"[Signal::update] At {:?}, you’re trying to \
update a Signal<{}> (defined at {defined_at}) \
that has already been disposed of. This is \
probably a logic error in a component that \
creates and disposes of scopes. If it does not \
cause any issues, it is safe to ignore this \
warning, which occurs only in debug mode.",
location,
std::any::type_name::<T>()
);
}
}
None
}
})
.unwrap_or_default()
}
#[inline(always)]
#[track_caller]
pub(crate) fn update<T, U>(
&self,
f: impl FnOnce(&mut T) -> U,
#[cfg(debug_assertions)] defined_at: Option<
&'static std::panic::Location<'static>,
>,
) -> Option<U>
where
T: 'static,
{
#[cfg(debug_assertions)]
let location = std::panic::Location::caller();
with_runtime(|runtime| {
let updated = if let Some(value) = runtime.get_value(*self) {
let mut value = value.borrow_mut();
if let Some(value) = value.downcast_mut::<T>() {
Some(f(value))
} else {
debug_warn!(
"[Signal::update] failed when downcasting to \
Signal<{}>",
std::any::type_name::<T>()
);
None
}
} else {
#[cfg(debug_assertions)]
{
if let Some(defined_at) = defined_at {
debug_warn!(
"[Signal::update] At {:?}, you’re trying to \
update a Signal<{}> (defined at {defined_at}) \
that has already been disposed of. This is \
probably a logic error in a component that \
creates and disposes of scopes. If it does not \
cause any issues, it is safe to ignore this \
warning, which occurs only in debug mode.",
location,
std::any::type_name::<T>()
);
}
}
None
};
if updated.is_some() {
runtime.mark_dirty(*self);
runtime.run_effects();
}
updated
})
.unwrap_or_default()
}
#[inline(always)]
pub(crate) fn update_with_no_effect<T, U>(
&self,
f: impl FnOnce(&mut T) -> U,
#[cfg(debug_assertions)] defined_at: Option<
&'static std::panic::Location<'static>,
>,
) -> Option<U>
where
T: 'static,
{
self.update_value(
f,
#[cfg(debug_assertions)]
defined_at,
)
}
}
#[cold]
#[inline(never)]
#[track_caller]
pub(crate) fn format_signal_warning(
msg: &str,
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: &'static std::panic::Location<'static>,
) -> String {
let location = std::panic::Location::caller();
let defined_at_msg = {
#[cfg(any(debug_assertions, feature = "ssr"))]
{
format!("signal created here: {defined_at}\n")
}
#[cfg(not(any(debug_assertions, feature = "ssr")))]
{
String::default()
}
};
format!("{msg}\n{defined_at_msg}warning happened here: {location}",)
}
#[cold]
#[inline(never)]
#[track_caller]
pub(crate) fn panic_getting_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: &'static std::panic::Location<'static>,
) -> ! {
panic!(
"{}",
format_signal_warning(
"Attempted to get a signal after it was disposed.",
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at,
)
)
}
#[cold]
#[inline(never)]
#[track_caller]
pub(crate) fn warn_updating_dead_signal(
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: &'static std::panic::Location<'static>,
) {
console_warn(&format_signal_warning(
"Attempted to update a signal after it was disposed.",
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at,
));
}