docs: initial work on porting docs from 0.6 to 0.7

This commit is contained in:
Greg Johnston 2024-07-10 20:47:24 -04:00
parent 6c72aaf444
commit 1f9ec1b979
25 changed files with 791 additions and 207 deletions

View file

@ -13,6 +13,7 @@ leptos = { path = "../../leptos", features = ["csr"] }
console_log = "1"
log = "0.4"
console_error_panic_hook = "0.1.7"
gloo-timers = { version = "0.3.0", features = ["futures"] }
[dev-dependencies]
wasm-bindgen = "0.2"

View file

@ -10,7 +10,7 @@ pub fn SimpleCounter(
/// The change that should be applied each time the button is clicked.
step: i32,
) -> impl IntoView {
let (value, set_value) = signal(initial_value);
/*let (value, set_value) = signal(initial_value);
view! {
<div>
@ -19,5 +19,24 @@ pub fn SimpleCounter(
<span>"Value: " {value} "!"</span>
<button on:click=move |_| set_value.update(|value| *value += step)>"+1"</button>
</div>
}
}*/
App()
}
use gloo_timers::future::TimeoutFuture;
use leptos::{html::Input, prelude::*};
#[component]
fn Widget() -> impl IntoView {
let input_ref = NodeRef::<Input>::new();
Effect::new(move |_| {
let Some(_) = input_ref.get() else {
log!("no ref");
return;
};
log!("ref");
});
view! { <input node_ref=input_ref type="text"/> }
}

View file

