add MultiAction/ServerMultiAction

This commit is contained in:
Greg Johnston 2024-04-19 20:13:51 -04:00
parent 883fd57fe1
commit 72e97047a5
5 changed files with 643 additions and 397 deletions

View File

@ -1,12 +1,12 @@
use reactive_graph::{
action::ArcAction,
actions::{Action, ArcAction},
owner::StoredValue,
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
traits::DefinedAt,
unwrap_signal,
};
use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError};
use std::panic::Location;
use std::{ops::Deref, panic::Location};
pub struct ArcServerAction<S>
where
@ -32,34 +32,17 @@ where
defined_at: Location::caller(),
}
}
#[track_caller]
pub fn dispatch(&self, input: S) {
self.inner.dispatch(input);
}
}
impl<S> ArcServerAction<S>
impl<S> Deref for ArcServerAction<S>
where
S: ServerFn + Clone + Send + Sync + 'static,
S: ServerFn + 'static,
S::Output: 'static,
S::Error: 'static,
{
#[track_caller]
pub fn version(&self) -> ArcRwSignal<usize> {
self.inner.version()
}
type Target = ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>;
#[track_caller]
pub fn input(&self) -> ArcRwSignal<Option<S>> {
self.inner.input()
}
#[track_caller]
pub fn value(
&self,
) -> ArcRwSignal<Option<Result<S::Output, ServerFnError<S::Error>>>> {
self.inner.value()
fn deref(&self) -> &Self::Target {
&self.inner
}
}
@ -110,11 +93,26 @@ where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: StoredValue<ArcServerAction<S>>,
inner: Action<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<S> ServerAction<S>
where
S: ServerFn + Send + Sync + Clone + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
inner: Action::new(|input: &S| S::run_on_client(input.clone())),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
}
impl<S> Clone for ServerAction<S>
where
S: ServerFn + 'static,
@ -132,50 +130,27 @@ where
{
}
impl<S> ServerAction<S>
impl<S> Deref for ServerAction<S>
where
S: ServerFn + Clone + Send + Sync + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
#[track_caller]
pub fn new() -> Self {
Self {
inner: StoredValue::new(ArcServerAction::new()),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
type Target = Action<S, Result<S::Output, ServerFnError<S::Error>>>;
#[track_caller]
pub fn dispatch(&self, input: S) {
self.inner.with_value(|inner| inner.dispatch(input));
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[track_caller]
pub fn version(&self) -> RwSignal<usize> {
self.inner
.with_value(|inner| inner.version())
.unwrap_or_else(unwrap_signal!(self))
.into()
}
#[track_caller]
pub fn input(&self) -> RwSignal<Option<S>> {
self.inner
.with_value(|inner| inner.input())
.unwrap_or_else(unwrap_signal!(self))
.into()
}
#[track_caller]
pub fn value(
&self,
) -> RwSignal<Option<Result<S::Output, ServerFnError<S::Error>>>> {
self.inner
.with_value(|inner| inner.value())
.unwrap_or_else(unwrap_signal!(self))
.into()
impl<S> From<ServerAction<S>>
for Action<S, Result<S::Output, ServerFnError<S::Error>>>
where
S: ServerFn + 'static,
S::Output: 'static,
{
fn from(value: ServerAction<S>) -> Self {
value.inner
}
}

View File

@ -3,6 +3,8 @@
mod action;
pub use action::*;
mod multi_action;
pub use multi_action::*;
#[cfg(feature = "hydration")]
mod resource;
#[cfg(feature = "hydration")]

View File

@ -1,350 +1,184 @@
use leptos_reactive::{
is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value,
untrack, StoredValue,
use reactive_graph::{
actions::{ArcMultiAction, MultiAction},
traits::DefinedAt,
};
use server_fn::{ServerFn, ServerFnError};
use std::{future::Future, pin::Pin, rc::Rc};
use std::{ops::Deref, panic::Location};
/// An action that synchronizes multiple imperative `async` calls to the reactive system,
/// tracking the progress of each one.
///
/// Where an [Action](crate::Action) fires a single call, a `MultiAction` allows you to
/// keep track of multiple in-flight actions.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [Resource](leptos_reactive::Resource) instead. If youre trying to occasionally
/// run an `async` function in response to something like a user adding a task to a todo list,
/// youre in the right place.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// }
/// let add_todo = create_multi_action(|task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
/// send_new_todo_to_api(task.clone())
/// });
///
/// # if false {
/// add_todo.dispatch("Buy milk".to_string());
/// add_todo.dispatch("???".to_string());
/// add_todo.dispatch("Profit!!!".to_string());
/// # }
///
/// # runtime.dispose();
/// ```
///
/// The input to the `async` function should always be a single value,
/// but it can be of any type. The argument is always passed by reference to the
/// function, because it is stored in [Submission::input] as well.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(|input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_multi_action(|input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_multi_action(|input: &(usize, String)| async { todo!() });
/// # runtime.dispose();
/// ```
pub struct MultiAction<I, O>(StoredValue<MultiActionState<I, O>>)
pub struct ArcServerMultiAction<S>
where
I: 'static,
O: 'static;
impl<I, O> MultiAction<I, O>
where
I: 'static,
O: 'static,
S: ServerFn + 'static,
S::Output: 'static,
{
inner: ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<I, O> Clone for MultiAction<I, O>
impl<S> ArcServerMultiAction<S>
where
I: 'static,
O: 'static,
S: ServerFn + Clone + Send + Sync + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
fn clone(&self) -> Self {
*self
}
}
impl<I, O> Copy for MultiAction<I, O>
where
I: 'static,
O: 'static,
{
}
impl<I, O> MultiAction<I, O>
where
I: 'static,
O: 'static,
{
/// Calls the `async` function with a reference to the input type as its argument.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn dispatch(&self, input: I) {
self.0.with_value(|a| a.dispatch(input))
}
/// The set of all submissions to this multi-action.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
self.0.with_value(|a| a.submissions())
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {
self.0.with_value(|a| a.url.as_ref().cloned())
}
/// How many times an action has successfully resolved.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn version(&self) -> RwSignal<usize> {
self.0.with_value(|a| a.version)
}
/// Associates the URL of the given server function with this action.
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn using_server_fn<T: ServerFn>(self) -> Self {
self.0.update_value(|a| {
a.url = Some(T::url().to_string());
});
self
}
}
struct MultiActionState<I, O>
where
I: 'static,
O: 'static,
{
/// How many times an action has successfully resolved.
pub version: RwSignal<usize>,
submissions: RwSignal<Vec<Submission<I, O>>>,
url: Option<String>,
#[allow(clippy::complexity)]
action_fn: Rc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
}
/// An action that has been submitted by dispatching it to a [MultiAction](crate::MultiAction).
pub struct Submission<I, O>
where
I: 'static,
O: 'static,
{
/// The current argument that was dispatched to the `async` function.
/// `Some` while we are waiting for it to resolve, `None` if it has resolved.
pub input: RwSignal<Option<I>>,
/// The most recent return value of the `async` function.
pub value: RwSignal<Option<O>>,
pub(crate) pending: RwSignal<bool>,
/// Controls this submission has been canceled.
pub canceled: RwSignal<bool>,
}
impl<I, O> Clone for Submission<I, O> {
fn clone(&self) -> Self {
*self
}
}
impl<I, O> Copy for Submission<I, O> {}
impl<I, O> Submission<I, O>
where
I: 'static,
O: 'static,
{
/// Whether this submission is currently waiting to resolve.
pub fn pending(&self) -> ReadSignal<bool> {
self.pending.read_only()
}
/// Cancels the submission, preventing it from resolving.
pub fn cancel(&self) {
self.canceled.set(true);
}
}
impl<I, O> MultiActionState<I, O>
where
I: 'static,
O: 'static,
{
/// Calls the `async` function with a reference to the input type as its argument.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn dispatch(&self, input: I) {
if !is_suppressing_resource_load() {
let fut = (self.action_fn)(&input);
let submission = Submission {
input: create_rw_signal(Some(input)),
value: create_rw_signal(None),
pending: create_rw_signal(true),
canceled: create_rw_signal(false),
};
self.submissions.update(|subs| subs.push(submission));
let canceled = submission.canceled;
let input = submission.input;
let pending = submission.pending;
let value = submission.value;
let version = self.version;
spawn_local(async move {
let new_value = fut.await;
let canceled = untrack(move || canceled.get());
if !canceled {
value.set(Some(new_value));
}
input.set(None);
pending.set(false);
version.update(|n| *n += 1);
})
#[track_caller]
pub fn new() -> Self {
Self {
inner: ArcMultiAction::new(|input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
}
/// The set of all submissions to this multi-action.
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
self.submissions.read_only()
impl<S> Deref for ArcServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
type Target = ArcMultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
/// Creates a [MultiAction] to synchronize an imperative `async` call to the synchronous reactive system.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [create_resource](leptos_reactive::create_resource) instead. If youre trying
/// to occasionally run an `async` function in response to something like a user clicking a button,
/// you're in the right place.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// }
/// let add_todo = create_multi_action(|task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
/// send_new_todo_to_api(task.clone())
/// });
/// # if false {
///
/// add_todo.dispatch("Buy milk".to_string());
/// add_todo.dispatch("???".to_string());
/// add_todo.dispatch("Profit!!!".to_string());
///
/// assert_eq!(add_todo.submissions().get().len(), 3);
/// # }
/// # runtime.dispose();
/// ```
///
/// The input to the `async` function should always be a single value,
/// but it can be of any type. The argument is always passed by reference to the
/// function, because it is stored in [Submission::input] as well.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(|input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_multi_action(|input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_multi_action(|input: &(usize, String)| async { todo!() });
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn create_multi_action<I, O, F, Fu>(action_fn: F) -> MultiAction<I, O>
impl<S> Clone for ArcServerMultiAction<S>
where
I: 'static,
O: 'static,
F: Fn(&I) -> Fu + 'static,
Fu: Future<Output = O> + 'static,
S: ServerFn + 'static,
S::Output: 'static,
{
let version = create_rw_signal(0);
let submissions = create_rw_signal(Vec::new());
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
MultiAction(store_value(MultiActionState {
version,
submissions,
url: None,
action_fn,
}))
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
}
/// Creates a [MultiAction] that can be used to call a server function.
///
/// ```rust,ignore
/// # use leptos::*;
///
/// #[server(MyServerFn)]
/// async fn my_server_fn() -> Result<(), ServerFnError> {
/// todo!()
/// }
///
/// # let runtime = create_runtime();
/// let my_server_multi_action = create_server_multi_action::<MyServerFn>();
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn create_server_multi_action<S>(
) -> MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
impl<S> Default for ArcServerMultiAction<S>
where
S: Clone + ServerFn,
S: ServerFn + Clone + Send + Sync + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
#[cfg(feature = "ssr")]
let c = move |args: &S| S::run_body(args.clone());
#[cfg(not(feature = "ssr"))]
let c = move |args: &S| S::run_on_client(args.clone());
create_multi_action(c).using_server_fn::<S>()
fn default() -> Self {
Self::new()
}
}
impl<S> DefinedAt for ArcServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
pub struct ServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
inner: MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<S> From<ServerMultiAction<S>>
for MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
where
S: ServerFn + 'static,
S::Output: 'static,
{
fn from(value: ServerMultiAction<S>) -> Self {
value.inner
}
}
impl<S> ServerMultiAction<S>
where
S: ServerFn + Send + Sync + Clone + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
inner: MultiAction::new(|input: &S| {
S::run_on_client(input.clone())
}),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
}
impl<S> Clone for ServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
fn clone(&self) -> Self {
*self
}
}
impl<S> Copy for ServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
}
impl<S> Deref for ServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
S::Error: 'static,
{
type Target = MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<S> Default for ServerMultiAction<S>
where
S: ServerFn + Clone + Send + Sync + 'static,
S::Output: Send + Sync + 'static,
S::Error: Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<S> DefinedAt for ServerMultiAction<S>
where
S: ServerFn + 'static,
S::Output: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}

View File

@ -0,0 +1,350 @@
use leptos_reactive::{
is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value,
untrack, StoredValue,
};
use server_fn::{ServerFn, ServerFnError};
use std::{future::Future, pin::Pin, rc::Rc};
/// An action that synchronizes multiple imperative `async` calls to the reactive system,
/// tracking the progress of each one.
///
/// Where an [Action](crate::Action) fires a single call, a `MultiAction` allows you to
/// keep track of multiple in-flight actions.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [Resource](leptos_reactive::Resource) instead. If youre trying to occasionally
/// run an `async` function in response to something like a user adding a task to a todo list,
/// youre in the right place.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// }
/// let add_todo = create_multi_action(|task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
/// send_new_todo_to_api(task.clone())
/// });
///
/// # if false {
/// add_todo.dispatch("Buy milk".to_string());
/// add_todo.dispatch("???".to_string());
/// add_todo.dispatch("Profit!!!".to_string());
/// # }
///
/// # runtime.dispose();
/// ```
///
/// The input to the `async` function should always be a single value,
/// but it can be of any type. The argument is always passed by reference to the
/// function, because it is stored in [Submission::input] as well.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(|input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_multi_action(|input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_multi_action(|input: &(usize, String)| async { todo!() });
/// # runtime.dispose();
/// ```
pub struct MultiAction<I, O>(StoredValue<MultiActionState<I, O>>)
where
I: 'static,
O: 'static;
impl<I, O> MultiAction<I, O>
where
I: 'static,
O: 'static,
{
}
impl<I, O> Clone for MultiAction<I, O>
where
I: 'static,
O: 'static,
{
fn clone(&self) -> Self {
*self
}
}
impl<I, O> Copy for MultiAction<I, O>
where
I: 'static,
O: 'static,
{
}
impl<I, O> MultiAction<I, O>
where
I: 'static,
O: 'static,
{
/// Calls the `async` function with a reference to the input type as its argument.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn dispatch(&self, input: I) {
self.0.with_value(|a| a.dispatch(input))
}
/// The set of all submissions to this multi-action.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
self.0.with_value(|a| a.submissions())
}
/// The URL associated with the action (typically as part of a server function.)
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
pub fn url(&self) -> Option<String> {
self.0.with_value(|a| a.url.as_ref().cloned())
}
/// How many times an action has successfully resolved.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn version(&self) -> RwSignal<usize> {
self.0.with_value(|a| a.version)
}
/// Associates the URL of the given server function with this action.
/// This enables integration with the `MultiActionForm` component in `leptos_router`.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn using_server_fn<T: ServerFn>(self) -> Self {
self.0.update_value(|a| {
a.url = Some(T::url().to_string());
});
self
}
}
struct MultiActionState<I, O>
where
I: 'static,
O: 'static,
{
/// How many times an action has successfully resolved.
pub version: RwSignal<usize>,
submissions: RwSignal<Vec<Submission<I, O>>>,
url: Option<String>,
#[allow(clippy::complexity)]
action_fn: Rc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
}
/// An action that has been submitted by dispatching it to a [MultiAction](crate::MultiAction).
pub struct Submission<I, O>
where
I: 'static,
O: 'static,
{
/// The current argument that was dispatched to the `async` function.
/// `Some` while we are waiting for it to resolve, `None` if it has resolved.
pub input: RwSignal<Option<I>>,
/// The most recent return value of the `async` function.
pub value: RwSignal<Option<O>>,
pub(crate) pending: RwSignal<bool>,
/// Controls this submission has been canceled.
pub canceled: RwSignal<bool>,
}
impl<I, O> Clone for Submission<I, O> {
fn clone(&self) -> Self {
*self
}
}
impl<I, O> Copy for Submission<I, O> {}
impl<I, O> Submission<I, O>
where
I: 'static,
O: 'static,
{
/// Whether this submission is currently waiting to resolve.
pub fn pending(&self) -> ReadSignal<bool> {
self.pending.read_only()
}
/// Cancels the submission, preventing it from resolving.
pub fn cancel(&self) {
self.canceled.set(true);
}
}
impl<I, O> MultiActionState<I, O>
where
I: 'static,
O: 'static,
{
/// Calls the `async` function with a reference to the input type as its argument.
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn dispatch(&self, input: I) {
if !is_suppressing_resource_load() {
let fut = (self.action_fn)(&input);
let submission = Submission {
input: create_rw_signal(Some(input)),
value: create_rw_signal(None),
pending: create_rw_signal(true),
canceled: create_rw_signal(false),
};
self.submissions.update(|subs| subs.push(submission));
let canceled = submission.canceled;
let input = submission.input;
let pending = submission.pending;
let value = submission.value;
let version = self.version;
spawn_local(async move {
let new_value = fut.await;
let canceled = untrack(move || canceled.get());
if !canceled {
value.set(Some(new_value));
}
input.set(None);
pending.set(false);
version.update(|n| *n += 1);
})
}
}
/// The set of all submissions to this multi-action.
pub fn submissions(&self) -> ReadSignal<Vec<Submission<I, O>>> {
self.submissions.read_only()
}
}
/// Creates an [MultiAction] to synchronize an imperative `async` call to the synchronous reactive system.
///
/// If youre trying to load data by running an `async` function reactively, you probably
/// want to use a [create_resource](leptos_reactive::create_resource) instead. If youre trying
/// to occasionally run an `async` function in response to something like a user clicking a button,
/// you're in the right place.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// async fn send_new_todo_to_api(task: String) -> usize {
/// // do something...
/// // return a task id
/// 42
/// }
/// let add_todo = create_multi_action(|task: &String| {
/// // `task` is given as `&String` because its value is available in `input`
/// send_new_todo_to_api(task.clone())
/// });
/// # if false {
///
/// add_todo.dispatch("Buy milk".to_string());
/// add_todo.dispatch("???".to_string());
/// add_todo.dispatch("Profit!!!".to_string());
///
/// assert_eq!(add_todo.submissions().get().len(), 3);
/// # }
/// # runtime.dispose();
/// ```
///
/// The input to the `async` function should always be a single value,
/// but it can be of any type. The argument is always passed by reference to the
/// function, because it is stored in [Submission::input] as well.
///
/// ```rust
/// # use leptos::*;
/// # let runtime = create_runtime();
/// // if there's a single argument, just use that
/// let action1 = create_multi_action(|input: &String| {
/// let input = input.clone();
/// async move { todo!() }
/// });
///
/// // if there are no arguments, use the unit type `()`
/// let action2 = create_multi_action(|input: &()| async { todo!() });
///
/// // if there are multiple arguments, use a tuple
/// let action3 =
/// create_multi_action(|input: &(usize, String)| async { todo!() });
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn create_multi_action<I, O, F, Fu>(action_fn: F) -> MultiAction<I, O>
where
I: 'static,
O: 'static,
F: Fn(&I) -> Fu + 'static,
Fu: Future<Output = O> + 'static,
{
let version = create_rw_signal(0);
let submissions = create_rw_signal(Vec::new());
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
MultiAction(store_value(MultiActionState {
version,
submissions,
url: None,
action_fn,
}))
}
/// Creates an [MultiAction] that can be used to call a server function.
///
/// ```rust,ignore
/// # use leptos::*;
///
/// #[server(MyServerFn)]
/// async fn my_server_fn() -> Result<(), ServerFnError> {
/// todo!()
/// }
///
/// # let runtime = create_runtime();
/// let my_server_multi_action = create_server_multi_action::<MyServerFn>();
/// # runtime.dispose();
/// ```
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn create_server_multi_action<S>(
) -> MultiAction<S, Result<S::Output, ServerFnError<S::Error>>>
where
S: Clone + ServerFn,
{
#[cfg(feature = "ssr")]
let c = move |args: &S| S::run_body(args.clone());
#[cfg(not(feature = "ssr"))]
let c = move |args: &S| S::run_on_client(args.clone());
create_multi_action(c).using_server_fn::<S>()
}

