docs: runtime warning if you use .track() outside a tracking context

This commit is contained in:
Greg Johnston 2024-05-11 16:31:43 -04:00
parent 5bc97654dc
commit dcec7af4f3
11 changed files with 134 additions and 30 deletions

View file

@ -22,6 +22,9 @@ thiserror = "1"
tracing = { version = "0.1", optional = true }
guardian = "1"
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
web-sys = "0.3"
[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "time", "macros"] }
any_spawner = { workspace = true, features = ["tokio"] }

View file

@ -132,6 +132,8 @@ impl<T: Send + Sync + 'static> ToAnySource for ArcMemo<T> {
AnySource(
Arc::as_ptr(&self.inner) as usize,
Arc::downgrade(&self.inner) as Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)]
self.defined_at,
)
}
}

View file

@ -233,6 +233,8 @@ impl<T: 'static> ToAnySource for ArcAsyncDerived<T> {
AnySource(
Arc::as_ptr(&self.inner) as usize,
Arc::downgrade(&self.inner) as Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)]
self.defined_at,
)
}
}

View file

@ -93,6 +93,7 @@ impl<T: Send + Sync + 'static> DefinedAt for Memo<T> {
}
impl<T: Send + Sync + 'static> Track for Memo<T> {
#[track_caller]
fn track(&self) {
if let Some(inner) = self.inner.get() {
inner.track();

View file

@ -33,9 +33,6 @@ thread_local! {
}
impl SpecialNonReactiveZone {
// TODO: the fact that this is unused probably means we haven't set diagnostics up at all
// we should do that! (i.e., warn if you're doing a reactive access with no owner but you're not
// inside a special zone)
#[inline(always)]
pub(crate) fn is_inside() -> bool {
if cfg!(debug_assertions) {

View file

@ -1,6 +1,7 @@
use super::{node::ReactiveNode, AnySubscriber};
use crate::traits::DefinedAt;
use core::{fmt::Debug, hash::Hash};
use std::sync::Weak;
use std::{panic::Location, sync::Weak};
pub trait ToAnySource {
/// Converts this type to its type-erased equivalent.
@ -20,7 +21,24 @@ pub trait Source: ReactiveNode {
}
#[derive(Clone)]
pub struct AnySource(pub usize, pub Weak<dyn Source + Send + Sync>);
pub struct AnySource(
pub(crate) usize,
pub(crate) Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)] pub(crate) &'static Location<'static>,
);
impl DefinedAt for AnySource {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.2)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl Debug for AnySource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View file

@ -3,39 +3,50 @@ use core::{fmt::Debug, hash::Hash};
use std::{cell::RefCell, mem, sync::Weak};
thread_local! {
// TODO this can be a Cell
static OBSERVER: RefCell<Option<AnySubscriber>> = const { RefCell::new(None) };
}
pub struct Observer;
struct SetObserverOnDrop(Option<AnySubscriber>);
impl Drop for SetObserverOnDrop {
fn drop(&mut self) {
Observer::set(self.0.take());
}
}
impl Observer {
pub fn get() -> Option<AnySubscriber> {
OBSERVER.with(|o| o.borrow().clone())
OBSERVER.with_borrow(Clone::clone)
}
pub(crate) fn is(observer: &AnySubscriber) -> bool {
OBSERVER.with(|o| o.borrow().as_ref() == Some(observer))
OBSERVER.with_borrow(|o| o.as_ref() == Some(observer))
}
fn take() -> Option<AnySubscriber> {
OBSERVER.with(|o| o.borrow_mut().take())
fn take() -> SetObserverOnDrop {
SetObserverOnDrop(OBSERVER.with_borrow_mut(Option::take))
}
fn set(observer: Option<AnySubscriber>) {
OBSERVER.with(|o| *o.borrow_mut() = observer);
OBSERVER.with_borrow_mut(|o| *o = observer);
}
fn replace(observer: AnySubscriber) -> Option<AnySubscriber> {
OBSERVER.with(|o| mem::replace(&mut *o.borrow_mut(), Some(observer)))
fn replace(observer: AnySubscriber) -> SetObserverOnDrop {
SetObserverOnDrop(
OBSERVER
.with(|o| mem::replace(&mut *o.borrow_mut(), Some(observer))),
)
}
}
pub fn untrack<T>(fun: impl FnOnce() -> T) -> T {
let prev = Observer::take();
let value = fun();
Observer::set(prev);
value
#[cfg(debug_assertions)]
let _warning_guard = crate::diagnostics::SpecialNonReactiveZone::enter();
let _prev = Observer::take();
fun()
}
/// Converts a [`Subscriber`] to a type-erased [`AnySubscriber`].
@ -107,10 +118,8 @@ impl ReactiveNode for AnySubscriber {
impl AnySubscriber {
pub fn with_observer<T>(&self, fun: impl FnOnce() -> T) -> T {
let prev = Observer::replace(self.clone());
let val = fun();
Observer::set(prev);
val
let _prev = Observer::replace(self.clone());
fun()
}
}

View file

@ -70,7 +70,7 @@
#![cfg_attr(feature = "nightly", feature(fn_traits))]
use futures::Stream;
use std::{future::Future, pin::Pin};
use std::{fmt::Arguments, future::Future, pin::Pin};
pub mod actions;
pub(crate) mod channel;
@ -98,3 +98,25 @@ pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
pub mod prelude {
pub use crate::traits::*;
}
fn log_warning(text: Arguments) {
#[cfg(feature = "tracing")]
{
tracing::warn!(text);
}
#[cfg(all(
not(feature = "tracing"),
target_arch = "wasm32",
target_os = "unknown"
))]
{
web_sys::console::warn_1(&text.to_string().into());
}
#[cfg(all(
not(feature = "tracing"),
not(all(target_arch = "wasm32", target_os = "unknown"))
))]
{
eprintln!("{}", text);
}
}

View file

@ -31,10 +31,7 @@ impl<T: Send + Sync + 'static> Dispose for RwSignal<T> {
}
impl<T: Send + Sync + 'static> RwSignal<T> {
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
)]
#[track_caller]
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
@ -44,6 +41,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
}
#[inline(always)]
#[track_caller]
pub fn read_only(&self) -> ReadSignal<T> {
ReadSignal {
#[cfg(debug_assertions)]
@ -58,6 +56,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
}
#[inline(always)]
#[track_caller]
pub fn write_only(&self) -> WriteSignal<T> {
WriteSignal {
#[cfg(debug_assertions)]
@ -71,6 +70,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
}
}
#[track_caller]
#[inline(always)]
pub fn split(&self) -> (ReadSignal<T>, WriteSignal<T>) {
(self.read_only(), self.write_only())

View file

@ -99,6 +99,8 @@ where
AnySource(
Arc::as_ptr(subs) as usize,
Arc::downgrade(subs) as Weak<dyn Source + Send + Sync>,
#[cfg(debug_assertions)]
self.defined_at().expect("no DefinedAt in debug mode"),
)
})
.unwrap_or_else(unwrap_signal!(self))