@ -169,8 +169,8 @@ pub mod prelude {
pub use leptos_server::*;
pub use oco_ref::*;
pub use reactive_graph::{
actions::*, computed::*, effect::*, owner::*, selector::*,
signal::*, wrappers::read::*, *,
actions::*, computed::*, effect::*, owner::*, signal::*,
wrappers::read::*, *,
};
pub use server_fn::{self, ServerFnError};
pub use tachys::{

View file

@ -31,7 +31,6 @@ use tachys::{
};
use throw_error::ErrorHookFuture;
/// TODO docs!
#[component]
pub fn Suspense<Chil>(
#[prop(optional, into)] fallback: ViewFnOnce,
@ -40,6 +39,7 @@ pub fn Suspense<Chil>(
where
Chil: IntoView + Send + 'static,
{
leptos::logging::log!("Suspense body");
let owner = Owner::new();
owner.with(|| {
let (starts_local, id) = {

View file

@ -56,6 +56,7 @@ where
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_with_options<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -235,6 +236,7 @@ where
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_wb<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -247,6 +249,7 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
#[track_caller]
pub fn new_serde_wb_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -270,6 +273,7 @@ where
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_miniserde<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -282,6 +286,7 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
#[track_caller]
pub fn new_miniserde_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -304,6 +309,7 @@ where
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_lite<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -316,6 +322,7 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
#[track_caller]
pub fn new_serde_lite_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -340,6 +347,7 @@ where
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_rkyv<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -352,6 +360,7 @@ where
ArcResource::new_with_options(source, fetcher, false)
}
#[track_caller]
pub fn new_rkyv_blocking<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -630,6 +639,7 @@ where
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
T: Send + Sync,
{
#[track_caller]
pub fn new_with_options<S, Fut>(
source: impl Fn() -> S + Send + Sync + 'static,
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
@ -656,6 +666,7 @@ where
type Output = T;
type IntoFuture = AsyncDerivedFuture<T>;
#[track_caller]
fn into_future(self) -> Self::IntoFuture {
self.data.into_future()
}

View file

@ -1,7 +1,11 @@
//! Computed reactive values that derive from other reactive values.
mod arc_memo;
mod async_derived;
mod inner;
mod memo;
mod selector;
pub use arc_memo::*;
pub use async_derived::*;
pub use memo::*;
pub use selector::*;

View file

@ -4,8 +4,11 @@ use crate::{
AnySource, AnySubscriber, ReactiveNode, Source, Subscriber,
ToAnySource, ToAnySubscriber,
},
signal::guards::{Mapped, Plain, ReadGuard},
traits::{DefinedAt, ReadUntracked},
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcReadSignal,
},
traits::{DefinedAt, Get, ReadUntracked},
};
use core::fmt::Debug;
use or_poisoned::OrPoisoned;
@ -207,3 +210,13 @@ impl<T: 'static> ReadUntracked for ArcMemo<T> {
.map(ReadGuard::new)
}
}
impl<T> From<ArcReadSignal<T>> for ArcMemo<T>
where
T: Clone + PartialEq + Send + Sync + 'static,
{
#[track_caller]
fn from(value: ArcReadSignal<T>) -> Self {
ArcMemo::new(move |_| value.get())
}
}

View file

@ -1,8 +1,11 @@
use super::{inner::MemoInner, ArcMemo};
use crate::{
owner::StoredValue,
signal::guards::{Mapped, Plain, ReadGuard},
traits::{DefinedAt, Dispose, ReadUntracked, Track},
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcReadSignal,
},
traits::{DefinedAt, Dispose, Get, ReadUntracked, Track},
unwrap_signal,
};
use std::{fmt::Debug, hash::Hash, panic::Location};
@ -11,7 +14,7 @@ use std::{fmt::Debug, hash::Hash, panic::Location};
///
/// 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
/// access its value.
/// access its value.
/// 2. The memo will only notify its dependents if the value of the computation changes.
///
/// This makes a memo the perfect tool for expensive computations.
@ -30,6 +33,7 @@ use std::{fmt::Debug, hash::Hash, panic::Location};
/// # use reactive_graph::signal::signal;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio();
/// # tokio::task::LocalSet::new().run_until(async {
/// # fn really_expensive_computation(value: i32) -> i32 { value };
/// let (value, set_value) = signal(0);
///
@ -64,6 +68,7 @@ use std::{fmt::Debug, hash::Hash, panic::Location};
/// // do something else...
/// });
/// # });
/// # });
/// ```
pub struct Memo<T> {
#[cfg(debug_assertions)]
@ -235,3 +240,13 @@ impl<T: Send + Sync + 'static> From<Memo<T>> for ArcMemo<T> {
value.inner.get().unwrap_or_else(unwrap_signal!(value))
}
}
impl<T> From<ArcReadSignal<T>> for Memo<T>
where
T: Clone + PartialEq + Send + Sync + 'static,
{
#[track_caller]
fn from(value: ArcReadSignal<T>) -> Self {
Memo::new(move |_| value.get())
}
}

View file

@ -1,3 +1,5 @@
//! Side effects that run in response to changes in the reactive values they read from.
#[allow(clippy::module_inception)]
mod effect;
mod inner;

View file

@ -1,3 +1,6 @@
//! Types that define the reactive graph itself. These are mostly internal, but can be used to
//! create custom reactive primitives.
mod node;
mod sets;
mod source;

View file

@ -69,9 +69,9 @@
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
#![cfg_attr(feature = "nightly", feature(fn_traits))]
//#![deny(missing_docs)]
use futures::Stream;
use std::{fmt::Arguments, future::Future, pin::Pin};
use std::fmt::Arguments;
pub mod actions;
pub(crate) mod channel;
@ -80,7 +80,6 @@ pub mod diagnostics;
pub mod effect;
pub mod graph;
pub mod owner;
pub mod selector;
#[cfg(feature = "serde")]
mod serde;
pub mod signal;
@ -93,10 +92,7 @@ pub use graph::untrack;
#[cfg(feature = "nightly")]
mod nightly;
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;
pub type PinnedLocalFuture<T> = Pin<Box<dyn Future<Output = T>>>;
pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
/// Reexports frequently-used traits.
pub mod prelude {
pub use crate::traits::*;
}

View file

@ -1,3 +1,5 @@
//! The reactive ownership model, which manages effect cancelation, cleanups, and arena allocation.
#[cfg(feature = "hydration")]
use hydration_context::SharedContext;
use or_poisoned::OrPoisoned;

View file

@ -1,93 +0,0 @@
use crate::{
effect::RenderEffect,
signal::ArcRwSignal,
traits::{Track, Update},
};
use or_poisoned::OrPoisoned;
use rustc_hash::FxHashMap;
use std::{
hash::Hash,
sync::{Arc, RwLock},
};
/// A conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether the given function is true.
#[derive(Clone)]
pub struct Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>>,
v: Arc<RwLock<Option<T>>>,
#[allow(clippy::type_complexity)]
f: Arc<dyn Fn(&T, &T) -> bool + Send + Sync>,
// owning the effect keeps it alive, to keep updating the selector
#[allow(dead_code)]
effect: Arc<RenderEffect<T>>,
}
impl<T> Selector<T>
where
T: PartialEq + Eq + Clone + Hash + 'static,
{
pub fn new(source: impl Fn() -> T + Clone + 'static) -> Self {
Self::new_with_fn(source, PartialEq::eq)
}
pub fn new_with_fn(
source: impl Fn() -> T + Clone + 'static,
f: impl Fn(&T, &T) -> bool + Send + Sync + Clone + 'static,
) -> Self {
let subs: Arc<RwLock<FxHashMap<T, ArcRwSignal<bool>>>> =
Default::default();
let v: Arc<RwLock<Option<T>>> = Default::default();
let f = Arc::new(f) as Arc<dyn Fn(&T, &T) -> bool + Send + Sync>;
let effect = Arc::new(RenderEffect::new({
let subs = Arc::clone(&subs);
let f = Arc::clone(&f);
let v = Arc::clone(&v);
move |prev: Option<T>| {
let next_value = source();
*v.write().or_poisoned() = Some(next_value.clone());
if prev.as_ref() != Some(&next_value) {
for (key, signal) in &*subs.read().or_poisoned() {
if f(key, &next_value)
|| (prev.is_some()
&& f(key, prev.as_ref().unwrap()))
{
signal.update(|n| *n = true);
}
}
}
next_value
}
}));
Selector { subs, v, f, effect }
}
/// Reactively checks whether the given key is selected.
pub fn selected(&self, key: T) -> bool {
let read = {
let mut subs = self.subs.write().or_poisoned();
subs.entry(key.clone())
.or_insert_with(|| ArcRwSignal::new(false))
.clone()
};
read.track();
(self.f)(&key, self.v.read().or_poisoned().as_ref().unwrap())
}
/// Removes the listener for the given key.
pub fn remove(&self, key: &T) {
let mut subs = self.subs.write().or_poisoned();
subs.remove(key);
}
/// Clears the listeners for all keys.
pub fn clear(&self) {
let mut subs = self.subs.write().or_poisoned();
subs.clear();
}
}

View file

@ -1,3 +1,6 @@
//! Reactive primitives for root values that can be changed, notifying other nodes in the reactive
//! graph.
mod arc_read;
mod arc_rw;
mod arc_trigger;
@ -16,18 +19,138 @@ pub use read::*;
pub use rw::*;
pub use write::*;
/// Creates a reference-counted signal.
///
/// A signal is a piece of data that may change over time, and notifies other
/// code when it has changed. This is the atomic unit of reactivity, which begins all other
/// processes of updating.
///
/// Takes the initial value as an argument, and returns a tuple containing an
/// [`ArcReadSignal`] and an [`ArcWriteSignal`].
///
/// This returns reference-counted signals, which are `Clone` but not `Copy`. For arena-allocated
/// `Copy` signals, use [`signal`].
///
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*;
/// let (count, set_count) = arc_signal(0);
///
/// // ✅ calling the getter clones and returns the value
/// // this can be `count()` on nightly
/// assert_eq!(count.get(), 0);
///
/// // ✅ calling the setter sets the value
/// // this can be `set_count(1)` on nightly
/// set_count.set(1);
/// assert_eq!(count.get(), 1);
///
/// // ❌ you could call the getter within the setter
/// // set_count.set(count.get() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// set_count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count.get(), 2);
///
/// // ✅ you can create "derived signals" with a Fn() -> T interface
/// let double_count = move || count.get() * 2;
/// set_count.set(0);
/// assert_eq!(double_count(), 0);
/// set_count.set(1);
/// assert_eq!(double_count(), 2);
/// ```
#[inline(always)]
#[track_caller]
pub fn arc_signal<T>(value: T) -> (ArcReadSignal<T>, ArcWriteSignal<T>) {
ArcRwSignal::new(value).split()
}
/// Creates an arena-allocated signal, the basic reactive primitive.
///
/// A signal is a piece of data that may change over time, and notifies other
/// code when it has changed. This is the atomic unit of reactivity, which begins all other
/// processes of updating.
///
/// Takes the initial value as an argument, and returns a tuple containing a
/// [`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`].
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*;
/// let (count, set_count) = signal(0);
///
/// // ✅ calling the getter clones and returns the value
/// // this can be `count()` on nightly
/// assert_eq!(count.get(), 0);
///
/// // ✅ calling the setter sets the value
/// // this can be `set_count(1)` on nightly
/// set_count.set(1);
/// assert_eq!(count.get(), 1);
///
/// // ❌ you could call the getter within the setter
/// // set_count.set(count.get() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// set_count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count.get(), 2);
///
/// // ✅ you can create "derived signals" with a Fn() -> T interface
/// let double_count = move || count.get() * 2; // signals are `Copy` so you can `move` them anywhere
/// set_count.set(0);
/// assert_eq!(double_count(), 0);
/// set_count.set(1);
/// assert_eq!(double_count(), 2);
/// ```
#[inline(always)]
#[track_caller]
pub fn signal<T: Send + Sync>(value: T) -> (ReadSignal<T>, WriteSignal<T>) {
RwSignal::new(value).split()
}
/// Creates an arena-allocated signal, the basic reactive primitive.
///
/// A signal is a piece of data that may change over time, and notifies other
/// code when it has changed. This is the atomic unit of reactivity, which begins all other
/// processes of updating.
///
/// Takes the initial value as an argument, and returns a tuple containing a
/// [`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`].
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*;
/// let (count, set_count) = create_signal(0);
///
/// // ✅ calling the getter clones and returns the value
/// // this can be `count()` on nightly
/// assert_eq!(count.get(), 0);
///
/// // ✅ calling the setter sets the value
/// // this can be `set_count(1)` on nightly
/// set_count.set(1);
/// assert_eq!(count.get(), 1);
///
/// // ❌ you could call the getter within the setter
/// // set_count.set(count.get() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// set_count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count.get(), 2);
///
/// // ✅ you can create "derived signals" with a Fn() -> T interface
/// let double_count = move || count.get() * 2; // signals are `Copy` so you can `move` them anywhere
/// set_count.set(0);
/// assert_eq!(double_count(), 0);
/// set_count.set(1);
/// assert_eq!(double_count(), 2);
/// ```
#[inline(always)]
#[track_caller]
#[deprecated = "This function is being renamed to `signal()` to conform to \

View file

@ -13,6 +13,46 @@ use std::{
sync::{Arc, RwLock},
};
/// A reference-counted getter for a reactive signal.
///
/// A signal is a piece of data that may change over time,
/// 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`].
///
/// ## Core Trait Implementations
/// - [`.get()`](crate::traits::Get) clones the current value of the signal.
/// If you call it within an effect, it will cause that effect to subscribe
/// to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of
/// the signal without reactively tracking it.
/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the
/// value of the signal by reference. If you call it within an effect, it will
/// cause that effect to subscribe to the signal, and to re-run whenever the
/// value of the signal changes.
/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the
/// current value of the signal without reactively tracking it.
/// - [`.with()`](crate::traits::With) allows you to reactively access the signals
/// value without cloning by applying a callback function.
/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access
/// the signals value by applying a callback function without reactively
/// tracking it.
/// - [`.to_stream()`](crate::traits::ToStream) converts the signal to an `async`
/// stream of values.
/// - [`::from_stream()`](crate::traits::FromStream) converts an `async` stream
/// of values into a signal containing the latest value.
///
/// ## Examples
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::signal::*;
/// let (count, set_count) = arc_signal(0);
///
/// // calling .get() clones and returns the value
/// assert_eq!(count.get(), 0);
/// // calling .read() accesses the value by reference
/// assert_eq!(count.read(), 0);
/// ```
pub struct ArcReadSignal<T> {
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static Location<'static>,

View file

@ -11,11 +11,87 @@ use crate::{
use core::fmt::{Debug, Formatter, Result};
use std::{
hash::Hash,
ops::DerefMut,
panic::Location,
sync::{Arc, RwLock},
};
/// A reference-counted signal that can be read from or written to.
///
/// A signal is a piece of data that may change over time, and notifies other
/// code when it has changed. This is the atomic unit of reactivity, which begins all other
/// processes of reactive updates.
///
/// This is a reference-counted signal, which is `Clone` but not `Copy`.
/// For arena-allocated `Copy` signals, use [`RwSignl`].
///
/// ## Core Trait Implementations
///
/// ### Reading the Value
/// - [`.get()`](crate::traits::Get) clones the current value of the signal.
/// If you call it within an effect, it will cause that effect to subscribe
/// to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of
/// the signal without reactively tracking it.
/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the
/// value of the signal by reference. If you call it within an effect, it will
/// cause that effect to subscribe to the signal, and to re-run whenever the
/// value of the signal changes.
/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the
/// current value of the signal without reactively tracking it.
/// - [`.with()`](crate::traits::With) allows you to reactively access the signals
/// value without cloning by applying a callback function.
/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access
/// the signals value by applying a callback function without reactively
/// tracking it.
/// - [`.to_stream()`](crate::traits::ToStream) converts the signal to an `async`
/// stream of values.
///
/// ### Updating the Value
/// - [`.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::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
/// > without notifying subscribers. Untracked updates are not desirable in most
/// > cases, as they cause “tearing” between the signals value and its observed
/// > value. If you want a non-reactive container, used [`StoredValue`] instead.
///
/// ## Examples
///
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*;
/// let count = ArcRwSignal::new(0);
///
/// // ✅ calling the getter clones and returns the value
/// // this can be `count()` on nightly
/// assert_eq!(count.get(), 0);
///
/// // ✅ calling the setter sets the value
/// // this can be `set_count(1)` on nightly
/// count.set(1);
/// assert_eq!(count.get(), 1);
///
/// // ❌ you could call the getter within the setter
/// // set_count.set(count.get() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count.get(), 2);
///
/// // ✅ you can create "derived signals" with a Fn() -> T interface
/// let double_count = {
/// // clone before moving into the closure because we use it below
/// let count = count.clone();
/// move || count.get() * 2
/// };
/// count.set(0);
/// assert_eq!(double_count(), 0);
/// count.set(1);
/// assert_eq!(double_count(), 2);
/// ```
pub struct ArcRwSignal<T> {
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static Location<'static>,
@ -69,6 +145,7 @@ where
}
impl<T> ArcRwSignal<T> {
/// Creates a new signal, taking the initial value as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
@ -83,6 +160,7 @@ impl<T> ArcRwSignal<T> {
}
}
/// Returns a read-only handle to the signal.
#[track_caller]
pub fn read_only(&self) -> ArcReadSignal<T> {
ArcReadSignal {
@ -93,6 +171,7 @@ impl<T> ArcRwSignal<T> {
}
}
/// Returns a write-only handle to the signal.
#[track_caller]
pub fn write_only(&self) -> ArcWriteSignal<T> {
ArcWriteSignal {
@ -103,10 +182,30 @@ impl<T> ArcRwSignal<T> {
}
}
/// Splits the signal into its readable and writable halves.
#[track_caller]
pub fn split(&self) -> (ArcReadSignal<T>, ArcWriteSignal<T>) {
(self.read_only(), self.write_only())
}
/// Reunites the two halves of a signal. Returns `None` if the two signals
/// provided were not created from the same signal.
#[track_caller]
pub fn unite(
read: ArcReadSignal<T>,
write: ArcWriteSignal<T>,
) -> Option<Self> {
if Arc::ptr_eq(&read.inner, &write.inner) {
Some(Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: read.value,
inner: read.inner,
})
} else {
None
}
}
}
impl<T> DefinedAt for ArcRwSignal<T> {
@ -163,6 +262,7 @@ impl<T: 'static> Writeable for ArcRwSignal<T> {
.map(|guard| WriteGuard::new(self.clone(), guard))
}
#[allow(refining_impl_trait)]
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {
UntrackedWriteGuard::try_new(Arc::clone(&self.value))
}

View file

@ -9,6 +9,9 @@ use std::{
sync::{Arc, RwLock},
};
/// A trigger is a data-less signal with the sole purpose of notifying other reactive code of a change.
///
/// This can be useful for when using external data not stored in signals, for example.
pub struct ArcTrigger {
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static Location<'static>,
@ -16,6 +19,7 @@ pub struct ArcTrigger {
}
impl ArcTrigger {
/// Creates a new trigger.
#[track_caller]
pub fn new() -> Self {
Self {

View file

@ -2,16 +2,56 @@ use super::guards::{UntrackedWriteGuard, WriteGuard};
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Trigger},
traits::{DefinedAt, Writeable},
traits::{DefinedAt, UntrackableGuard, Writeable},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
hash::Hash,
ops::DerefMut,
panic::Location,
sync::{Arc, RwLock},
};
/// A reference-counted setter for a reactive signal.
///
/// A signal is a piece of data that may change over time,
/// 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`].
///
/// ## Core Trait Implementations
/// - [`.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::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
/// > without notifying subscribers. Untracked updates are not desirable in most
/// > cases, as they cause “tearing” between the signals value and its observed
/// > value. If you want a non-reactive container, used [`StoredValue`] instead.
///
/// ## Examples
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::signal::*;
/// let (count, set_count) = arc_signal(0);
///
/// // ✅ calling the setter sets the value
/// // `set_count(1)` on nightly
/// set_count.set(1);
/// assert_eq!(count.get(), 1);
///
/// // ❌ you could call the getter within the setter
/// // set_count.set(count.get() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// set_count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count.get(), 2);
///
/// // ✅ `.write()` returns a guard that implements `DerefMut` and will notify when dropped
/// *set_count.write() += 1;
/// assert_eq!(count.get(), 3);
/// ```
pub struct ArcWriteSignal<T> {
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static Location<'static>,
@ -40,12 +80,6 @@ impl<T> Debug for ArcWriteSignal<T> {
}
}
impl<T: Default> Default for ArcWriteSignal<T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T> PartialEq for ArcWriteSignal<T> {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.value, &other.value)
@ -60,21 +94,6 @@ impl<T> Hash for ArcWriteSignal<T> {
}
}
impl<T> ArcWriteSignal<T> {
#[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<T> DefinedAt for ArcWriteSignal<T> {
#[inline(always)]
fn defined_at(&self) -> Option<&'static Location<'static>> {
@ -105,15 +124,14 @@ impl<T> Trigger for ArcWriteSignal<T> {
impl<T: 'static> Writeable for ArcWriteSignal<T> {
type Value = T;
fn try_write(
&self,
) -> Option<WriteGuard<Self, impl DerefMut<Target = Self::Value>>> {
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.value
.write()
.ok()
.map(|guard| WriteGuard::new(self.clone(), guard))
}
#[allow(refining_impl_trait)]
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {
UntrackedWriteGuard::try_new(Arc::clone(&self.value))
}

View file

@ -1,3 +1,5 @@
//! Guards that integrate with the reactive system, wrapping references to the values of signals.
use crate::{
computed::BlockingLock,
traits::{Trigger, UntrackableGuard},
@ -12,6 +14,9 @@ use std::{
sync::{Arc, RwLock},
};
/// A wrapper type for any kind of guard returned by [`Read`](crate::traits::Read).
///
/// If `Inner` implements `Deref`, so does `ReadGuard<_, Inner>`.
#[derive(Debug)]
pub struct ReadGuard<T, Inner> {
ty: PhantomData<T>,
@ -19,6 +24,7 @@ pub struct ReadGuard<T, Inner> {
}
impl<T, Inner> ReadGuard<T, Inner> {
/// Creates a new wrapper around another guard type.
pub fn new(inner: Inner) -> Self {
Self {
inner,
@ -79,6 +85,7 @@ where
}
}
/// A guard that provides access to a signal's inner value.
pub struct Plain<T: 'static> {
guard: ArcRwLockReadGuardian<T>,
}
@ -123,6 +130,7 @@ impl<T: Display> Display for Plain<T> {
}
}
/// A guard that provides access to an async signal's value.
pub struct AsyncPlain<T: 'static> {
pub(crate) guard: async_lock::RwLockReadGuardArc<T>,
}
@ -167,6 +175,7 @@ impl<T: Display> Display for AsyncPlain<T> {
}
}
/// A guard that maps over another guard.
#[derive(Debug)]
pub struct Mapped<Inner, U>
where
@ -236,6 +245,8 @@ where
}
}
/// A guard that provides mutable access to a signal's value, triggering some reactive change
/// when it is dropped.
#[derive(Debug)]
pub struct WriteGuard<S, G>
where
@ -249,6 +260,8 @@ impl<S, G> WriteGuard<S, G>
where
S: Trigger,
{
/// Creates a new guard from the inner mutable guard type, and the signal that should be
/// triggered on drop.
pub fn new(triggerable: S, guard: G) -> Self {
Self {
triggerable: Some(triggerable),
@ -262,6 +275,7 @@ where
S: Trigger,
G: DerefMut,
{
/// Removes the triggerable type, so that it is no longer notifies when dropped.
fn untrack(&mut self) {
self.triggerable.take();
}
@ -301,6 +315,8 @@ where
}
}
/// A guard that provides mutable access to a signal's inner value, but does not notify of any
/// changes.
pub struct UntrackedWriteGuard<T: 'static>(ArcRwLockWriteGuardian<T>);
impl<T: 'static> UntrackedWriteGuard<T> {

View file

@ -1,5 +1,4 @@
use super::{
arc_signal,
guards::{Plain, ReadGuard},
subscriber_traits::AsSubscriberSet,
ArcReadSignal,
@ -7,18 +6,57 @@ use super::{
use crate::{
graph::SubscriberSet,
owner::StoredValue,
traits::{DefinedAt, Dispose, IsDisposed, ReadUntracked, Set},
traits::{DefinedAt, Dispose, IsDisposed, ReadUntracked},
unwrap_signal,
};
use any_spawner::Executor;
use core::fmt::Debug;
use futures::{Stream, StreamExt};
use std::{
hash::Hash,
panic::Location,
sync::{Arc, RwLock},
};
/// An arena-allocated getter for a reactive signal.
///
/// A signal is a piece of data that may change over time,
/// 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`].
///
/// ## Core Trait Implementations
/// - [`.get()`](crate::traits::Get) clones the current value of the signal.
/// If you call it within an effect, it will cause that effect to subscribe
/// to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of
/// the signal without reactively tracking it.
/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the
/// value of the signal by reference. If you call it within an effect, it will
/// cause that effect to subscribe to the signal, and to re-run whenever the
/// value of the signal changes.
/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the
/// current value of the signal without reactively tracking it.
/// - [`.with()`](crate::traits::With) allows you to reactively access the signals
/// value without cloning by applying a callback function.
/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access
/// the signals value by applying a callback function without reactively
/// tracking it.
/// - [`.to_stream()`](crate::traits::ToStream) converts the signal to an `async`
/// stream of values.
/// - [`::from_stream()`](crate::traits::FromStream) converts an `async` stream
/// of values into a signal containing the latest value.
///
/// ## Examples
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::signal::*;
/// let (count, set_count) = signal(0);
///
/// // calling .get() clones and returns the value
/// assert_eq!(count.get(), 0);
/// // calling .read() accesses the value by reference
/// assert_eq!(count.read(), 0);
/// ```
pub struct ReadSignal<T: 'static> {
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static Location<'static>,
@ -118,63 +156,3 @@ impl<T: Send + Sync + 'static> From<ReadSignal<T>> for ArcReadSignal<T> {
value.inner.get().unwrap_or_else(unwrap_signal!(value))
}
}
impl<T: Send + Sync + 'static> ArcReadSignal<T> {
pub fn from_stream(
stream: impl Stream<Item = T> + Send + 'static,
) -> ArcReadSignal<Option<T>> {
let (read, write) = arc_signal(None);
let mut stream = Box::pin(stream);
Executor::spawn(async move {
while let Some(value) = stream.next().await {
write.set(Some(value));
}
});
read
}
}
impl<T: 'static> ArcReadSignal<T> {
pub fn from_stream_unsync(
stream: impl Stream<Item = T> + 'static,
) -> ArcReadSignal<Option<T>> {
let (read, write) = arc_signal(None);
let mut stream = Box::pin(stream);
Executor::spawn_local(async move {
while let Some(value) = stream.next().await {
write.set(Some(value));
}
});
read
}
}
impl<T: Send + Sync + 'static> ReadSignal<T> {
pub fn from_stream(
stream: impl Stream<Item = T> + Send + 'static,
) -> ArcReadSignal<Option<T>> {
let (read, write) = arc_signal(None);
let mut stream = Box::pin(stream);
Executor::spawn(async move {
while let Some(value) = stream.next().await {
write.set(Some(value));
}
});
read
}
}
impl<T: 'static> ReadSignal<T> {
pub fn from_stream_unsync(
stream: impl Stream<Item = T> + 'static,
) -> ArcReadSignal<Option<T>> {
let (read, write) = arc_signal(None);
let mut stream = Box::pin(stream);
Executor::spawn_local(async move {
while let Some(value) = stream.next().await {
write.set(Some(value));
}
});
read
}
}

View file

@ -17,11 +17,88 @@ use core::fmt::Debug;
use guardian::ArcRwLockWriteGuardian;
use std::{
hash::Hash,
ops::DerefMut,
panic::Location,
sync::{Arc, RwLock},
};
/// An arena-allocated signal that can be read from or written to.
///
/// A signal is a piece of data that may change over time, and notifies other
/// code when it has changed. This is the atomic unit of reactivity, which begins all other
/// 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.
///
/// ## Core Trait Implementations
///
/// ### Reading the Value
/// - [`.get()`](crate::traits::Get) clones the current value of the signal.
/// If you call it within an effect, it will cause that effect to subscribe
/// to the signal, and to re-run whenever the value of the signal changes.
/// - [`.get_untracked()`](crate::traits::GetUntracked) clones the value of
/// the signal without reactively tracking it.
/// - [`.read()`](crate::traits::Read) returns a guard that allows accessing the
/// value of the signal by reference. If you call it within an effect, it will
/// cause that effect to subscribe to the signal, and to re-run whenever the
/// value of the signal changes.
/// - [`.read_untracked()`](crate::traits::ReadUntracked) gives access to the
/// current value of the signal without reactively tracking it.
/// - [`.with()`](crate::traits::With) allows you to reactively access the signals
/// value without cloning by applying a callback function.
/// - [`.with_untracked()`](crate::traits::WithUntracked) allows you to access
/// the signals value by applying a callback function without reactively
/// tracking it.
/// - [`.to_stream()`](crate::traits::ToStream) converts the signal to an `async`
/// stream of values.
///
/// ### Updating the Value
/// - [`.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
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > 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 signals value and its observed
/// > value. If you want a non-reactive container, used [`StoredValue`] instead.
///
/// ## Examples
///
/// ```
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::signal::*;
/// let count = ArcRwSignal::new(0);
///
/// // ✅ calling the getter clones and returns the value
/// // this can be `count()` on nightly
/// assert_eq!(count.get(), 0);
///
/// // ✅ calling the setter sets the value
/// // this can be `set_count(1)` on nightly
/// count.set(1);
/// assert_eq!(count.get(), 1);
///
/// // ❌ you could call the getter within the setter
/// // set_count.set(count.get() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count.get(), 2);
///
/// // ✅ you can create "derived signals" with a Fn() -> T interface
/// let double_count = {
/// // clone before moving into the closure because we use it below
/// let count = count.clone();
/// move || count.get() * 2
/// };
/// count.set(0);
/// assert_eq!(double_count(), 0);
/// count.set(1);
/// assert_eq!(double_count(), 2);
/// ```
pub struct RwSignal<T> {
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
@ -35,6 +112,11 @@ impl<T: Send + Sync + 'static> Dispose for RwSignal<T> {
}
impl<T: Send + Sync + 'static> RwSignal<T> {
/// Creates a new signal, taking the initial value as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all,)
)]
#[track_caller]
pub fn new(value: T) -> Self {
Self {
@ -44,6 +126,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
}
}
/// Returns a read-only handle to the signal.
#[inline(always)]
#[track_caller]
pub fn read_only(&self) -> ReadSignal<T> {
@ -59,6 +142,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
}
}
/// Returns a write-only handle to the signal.
#[inline(always)]
#[track_caller]
pub fn write_only(&self) -> WriteSignal<T> {
@ -74,12 +158,15 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
}
}
/// Splits the signal into its readable and writable halves.
#[track_caller]
#[inline(always)]
pub fn split(&self) -> (ReadSignal<T>, WriteSignal<T>) {
(self.read_only(), self.write_only())
}
/// Reunites the two halves of a signal. Returns `None` if the two signals
/// provided were not created from the same signal.
#[track_caller]
pub fn unite(read: ReadSignal<T>, write: WriteSignal<T>) -> Option<Self> {
match (read.inner.get(), write.inner.get()) {
@ -195,6 +282,7 @@ impl<T: 'static> Writeable for RwSignal<T> {
Some(WriteGuard::new(*self, guard))
}
#[allow(refining_impl_trait)]
fn try_write_untracked(&self) -> Option<UntrackedWriteGuard<Self::Value>> {
self.inner.with_value(|n| n.try_write_untracked())
}

View file

@ -1,7 +1,4 @@
use super::{
guards::{UntrackedWriteGuard, WriteGuard},
ArcWriteSignal,
};
use super::{guards::WriteGuard, ArcWriteSignal};
use crate::{
owner::StoredValue,
traits::{
@ -12,6 +9,48 @@ use core::fmt::Debug;
use guardian::ArcRwLockWriteGuardian;
use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc};
/// An arena-allocated setter for a reactive signal.
///
/// A signal is a piece of data that may change over time,
/// 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`].
///
/// ## Core Trait Implementations
/// - [`.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::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
/// > without notifying subscribers. Untracked updates are not desirable in most
/// > cases, as they cause “tearing” between the signals value and its observed
/// > value. If you want a non-reactive container, used [`StoredValue`] instead.
///
/// ## Examples
/// ```
/// # use reactive_graph::prelude::*; use reactive_graph::signal::*;
/// let (count, set_count) = signal(0);
///
/// // ✅ calling the setter sets the value
/// // `set_count(1)` on nightly
/// set_count.set(1);
/// assert_eq!(count.get(), 1);
///
/// // ❌ you could call the getter within the setter
/// // set_count.set(count.get() + 1);
///
/// // ✅ however it's more efficient to use .update() and mutate the value in place
/// set_count.update(|count: &mut i32| *count += 1);
/// assert_eq!(count.get(), 2);
///
/// // ✅ `.write()` returns a guard that implements `DerefMut` and will notify when dropped
/// *set_count.write() += 1;
/// assert_eq!(count.get(), 3);
/// ```
pub struct WriteSignal<T> {
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static Location<'static>,

View file

@ -49,14 +49,19 @@
//! but you can still implement [`WithUntracked`] and [`Track`], the same traits will still be implemented.
use crate::{
effect::Effect,
graph::{Observer, Source, Subscriber, ToAnySource},
signal::guards::{UntrackedWriteGuard, WriteGuard},
owner::Owner,
signal::{arc_signal, ArcReadSignal},
};
use any_spawner::Executor;
use futures::{Stream, StreamExt};
use std::{
ops::{Deref, DerefMut},
panic::Location,
};
/// Provides a sensible panic message for accessing disposed signals.
#[macro_export]
macro_rules! unwrap_signal {
($signal:ident) => {{
@ -84,11 +89,18 @@ macro_rules! unwrap_signal {
}};
}
/// Allows disposing an arena-allocated signal before its owner has been disposed.
pub trait Dispose {
/// Disposes of the signal. This:
/// 1. Detaches the signal from the reactive graph, preventing it from triggering
/// further updates; and
/// 2. Drops the value contained in the signal.
fn dispose(self);
}
/// Allows tracking the value of some reactive data.
pub trait Track {
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
fn track(&self);
}
@ -132,12 +144,20 @@ impl<T: Source + ToAnySource + DefinedAt> Track for T {
}
}
/// Give read-only access to a signal's value by reference through a guard type,
/// without tracking the value reactively.
pub trait ReadUntracked: Sized + DefinedAt {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
/// Returns the guard, or `None` if the signal has already been disposed.
#[track_caller]
fn try_read_untracked(&self) -> Option<Self::Value>;
/// Returns the guard.
///
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn read_untracked(&self) -> Self::Value {
self.try_read_untracked()
@ -145,12 +165,20 @@ pub trait ReadUntracked: Sized + DefinedAt {
}
}
/// Give read-only access to a signal's value by reference through a guard type,
/// and subscribes the active reactive observer (an effect or computed) to changes in its value.
pub trait Read {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
/// Subscribes to the signal, and returns the guard, or `None` if the signal has already been disposed.
#[track_caller]
fn try_read(&self) -> Option<Self::Value>;
/// Subscribes to the signal, and returns the guard.
///
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn read(&self) -> Self::Value;
}
@ -173,37 +201,61 @@ where
}
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);
}
/// Gives mutable access to a signal's value through a guard type. When the guard is dropped, the
/// signal's subscribers will be notified.
pub trait Writeable: Sized + DefinedAt + Trigger {
/// The type of the signal's value.
type Value: Sized + 'static;
/// Returns the guard, or `None` if the signal has already been disposed.
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>>;
// Returns a guard that will not notify subscribers when dropped,
/// or `None` if the signal has already been disposed.
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>>;
/// Returns the guard.
///
/// # Panics
/// Panics if you try to access a signal that has been disposed.
fn write(&self) -> impl UntrackableGuard<Target = Self::Value> {
self.try_write().unwrap_or_else(unwrap_signal!(self))
}
/// Returns a guard that will not notify subscribers when dropped.
///
/// # Panics
/// Panics if you try to access a signal that has been disposed.
fn write_untracked(&self) -> impl DerefMut<Target = Self::Value> {
self.try_write_untracked()
.unwrap_or_else(unwrap_signal!(self))
}
}
/// Give read-only access to a signal's value by reference inside a closure,
/// without tracking the value reactively.
pub trait WithUntracked: DefinedAt {
/// The type of the value contained in the signal.
type Value: ?Sized;
/// Applies the closure to the value, and returns the result,
/// or `None` if the signal has already been disposed.
#[track_caller]
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U>;
/// Applies the closure to the value, and returns the result.
///
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn with_untracked<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
self.try_with_untracked(fun)
@ -225,11 +277,21 @@ where
}
}
/// Give read-only access to a signal's value by reference inside a closure,
/// and subscribes the active reactive observer (an effect or computed) to changes in its value.
pub trait With: DefinedAt {
/// The type of the value contained in the signal.
type Value: ?Sized;
/// Subscribes to the signal, applies the closure to the value, and returns the result,
/// or `None` if the signal has already been disposed.
#[track_caller]
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U>;
/// Subscribes to the signal, applies the closure to the value, and returns the result.
///
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
self.try_with(fun).unwrap_or_else(unwrap_signal!(self))
@ -249,11 +311,20 @@ where
}
}
/// Clones the value of the signal, without tracking the value reactively.
pub trait GetUntracked: DefinedAt {
/// The type of the value contained in the signal.
type Value;
/// Clones and returns the value of the signal,
/// or `None` if the signal has already been disposed.
#[track_caller]
fn try_get_untracked(&self) -> Option<Self::Value>;
/// Clones and returns the value of the signal,
///
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn get_untracked(&self) -> Self::Value {
self.try_get_untracked()
@ -273,11 +344,21 @@ where
}
}
/// Clones the value of the signal, without tracking the value reactively.
/// and subscribes the active reactive observer (an effect or computed) to changes in its value.
pub trait Get: DefinedAt {
/// The type of the value contained in the signal.
type Value: Clone;
/// Subscribes to the signal, then clones and returns the value of the signal,
/// or `None` if the signal has already been disposed.
#[track_caller]
fn try_get(&self) -> Option<Self::Value>;
/// Subscribes to the signal, then clones and returns the value of the signal.
///
/// # Panics
/// Panics if you try to access a signal that has been disposed.
#[track_caller]
fn get(&self) -> Self::Value {
self.try_get().unwrap_or_else(unwrap_signal!(self))
@ -297,13 +378,23 @@ where
}
}
/// Notifies subscribers of a change in this signal.
pub trait Trigger {
/// Notifies subscribers of a change in this signal.
fn trigger(&self);
}
/// Updates the value of a signal by applying a function that updates it in place,
/// without notifying subscribers.
pub trait UpdateUntracked: DefinedAt {
/// The type of the value contained in the signal.
type Value;
/// Updates the value by applying a function, returning the value returned by that function.
/// Does not notify subscribers that the signal has changed.
///
/// # Panics
/// Panics if you try to update a signal that has been disposed.
#[track_caller]
fn update_untracked<U>(
&self,
@ -313,6 +404,9 @@ pub trait UpdateUntracked: DefinedAt {
.unwrap_or_else(unwrap_signal!(self))
}
/// Updates the value by applying a function, returning the value returned by that function,
/// or `None` if the signal has already been disposed.
/// Does not notify subscribers that the signal has changed.
fn try_update_untracked<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
@ -335,14 +429,20 @@ where
}
}
/// Updates the value of a signal by applying a function that updates it in place,
/// notifying its subscribers that the value has changed.
pub trait Update {
/// The type of the value contained in the signal.
type Value;
/// Updates the value of the signal and notifies subscribers.
#[track_caller]
fn update(&self, fun: impl FnOnce(&mut Self::Value)) {
self.try_update(fun);
}
/// Updates the value of the signal, but only notifies subscribers if the function
/// returns `true`.
#[track_caller]
fn maybe_update(&self, fun: impl FnOnce(&mut Self::Value) -> bool) {
self.try_maybe_update(|val| {
@ -351,6 +451,8 @@ pub trait Update {
});
}
/// Updates the value of the signal and notifies subscribers, returning the value that is
/// returned by the update function, or `None` if the signal has already been disposed.
#[track_caller]
fn try_update<U>(
&self,
@ -359,6 +461,9 @@ pub trait Update {
self.try_maybe_update(|val| (true, fun(val)))
}
/// Updates the value of the signal, notifying subscribers if the update function returns
/// `(true, _)`, and returns the value returned by the update function,
/// or `None` if the signal has already been disposed.
fn try_maybe_update<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> (bool, U),
@ -386,11 +491,18 @@ where
}
}
/// Updates the value of the signal by replacing it.
pub trait Set {
/// The type of the value contained in the signal.
type Value;
/// Updates the value by replacing it, and notifies subscribers that it has changed.
fn set(&self, value: Self::Value);
/// Updates the value by replacing it, and notifies subscribers that it has changed.
///
/// If the signal has already been disposed, returns `Some(value)` with the value that was
/// passed in. Otherwise, returns `None`.
fn try_set(&self, value: Self::Value) -> Option<Self::Value>;
}
@ -416,11 +528,90 @@ where
}
}
/// Allows converting a signal into an async [`Stream`].
pub trait ToStream<T> {
/// Generates a [`Stream`] that emits the new value of the signal
/// whenever it changes.
///
/// # Panics
/// Panics if you try to access a signal that is owned by a reactive node that has been disposed.
#[track_caller]
fn to_stream(&self) -> impl Stream<Item = T> + Send;
}
impl<S> ToStream<S::Value> for S
where
S: Clone + Get + Send + Sync + 'static,
S::Value: Send + 'static,
{
fn to_stream(&self) -> impl Stream<Item = S::Value> + Send {
let (tx, rx) = futures::channel::mpsc::unbounded();
let close_channel = tx.clone();
Owner::on_cleanup(move || close_channel.close_channel());
Effect::new_isomorphic({
let this = self.clone();
move |_| {
let _ = tx.unbounded_send(this.get());
}
});
rx
}
}
/// Allows creating a signal from an async [`Stream`].
pub trait FromStream<T> {
/// Creates a signal that contains the latest value of the stream.
#[track_caller]
fn from_stream(stream: impl Stream<Item = T> + Send + 'static) -> Self;
/// Creates a signal that contains the latest value of the stream.
#[track_caller]
fn from_stream_unsync(stream: impl Stream<Item = T> + 'static) -> Self;
}
impl<S, T> FromStream<T> for S
where
S: From<ArcReadSignal<Option<T>>> + Send + Sync,
T: Send + Sync + 'static,
{
fn from_stream(stream: impl Stream<Item = T> + Send + 'static) -> Self {
let (read, write) = arc_signal(None);
let mut stream = Box::pin(stream);
Executor::spawn(async move {
while let Some(value) = stream.next().await {
write.set(Some(value));
}
});
read.into()
}
fn from_stream_unsync(stream: impl Stream<Item = T> + 'static) -> Self {
let (read, write) = arc_signal(None);
let mut stream = Box::pin(stream);
Executor::spawn_local(async move {
while let Some(value) = stream.next().await {
write.set(Some(value));
}
});
read.into()
}
}
/// Checks whether a signal has already been disposed.
pub trait IsDisposed {
/// If `true`, the signal cannot be accessed without a panic.
fn is_disposed(&self) -> bool;
}
/// Describes where the signal was defined. This is used for diagnostic warnings and is purely a
/// debug-mode tool.
pub trait DefinedAt {
/// Returns the location at which the signal was defined. This is usually simply `None` in
/// release mode.
fn defined_at(&self) -> Option<&'static Location<'static>>;
}

View file

@ -1,3 +1,5 @@
//! Utilities to wait for asynchronous primitives to resolve.
use futures::{channel::oneshot, future::join_all};
use or_poisoned::OrPoisoned;
use std::{
@ -16,10 +18,18 @@ struct TransitionInner {
tx: mpsc::Sender<oneshot::Receiver<()>>,
}
/// Transitions allow you to wait for all asynchronous resources created during them to resolve.
#[derive(Debug)]
pub struct AsyncTransition;
impl AsyncTransition {
/// Calls the `action` function, and returns a `Future` that resolves when any
/// [`AsyncDerived`](crate::computed::AsyncDerived) or
/// or [`ArcAsyncDerived`](crate::computed::ArcAsyncDerived) that is read during the action
/// has resolved.
///
/// This allows for an inversion of control: the caller does not need to know when all the
/// resources created inside the `action` will resolve, but can wait for them to notify it.
pub async fn run<T, U>(action: impl FnOnce() -> T) -> U
where
T: Future<Output = U>,

View file

@ -1,3 +1,6 @@
//! Types to abstract over different kinds of readable or writable reactive values.
/// Types that abstract over signals with values that can be read.
pub mod read {
use crate::{
computed::{ArcMemo, Memo},
@ -672,6 +675,7 @@ pub mod read {
}
}
/// Types that abstract over the ability to update a signal.
pub mod write {
use crate::{
owner::StoredValue,