View File

@ -2,10 +2,11 @@ use crate::{
diagnostics::is_suppressing_resource_load,
owner::StoredValue,
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
traits::{GetUntracked, Set, Update},
traits::{DefinedAt, GetUntracked, Set, Update},
unwrap_signal,
};
use any_spawner::Executor;
use std::{future::Future, pin::Pin, sync::Arc};
use std::{fmt::Debug, future::Future, panic::Location, pin::Pin, sync::Arc};
pub struct MultiAction<I, O>
where
@ -13,6 +14,25 @@ where
O: 'static,
{
inner: StoredValue<ArcMultiAction<I, O>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<I, O> DefinedAt for MultiAction<I, O>
where
I: 'static,
O: 'static,
{
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<I, O> Copy for MultiAction<I, O>
@ -38,13 +58,16 @@ where
O: Send + Sync + 'static,
{
#[track_caller]
pub fn new<Fut>(action_fn: impl Fn(&I) -> Fut + 'static) -> Self
pub fn new<Fut>(
action_fn: impl Fn(&I) -> Fut + Send + Sync + 'static,
) -> Self
where
Fut: Future<Output = O> + Send + 'static,
ArcMultiAction<I, O>: Send + Sync,
{
Self {
inner: StoredValue::new(ArcMultiAction::new(action_fn)),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
}
@ -55,14 +78,28 @@ where
}
}
/// Synchronously adds a submission with the given value.
///
/// This can be useful for use cases like handling errors, where the error can already be known
/// on the client side.
pub fn dispatch_sync(&self, value: O) {
self.inner.with_value(|inner| inner.dispatch_sync(value));
}
/// The set of all submissions to this multi-action.
pub fn submissions(&self) -> ReadSignal<Vec<ArcSubmission<I, O>>> {
todo!()
self.inner
.with_value(|inner| inner.submissions())
.unwrap_or_else(unwrap_signal!(self))
.into()
}
/// How many times an action has successfully resolved.
pub fn version(&self) -> RwSignal<usize> {
todo!()
self.inner
.with_value(|inner| inner.version())
.unwrap_or_else(unwrap_signal!(self))
.into()
}
}
@ -74,7 +111,36 @@ where
version: ArcRwSignal<usize>,
submissions: ArcRwSignal<Vec<ArcSubmission<I, O>>>,
#[allow(clippy::complexity)]
action_fn: Arc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>>>,
action_fn: Arc<
dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>> + Send + Sync,
>,
}
impl<I, O> Debug for ArcMultiAction<I, O>
where
I: 'static,
O: 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArcMultiAction")
.field("version", &self.version)
.field("submissions", &self.submissions)
.finish()
}
}
impl<I, O> Clone for ArcMultiAction<I, O>
where
I: 'static,
O: 'static,
{
fn clone(&self) -> Self {
Self {
version: self.version.clone(),
submissions: self.submissions.clone(),
action_fn: Arc::clone(&self.action_fn),
}
}
}
impl<I, O> ArcMultiAction<I, O>
@ -82,7 +148,9 @@ where
I: 'static,
O: 'static,
{
pub fn new<Fut>(action_fn: impl Fn(&I) -> Fut + 'static) -> Self
pub fn new<Fut>(
action_fn: impl Fn(&I) -> Fut + Send + Sync + 'static,
) -> Self
where
Fut: Future<Output = O> + Send + 'static,
{
@ -127,6 +195,23 @@ where
}
}
/// Synchronously adds a submission with the given value.
///
/// This can be useful for use cases like handling errors, where the error can already be known
/// on the client side.
pub fn dispatch_sync(&self, value: O) {
let submission = ArcSubmission {
input: ArcRwSignal::new(None),
value: ArcRwSignal::new(Some(value)),
pending: ArcRwSignal::new(true),
canceled: ArcRwSignal::new(false),
};
self.submissions
.try_update(|subs| subs.push(submission.clone()));
self.version.try_update(|n| *n += 1);
}
/// The set of all submissions to this multi-action.
pub fn submissions(&self) -> ArcReadSignal<Vec<ArcSubmission<I, O>>> {
self.submissions.read_only()