use crate::{
create_isomorphic_effect, diagnostics::AccessDiagnostics, node::NodeId,
on_cleanup, with_runtime, AnyComputation, Runtime, SignalDispose,
SignalGet, SignalGetUntracked, SignalStream, SignalWith,
SignalWithUntracked,
};
use std::{any::Any, cell::RefCell, fmt, marker::PhantomData, rc::Rc};
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
Runtime::current().create_owning_memo(move |current_value| {
let new_value = f(current_value.as_ref());
let is_different = current_value.as_ref() != Some(&new_value);
(new_value, is_different)
})
}
#[cfg_attr(
any(debug_assertions, feature="ssr"),
instrument(
level = "trace",
skip_all,
fields(
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
pub fn create_owning_memo<T>(
f: impl Fn(Option<T>) -> (T, bool) + 'static,
) -> Memo<T>
where
T: 'static,
{
Runtime::current().create_owning_memo(f)
}
pub struct Memo<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> Memo<T> {
#[inline(always)]
#[track_caller]
pub fn new(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + 'static,
{
create_memo(f)
}
#[inline(always)]
#[track_caller]
pub fn new_owning(f: impl Fn(Option<T>) -> (T, bool) + 'static) -> Memo<T>
where
T: 'static,
{
create_owning_memo(f)
}
}
impl<T> Clone for Memo<T>
where
T: 'static,
{
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Memo<T> {}
impl<T> fmt::Debug for Memo<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Memo");
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 Memo<T> {}
impl<T> PartialEq for Memo<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
fn forward_ref_to<T, O, F: FnOnce(&T) -> O>(
f: F,
) -> impl FnOnce(&Option<T>) -> O {
|maybe_value: &Option<T>| {
let ref_t = maybe_value
.as_ref()
.expect("invariant: must have already been initialized");
f(ref_t)
}
}
impl<T: Clone> SignalGetUntracked for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::get_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn get_untracked(&self) -> T {
with_runtime(move |runtime| {
let f = |maybe_value: &Option<T>| {
maybe_value
.clone()
.expect("invariant: must have already been initialized")
};
match self.id.try_with_no_subscription(runtime, f) {
Ok(t) => t,
Err(_) => panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
})
.expect("runtime to be alive")
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::try_get_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline(always)]
fn try_get_untracked(&self) -> Option<T> {
self.try_with_untracked(T::clone)
}
}
impl<T> SignalWithUntracked for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::with_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O {
with_runtime(|runtime| {
match self.id.try_with_no_subscription(runtime, forward_ref_to(f)) {
Ok(t) => t,
Err(_) => panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
})
.expect("runtime to be alive")
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::try_with_untracked()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[inline]
fn try_with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
with_runtime(|runtime| {
self.id
.try_with_no_subscription(runtime, |v: &Option<T>| {
v.as_ref().map(f)
})
.ok()
.flatten()
})
.ok()
.flatten()
}
}
impl<T: Clone> SignalGet for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
name = "Memo::get()",
level = "trace",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn get(&self) -> T {
self.with(T::clone)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::try_get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
#[inline(always)]
fn try_get(&self) -> Option<T> {
self.try_with(T::clone)
}
}
impl<T> SignalWith for Memo<T> {
type Value = T;
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::with()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
match self.try_with(f) {
Some(t) => t,
None => panic_getting_dead_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
self.defined_at,
),
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::try_with()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
#[track_caller]
fn try_with<O>(&self, f: impl FnOnce(&T) -> O) -> Option<O> {
let diagnostics = diagnostics!(self);
with_runtime(|runtime| {
self.id.subscribe(runtime, diagnostics);
self.id
.try_with_no_subscription(runtime, forward_ref_to(f))
.ok()
})
.ok()
.flatten()
}
}
impl<T: Clone> SignalStream<T> for Memo<T> {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
level = "trace",
name = "Memo::to_stream()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn to_stream(&self) -> std::pin::Pin<Box<dyn futures::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 Memo<T> {
fn dispose(self) {
_ = with_runtime(|runtime| runtime.dispose_node(self.id));
}
}
impl_get_fn_traits![Memo];
pub(crate) struct MemoState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> (T, bool),
{
pub f: F,
pub t: PhantomData<T>,
#[cfg(any(debug_assertions, feature = "ssr"))]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl<T, F> AnyComputation for MemoState<T, F>
where
T: 'static,
F: Fn(Option<T>) -> (T, bool),
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(
name = "Memo::run()",
level = "trace",
skip_all,
fields(
defined_at = %self.defined_at,
ty = %std::any::type_name::<T>()
)
)
)]
fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
let mut value = value.borrow_mut();
let curr_value = value
.downcast_mut::<Option<T>>()
.expect("to downcast memo value");
let (new_value, is_different) = (self.f)(curr_value.take());
*curr_value = Some(new_value);
is_different
}
}
#[cold]
#[inline(never)]
#[track_caller]
fn format_memo_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_memo(
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at: &'static std::panic::Location<'static>,
) -> ! {
panic!(
"{}",
format_memo_warning(
"Attempted to get a memo after it was disposed.",
#[cfg(any(debug_assertions, feature = "ssr"))]
defined_at,
)
)
}