fix: remove SendOption from public API of actions (#3812)

This commit is contained in:
Greg Johnston
2025-04-02 13:03:21 -04:00
committed by GitHub
parent f102b2f43b
commit 98ee336d0c
7 changed files with 446 additions and 70 deletions

View File

@@ -39,7 +39,6 @@ fn HomePage() -> impl IntoView {
do_something_action
.value()
.get()
.take()
.unwrap_or_else(|| Ok(String::new()))
});

View File

@@ -389,7 +389,7 @@ pub fn FileUpload() -> impl IntoView {
"Upload a file.".to_string()
} else if upload_action.pending().get() {
"Uploading...".to_string()
} else if let Some(Ok(value)) = *upload_action.value().get() {
} else if let Some(Ok(value)) = upload_action.value().get() {
value.to_string()
} else {
format!("{:?}", upload_action.value().get())

View File

@@ -125,13 +125,10 @@ where
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(
Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)
.into_app_error()))
.into(),
);
value.set(Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)
.into_app_error())));
version.update(|n| *n += 1);
}
}

View File

@@ -1,16 +1,22 @@
use crate::{
computed::{ArcMemo, Memo},
diagnostics::is_suppressing_resource_load,
owner::{ArcStoredValue, ArenaItem, FromLocal, LocalStorage},
owner::{ArcStoredValue, ArenaItem},
send_wrapper_ext::SendOption,
signal::{ArcRwSignal, RwSignal},
signal::{ArcMappedSignal, ArcRwSignal, MappedSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, GetUntracked, GetValue, Update, Write},
unwrap_signal,
};
use any_spawner::Executor;
use futures::{channel::oneshot, select, FutureExt};
use send_wrapper::SendWrapper;
use std::{future::Future, panic::Location, pin::Pin, sync::Arc};
use std::{
future::Future,
ops::{Deref, DerefMut},
panic::Location,
pin::Pin,
sync::Arc,
};
/// An action runs some asynchronous code when you dispatch a new value to it, and gives you
/// reactive access to the result.
@@ -48,25 +54,25 @@ use std::{future::Future, panic::Location, pin::Pin, sync::Arc};
/// let version = save_data.version();
///
/// // before we do anything
/// assert_eq!(*input.get(), None); // no argument yet
/// assert_eq!(input.get(), None); // no argument yet
/// assert_eq!(pending.get(), false); // isn't pending a response
/// assert_eq!(*result_of_call.get(), None); // there's no "last value"
/// assert_eq!(result_of_call.get(), None); // there's no "last value"
/// assert_eq!(version.get(), 0);
///
/// // dispatch the action
/// save_data.dispatch("My todo".to_string());
///
/// // when we're making the call
/// assert_eq!(*input.get(), Some("My todo".to_string()));
/// assert_eq!(input.get(), Some("My todo".to_string()));
/// assert_eq!(pending.get(), true); // is pending
/// assert_eq!(*result_of_call.get(), None); // has not yet gotten a response
/// assert_eq!(result_of_call.get(), None); // has not yet gotten a response
///
/// # any_spawner::Executor::tick().await;
///
/// // after call has resolved
/// assert_eq!(*input.get(), None); // input clears out after resolved
/// assert_eq!(input.get(), None); // input clears out after resolved
/// assert_eq!(pending.get(), false); // no longer pending
/// assert_eq!(*result_of_call.get(), Some(42));
/// assert_eq!(result_of_call.get(), Some(42));
/// assert_eq!(version.get(), 1);
/// # });
/// ```
@@ -146,7 +152,7 @@ where
/// });
///
/// act.dispatch(3);
/// assert_eq!(*act.input().get(), Some(3));
/// assert_eq!(act.input().get(), Some(3));
///
/// // Remember that async functions already return a future if they are
/// // not `await`ed. You can save keystrokes by leaving out the `async move`
@@ -156,7 +162,7 @@ where
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
///
/// // after it resolves
/// assert_eq!(*act2.value().get(), Some("I'M IN A DOCTEST".to_string()));
/// assert_eq!(act2.value().get(), Some("I'M IN A DOCTEST".to_string()));
///
/// async fn yell(n: String) -> String {
/// n.to_uppercase()
@@ -380,7 +386,11 @@ where
}
}
impl<I, O> ArcAction<I, O> {
impl<I, O> ArcAction<I, O>
where
I: 'static,
O: 'static,
{
/// The number of times the action has successfully completed.
///
/// ```rust
@@ -423,18 +433,22 @@ impl<I, O> ArcAction<I, O> {
/// });
///
/// let input = act.input();
/// assert_eq!(*input.get(), None);
/// assert_eq!(input.get(), None);
/// act.dispatch(3);
/// assert_eq!(*input.get(), Some(3));
/// assert_eq!(input.get(), Some(3));
///
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
/// // after it resolves
/// assert_eq!(*input.get(), None);
/// assert_eq!(input.get(), None);
/// # });
/// ```
#[track_caller]
pub fn input(&self) -> ArcRwSignal<SendOption<I>> {
self.input.clone()
pub fn input(&self) -> ArcMappedSignal<Option<I>> {
ArcMappedSignal::new(
self.input.clone(),
|n| n.deref(),
|n| n.deref_mut(),
)
}
/// The most recent return value of the `async` function. This will be `None` before
@@ -453,21 +467,25 @@ impl<I, O> ArcAction<I, O> {
/// });
///
/// let value = act.value();
/// assert_eq!(*value.get(), None);
/// assert_eq!(value.get(), None);
/// act.dispatch(3);
/// assert_eq!(*value.get(), None);
/// assert_eq!(value.get(), None);
///
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
/// // after it resolves
/// assert_eq!(*value.get(), Some(6));
/// assert_eq!(value.get(), Some(6));
/// // dispatch another value, and it still holds the old value
/// act.dispatch(3);
/// assert_eq!(*value.get(), Some(6));
/// assert_eq!(value.get(), Some(6));
/// # });
/// ```
#[track_caller]
pub fn value(&self) -> ArcRwSignal<SendOption<O>> {
self.value.clone()
pub fn value(&self) -> ArcMappedSignal<Option<O>> {
ArcMappedSignal::new(
self.value.clone(),
|n| n.deref(),
|n| n.deref_mut(),
)
}
/// Whether the action has been dispatched and is currently waiting to resolve.
@@ -553,25 +571,25 @@ where
/// let version = save_data.version();
///
/// // before we do anything
/// assert_eq!(*input.get(), None); // no argument yet
/// assert_eq!(input.get(), None); // no argument yet
/// assert_eq!(pending.get(), false); // isn't pending a response
/// assert_eq!(*result_of_call.get(), None); // there's no "last value"
/// assert_eq!(result_of_call.get(), None); // there's no "last value"
/// assert_eq!(version.get(), 0);
///
/// // dispatch the action
/// save_data.dispatch("My todo".to_string());
///
/// // when we're making the call
/// assert_eq!(*input.get(), Some("My todo".to_string()));
/// assert_eq!(input.get(), Some("My todo".to_string()));
/// assert_eq!(pending.get(), true); // is pending
/// assert_eq!(*result_of_call.get(), None); // has not yet gotten a response
/// assert_eq!(result_of_call.get(), None); // has not yet gotten a response
///
/// # any_spawner::Executor::tick().await;
///
/// // after call has resolved
/// assert_eq!(*input.get(), None); // input clears out after resolved
/// assert_eq!(input.get(), None); // input clears out after resolved
/// assert_eq!(pending.get(), false); // no longer pending
/// assert_eq!(*result_of_call.get(), Some(42));
/// assert_eq!(result_of_call.get(), Some(42));
/// assert_eq!(version.get(), 1);
/// # });
/// ```
@@ -635,7 +653,7 @@ where
/// });
///
/// act.dispatch(3);
/// assert_eq!(*act.input().get(), Some(3));
/// assert_eq!(act.input().get(), Some(3));
///
/// // Remember that async functions already return a future if they are
/// // not `await`ed. You can save keystrokes by leaving out the `async move`
@@ -645,7 +663,7 @@ where
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
///
/// // after it resolves
/// assert_eq!(*act2.value().get(), Some("I'M IN A DOCTEST".to_string()));
/// assert_eq!(act2.value().get(), Some("I'M IN A DOCTEST".to_string()));
///
/// async fn yell(n: String) -> String {
/// n.to_uppercase()
@@ -829,28 +847,27 @@ where
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # let _guard = reactive_graph::diagnostics::SpecialNonReactiveZone::enter();
/// let act = ArcAction::new(|n: &u8| {
/// let act = Action::new(|n: &u8| {
/// let n = n.to_owned();
/// async move { n * 2 }
/// });
///
/// let input = act.input();
/// assert_eq!(*input.get(), None);
/// assert_eq!(input.get(), None);
/// act.dispatch(3);
/// assert_eq!(*input.get(), Some(3));
/// assert_eq!(input.get(), Some(3));
///
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
/// // after it resolves
/// assert_eq!(*input.get(), None);
/// assert_eq!(input.get(), None);
/// # });
/// ```
#[track_caller]
pub fn input(&self) -> RwSignal<SendOption<I>> {
let inner = self
.inner
pub fn input(&self) -> MappedSignal<Option<I>> {
self.inner
.try_with_value(|inner| inner.input())
.unwrap_or_else(unwrap_signal!(self));
inner.into()
.unwrap_or_else(unwrap_signal!(self))
.into()
}
/// The current argument that was dispatched to the async function. This value will
@@ -860,12 +877,11 @@ where
#[track_caller]
#[deprecated = "You can now use .input() for any value, whether it's \
thread-safe or not."]
pub fn input_local(&self) -> RwSignal<SendOption<I>, LocalStorage> {
let inner = self
.inner
pub fn input_local(&self) -> MappedSignal<Option<I>> {
self.inner
.try_with_value(|inner| inner.input())
.unwrap_or_else(unwrap_signal!(self));
RwSignal::from_local(inner)
.unwrap_or_else(unwrap_signal!(self))
.into()
}
}
@@ -890,25 +906,24 @@ where
/// });
///
/// let value = act.value();
/// assert_eq!(*value.get(), None);
/// assert_eq!(value.get(), None);
/// act.dispatch(3);
/// assert_eq!(*value.get(), None);
/// assert_eq!(value.get(), None);
///
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
/// // after it resolves
/// assert_eq!(*value.get(), Some(6));
/// assert_eq!(value.get(), Some(6));
/// // dispatch another value, and it still holds the old value
/// act.dispatch(3);
/// assert_eq!(*value.get(), Some(6));
/// assert_eq!(value.get(), Some(6));
/// # });
/// ```
#[track_caller]
pub fn value(&self) -> RwSignal<SendOption<O>> {
let inner = self
.inner
pub fn value(&self) -> MappedSignal<Option<O>> {
self.inner
.try_with_value(|inner| inner.value())
.unwrap_or_else(unwrap_signal!(self));
inner.into()
.unwrap_or_else(unwrap_signal!(self))
.into()
}
/// The most recent return value of the `async` function. This will be `None` before
@@ -919,15 +934,14 @@ where
#[deprecated = "You can now use .value() for any value, whether it's \
thread-safe or not."]
#[track_caller]
pub fn value_local(&self) -> RwSignal<SendOption<O>, LocalStorage>
pub fn value_local(&self) -> MappedSignal<Option<O>>
where
O: Send + Sync,
{
let inner = self
.inner
self.inner
.try_with_value(|inner| inner.value())
.unwrap_or_else(unwrap_signal!(self));
RwSignal::from_local(inner)
.unwrap_or_else(unwrap_signal!(self))
.into()
}
}
@@ -1100,7 +1114,7 @@ impl<I, O> Copy for Action<I, O> {}
/// });
///
/// act.dispatch(3);
/// assert_eq!(*act.input().get(), Some(3));
/// assert_eq!(act.input().get(), Some(3));
///
/// // Remember that async functions already return a future if they are
/// // not `await`ed. You can save keystrokes by leaving out the `async move`
@@ -1110,7 +1124,7 @@ impl<I, O> Copy for Action<I, O> {}
/// # tokio::time::sleep(std::time::Duration::from_millis(10)).await;
///
/// // after it resolves
/// assert_eq!(*act2.value().get(), Some("I'M IN A DOCTEST".to_string()));
/// assert_eq!(act2.value().get(), Some("I'M IN A DOCTEST".to_string()));
///
/// async fn yell(n: String) -> String {
/// n.to_uppercase()

View File

@@ -6,6 +6,7 @@ mod arc_rw;
mod arc_trigger;
mod arc_write;
pub mod guards;
mod mapped;
mod read;
mod rw;
mod subscriber_traits;
@@ -17,6 +18,7 @@ pub use arc_read::*;
pub use arc_rw::*;
pub use arc_trigger::*;
pub use arc_write::*;
pub use mapped::*;
pub use read::*;
pub use rw::*;
pub use trigger::*;

View File

@@ -0,0 +1,358 @@
use super::{
guards::{Mapped, MappedMutArc},
ArcRwSignal, RwSignal,
};
use crate::{
owner::{StoredValue, SyncStorage},
signal::guards::WriteGuard,
traits::{
DefinedAt, GetValue, IsDisposed, Notify, ReadUntracked, Track,
UntrackableGuard, Write,
},
};
use guardian::ArcRwLockWriteGuardian;
use std::{
fmt::Debug,
ops::{Deref, DerefMut},
panic::Location,
sync::Arc,
};
/// A derived signal type that wraps an [`ArcRwSignal`] with a mapping function,
/// allowing you to read or write directly to one of its field.
///
/// Tracking the mapped signal tracks changes to *any* part of the signal, and updating the signal notifies
/// and notifies *all* depenendencies of the signal. This is not a mechanism for fine-grained reactive updates
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
/// without exposing the original type directly to users.
pub struct ArcMappedSignal<T> {
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
#[allow(clippy::type_complexity)]
try_read_untracked: Arc<
dyn Fn() -> Option<DoubleDeref<Box<dyn Deref<Target = T>>>>
+ Send
+ Sync,
>,
try_write: Arc<
dyn Fn() -> Option<Box<dyn UntrackableGuard<Target = T>>> + Send + Sync,
>,
notify: Arc<dyn Fn() + Send + Sync>,
track: Arc<dyn Fn() + Send + Sync>,
}
impl<T> Clone for ArcMappedSignal<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
try_read_untracked: self.try_read_untracked.clone(),
try_write: self.try_write.clone(),
notify: self.notify.clone(),
track: self.track.clone(),
}
}
}
impl<T> ArcMappedSignal<T> {
/// Wraps a signal with the given mapping functions for shared and exclusive references.
#[track_caller]
pub fn new<U>(
inner: ArcRwSignal<U>,
map: fn(&U) -> &T,
map_mut: fn(&mut U) -> &mut T,
) -> Self
where
T: 'static,
U: Send + Sync + 'static,
{
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
try_read_untracked: {
let this = inner.clone();
Arc::new(move || {
this.try_read_untracked().map(|guard| DoubleDeref {
inner: Box::new(Mapped::new_with_guard(guard, map))
as Box<dyn Deref<Target = T>>,
})
})
},
try_write: {
let this = inner.clone();
Arc::new(move || {
let guard = ArcRwLockWriteGuardian::try_take(Arc::clone(
&this.value,
))?
.ok()?;
let mapped = WriteGuard::new(
this.clone(),
MappedMutArc::new(guard, map, map_mut),
);
Some(Box::new(mapped))
})
},
notify: {
let this = inner.clone();
Arc::new(move || {
this.notify();
})
},
track: {
Arc::new(move || {
inner.track();
})
},
}
}
}
impl<T> Debug for ArcMappedSignal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut partial = f.debug_struct("ArcMappedSignal");
#[cfg(debug_assertions)]
partial.field("defined_at", &self.defined_at);
partial.finish()
}
}
impl<T> DefinedAt for ArcMappedSignal<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<T> Notify for ArcMappedSignal<T> {
fn notify(&self) {
(self.notify)()
}
}
impl<T> Track for ArcMappedSignal<T> {
fn track(&self) {
(self.track)()
}
}
impl<T> ReadUntracked for ArcMappedSignal<T> {
type Value = DoubleDeref<Box<dyn Deref<Target = T>>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
(self.try_read_untracked)()
}
}
impl<T> IsDisposed for ArcMappedSignal<T> {
fn is_disposed(&self) -> bool {
false
}
}
impl<T> Write for ArcMappedSignal<T>
where
T: 'static,
{
type Value = T;
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
let mut guard = self.try_write()?;
guard.untrack();
Some(guard)
}
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
let inner = (self.try_write)()?;
let inner = DoubleDeref { inner };
Some(inner)
}
}
/// A wrapper for a smart pointer that implements [`Deref`] and [`DerefMut`]
/// by dereferencing the type *inside* the smart pointer.
///
/// This is quite obscure and mostly useful for situations in which we want
/// a wrapper for `Box<dyn Deref<Target = T>>` that dereferences to `T` rather
/// than dereferencing to `dyn Deref<Target = T>`.
///
/// This is used internally in [`MappedSignal`] and [`ArcMappedSignal`].
pub struct DoubleDeref<T> {
inner: T,
}
impl<T> Deref for DoubleDeref<T>
where
T: Deref,
T::Target: Deref,
{
type Target = <T::Target as Deref>::Target;
fn deref(&self) -> &Self::Target {
self.inner.deref().deref()
}
}
impl<T> DerefMut for DoubleDeref<T>
where
T: DerefMut,
T::Target: DerefMut,
{
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner.deref_mut().deref_mut()
}
}
impl<T> UntrackableGuard for DoubleDeref<T>
where
T: UntrackableGuard,
T::Target: DerefMut,
{
fn untrack(&mut self) {
self.inner.untrack();
}
}
/// A derived signal type that wraps an [`RwSignal`] with a mapping function,
/// allowing you to read or write directly to one of its field.
///
/// Tracking the mapped signal tracks changes to *any* part of the signal, and updating the signal notifies
/// and notifies *all* depenendencies of the signal. This is not a mechanism for fine-grained reactive updates
/// to more complex data structures. Instead, it allows you to provide a signal-like API for wrapped types
/// without exposing the original type directly to users.
pub struct MappedSignal<T, S = SyncStorage> {
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
inner: StoredValue<ArcMappedSignal<T>, S>,
}
impl<T> MappedSignal<T> {
/// Wraps a signal with the given mapping functions for shared and exclusive references.
#[track_caller]
pub fn new<U>(
inner: RwSignal<U>,
map: fn(&U) -> &T,
map_mut: fn(&mut U) -> &mut T,
) -> Self
where
T: Send + Sync + 'static,
U: Send + Sync + 'static,
{
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
inner: {
let this = ArcRwSignal::from(inner);
StoredValue::new_with_storage(ArcMappedSignal::new(
this, map, map_mut,
))
},
}
}
}
impl<T> Debug for MappedSignal<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut partial = f.debug_struct("MappedSignal");
#[cfg(debug_assertions)]
partial.field("defined_at", &self.defined_at);
partial.finish()
}
}
impl<T> DefinedAt for MappedSignal<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<T> Notify for MappedSignal<T>
where
T: 'static,
{
fn notify(&self) {
if let Some(inner) = self.inner.try_get_value() {
inner.notify();
}
}
}
impl<T> Track for MappedSignal<T>
where
T: 'static,
{
fn track(&self) {
if let Some(inner) = self.inner.try_get_value() {
inner.track();
}
}
}
impl<T> ReadUntracked for MappedSignal<T>
where
T: 'static,
{
type Value = DoubleDeref<Box<dyn Deref<Target = T>>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.inner
.try_get_value()
.and_then(|inner| inner.try_read_untracked())
}
}
impl<T> Write for MappedSignal<T>
where
T: 'static,
{
type Value = T;
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
let mut guard = self.try_write()?;
guard.untrack();
Some(guard)
}
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
let inner = self.inner.try_get_value()?;
let inner = (inner.try_write)()?;
let inner = DoubleDeref { inner };
Some(inner)
}
}
impl<T> From<ArcMappedSignal<T>> for MappedSignal<T>
where
T: 'static,
{
#[track_caller]
fn from(value: ArcMappedSignal<T>) -> Self {
MappedSignal {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
inner: StoredValue::new(value),
}
}
}
impl<T> IsDisposed for MappedSignal<T> {
fn is_disposed(&self) -> bool {
self.inner.is_disposed()
}
}

View File

@@ -230,6 +230,12 @@ pub trait UntrackableGuard: DerefMut {
fn untrack(&mut self);
}
impl<T> UntrackableGuard for Box<dyn UntrackableGuard<Target = T>> {
fn untrack(&mut self) {
(**self).untrack();
}
}
/// 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 Write: Sized + DefinedAt + Notify {