View file

@ -89,12 +89,40 @@ pub trait Track {
fn track(&self);
}
impl<T: Source + ToAnySource> Track for T {
impl<T: Source + ToAnySource + DefinedAt> Track for T {
#[track_caller]
fn track(&self) {
if let Some(subscriber) = Observer::get() {
subscriber.add_source(self.to_any_source());
self.add_subscriber(subscriber);
} else {
#[cfg(debug_assertions)]
{
use crate::diagnostics::SpecialNonReactiveZone;
//if !SpecialNonReactiveZone::is_inside() {
let called_at = Location::caller();
let ty = std::any::type_name::<T>();
let defined_at = self
.defined_at()
.map(ToString::to_string)
.unwrap_or_else(|| String::from("{unknown}"));
crate::log_warning(format_args!(
"At {called_at}, you access a {ty} (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\nHeres 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 its 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 youre *trying* to access the value without tracking, \
use `.get_untracked()` or `.with_untracked()` instead."
));
//}
}
}
}
}
@ -254,6 +282,7 @@ where
{
type Value = <T as With>::Value;
#[track_caller]
fn try_get(&self) -> Option<Self::Value> {
self.try_with(Self::Value::clone)
}
@ -266,6 +295,7 @@ pub trait Trigger {
pub trait UpdateUntracked: DefinedAt {
type Value;
#[track_caller]
fn update_untracked<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
@ -286,6 +316,7 @@ where
{
type Value = <Self as Writeable>::Value;
#[track_caller]
fn try_update_untracked<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
@ -298,10 +329,12 @@ where
pub trait Update {
type Value;
#[track_caller]
fn update(&self, fun: impl FnOnce(&mut Self::Value)) {
self.try_update(fun);
}
#[track_caller]
fn maybe_update(&self, fun: impl FnOnce(&mut Self::Value) -> bool) {
self.try_maybe_update(|val| {
let did_update = fun(val);
@ -309,6 +342,7 @@ pub trait Update {
});
}
#[track_caller]
fn try_update<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
@ -328,6 +362,7 @@ where
{
type Value = <Self as UpdateUntracked>::Value;
#[track_caller]
fn try_maybe_update<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> (bool, U),
@ -340,11 +375,26 @@ where
}
}
pub trait Set: Update + IsDisposed {
pub trait Set {
type Value;
fn set(&self, value: impl Into<Self::Value>);
fn try_set(&self, value: impl Into<Self::Value>) -> Option<Self::Value>;
}
impl<T> Set for T
where
T: Update + IsDisposed,
{
type Value = <Self as Update>::Value;
#[track_caller]
fn set(&self, value: impl Into<Self::Value>) {
self.update(|n| *n = value.into());
}
#[track_caller]
fn try_set(&self, value: impl Into<Self::Value>) -> Option<Self::Value> {
if self.is_disposed() {
Some(value.into())
@ -355,8 +405,6 @@ pub trait Set: Update + IsDisposed {
}
}
impl<T> Set for T where T: Update + IsDisposed {}
pub trait IsDisposed {
fn is_disposed(&self) -> bool;
}