use crate::{ServerFn, ServerFnError};
use leptos_reactive::{
batch, create_rw_signal, signal_prelude::*, spawn_local, store_value,
ReadSignal, RwSignal, StoredValue,
};
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
pub struct Action<I, O>(StoredValue<ActionState<I, O>>)
where
I: 'static,
O: 'static;
impl<I, O> Action<I, O>
where
I: 'static,
O: 'static,
{
#[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))
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn pending(&self) -> ReadSignal<bool> {
self.0.with_value(|a| a.pending.read_only())
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn set_pending(&self, pending: bool) {
self.0.try_with_value(|a| {
let pending_dispatches = &a.pending_dispatches;
let still_pending = {
pending_dispatches.set(if pending {
pending_dispatches.get().wrapping_add(1)
} else {
pending_dispatches.get().saturating_sub(1)
});
pending_dispatches.get()
};
if still_pending == 0 {
a.pending.set(false);
} else {
a.pending.set(true);
}
});
}
pub fn url(&self) -> Option<String> {
self.0.with_value(|a| a.url.as_ref().cloned())
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn using_server_fn<T: ServerFn>(self) -> Self {
let prefix = T::prefix();
self.0.update_value(|state| {
state.url = if prefix.is_empty() {
Some(T::url().to_string())
} else {
Some(prefix.to_string() + "/" + T::url())
};
});
self
}
pub fn version(&self) -> RwSignal<usize> {
self.0.with_value(|a| a.version)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn input(&self) -> RwSignal<Option<I>> {
self.0.with_value(|a| a.input)
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn value(&self) -> RwSignal<Option<O>> {
self.0.with_value(|a| a.value)
}
}
impl<I, O> Clone for Action<I, O>
where
I: 'static,
O: 'static,
{
fn clone(&self) -> Self {
*self
}
}
impl<I, O> Copy for Action<I, O>
where
I: 'static,
O: 'static,
{
}
struct ActionState<I, O>
where
I: 'static,
O: 'static,
{
pub version: RwSignal<usize>,
pub input: RwSignal<Option<I>>,
pub value: RwSignal<Option<O>>,
pending: RwSignal<bool>,
url: Option<String>,
pending_dispatches: Rc<Cell<usize>>,
#[allow(clippy::complexity)]
action_fn: Rc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
}
impl<I, O> ActionState<I, O>
where
I: 'static,
O: 'static,
{
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn dispatch(&self, input: I) {
let fut = (self.action_fn)(&input);
self.input.set(Some(input));
let input = self.input;
let version = self.version;
let pending = self.pending;
let pending_dispatches = Rc::clone(&self.pending_dispatches);
let value = self.value;
pending.set(true);
pending_dispatches.set(pending_dispatches.get().saturating_sub(1));
spawn_local(async move {
let new_value = fut.await;
batch(move || {
value.set(Some(new_value));
input.set(None);
version.update(|n| *n += 1);
pending_dispatches
.set(pending_dispatches.get().saturating_sub(1));
if pending_dispatches.get() == 0 {
pending.set(false);
}
});
})
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn create_action<I, O, F, Fu>(action_fn: F) -> Action<I, O>
where
I: 'static,
O: 'static,
F: Fn(&I) -> Fu + 'static,
Fu: Future<Output = O> + 'static,
{
let version = create_rw_signal(0);
let input = create_rw_signal(None);
let value = create_rw_signal(None);
let pending = create_rw_signal(false);
let pending_dispatches = Rc::new(Cell::new(0));
let action_fn = Rc::new(move |input: &I| {
let fut = action_fn(input);
Box::pin(fut) as Pin<Box<dyn Future<Output = O>>>
});
Action(store_value(ActionState {
version,
url: None,
input,
value,
pending,
pending_dispatches,
action_fn,
}))
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
tracing::instrument(level = "trace", skip_all,)
)]
pub fn create_server_action<S>() -> Action<S, Result<S::Output, ServerFnError>>
where
S: Clone + ServerFn,
{
#[cfg(feature = "ssr")]
let c = move |args: &S| S::call_fn(args.clone(), ());
#[cfg(not(feature = "ssr"))]
let c = move |args: &S| S::call_fn_client(args.clone(), ());
create_action(c).using_server_fn::<S>()
}