From dac458919447e168cb70ddd2fb6a5829eef2cb3f Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 12 Jul 2024 15:58:15 -0400 Subject: [PATCH] docs: finish reactive graph docs for 0.7 --- reactive_graph/src/actions/action.rs | 2 +- reactive_graph/src/actions/multi_action.rs | 2 +- reactive_graph/src/computed/arc_memo.rs | 48 +++--- .../async_derived/arc_async_derived.rs | 42 +++++- .../computed/async_derived/async_derived.rs | 83 ++++++++++ .../computed/async_derived/future_impls.rs | 4 + reactive_graph/src/computed/memo.rs | 33 ++++ reactive_graph/src/computed/selector.rs | 49 ++++++ reactive_graph/src/effect/effect.rs | 73 +++++++++ reactive_graph/src/effect/render_effect.rs | 18 ++- reactive_graph/src/graph/node.rs | 6 + reactive_graph/src/graph/source.rs | 2 + reactive_graph/src/graph/subscriber.rs | 37 ++++- reactive_graph/src/lib.rs | 2 +- reactive_graph/src/owner/stored_value.rs | 56 ++++++- reactive_graph/src/signal.rs | 8 +- reactive_graph/src/signal/arc_read.rs | 25 +-- reactive_graph/src/signal/arc_rw.rs | 5 +- reactive_graph/src/signal/arc_write.rs | 5 +- reactive_graph/src/signal/guards.rs | 13 ++ reactive_graph/src/signal/read.rs | 4 +- reactive_graph/src/signal/rw.rs | 6 +- reactive_graph/src/signal/write.rs | 4 +- reactive_graph/src/traits.rs | 4 +- reactive_graph/src/wrappers.rs | 142 +++++++++++++++++- 25 files changed, 603 insertions(+), 70 deletions(-) diff --git a/reactive_graph/src/actions/action.rs b/reactive_graph/src/actions/action.rs index 546679df2..1fbb5fa3d 100644 --- a/reactive_graph/src/actions/action.rs +++ b/reactive_graph/src/actions/action.rs @@ -335,7 +335,7 @@ where /// Creates a new action that will only run on the current thread, initializing it with the given value. /// - /// In all other ways, this is identical to [`ArcAsync::new_with_value`]. + /// In all other ways, this is identical to [`ArcAction::new_with_value`]. #[track_caller] pub fn new_unsync_with_value(value: Option, action_fn: F) -> Self where diff --git a/reactive_graph/src/actions/multi_action.rs b/reactive_graph/src/actions/multi_action.rs index da1ccbe76..3ea11f5a2 100644 --- a/reactive_graph/src/actions/multi_action.rs +++ b/reactive_graph/src/actions/multi_action.rs @@ -622,7 +622,7 @@ where } } -/// An action that has been submitted by dispatching it to a [MultiAction](crate::MultiAction). +/// An action that has been submitted by dispatching it to a [`MultiAction`]. #[derive(Debug, PartialEq, Eq, Hash)] pub struct ArcSubmission where diff --git a/reactive_graph/src/computed/arc_memo.rs b/reactive_graph/src/computed/arc_memo.rs index a499b9841..c781ffcf8 100644 --- a/reactive_graph/src/computed/arc_memo.rs +++ b/reactive_graph/src/computed/arc_memo.rs @@ -21,7 +21,7 @@ use std::{ /// An efficient derived reactive value based on other reactive values. /// /// This is a reference-counted memo, which is `Clone` but not `Copy`. -/// For arena-allocated `Copy` memos, use [`Memo`]. +/// For arena-allocated `Copy` memos, use [`Memo`](super::Memo). /// /// Unlike a "derived signal," a memo comes with two guarantees: /// 1. The memo will only run *once* per change, no matter how many times you @@ -34,31 +34,9 @@ use std::{ /// create a derived signal. But if the derivation calculation is expensive, you should /// create a memo. /// -/// As with an [`Effect`](crate::effects::Effect), the argument to the memo function is the previous value, +/// As with an [`Effect`](crate::effect::Effect), the argument to the memo function is the previous value, /// i.e., the current value of the memo, which will be `None` for the initial calculation. /// -/// ## Core Trait Implementations -/// - [`.get()`](crate::traits::Get) clones the current value of the memo. -/// If you call it within an effect, it will cause that effect to subscribe -/// to the memo, and to re-run whenever the value of the memo changes. -/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of -/// the memo without reactively tracking it. -/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the -/// value of the memo by reference. If you call it within an effect, it will -/// cause that effect to subscribe to the memo, and to re-run whenever the -/// value of the memo changes. -/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the -/// current value of the memo without reactively tracking it. -/// - [`.with()`](crate::traits::With) allows you to reactively access the memo’s -/// value without cloning by applying a callback function. -/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access -/// the signal’s value by applying a callback function without reactively -/// tracking it. -/// - [`.to_stream()`](crate::traits::ToStream) converts the memo to an `async` -/// stream of values. -/// - [`::from_stream()`](crate::traits::FromStream) converts an `async` stream -/// of values into a memo containing the latest value. -/// /// ## Examples /// ``` /// # use reactive_graph::prelude::*; @@ -88,6 +66,28 @@ use std::{ /// // ✅ reads the current value **without re-running the calculation** /// let some_value = memoized.get(); /// ``` +/// +/// ## Core Trait Implementations +/// - [`.get()`](crate::traits::Get) clones the current value of the memo. +/// If you call it within an effect, it will cause that effect to subscribe +/// to the memo, and to re-run whenever the value of the memo changes. +/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of +/// the memo without reactively tracking it. +/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the +/// value of the memo by reference. If you call it within an effect, it will +/// cause that effect to subscribe to the memo, and to re-run whenever the +/// value of the memo changes. +/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the +/// current value of the memo without reactively tracking it. +/// - [`.with()`](crate::traits::With) allows you to reactively access the memo’s +/// value without cloning by applying a callback function. +/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access +/// the memo’s value by applying a callback function without reactively +/// tracking it. +/// - [`.to_stream()`](crate::traits::ToStream) converts the memo to an `async` +/// stream of values. +/// - [`::from_stream()`](crate::traits::FromStream) converts an `async` stream +/// of values into a memo containing the latest value. pub struct ArcMemo { #[cfg(debug_assertions)] defined_at: &'static Location<'static>, diff --git a/reactive_graph/src/computed/async_derived/arc_async_derived.rs b/reactive_graph/src/computed/async_derived/arc_async_derived.rs index f6f76364d..b77768df8 100644 --- a/reactive_graph/src/computed/async_derived/arc_async_derived.rs +++ b/reactive_graph/src/computed/async_derived/arc_async_derived.rs @@ -38,6 +38,9 @@ use std::{ /// When one of its dependencies changes, this will re-run its async computation, then notify other /// values that depend on it that it has changed. /// +/// This is a reference-counted type, which is `Clone` but not `Copy`. +/// For arena-allocated `Copy` memos, use [`AsyncDerived`](super::AsyncDerived). +/// /// ## Examples /// ```rust /// # use reactive_graph::computed::*; @@ -71,9 +74,29 @@ use std::{ /// /// // setting multiple dependencies will hold until the latest change is ready /// signal2.set(1); -/// assert_eq!(derived.clone().await, 2); +/// assert_eq!(derived.await, 2); /// # }); /// ``` +/// +/// ## Core Trait Implementations +/// - [`.get()`](crate::traits::Get) clones the current value as an `Option`. +/// If you call it within an effect, it will cause that effect to subscribe +/// to the memo, and to re-run whenever the value of the memo changes. +/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of +/// without reactively tracking it. +/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the +/// value by reference. If you call it within an effect, it will +/// cause that effect to subscribe to the memo, and to re-run whenever the +/// value changes. +/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the +/// current value without reactively tracking it. +/// - [`.with()`](crate::traits::With) allows you to reactively access the +/// value without cloning by applying a callback function. +/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access +/// the value by applying a callback function without reactively +/// tracking it. +/// - [`IntoFuture`](std::future::Future) allows you to create a [`Future`] that resolves +/// when this resource is done loading. pub struct ArcAsyncDerived { #[cfg(debug_assertions)] pub(crate) defined_at: &'static Location<'static>, @@ -310,6 +333,10 @@ macro_rules! spawn_derived { } impl ArcAsyncDerived { + /// Creates a new async derived computation. + /// + /// This runs eagerly: i.e., calls `fun` once when created and immediately spawns the `Future` + /// as a new task. #[track_caller] pub fn new(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self where @@ -319,6 +346,9 @@ impl ArcAsyncDerived { Self::new_with_initial(None, fun) } + /// Creates a new async derived computation with an initial value. + /// + /// If the initial value is `Some(_)`, the task will not be run initially. #[track_caller] pub fn new_with_initial( initial_value: Option, @@ -332,6 +362,11 @@ impl ArcAsyncDerived { this } + /// Creates a new async derived computation that will be guaranteed to run on the current + /// thread. + /// + /// This runs eagerly: i.e., calls `fun` once when created and immediately spawns the `Future` + /// as a new task. #[track_caller] pub fn new_unsync(fun: impl Fn() -> Fut + 'static) -> Self where @@ -341,6 +376,10 @@ impl ArcAsyncDerived { Self::new_unsync_with_initial(None, fun) } + /// Creates a new async derived computation with an initial value. Async work will be + /// guaranteed to run only on the current thread. + /// + /// If the initial value is `Some(_)`, the task will not be run initially. #[track_caller] pub fn new_unsync_with_initial( initial_value: Option, @@ -355,6 +394,7 @@ impl ArcAsyncDerived { this } + /// Returns a `Future` that is ready when this resource has next finished loading. pub fn ready(&self) -> AsyncDerivedReadyFuture { AsyncDerivedReadyFuture { source: self.to_any_source(), diff --git a/reactive_graph/src/computed/async_derived/async_derived.rs b/reactive_graph/src/computed/async_derived/async_derived.rs index bcfffd88f..29f2926b8 100644 --- a/reactive_graph/src/computed/async_derived/async_derived.rs +++ b/reactive_graph/src/computed/async_derived/async_derived.rs @@ -12,6 +12,72 @@ use crate::{ use core::fmt::Debug; use std::{future::Future, panic::Location}; +/// A reactive value that is derived by running an asynchronous computation in response to changes +/// in its sources. +/// +/// When one of its dependencies changes, this will re-run its async computation, then notify other +/// values that depend on it that it has changed. +/// +/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive +/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas +/// as long as a reference to it is alive, see [`ArcAsyncDerived`]. +/// +/// ## Examples +/// ```rust +/// # use reactive_graph::computed::*; +/// # use reactive_graph::signal::*; +/// # use reactive_graph::prelude::*; +/// # tokio_test::block_on(async move { +/// # any_spawner::Executor::init_tokio(); +/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter(); +/// +/// let signal1 = RwSignal::new(0); +/// let signal2 = RwSignal::new(0); +/// let derived = AsyncDerived::new(move || async move { +/// // reactive values can be tracked anywhere in the `async` block +/// let value1 = signal1.get(); +/// tokio::time::sleep(std::time::Duration::from_millis(25)).await; +/// let value2 = signal2.get(); +/// +/// value1 + value2 +/// }); +/// +/// // the value can be accessed synchronously as `Option` +/// assert_eq!(derived.get(), None); +/// // we can also .await the value, i.e., convert it into a Future +/// assert_eq!(derived.await, 0); +/// assert_eq!(derived.get(), Some(0)); +/// +/// signal1.set(1); +/// // while the new value is still pending, the signal holds the old value +/// tokio::time::sleep(std::time::Duration::from_millis(5)).await; +/// assert_eq!(derived.get(), Some(0)); +/// +/// // setting multiple dependencies will hold until the latest change is ready +/// signal2.set(1); +/// assert_eq!(derived.await, 2); +/// # }); +/// ``` +/// +/// ## Core Trait Implementations +/// - [`.get()`](crate::traits::Get) clones the current value as an `Option`. +/// If you call it within an effect, it will cause that effect to subscribe +/// to the memo, and to re-run whenever the value of the memo changes. +/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of +/// without reactively tracking it. +/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the +/// value by reference. If you call it within an effect, it will +/// cause that effect to subscribe to the memo, and to re-run whenever the +/// value changes. +/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the +/// current value without reactively tracking it. +/// - [`.with()`](crate::traits::With) allows you to reactively access the +/// value without cloning by applying a callback function. +/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access +/// the value by applying a callback function without reactively +/// tracking it. +/// - [`IntoFuture`](std::future::Future) allows you to create a [`Future`] that resolves +/// when this resource is done loading. pub struct AsyncDerived { #[cfg(debug_assertions)] defined_at: &'static Location<'static>, @@ -37,6 +103,10 @@ impl From> for AsyncDerived { } impl AsyncDerived { + /// Creates a new async derived computation. + /// + /// This runs eagerly: i.e., calls `fun` once when created and immediately spawns the `Future` + /// as a new task. #[track_caller] pub fn new(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self where @@ -50,6 +120,9 @@ impl AsyncDerived { } } + /// Creates a new async derived computation with an initial value. + /// + /// If the initial value is `Some(_)`, the task will not be run initially. pub fn new_with_initial( initial_value: Option, fun: impl Fn() -> Fut + Send + Sync + 'static, @@ -68,6 +141,11 @@ impl AsyncDerived { } } + /// Creates a new async derived computation that will be guaranteed to run on the current + /// thread. + /// + /// This runs eagerly: i.e., calls `fun` once when created and immediately spawns the `Future` + /// as a new task. pub fn new_unsync(fun: impl Fn() -> Fut + 'static) -> Self where T: 'static, @@ -80,6 +158,10 @@ impl AsyncDerived { } } + /// Creates a new async derived computation with an initial value. Async work will be + /// guaranteed to run only on the current thread. + /// + /// If the initial value is `Some(_)`, the task will not be run initially. pub fn new_unsync_with_initial( initial_value: Option, fun: impl Fn() -> Fut + 'static, @@ -98,6 +180,7 @@ impl AsyncDerived { } } + /// Returns a `Future` that is ready when this resource has next finished loading. #[track_caller] pub fn ready(&self) -> AsyncDerivedReadyFuture { let this = self.inner.get().unwrap_or_else(unwrap_signal!(self)); diff --git a/reactive_graph/src/computed/async_derived/future_impls.rs b/reactive_graph/src/computed/async_derived/future_impls.rs index 722735792..c48e0bdc4 100644 --- a/reactive_graph/src/computed/async_derived/future_impls.rs +++ b/reactive_graph/src/computed/async_derived/future_impls.rs @@ -111,6 +111,8 @@ where } impl ArcAsyncDerived { + /// Returns a `Future` that resolves when the computation is finished, and accesses the inner + /// value by reference rather than by cloning it. #[track_caller] pub fn by_ref(&self) -> AsyncDerivedRefFuture { AsyncDerivedRefFuture { @@ -123,6 +125,8 @@ impl ArcAsyncDerived { } impl AsyncDerived { + /// Returns a `Future` that resolves when the computation is finished, and accesses the inner + /// value by reference rather than by cloning it. #[track_caller] pub fn by_ref(&self) -> AsyncDerivedRefFuture { let this = self.inner.get().unwrap_or_else(unwrap_signal!(self)); diff --git a/reactive_graph/src/computed/memo.rs b/reactive_graph/src/computed/memo.rs index ba02e6b01..0b113e561 100644 --- a/reactive_graph/src/computed/memo.rs +++ b/reactive_graph/src/computed/memo.rs @@ -26,6 +26,10 @@ use std::{fmt::Debug, hash::Hash, panic::Location}; /// Memos are lazy: they do not run at all until they are read for the first time, and they will /// not re-run the calculation when a source signal changes until they are read again. /// +/// This is an arena-allocated type, which is `Copy` and is disposed when its reactive +/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that livesas +/// as long as a reference to it is alive, see [`ArcMemo`]. +/// /// ``` /// # use reactive_graph::prelude::*; /// # use reactive_graph::computed::Memo; @@ -70,6 +74,28 @@ use std::{fmt::Debug, hash::Hash, panic::Location}; /// # }); /// # }); /// ``` +/// +/// ## Core Trait Implementations +/// - [`.get()`](crate::traits::Get) clones the current value of the memo. +/// If you call it within an effect, it will cause that effect to subscribe +/// to the memo, and to re-run whenever the value of the memo changes. +/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of +/// the memo without reactively tracking it. +/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the +/// value of the memo by reference. If you call it within an effect, it will +/// cause that effect to subscribe to the memo, and to re-run whenever the +/// value of the memo changes. +/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the +/// current value of the memo without reactively tracking it. +/// - [`.with()`](crate::traits::With) allows you to reactively access the memo’s +/// value without cloning by applying a callback function. +/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access +/// the memo’s value by applying a callback function without reactively +/// tracking it. +/// - [`.to_stream()`](crate::traits::ToStream) converts the memo to an `async` +/// stream of values. +/// - [`::from_stream()`](crate::traits::FromStream) converts an `async` stream +/// of values into a memo containing the latest value. pub struct Memo { #[cfg(debug_assertions)] defined_at: &'static Location<'static>, @@ -154,6 +180,13 @@ impl Memo { } } + /// Creates a new memo by passing a function that computes the value. + /// + /// Unlike [`ArcMemo::new`](), this receives ownership of the previous value. As a result, it + /// must return both the new value and a `bool` that is `true` if the value has changed. + /// + /// This is lazy: the function will not be called until the memo's value is read for the first + /// time. #[track_caller] #[cfg_attr( feature = "tracing", diff --git a/reactive_graph/src/computed/selector.rs b/reactive_graph/src/computed/selector.rs index 9d9cd085b..d359abfe8 100644 --- a/reactive_graph/src/computed/selector.rs +++ b/reactive_graph/src/computed/selector.rs @@ -12,6 +12,52 @@ use std::{ /// A conditional signal that only notifies subscribers when a change /// in the source signal’s value changes whether the given function is true. +/// +/// **You probably don’t need this,** but it can be a very useful optimization +/// in certain situations (e.g., “set the class `selected` if `selected() == this_row_index`) +/// because it reduces them from `O(n)` to `O(1)`. +/// +/// ``` +/// # use reactive_graph::computed::*; +/// # use reactive_graph::signal::*; +/// # use reactive_graph::prelude::*; +/// # use reactive_graph::effect::Effect; +/// # use reactive_graph::owner::StoredValue; +/// # tokio_test::block_on(async move { +/// # tokio::task::LocalSet::new().run_until(async move { +/// # any_spawner::Executor::init_tokio(); +/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter(); +/// let a = RwSignal::new(0); +/// let is_selected = Selector::new(move || a.get()); +/// let total_notifications = StoredValue::new(0); +/// Effect::new({ +/// let is_selected = is_selected.clone(); +/// move |_| { +/// if is_selected.selected(5) { +/// total_notifications.update_value(|n| *n += 1); +/// } +/// } +/// }); +/// +/// assert_eq!(is_selected.selected(5), false); +/// assert_eq!(total_notifications.get_value(), 0); +/// a.set(5); +/// # any_spawner::Executor::tick().await; +/// +/// assert_eq!(is_selected.selected(5), true); +/// assert_eq!(total_notifications.get_value(), 1); +/// a.set(5); +/// # any_spawner::Executor::tick().await; +/// +/// assert_eq!(is_selected.selected(5), true); +/// assert_eq!(total_notifications.get_value(), 1); +/// a.set(4); +/// +/// # any_spawner::Executor::tick().await; +/// assert_eq!(is_selected.selected(5), false); +/// # }); +/// # }); +/// ``` #[derive(Clone)] pub struct Selector where @@ -30,10 +76,13 @@ impl Selector where T: PartialEq + Eq + Clone + Hash + 'static, { + /// Creates a new selector that compares values using [`PartialEq`]. pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self { Self::new_with_fn(source, PartialEq::eq) } + /// Creates a new selector that compares values by returning `true` from a comparator function + /// if the values are the same. pub fn new_with_fn( source: impl Fn() -> T + Clone + 'static, f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static, diff --git a/reactive_graph/src/effect/effect.rs b/reactive_graph/src/effect/effect.rs index 7dad75f5f..1cfff86ed 100644 --- a/reactive_graph/src/effect/effect.rs +++ b/reactive_graph/src/effect/effect.rs @@ -15,6 +15,62 @@ use std::{ sync::{Arc, RwLock}, }; +/// Effects run a certain chunk of code whenever the signals they depend on change. +/// Creating an effect runs the given function once after any current synchronous work is done. +/// This tracks its reactive values read within it, and reruns the function whenever the value +/// of a dependency changes. +/// +/// Effects are intended to run *side-effects* of the system, not to synchronize state +/// *within* the system. In other words: In most cases, you usually should not write to +/// signals inside effects. (If you need to define a signal that depends on the value of +/// other signals, use a derived signal or a [`Memo`](crate::computed::Memo)). +/// +/// The effect function is called with an argument containing whatever value it returned +/// the last time it ran. On the initial run, this is `None`. +/// +/// Effects stop running when their reactive [`Owner`] is disposed. +/// +/// +/// ## Example +/// +/// ``` +/// # use reactive_graph::computed::*; +/// # use reactive_graph::signal::*; +/// # use reactive_graph::prelude::*; +/// # use reactive_graph::effect::Effect; +/// # use reactive_graph::owner::StoredValue; +/// # tokio_test::block_on(async move { +/// # tokio::task::LocalSet::new().run_until(async move { +/// let a = RwSignal::new(0); +/// let b = RwSignal::new(0); +/// +/// // ✅ use effects to interact between reactive state and the outside world +/// Effect::new(move |_| { +/// // on the next “tick” prints "Value: 0" and subscribes to `a` +/// println!("Value: {}", a.get()); +/// }); +/// +/// a.set(1); +/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1" +/// +/// // ❌ don't use effects to synchronize state within the reactive system +/// Effect::new(move |_| { +/// // this technically works but can cause unnecessary re-renders +/// // and easily lead to problems like infinite loops +/// b.set(a.get() + 1); +/// }); +/// # }); +/// # }); +/// ``` +/// ## Web-Specific Notes +/// +/// 1. **Scheduling**: Effects run after synchronous work, on the next “tick” of the reactive +/// system. This makes them suitable for “on mount” actions: they will fire immediately after +/// DOM rendering. +/// 2. By default, effects do not run unless the `effects` feature is enabled. If you are using +/// this with a web framework, this generally means that effects **do not run on the server**. +/// and you can call browser-specific APIs within the effect function without causing issues. +/// If you need an effect to run on the server, use [`Effect::new_isomorphic`]. pub struct Effect { inner: StoredValue>>>, } @@ -44,10 +100,17 @@ fn effect_base() -> (Receiver, Owner, Arc>) { } impl Effect { + /// Stops this effect before it is disposed. pub fn stop(self) { drop(self.inner.try_update_value(|inner| inner.take())); } + /// Creates a new effect, which runs once on the next “tick”, and then runs again when reactive values + /// that are read inside it change. + /// + /// This spawns a task on the local thread using + /// [`spawn_local`](any_spawner::Executor::spawn_local). For an effect that can be spawned on + /// any thread, use [`new_sync`](Effect::new_sync). pub fn new(mut fun: impl FnMut(Option) -> T + 'static) -> Self where T: 'static, @@ -88,6 +151,11 @@ impl Effect { } } + /// Creates a new effect, which runs once on the next “tick”, and then runs again when reactive values + /// that are read inside it change. + /// + /// This spawns a task that can be run on any thread. For an effect that will be spawned on + /// the current thread, use [`new`](Effect::new). pub fn new_sync( mut fun: impl FnMut(Option) -> T + Send + Sync + 'static, ) -> Self @@ -130,6 +198,10 @@ impl Effect { } } + /// Creates a new effect, which runs once on the next “tick”, and then runs again when reactive values + /// that are read inside it change. + /// + /// This will run whether the `effects` feature is enabled or not. pub fn new_isomorphic( mut fun: impl FnMut(Option) -> T + Send + Sync + 'static, ) -> Self @@ -180,6 +252,7 @@ impl ToAnySubscriber for Effect { } } +/// Creates an [`Effect`]. #[inline(always)] #[track_caller] #[deprecated = "This function is being removed to conform to Rust \ diff --git a/reactive_graph/src/effect/render_effect.rs b/reactive_graph/src/effect/render_effect.rs index de2c38865..4587ed5d1 100644 --- a/reactive_graph/src/effect/render_effect.rs +++ b/reactive_graph/src/effect/render_effect.rs @@ -15,6 +15,18 @@ use std::{ sync::{Arc, RwLock, Weak}, }; +/// A render effect is similar to an [`Effect`](super::Effect), but with two key differences: +/// 1. Its first run takes place immediately and synchronously: for example, if it is being used to +/// drive a user interface, it will run during rendering, not on the next tick after rendering. +/// (Hence “render effect.”) +/// 2. It is canceled when the `RenderEffect` itself is dropped, rather than being stored in the +/// reactive system and canceled when the `Owner` cleans up. +/// +/// Unless you are implementing a rendering framework, or require one of these two characteristics, +/// it is unlikely you will use render effects directly. +/// +/// Like an [`Effect`](super::Effect), a render effect runs only with the `effects` feature +/// enabled. #[must_use = "A RenderEffect will be canceled when it is dropped. Creating a \ RenderEffect that is not stored in some other data structure or \ leaked will drop it immediately, and it will not react to \ @@ -39,10 +51,12 @@ impl RenderEffect where T: 'static, { + /// Creates a new render effect, which immediately runs `fun`. pub fn new(fun: impl FnMut(Option) -> T + 'static) -> Self { Self::new_with_value(fun, None) } + /// Creates a new render effect with an initial value. pub fn new_with_value( fun: impl FnMut(Option) -> T + 'static, initial_value: Option, @@ -100,6 +114,7 @@ where erased(Box::new(fun), initial_value) } + /// Mutably accesses the current value. pub fn with_value_mut( &self, fun: impl FnOnce(&mut T) -> U, @@ -107,6 +122,7 @@ where self.value.write().or_poisoned().as_mut().map(fun) } + /// Takes the current value, replacing it with `None`. pub fn take_value(&self) -> Option { self.value.write().or_poisoned().take() } @@ -116,7 +132,7 @@ impl RenderEffect where T: Send + Sync + 'static, { - #[doc(hidden)] + /// Creates a render effect that will run whether the `effects` feature is enabled or not. pub fn new_isomorphic( mut fun: impl FnMut(Option) -> T + Send + 'static, ) -> Self { diff --git a/reactive_graph/src/graph/node.rs b/reactive_graph/src/graph/node.rs index f015e2156..24e7d3963 100644 --- a/reactive_graph/src/graph/node.rs +++ b/reactive_graph/src/graph/node.rs @@ -1,3 +1,4 @@ +/// A node in the reactive graph. pub trait ReactiveNode { /// Notifies the source's dependencies that it has changed. fn mark_dirty(&self); @@ -13,9 +14,14 @@ pub trait ReactiveNode { fn update_if_necessary(&self) -> bool; } +/// The current state of a reactive node. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum ReactiveNodeState { + /// The node is known to be clean: i.e., either none of its sources have changed, or its + /// sources have changed but its value is unchanged and its dependencies do not need to change. Clean, + /// The node may have changed, but it is not yet known whether it has actually changed. Check, + /// The node's value has definitely changed, and subscribers will need to update. Dirty, } diff --git a/reactive_graph/src/graph/source.rs b/reactive_graph/src/graph/source.rs index c06519844..0558779b6 100644 --- a/reactive_graph/src/graph/source.rs +++ b/reactive_graph/src/graph/source.rs @@ -3,6 +3,7 @@ use crate::traits::DefinedAt; use core::{fmt::Debug, hash::Hash}; use std::{panic::Location, sync::Weak}; +/// Abstracts over the type of any reactive source. pub trait ToAnySource { /// Converts this type to its type-erased equivalent. fn to_any_source(&self) -> AnySource; @@ -20,6 +21,7 @@ pub trait Source: ReactiveNode { fn clear_subscribers(&self); } +/// A weak reference to any reactive source node. #[derive(Clone)] pub struct AnySource( pub(crate) usize, diff --git a/reactive_graph/src/graph/subscriber.rs b/reactive_graph/src/graph/subscriber.rs index 01014e0b6..d5f9c1387 100644 --- a/reactive_graph/src/graph/subscriber.rs +++ b/reactive_graph/src/graph/subscriber.rs @@ -6,6 +6,11 @@ thread_local! { static OBSERVER: RefCell> = const { RefCell::new(None) }; } +/// The current reactive observer. +/// +/// The observer is whatever reactive node is currently listening for signals that need to be +/// tracked. For example, if an effect is running, that effect is the observer, which means it will +/// subscribe to changes in any signals that are read. pub struct Observer; struct SetObserverOnDrop(Option); @@ -17,6 +22,7 @@ impl Drop for SetObserverOnDrop { } impl Observer { + /// Returns the current observer, if any. pub fn get() -> Option { OBSERVER.with_borrow(Clone::clone) } @@ -41,6 +47,34 @@ impl Observer { } } +/// Suspends reactive tracking while running the given function. +/// +/// This can be used to isolate parts of the reactive graph from one another. +/// +/// ```rust +/// # use reactive_graph::computed::*; +/// # use reactive_graph::signal::*; +/// # use reactive_graph::prelude::*; +/// # use reactive_graph::untrack; +/// # tokio_test::block_on(async move { +/// # any_spawner::Executor::init_tokio(); +/// let (a, set_a) = signal(0); +/// let (b, set_b) = signal(0); +/// let c = Memo::new(move |_| { +/// // this memo will *only* update when `a` changes +/// a.get() + untrack(move || b.get()) +/// }); +/// +/// assert_eq!(c.get(), 0); +/// set_a.set(1); +/// assert_eq!(c.get(), 1); +/// set_b.set(1); +/// // hasn't updated, because we untracked before reading b +/// assert_eq!(c.get(), 1); +/// set_a.set(2); +/// assert_eq!(c.get(), 3); +/// # }); +/// ``` pub fn untrack(fun: impl FnOnce() -> T) -> T { #[cfg(debug_assertions)] let _warning_guard = crate::diagnostics::SpecialNonReactiveZone::enter(); @@ -60,7 +94,7 @@ pub trait Subscriber: ReactiveNode { /// Adds a subscriber to this subscriber's list of dependencies. fn add_source(&self, source: AnySource); - // Clears the set of sources for this subscriber. + /// Clears the set of sources for this subscriber. fn clear_sources(&self, subscriber: &AnySubscriber); } @@ -117,6 +151,7 @@ impl ReactiveNode for AnySubscriber { } impl AnySubscriber { + /// Runs the given function with this subscriber as the thread-local [`Observer`]. pub fn with_observer(&self, fun: impl FnOnce() -> T) -> T { let _prev = Observer::replace(self.clone()); fun() diff --git a/reactive_graph/src/lib.rs b/reactive_graph/src/lib.rs index 62aab3432..02bcaa9c1 100644 --- a/reactive_graph/src/lib.rs +++ b/reactive_graph/src/lib.rs @@ -69,7 +69,7 @@ #![cfg_attr(feature = "nightly", feature(unboxed_closures))] #![cfg_attr(feature = "nightly", feature(fn_traits))] -//#![deny(missing_docs)] +#![deny(missing_docs)] use std::fmt::Arguments; diff --git a/reactive_graph/src/owner/stored_value.rs b/reactive_graph/src/owner/stored_value.rs index 5aab31af1..a69fa1775 100644 --- a/reactive_graph/src/owner/stored_value.rs +++ b/reactive_graph/src/owner/stored_value.rs @@ -3,11 +3,18 @@ use super::{ OWNER, }; use crate::{ - traits::{DefinedAt, Dispose}, + traits::{DefinedAt, Dispose, IsDisposed}, unwrap_signal, }; use std::{any::Any, hash::Hash, marker::PhantomData, panic::Location}; +/// A **non-reactive**, `Copy` handle for any value. +/// +/// This allows you to create a stable reference for any value by storing it within +/// the reactive system. Like the signal types (e.g., [`ReadSignal`](crate::signal::ReadSignal) +/// and [`RwSignal`](crate::signal::RwSignal)), it is `Copy` and `'static`. Unlike the signal +/// types, it is not reactive; accessing it does not cause effects to subscribe, and +/// updating it does not notify anything else. #[derive(Debug)] pub struct StoredValue { node: NodeId, @@ -56,6 +63,7 @@ impl StoredValue where T: Send + Sync + 'static, { + /// Stores the given value in the arena allocator. #[track_caller] pub fn new(value: T) -> Self { let node = { @@ -79,6 +87,8 @@ where } impl StoredValue { + /// Same as [`StoredValue::with_value`] but returns `Some(O)` only if + /// the stored value has not yet been disposed, `None` otherwise. pub fn try_with_value(&self, fun: impl FnOnce(&T) -> U) -> Option { Arena::with(|arena| { let m = arena.get(self.node); @@ -86,11 +96,28 @@ impl StoredValue { }) } + /// Applies a function to the current stored value and returns the result. + /// + /// # Panics + /// Panics if you try to access a value owned by a reactive node that has been disposed. + /// + /// # Examples + /// ``` + /// # use reactive_graph::owner::StoredValue; + /// pub struct MyUncloneableData { + /// pub value: String, + /// } + /// let data = StoredValue::new(MyUncloneableData { value: "a".into() }); + /// + /// // calling .with_value() to extract the value + /// data.with_value(|data| assert_eq!(data.value, "a")); pub fn with_value(&self, fun: impl FnOnce(&T) -> U) -> U { self.try_with_value(fun) .unwrap_or_else(unwrap_signal!(self)) } + /// Updates the current value by applying the given closure, returning the return value of the + /// closure, or `None` if the value has already been disposed. pub fn try_update_value( &self, fun: impl FnOnce(&mut T) -> U, @@ -101,10 +128,23 @@ impl StoredValue { }) } + /// Updates the stored value by applying the given closure. + /// + /// ## Examples + /// ``` + /// # use reactive_graph::owner::StoredValue; + /// pub struct MyUncloneableData { + /// pub value: String, + /// } + /// let data = StoredValue::new(MyUncloneableData { value: "a".into() }); + /// data.update_value(|data| data.value = "b".into()); + /// assert_eq!(data.with_value(|data| data.value.clone()), "b"); + /// ``` pub fn update_value(&self, fun: impl FnOnce(&mut T) -> U) { self.try_update_value(fun); } + /// Tries to set the value. If the value has been disposed, returns `Some(value)`. pub fn try_set_value(&self, value: T) -> Option { Arena::with_mut(|arena| { let m = arena.get_mut(self.node); @@ -118,10 +158,12 @@ impl StoredValue { }) } + /// Sets the value to a new value. pub fn set_value(&self, value: T) { self.update_value(|n| *n = value); } + /// Returns `true` if the value has not yet been disposed. pub fn exists(&self) -> bool where T: Clone, @@ -130,14 +172,25 @@ impl StoredValue { } } +impl IsDisposed for StoredValue { + fn is_disposed(&self) -> bool { + Arena::with(|arena| arena.contains_key(self.node)) + } +} + impl StoredValue where T: Clone + 'static, { + /// Clones and returns the current value, or `None` if it has already been disposed. pub fn try_get_value(&self) -> Option { self.try_with_value(T::clone) } + /// Clones and returns the current value. + /// + /// # Panics + /// Panics if you try to access a value owned by a reactive node that has been disposed. pub fn get_value(&self) -> T { self.with_value(T::clone) } @@ -153,6 +206,7 @@ impl Dispose for StoredValue { } } +/// Creates a new [`StoredValue`]. #[inline(always)] #[track_caller] #[deprecated = "This function is being removed to conform to Rust idioms. \ diff --git a/reactive_graph/src/signal.rs b/reactive_graph/src/signal.rs index 7bbefadd8..f72c45ef4 100644 --- a/reactive_graph/src/signal.rs +++ b/reactive_graph/src/signal.rs @@ -75,8 +75,8 @@ pub fn arc_signal(value: T) -> (ArcReadSignal, ArcWriteSignal) { /// [`ReadSignal`] and a [`WriteSignal`]. /// /// This returns an arena-allocated signal, which is `Copy` and is disposed when its reactive -/// [`Owner`] cleans up. For a reference-counted signal that lives as long as a reference to it is -/// alive, see [`arc_signal`]. +/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives +/// as long as a reference to it is alive, see [`arc_signal`]. /// ``` /// # use reactive_graph::prelude::*; /// # use reactive_graph::signal::*; @@ -121,8 +121,8 @@ pub fn signal(value: T) -> (ReadSignal, WriteSignal) { /// [`ReadSignal`] and a [`WriteSignal`]. /// /// This returns an arena-allocated signal, which is `Copy` and is disposed when its reactive -/// [`Owner`] cleans up. For a reference-counted signal that lives as long as a reference to it is -/// alive, see [`arc_signal`]. +/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives +/// as long as a reference to it is alive, see [`arc_signal`]. /// ``` /// # use reactive_graph::prelude::*; /// # use reactive_graph::signal::*; diff --git a/reactive_graph/src/signal/arc_read.rs b/reactive_graph/src/signal/arc_read.rs index a6c8a956b..b4a62567d 100644 --- a/reactive_graph/src/signal/arc_read.rs +++ b/reactive_graph/src/signal/arc_read.rs @@ -19,7 +19,7 @@ use std::{ /// and notifies other code when it has changed. /// /// This is a reference-counted signal, which is `Clone` but not `Copy`. -/// For arena-allocated `Copy` signals, use [`ReadSignal`]. +/// For arena-allocated `Copy` signals, use [`ReadSignal`](super::ReadSignal). /// /// ## Core Trait Implementations /// - [`.get()`](crate::traits::Get) clones the current value of the signal. @@ -82,8 +82,14 @@ impl Debug for ArcReadSignal { } impl Default for ArcReadSignal { + #[track_caller] fn default() -> Self { - Self::new(T::default()) + Self { + #[cfg(debug_assertions)] + defined_at: Location::caller(), + value: Arc::new(RwLock::new(T::default())), + inner: Arc::new(RwLock::new(SubscriberSet::new())), + } } } @@ -101,21 +107,6 @@ impl Hash for ArcReadSignal { } } -impl ArcReadSignal { - #[cfg_attr( - feature = "tracing", - tracing::instrument(level = "trace", skip_all,) - )] - pub fn new(value: T) -> Self { - Self { - #[cfg(debug_assertions)] - defined_at: Location::caller(), - value: Arc::new(RwLock::new(value)), - inner: Arc::new(RwLock::new(SubscriberSet::new())), - } - } -} - impl DefinedAt for ArcReadSignal { #[inline(always)] fn defined_at(&self) -> Option<&'static Location<'static>> { diff --git a/reactive_graph/src/signal/arc_rw.rs b/reactive_graph/src/signal/arc_rw.rs index 4756206c0..ed9d366fb 100644 --- a/reactive_graph/src/signal/arc_rw.rs +++ b/reactive_graph/src/signal/arc_rw.rs @@ -22,7 +22,7 @@ use std::{ /// processes of reactive updates. /// /// This is a reference-counted signal, which is `Clone` but not `Copy`. -/// For arena-allocated `Copy` signals, use [`RwSignl`]. +/// For arena-allocated `Copy` signals, use [`RwSignal`](super::RwSignal). /// /// ## Core Trait Implementations /// @@ -56,7 +56,8 @@ use std::{ /// > Each of these has a related `_untracked()` method, which updates the signal /// > without notifying subscribers. Untracked updates are not desirable in most /// > cases, as they cause “tearing” between the signal’s value and its observed -/// > value. If you want a non-reactive container, used [`StoredValue`] instead. +/// > value. If you want a non-reactive container, used [`StoredValue`](crate::owner::StoredValue) +/// > instead. /// /// ## Examples /// diff --git a/reactive_graph/src/signal/arc_write.rs b/reactive_graph/src/signal/arc_write.rs index cf1e21b56..c3eaad581 100644 --- a/reactive_graph/src/signal/arc_write.rs +++ b/reactive_graph/src/signal/arc_write.rs @@ -17,7 +17,7 @@ use std::{ /// and notifies other code when it has changed. /// /// This is a reference-counted signal, which is `Clone` but not `Copy`. -/// For arena-allocated `Copy` signals, use [`ReadSignal`]. +/// For arena-allocated `Copy` signals, use [`WriteSignal`](super::WriteSignal). /// /// ## Core Trait Implementations /// - [`.set()`](crate::traits::Set) sets the signal to a new value. @@ -29,7 +29,8 @@ use std::{ /// > Each of these has a related `_untracked()` method, which updates the signal /// > without notifying subscribers. Untracked updates are not desirable in most /// > cases, as they cause “tearing” between the signal’s value and its observed -/// > value. If you want a non-reactive container, used [`StoredValue`] instead. +/// > value. If you want a non-reactive container, used [`StoredValue`](crate::owner::StoredValue) +/// > instead. /// /// ## Examples /// ``` diff --git a/reactive_graph/src/signal/guards.rs b/reactive_graph/src/signal/guards.rs index f90576ef4..885ad086c 100644 --- a/reactive_graph/src/signal/guards.rs +++ b/reactive_graph/src/signal/guards.rs @@ -97,6 +97,7 @@ impl Debug for Plain { } impl Plain { + /// Takes a reference-counted read guard on the given lock. pub fn try_new(inner: Arc>) -> Option { ArcRwLockReadGuardian::take(inner) .ok() @@ -142,6 +143,7 @@ impl Debug for AsyncPlain { } impl AsyncPlain { + /// Takes a reference-counted async read guard on the given lock. pub fn try_new(inner: &Arc>) -> Option { Some(Self { guard: inner.blocking_read_arc(), @@ -186,6 +188,7 @@ where } impl Mapped, U> { + /// Creates a mapped read guard from the inner lock. pub fn try_new( inner: Arc>, map_fn: fn(&T) -> &U, @@ -199,6 +202,7 @@ impl Mapped where Inner: Deref, { + /// Creates a mapped read guard from the inner guard. pub fn new_with_guard( inner: Inner, map_fn: fn(&Inner::Target) -> &U, @@ -320,6 +324,7 @@ where pub struct UntrackedWriteGuard(ArcRwLockWriteGuardian); impl UntrackedWriteGuard { + /// Creates a write guard from the given lock. pub fn try_new(inner: Arc>) -> Option { ArcRwLockWriteGuardian::take(inner) .ok() @@ -357,6 +362,7 @@ where } } +/// A mutable guard that maps over an inner mutable guard. #[derive(Debug)] pub struct MappedMut where @@ -380,6 +386,7 @@ impl MappedMut where Inner: DerefMut, { + /// Creates a new writable guard from the inner guard. pub fn new( inner: Inner, map_fn: fn(&Inner::Target) -> &U, @@ -431,6 +438,8 @@ where } } +/// A mapped read guard in which the mapping function is a closure. If the mapping function is a +/// function pointed, use [`Mapped`]. pub struct MappedArc where Inner: Deref, @@ -467,6 +476,7 @@ impl MappedArc where Inner: Deref, { + /// Creates a new mapped guard from the inner guard and the map function. pub fn new( inner: Inner, map_fn: impl Fn(&Inner::Target) -> &U + 'static, @@ -507,6 +517,8 @@ where } } +/// A mapped write guard in which the mapping function is a closure. If the mapping function is a +/// function pointed, use [`MappedMut`]. pub struct MappedMutArc where Inner: Deref, @@ -555,6 +567,7 @@ impl MappedMutArc where Inner: Deref, { + /// Creates the new mapped mutable guard from the inner guard and mapping functions. pub fn new( inner: Inner, map_fn: impl Fn(&Inner::Target) -> &U + 'static, diff --git a/reactive_graph/src/signal/read.rs b/reactive_graph/src/signal/read.rs index 92768f21a..46ae144fe 100644 --- a/reactive_graph/src/signal/read.rs +++ b/reactive_graph/src/signal/read.rs @@ -22,8 +22,8 @@ use std::{ /// and notifies other code when it has changed. /// /// This is an arena-allocated signal, which is `Copy` and is disposed when its reactive -/// [`Owner`] cleans up. For a reference-counted signal that lives as long as a reference to it is -/// alive, see [`ArcReadSignal`]. +/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives +/// as long as a reference to it is alive, see [`ArcReadSignal`]. /// /// ## Core Trait Implementations /// - [`.get()`](crate::traits::Get) clones the current value of the signal. diff --git a/reactive_graph/src/signal/rw.rs b/reactive_graph/src/signal/rw.rs index 7169230d5..d30ee3126 100644 --- a/reactive_graph/src/signal/rw.rs +++ b/reactive_graph/src/signal/rw.rs @@ -28,8 +28,8 @@ use std::{ /// processes of reactive updates. /// /// This is an arena-allocated signal, which is `Copy` and is disposed when its reactive -/// [`Owner`] cleans up. For a reference-counted signal that lives as long as a reference to it is -/// alive, see [`ArcRwSignal`e. +/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives +/// as long as a reference to it is alive, see [`ArcRwSignal`]. /// /// ## Core Trait Implementations /// @@ -57,7 +57,7 @@ use std::{ /// - [`.set()`](crate::traits::Set) sets the signal to a new value. /// - [`.update()`](crate::traits::Update) updates the value of the signal by /// applying a closure that takes a mutable reference. -/// - [`.write()`](crate::traits::Write) returns a guard through which the signal +/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal /// can be mutated, and which notifies subscribers when it is dropped. /// /// > Each of these has a related `_untracked()` method, which updates the signal diff --git a/reactive_graph/src/signal/write.rs b/reactive_graph/src/signal/write.rs index caa0886ef..a843ba60d 100644 --- a/reactive_graph/src/signal/write.rs +++ b/reactive_graph/src/signal/write.rs @@ -15,8 +15,8 @@ use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc}; /// and notifies other code when it has changed. /// /// This is an arena-allocated signal, which is `Copy` and is disposed when its reactive -/// [`Owner`] cleans up. For a reference-counted signal that lives as long as a reference to it is -/// alive, see [`ArcWriteSignal`]. +/// [`Owner`](crate::owner::Owner) cleans up. For a reference-counted signal that lives +/// as long as a reference to it is alive, see [`ArcWriteSignal`]. /// /// ## Core Trait Implementations /// - [`.set()`](crate::traits::Set) sets the signal to a new value. diff --git a/reactive_graph/src/traits.rs b/reactive_graph/src/traits.rs index 28b3add4e..dc5b0476b 100644 --- a/reactive_graph/src/traits.rs +++ b/reactive_graph/src/traits.rs @@ -45,7 +45,7 @@ //! For example, if you have a struct for which you can implement [`ReadUntracked`] and [`Track`], then //! [`WithUntracked`] and [`With`] will be implemented automatically (as will [`GetUntracked`] and //! [`Get`] for `Clone` types). But if you cannot implement [`ReadUntracked`] (because, for example, -//! there isn't an `RwLock` you can wrap in a [`SignalReadGuard`](crate::signal::SignalReadGuard), +//! there isn't an `RwLock` so you can't wrap in a [`ReadGuard`](crate::signal::guards::ReadGuard), //! but you can still implement [`WithUntracked`] and [`Track`], the same traits will still be implemented. use crate::{ @@ -200,6 +200,8 @@ where } } +/// A reactive, mutable guard that can be untracked to prevent it from notifying subscribers when +/// it is dropped. pub trait UntrackableGuard: DerefMut { /// Removes the notifier from the guard, such that it will no longer notify subscribers when it is dropped. fn untrack(&mut self); diff --git a/reactive_graph/src/wrappers.rs b/reactive_graph/src/wrappers.rs index e77b4f7f2..d469277b5 100644 --- a/reactive_graph/src/wrappers.rs +++ b/reactive_graph/src/wrappers.rs @@ -54,6 +54,13 @@ pub mod read { } } + /// A wrapper for any kind of reference-counted reactive signal: + /// an [`ArcReadSignal`], [`ArcMemo`], [`ArcRwSignal`], + /// or derived signal closure. + /// + /// This allows you to create APIs that take any kind of `ArcSignal` as an argument, + /// rather than adding a generic `F: Fn() -> T`. Values can be access with the same + /// function call, `with()`, and `get()` APIs as other signals. pub struct ArcSignal { #[cfg(debug_assertions)] defined_at: &'static Location<'static>, @@ -92,6 +99,26 @@ pub mod read { where T: Send + Sync + 'static, { + /// Wraps a derived signal, i.e., any computation that accesses one or more + /// reactive signals. + /// ```rust + /// # use reactive_graph::signal::*; + /// # use reactive_graph::wrappers::read::ArcSignal; + /// # use reactive_graph::prelude::*; + /// let (count, set_count) = arc_signal(2); + /// let double_count = ArcSignal::derive({ + /// let count = count.clone(); + /// move || count.get() * 2 + /// }); + /// + /// // this function takes any kind of wrapped signal + /// fn above_3(arg: &ArcSignal) -> bool { + /// arg.get() > 3 + /// } + /// + /// assert_eq!(above_3(&count.into()), false); + /// assert_eq!(above_3(&double_count), true); + /// ``` #[track_caller] pub fn derive( derived_signal: impl Fn() -> T + Send + Sync + 'static, @@ -204,6 +231,12 @@ pub mod read { } } + /// A wrapper for any kind of arena-allocated reactive signal: + /// an [`ReadSignal`], [`Memo`], [`RwSignal`], or derived signal closure. + /// + /// This allows you to create APIs that take any kind of `Signal` as an argument, + /// rather than adding a generic `F: Fn() -> T`. Values can be access with the same + /// function call, `with()`, and `get()` APIs as other signals. pub struct Signal { #[cfg(debug_assertions)] defined_at: &'static Location<'static>, @@ -305,6 +338,23 @@ pub mod read { where T: Send + Sync + 'static, { + /// Wraps a derived signal, i.e., any computation that accesses one or more + /// reactive signals. + /// ```rust + /// # use reactive_graph::signal::*; + /// # use reactive_graph::wrappers::read::Signal; + /// # use reactive_graph::prelude::*; + /// let (count, set_count) = signal(2); + /// let double_count = Signal::derive(move || count.get() * 2); + /// + /// // this function takes any kind of wrapped signal + /// fn above_3(arg: &Signal) -> bool { + /// arg.get() > 3 + /// } + /// + /// assert_eq!(above_3(&count.into()), false); + /// assert_eq!(above_3(&double_count), true); + /// ``` #[track_caller] pub fn derive( derived_signal: impl Fn() -> T + Send + Sync + 'static, @@ -408,6 +458,33 @@ pub mod read { } } + /// A wrapper for a value that is *either* `T` or [`Signal`]. + /// + /// This allows you to create APIs that take either a reactive or a non-reactive value + /// of the same type. This is especially useful for component properties. + /// + /// ``` + /// # use reactive_graph::signal::*; + /// # use reactive_graph::wrappers::read::MaybeSignal; + /// # use reactive_graph::computed::Memo; + /// # use reactive_graph::prelude::*; + /// let (count, set_count) = signal(2); + /// let double_count = MaybeSignal::derive(move || count.get() * 2); + /// let memoized_double_count = Memo::new(move |_| count.get() * 2); + /// let static_value = 5; + /// + /// // this function takes either a reactive or non-reactive value + /// fn above_3(arg: &MaybeSignal) -> bool { + /// // ✅ calling the signal clones and returns the value + /// // it is a shorthand for arg.get() + /// arg.get() > 3 + /// } + /// + /// assert_eq!(above_3(&static_value.into()), true); + /// assert_eq!(above_3(&count.into()), false); + /// assert_eq!(above_3(&double_count), true); + /// assert_eq!(above_3(&memoized_double_count.into()), true); + /// ``` #[derive(Debug, PartialEq, Eq)] pub enum MaybeSignal where @@ -476,6 +553,8 @@ pub mod read { where T: Send + Sync + 'static, { + /// Wraps a derived signal, i.e., any computation that accesses one or more + /// reactive signals. pub fn derive( derived_signal: impl Fn() -> T + Send + Sync + 'static, ) -> Self { @@ -537,6 +616,37 @@ pub mod read { } } + /// A wrapping type for an optional component prop, which can either be a signal or a + /// non-reactive value, and which may or may not have a value. In other words, this is + /// an `Option>>` that automatically flattens its getters. + /// + /// This creates an extremely flexible type for component libraries, etc. + /// + /// ## Examples + /// ```rust + /// # use reactive_graph::signal::*; + /// # use reactive_graph::wrappers::read::MaybeProp; + /// # use reactive_graph::computed::Memo; + /// # use reactive_graph::prelude::*; + /// let (count, set_count) = signal(Some(2)); + /// let double = |n| n * 2; + /// let double_count = MaybeProp::derive(move || count.get().map(double)); + /// let memoized_double_count = Memo::new(move |_| count.get().map(double)); + /// let static_value = 5; + /// + /// // this function takes either a reactive or non-reactive value + /// fn above_3(arg: &MaybeProp) -> bool { + /// // ✅ calling the signal clones and returns the value + /// // it is a shorthand for arg.get()q + /// arg.get().map(|arg| arg > 3).unwrap_or(false) + /// } + /// + /// assert_eq!(above_3(&None::.into()), false); + /// assert_eq!(above_3(&static_value.into()), true); + /// assert_eq!(above_3(&count.into()), false); + /// assert_eq!(above_3(&double_count), true); + /// assert_eq!(above_3(&memoized_double_count.into()), true); + /// ``` #[derive(Clone, Debug, PartialEq, Eq)] pub struct MaybeProp( pub(crate) Option>>, @@ -589,6 +699,8 @@ pub mod read { where T: Send + Sync + 'static, { + /// Wraps a derived signal, i.e., any computation that accesses one or more + /// reactive signals. pub fn derive( derived_signal: impl Fn() -> Option + Send + Sync + 'static, ) -> Self { @@ -699,8 +811,8 @@ pub mod write { } } - /// A wrapper for any kind of settable reactive signal: a [`WriteSignal`](crate::WriteSignal), - /// [`RwSignal`](crate::RwSignal), or closure that receives a value and sets a signal depending + /// A wrapper for any kind of settable reactive signal: a [`WriteSignal`], + /// [`RwSignal`], or closure that receives a value and sets a signal depending /// on it. /// /// This allows you to create APIs that take any kind of `SignalSetter` as an argument, @@ -717,9 +829,6 @@ pub mod write { /// # use reactive_graph::prelude::*; /// # use reactive_graph::wrappers::write::SignalSetter; /// # use reactive_graph::signal::signal; - /// # tokio_test::block_on(async move { - /// # any_spawner::Executor::init_tokio(); - /// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter(); /// let (count, set_count) = signal(2); /// let set_double_input = SignalSetter::map(move |n| set_count.set(n * 2)); /// @@ -734,7 +843,6 @@ pub mod write { /// assert_eq!(count.get(), 4); /// set_to_4(&set_double_input); /// assert_eq!(count.get(), 8); - /// # }); /// ``` #[derive(Debug, PartialEq, Eq)] pub struct SignalSetter @@ -799,6 +907,7 @@ pub mod write { where T: Send + Sync + 'static, { + /// Wraps a signal-setting closure, i.e., any computation that sets one or more reactive signals. #[track_caller] pub fn map(mapped_setter: impl Fn(T) + Send + Sync + 'static) -> Self { Self { @@ -815,6 +924,27 @@ pub mod write { where T: 'static, { + /// Calls the setter function with the given value. + /// + /// ``` + /// # use reactive_graph::wrappers::write::SignalSetter; + /// # use reactive_graph::signal::signal; + /// # use reactive_graph::prelude::*; + /// let (count, set_count) = signal(2); + /// let set_double_count = SignalSetter::map(move |n| set_count.set(n * 2)); + /// + /// // this function takes any kind of signal setter + /// fn set_to_4(setter: &SignalSetter) { + /// // ✅ calling the signal sets the value + /// // can be `setter(4)` on nightly + /// setter.set(4); + /// } + /// + /// set_to_4(&set_count.into()); + /// assert_eq!(count.get(), 4); + /// set_to_4(&set_double_count); + /// assert_eq!(count.get(), 8); + /// ``` #[track_caller] pub fn set(&self, value: T) { match &self.inner {