mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
work related to 0.7 blog port
This commit is contained in:
parent
b41fde3ff9
commit
2bc04444e1
16 changed files with 888 additions and 613 deletions
|
@ -58,12 +58,10 @@ leptos_reactive = { path = "./leptos_reactive", version = "0.6.5" }
|
||||||
leptos_router = { path = "./router", version = "0.6.5" }
|
leptos_router = { path = "./router", version = "0.6.5" }
|
||||||
leptos_server = { path = "./leptos_server", version = "0.6.5" }
|
leptos_server = { path = "./leptos_server", version = "0.6.5" }
|
||||||
leptos_meta = { path = "./meta", version = "0.6.5" }
|
leptos_meta = { path = "./meta", version = "0.6.5" }
|
||||||
next_tuple = { path = "./next_tuple" }
|
|
||||||
oco = { path = "./oco" }
|
oco = { path = "./oco" }
|
||||||
or_poisoned = { path = "./or_poisoned" }
|
or_poisoned = { path = "./or_poisoned" }
|
||||||
reactive_graph = { path = "./reactive_graph" }
|
reactive_graph = { path = "./reactive_graph" }
|
||||||
routing = { path = "./routing" }
|
routing = { path = "./routing" }
|
||||||
routing_utils = { path = "./routing_utils" }
|
|
||||||
server_fn = { path = "./server_fn", version = "0.6.5" }
|
server_fn = { path = "./server_fn", version = "0.6.5" }
|
||||||
server_fn_macro = { path = "./server_fn_macro", version = "0.6.5" }
|
server_fn_macro = { path = "./server_fn_macro", version = "0.6.5" }
|
||||||
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.6" }
|
||||||
|
|
|
@ -16,6 +16,7 @@ pub fn App() -> impl IntoView {
|
||||||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||||
|
<Example/>
|
||||||
<Router>
|
<Router>
|
||||||
<Nav />
|
<Nav />
|
||||||
<main>
|
<main>
|
||||||
|
@ -29,6 +30,48 @@ pub fn App() -> impl IntoView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use leptos::*;
|
||||||
|
|
||||||
|
#[island]
|
||||||
|
pub fn CommonIsland() -> impl IntoView {
|
||||||
|
let val = RwSignal::new(0);
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
{move || format!("CommonIsland value is {}", val.get())}
|
||||||
|
<button on:click=move|_| val.update(|x| {*x += 1})>Click</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[island]
|
||||||
|
pub fn OuterWorking(children: Children) -> impl IntoView {
|
||||||
|
let val = RwSignal::new(0);
|
||||||
|
view! {
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
{move || format!("outer value is {}", val.get())}
|
||||||
|
<button on:click=move|_| val.update(|x| {*x += 1})>Click</button>
|
||||||
|
</div>
|
||||||
|
{children()}
|
||||||
|
</>
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Example() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<OuterFailing/>
|
||||||
|
|
||||||
|
<OuterWorking>
|
||||||
|
<CommonIsland/>
|
||||||
|
</OuterWorking>
|
||||||
|
|
||||||
|
<CommonIsland/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn hydrate() {
|
pub fn hydrate() {
|
||||||
|
|
|
@ -24,10 +24,11 @@ paste = "1"
|
||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
reactive_graph = { workspace = true, features = ["serde"] }
|
reactive_graph = { workspace = true, features = ["serde"] }
|
||||||
tachys = { workspace = true, features = ["reactive_graph"] }
|
tachys = { workspace = true, features = ["reactive_graph"] }
|
||||||
|
thiserror = "1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
typed-builder = "0.18"
|
typed-builder = "0.18"
|
||||||
typed-builder-macro = "0.18"
|
typed-builder-macro = "0.18"
|
||||||
serde = { version = "1", optional = true }
|
serde = "1"
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
server_fn = { workspace = true, features = [
|
server_fn = { workspace = true, features = [
|
||||||
"form-redirects",
|
"form-redirects",
|
||||||
|
@ -41,6 +42,7 @@ web-sys = { version = "0.3.63", features = [
|
||||||
"ShadowRootMode",
|
"ShadowRootMode",
|
||||||
] }
|
] }
|
||||||
wasm-bindgen = { version = "0.2", optional = true }
|
wasm-bindgen = { version = "0.2", optional = true }
|
||||||
|
serde_qs = "0.12.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serde"]
|
default = ["serde"]
|
||||||
|
@ -78,7 +80,6 @@ experimental-islands = [
|
||||||
"leptos_dom/experimental-islands",
|
"leptos_dom/experimental-islands",
|
||||||
"leptos_macro/experimental-islands",
|
"leptos_macro/experimental-islands",
|
||||||
"leptos_reactive/experimental-islands",
|
"leptos_reactive/experimental-islands",
|
||||||
"dep:serde",
|
|
||||||
"dep:serde_json",
|
"dep:serde_json",
|
||||||
]
|
]
|
||||||
trace-component-props = [
|
trace-component-props = [
|
||||||
|
|
180
leptos/src/action_form.rs
Normal file
180
leptos/src/action_form.rs
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
use crate::{
|
||||||
|
children::Children, component, from_form_data::FromFormData, prelude::*,
|
||||||
|
IntoView,
|
||||||
|
};
|
||||||
|
use leptos_dom::{events::submit, helpers::window};
|
||||||
|
use leptos_server::ServerAction;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use server_fn::{
|
||||||
|
client::Client, codec::PostUrl, request::ClientReq, ServerFn, ServerFnError,
|
||||||
|
};
|
||||||
|
use tachys::{
|
||||||
|
either::Either,
|
||||||
|
html::{
|
||||||
|
attribute::any_attribute::AnyAttribute,
|
||||||
|
element::{form, Form},
|
||||||
|
},
|
||||||
|
reactive_graph::node_ref::NodeRef,
|
||||||
|
renderer::dom::Dom,
|
||||||
|
};
|
||||||
|
use web_sys::{FormData, SubmitEvent};
|
||||||
|
|
||||||
|
/// Automatically turns a server [Action](leptos_server::Action) into an HTML
|
||||||
|
/// [`form`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form)
|
||||||
|
/// progressively enhanced to use client-side routing.
|
||||||
|
///
|
||||||
|
/// ## Encoding
|
||||||
|
/// **Note:** `<ActionForm/>` only works with server functions that use the
|
||||||
|
/// default `Url` encoding. This is to ensure that `<ActionForm/>` works correctly
|
||||||
|
/// both before and after WASM has loaded.
|
||||||
|
///
|
||||||
|
/// ## Complex Inputs
|
||||||
|
/// Server function arguments that are structs with nested serializable fields
|
||||||
|
/// should make use of indexing notation of `serde_qs`.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use leptos::*;
|
||||||
|
/// # use leptos_router::*;
|
||||||
|
///
|
||||||
|
/// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
|
/// struct HeftyData {
|
||||||
|
/// first_name: String,
|
||||||
|
/// last_name: String,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[component]
|
||||||
|
/// fn ComplexInput() -> impl IntoView {
|
||||||
|
/// let submit = Action::<VeryImportantFn, _>::server();
|
||||||
|
///
|
||||||
|
/// view! {
|
||||||
|
/// <ActionForm action=submit>
|
||||||
|
/// <input type="text" name="hefty_arg[first_name]" value="leptos"/>
|
||||||
|
/// <input
|
||||||
|
/// type="text"
|
||||||
|
/// name="hefty_arg[last_name]"
|
||||||
|
/// value="closures-everywhere"
|
||||||
|
/// />
|
||||||
|
/// <input type="submit"/>
|
||||||
|
/// </ActionForm>
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[server]
|
||||||
|
/// async fn very_important_fn(
|
||||||
|
/// hefty_arg: HeftyData,
|
||||||
|
/// ) -> Result<(), ServerFnError> {
|
||||||
|
/// assert_eq!(hefty_arg.first_name.as_str(), "leptos");
|
||||||
|
/// assert_eq!(hefty_arg.last_name.as_str(), "closures-everywhere");
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[cfg_attr(
|
||||||
|
any(debug_assertions, feature = "ssr"),
|
||||||
|
tracing::instrument(level = "trace", skip_all,)
|
||||||
|
)]
|
||||||
|
#[component]
|
||||||
|
pub fn ActionForm<ServFn>(
|
||||||
|
/// The action from which to build the form. This should include a URL, which can be generated
|
||||||
|
/// by default using [`create_server_action`](leptos_server::create_server_action) or added
|
||||||
|
/// manually using [`using_server_fn`](leptos_server::Action::using_server_fn).
|
||||||
|
action: ServerAction<ServFn>,
|
||||||
|
/// A [`NodeRef`] in which the `<form>` element should be stored.
|
||||||
|
#[prop(optional)]
|
||||||
|
node_ref: Option<NodeRef<Form>>,
|
||||||
|
/// Arbitrary attributes to add to the `<form>`
|
||||||
|
#[prop(attrs, optional)]
|
||||||
|
attributes: Vec<AnyAttribute<Dom>>,
|
||||||
|
/// Component children; should include the HTML of the form elements.
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView
|
||||||
|
where
|
||||||
|
ServFn: DeserializeOwned
|
||||||
|
+ ServerFn<InputEncoding = PostUrl>
|
||||||
|
+ Clone
|
||||||
|
+ Send
|
||||||
|
+ Sync
|
||||||
|
+ 'static,
|
||||||
|
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<
|
||||||
|
ServFn::Error,
|
||||||
|
>>::FormData: From<FormData>,
|
||||||
|
ServFn: Send + Sync + 'static,
|
||||||
|
ServFn::Output: Send + Sync + 'static,
|
||||||
|
ServFn::Error: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
// if redirect hook has not yet been set (by a router), defaults to a browser redirect
|
||||||
|
_ = server_fn::redirect::set_redirect_hook(|loc: &str| {
|
||||||
|
if let Some(url) = resolve_redirect_url(loc) {
|
||||||
|
_ = window().location().set_href(&url.href());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let action_url = ServFn::url();
|
||||||
|
let version = action.version();
|
||||||
|
let value = action.value();
|
||||||
|
|
||||||
|
let on_submit = {
|
||||||
|
move |ev: SubmitEvent| {
|
||||||
|
if ev.default_prevented() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.prevent_default();
|
||||||
|
|
||||||
|
match ServFn::from_event(&ev) {
|
||||||
|
Ok(new_input) => {
|
||||||
|
action.dispatch(new_input);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
crate::logging::error!(
|
||||||
|
"Error converting form field into server function \
|
||||||
|
arguments: {err:?}"
|
||||||
|
);
|
||||||
|
value.set(Some(Err(ServerFnError::Serialization(
|
||||||
|
err.to_string(),
|
||||||
|
))));
|
||||||
|
version.update(|n| *n += 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let action_form = form()
|
||||||
|
.action(ServFn::url())
|
||||||
|
.method("post")
|
||||||
|
.on(submit, on_submit)
|
||||||
|
.child(children());
|
||||||
|
if let Some(node_ref) = node_ref {
|
||||||
|
Either::Left(action_form.node_ref(node_ref))
|
||||||
|
} else {
|
||||||
|
Either::Right(action_form)
|
||||||
|
}
|
||||||
|
// TODO add other attributes
|
||||||
|
/*for (attr_name, attr_value) in attributes {
|
||||||
|
action_form = action_form.attr(attr_name, attr_value);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves a redirect location to an (absolute) URL.
|
||||||
|
pub(crate) fn resolve_redirect_url(loc: &str) -> Option<web_sys::Url> {
|
||||||
|
let origin = match window().location().origin() {
|
||||||
|
Ok(origin) => origin,
|
||||||
|
Err(e) => {
|
||||||
|
leptos::logging::error!("Failed to get origin: {:#?}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Use server function's URL as base instead.
|
||||||
|
let base = origin;
|
||||||
|
|
||||||
|
match web_sys::Url::new_with_base(loc, &base) {
|
||||||
|
Ok(url) => Some(url),
|
||||||
|
Err(e) => {
|
||||||
|
leptos::logging::error!(
|
||||||
|
"Invalid redirect location: {}",
|
||||||
|
e.as_string().unwrap_or_default(),
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
leptos/src/from_form_data.rs
Normal file
88
leptos/src/from_form_data.rs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
use thiserror::Error;
|
||||||
|
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||||
|
use web_sys::{
|
||||||
|
Event, FormData, HtmlButtonElement, HtmlFormElement, HtmlInputElement,
|
||||||
|
SubmitEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Tries to deserialize a type from form data. This can be used for client-side
|
||||||
|
/// validation during form submission.
|
||||||
|
pub trait FromFormData
|
||||||
|
where
|
||||||
|
Self: Sized + serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
/// Tries to deserialize the data, given only the `submit` event.
|
||||||
|
fn from_event(ev: &web_sys::Event) -> Result<Self, FromFormDataError>;
|
||||||
|
|
||||||
|
/// Tries to deserialize the data, given the actual form data.
|
||||||
|
fn from_form_data(
|
||||||
|
form_data: &web_sys::FormData,
|
||||||
|
) -> Result<Self, serde_qs::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum FromFormDataError {
|
||||||
|
#[error("Could not find <form> connected to event.")]
|
||||||
|
MissingForm(Event),
|
||||||
|
#[error("Could not create FormData from <form>: {0:?}")]
|
||||||
|
FormData(JsValue),
|
||||||
|
#[error("Deserialization error: {0:?}")]
|
||||||
|
Deserialization(serde_qs::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FromFormData for T
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
fn from_event(ev: &Event) -> Result<Self, FromFormDataError> {
|
||||||
|
let submit_ev = ev.unchecked_ref();
|
||||||
|
let form_data = form_data_from_event(submit_ev)?;
|
||||||
|
Self::from_form_data(&form_data)
|
||||||
|
.map_err(FromFormDataError::Deserialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_form_data(
|
||||||
|
form_data: &web_sys::FormData,
|
||||||
|
) -> Result<Self, serde_qs::Error> {
|
||||||
|
let data =
|
||||||
|
web_sys::UrlSearchParams::new_with_str_sequence_sequence(form_data)
|
||||||
|
.unwrap_throw();
|
||||||
|
let data = data.to_string().as_string().unwrap_or_default();
|
||||||
|
serde_qs::Config::new(5, false).deserialize_str::<Self>(&data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn form_data_from_event(
|
||||||
|
ev: &SubmitEvent,
|
||||||
|
) -> Result<FormData, FromFormDataError> {
|
||||||
|
let submitter = ev.submitter();
|
||||||
|
let mut submitter_name_value = None;
|
||||||
|
let opt_form = match &submitter {
|
||||||
|
Some(el) => {
|
||||||
|
if let Some(form) = el.dyn_ref::<HtmlFormElement>() {
|
||||||
|
Some(form.clone())
|
||||||
|
} else if let Some(input) = el.dyn_ref::<HtmlInputElement>() {
|
||||||
|
submitter_name_value = Some((input.name(), input.value()));
|
||||||
|
Some(ev.target().unwrap().unchecked_into())
|
||||||
|
} else if let Some(button) = el.dyn_ref::<HtmlButtonElement>() {
|
||||||
|
submitter_name_value = Some((button.name(), button.value()));
|
||||||
|
Some(ev.target().unwrap().unchecked_into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => ev.target().map(|form| form.unchecked_into()),
|
||||||
|
};
|
||||||
|
match opt_form.as_ref().map(FormData::new_with_form) {
|
||||||
|
None => Err(FromFormDataError::MissingForm(ev.clone().into())),
|
||||||
|
Some(Err(e)) => Err(FromFormDataError::FormData(e)),
|
||||||
|
Some(Ok(form_data)) => {
|
||||||
|
if let Some((name, value)) = submitter_name_value {
|
||||||
|
form_data
|
||||||
|
.append_with_str(&name, &value)
|
||||||
|
.map_err(FromFormDataError::FormData)?;
|
||||||
|
}
|
||||||
|
Ok(form_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -151,6 +151,8 @@ pub mod prelude {
|
||||||
pub use tachys::prelude::*;
|
pub use tachys::prelude::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod action_form;
|
||||||
|
pub use action_form::*;
|
||||||
pub mod callback;
|
pub mod callback;
|
||||||
pub mod children;
|
pub mod children;
|
||||||
pub mod component;
|
pub mod component;
|
||||||
|
@ -184,15 +186,13 @@ pub use leptos_config as config;
|
||||||
pub use mount::hydrate_body;
|
pub use mount::hydrate_body;
|
||||||
pub use mount::mount_to_body;
|
pub use mount::mount_to_body;
|
||||||
pub use oco;
|
pub use oco;
|
||||||
|
pub mod from_form_data;
|
||||||
|
|
||||||
pub mod context {
|
pub mod context {
|
||||||
pub use reactive_graph::owner::{provide_context, use_context};
|
pub use reactive_graph::owner::{provide_context, use_context};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "hydration")]
|
pub use leptos_server as server;
|
||||||
pub mod server {
|
|
||||||
pub use leptos_server::{ArcResource, Resource};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||||
pub mod logging {
|
pub mod logging {
|
||||||
|
|
|
@ -1,537 +1,208 @@
|
||||||
//use crate::{ServerFn, ServerFnError};
|
use reactive_graph::{
|
||||||
#[cfg(debug_assertions)]
|
action::ArcAction,
|
||||||
use leptos_reactive::console_warn;
|
owner::StoredValue,
|
||||||
use leptos_reactive::{
|
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||||
is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value,
|
traits::DefinedAt,
|
||||||
try_batch, use_context, StoredValue,
|
unwrap_signal,
|
||||||
};
|
};
|
||||||
use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError};
|
use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError};
|
||||||
use std::{cell::Cell, future::Future, pin::Pin, rc::Rc};
|
use std::panic::Location;
|
||||||
|
|
||||||
/// An action synchronizes an imperative `async` call to the synchronous reactive system.
|
pub struct ArcServerAction<S>
|
||||||
///
|
|
||||||
/// If you’re trying to load data by running an `async` function reactively, you probably
|
|
||||||
/// want to use a [Resource](leptos_reactive::Resource) instead. If you’re 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 save_data = create_action(|task: &String| {
|
|
||||||
/// // `task` is given as `&String` because its value is available in `input`
|
|
||||||
/// send_new_todo_to_api(task.clone())
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// // the argument currently running
|
|
||||||
/// let input = save_data.input();
|
|
||||||
/// // the most recent returned result
|
|
||||||
/// let result_of_call = save_data.value();
|
|
||||||
/// // whether the call is pending
|
|
||||||
/// let pending = save_data.pending();
|
|
||||||
/// // how many times the action has run
|
|
||||||
/// // useful for reactively updating something else in response to a `dispatch` and response
|
|
||||||
/// let version = save_data.version();
|
|
||||||
///
|
|
||||||
/// // before we do anything
|
|
||||||
/// 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!(version.get(), 0);
|
|
||||||
/// # if false {
|
|
||||||
/// // 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!(pending.get(), true); // is pending
|
|
||||||
/// // assert_eq!(result_of_call.get(), None); // has not yet gotten a response
|
|
||||||
///
|
|
||||||
/// // after call has 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!(version.get(), 1);
|
|
||||||
/// # };
|
|
||||||
/// # 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 [Action::input] as well.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use leptos::*;
|
|
||||||
/// # let runtime = create_runtime();
|
|
||||||
/// // if there's a single argument, just use that
|
|
||||||
/// let action1 = create_action(|input: &String| {
|
|
||||||
/// let input = input.clone();
|
|
||||||
/// async move { todo!() }
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// // if there are no arguments, use the unit type `()`
|
|
||||||
/// let action2 = create_action(|input: &()| async { todo!() });
|
|
||||||
///
|
|
||||||
/// // if there are multiple arguments, use a tuple
|
|
||||||
/// let action3 = create_action(|input: &(usize, String)| async { todo!() });
|
|
||||||
/// # runtime.dispose();
|
|
||||||
/// ```
|
|
||||||
pub struct Action<I, O>(StoredValue<ActionState<I, O>>)
|
|
||||||
where
|
where
|
||||||
I: 'static,
|
S: ServerFn + 'static,
|
||||||
O: 'static;
|
S::Output: 'static,
|
||||||
|
{
|
||||||
impl<I, O> Action<I, O>
|
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||||
where
|
#[cfg(debug_assertions)]
|
||||||
I: 'static,
|
defined_at: &'static Location<'static>,
|
||||||
O: 'static,
|
}
|
||||||
|
|
||||||
|
impl<S> ArcServerAction<S>
|
||||||
|
where
|
||||||
|
S: ServerFn + Clone + Send + Sync + 'static,
|
||||||
|
S::Output: Send + Sync + 'static,
|
||||||
|
S::Error: Send + Sync + '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,)
|
|
||||||
)]
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn dispatch(&self, input: I) {
|
pub fn new() -> Self {
|
||||||
#[cfg(debug_assertions)]
|
Self {
|
||||||
let loc = std::panic::Location::caller();
|
inner: ArcAction::new(|input: &S| S::run_on_client(input.clone())),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
self.0.with_value(|a| {
|
defined_at: Location::caller(),
|
||||||
a.dispatch(
|
}
|
||||||
input,
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
loc,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an [Action].
|
#[track_caller]
|
||||||
///
|
pub fn dispatch(&self, input: S) {
|
||||||
/// [Action] is a type of [Signal] which represent imperative calls to
|
self.inner.dispatch(input);
|
||||||
/// an asynchronous function. Where a [Resource](leptos_reactive::Resource) is driven as a function
|
|
||||||
/// of a [Signal], [Action]s are [Action::dispatch]ed by events or handlers.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use leptos::*;
|
|
||||||
/// # let runtime = create_runtime();
|
|
||||||
///
|
|
||||||
/// let act = Action::new(|n: &u8| {
|
|
||||||
/// let n = n.to_owned();
|
|
||||||
/// async move { n * 2 }
|
|
||||||
/// });
|
|
||||||
/// # if false {
|
|
||||||
/// act.dispatch(3);
|
|
||||||
/// assert_eq!(act.value().get(), Some(6));
|
|
||||||
///
|
|
||||||
/// // Remember that async functions already return a future if they are
|
|
||||||
/// // not `await`ed. You can save keystrokes by leaving out the `async move`
|
|
||||||
///
|
|
||||||
/// let act2 = Action::new(|n: &String| yell(n.to_owned()));
|
|
||||||
/// act2.dispatch(String::from("i'm in a doctest"));
|
|
||||||
/// assert_eq!(act2.value().get(), Some("I'M IN A DOCTEST".to_string()));
|
|
||||||
/// # }
|
|
||||||
///
|
|
||||||
/// async fn yell(n: String) -> String {
|
|
||||||
/// n.to_uppercase()
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// # runtime.dispose();
|
|
||||||
/// ```
|
|
||||||
#[cfg_attr(
|
|
||||||
any(debug_assertions, feature = "ssr"),
|
|
||||||
tracing::instrument(level = "trace", skip_all,)
|
|
||||||
)]
|
|
||||||
pub fn new<F, Fu>(action_fn: F) -> Self
|
|
||||||
where
|
|
||||||
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,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the action has been dispatched and is currently waiting for its future to be resolved.
|
|
||||||
#[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())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates whether the action is currently pending. If the action has been dispatched
|
|
||||||
/// multiple times, and some of them are still pending, it will *not* update the `pending`
|
|
||||||
/// signal.
|
|
||||||
#[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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The URL associated with the action (typically as part of a server function.)
|
|
||||||
/// This enables integration with the `ActionForm` component in `leptos_router`.
|
|
||||||
pub fn url(&self) -> Option<String> {
|
|
||||||
self.0.with_value(|a| a.url.as_ref().cloned())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How many times the action has successfully resolved.
|
|
||||||
pub fn version(&self) -> RwSignal<usize> {
|
|
||||||
self.0.with_value(|a| a.version)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current argument that was dispatched to the `async` function.
|
|
||||||
/// `Some` while we are waiting for it to resolve, `None` if it has resolved.
|
|
||||||
#[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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The most recent return value of the `async` function.
|
|
||||||
#[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> Action<I, Result<I::Output, ServerFnError<I::Error>>>
|
impl<S> ArcServerAction<S>
|
||||||
where
|
where
|
||||||
I: ServerFn + 'static,
|
S: ServerFn + Clone + Send + Sync + 'static,
|
||||||
|
S::Output: 'static,
|
||||||
|
S::Error: 'static,
|
||||||
{
|
{
|
||||||
/// Create an [Action] to imperatively call a [server](leptos_macro::server) function.
|
#[track_caller]
|
||||||
///
|
pub fn version(&self) -> ArcRwSignal<usize> {
|
||||||
/// The struct representing your server function's arguments should be
|
self.inner.version()
|
||||||
/// provided to the [Action]. Unless specified as an argument to the server
|
|
||||||
/// macro, the generated struct is your function's name converted to CamelCase.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # // Not in a localset, so this would always panic.
|
|
||||||
/// # if false {
|
|
||||||
/// # use leptos::*;
|
|
||||||
/// # let rt = create_runtime();
|
|
||||||
///
|
|
||||||
/// // The type argument can be on the right of the equal sign.
|
|
||||||
/// let act = Action::<Add, _>::server();
|
|
||||||
/// let args = Add { lhs: 5, rhs: 7 };
|
|
||||||
/// act.dispatch(args);
|
|
||||||
/// assert_eq!(act.value().get(), Some(Ok(12)));
|
|
||||||
///
|
|
||||||
/// // Or on the left of the equal sign.
|
|
||||||
/// let act: Action<Sub, _> = Action::server();
|
|
||||||
/// let args = Sub { lhs: 20, rhs: 5 };
|
|
||||||
/// act.dispatch(args);
|
|
||||||
/// assert_eq!(act.value().get(), Some(Ok(15)));
|
|
||||||
///
|
|
||||||
/// let not_dispatched = Action::<Add, _>::server();
|
|
||||||
/// assert_eq!(not_dispatched.value().get(), None);
|
|
||||||
///
|
|
||||||
/// #[server]
|
|
||||||
/// async fn add(lhs: u8, rhs: u8) -> Result<u8, ServerFnError> {
|
|
||||||
/// Ok(lhs + rhs)
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// #[server]
|
|
||||||
/// async fn sub(lhs: u8, rhs: u8) -> Result<u8, ServerFnError> {
|
|
||||||
/// Ok(lhs - rhs)
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// # rt.dispose();
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
#[cfg_attr(
|
|
||||||
any(debug_assertions, feature = "ssr"),
|
|
||||||
tracing::instrument(level = "trace", skip_all,)
|
|
||||||
)]
|
|
||||||
pub fn server() -> Action<I, Result<I::Output, ServerFnError<I::Error>>>
|
|
||||||
where
|
|
||||||
I: ServerFn + Clone,
|
|
||||||
I::Error: Clone + 'static,
|
|
||||||
{
|
|
||||||
// The server is able to call the function directly
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
let action_function = |args: &I| I::run_body(args.clone());
|
|
||||||
|
|
||||||
// When not on the server send a fetch to request the fn call.
|
|
||||||
#[cfg(not(feature = "ssr"))]
|
|
||||||
let action_function = |args: &I| I::run_on_client(args.clone());
|
|
||||||
|
|
||||||
// create the action
|
|
||||||
Action::new(action_function).using_server_fn()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Associates the URL of the given server function with this action.
|
#[track_caller]
|
||||||
/// This enables integration with the `ActionForm` component in `leptos_router`.
|
pub fn input(&self) -> ArcRwSignal<Option<S>> {
|
||||||
#[cfg_attr(
|
self.inner.input()
|
||||||
any(debug_assertions, feature = "ssr"),
|
}
|
||||||
tracing::instrument(level = "trace", skip_all,)
|
|
||||||
)]
|
#[track_caller]
|
||||||
pub fn using_server_fn(self) -> Self
|
pub fn value(
|
||||||
where
|
&self,
|
||||||
I::Error: Clone + 'static,
|
) -> ArcRwSignal<Option<Result<S::Output, ServerFnError<S::Error>>>> {
|
||||||
{
|
self.inner.value()
|
||||||
let url = I::url();
|
|
||||||
let action_error = use_context::<Rc<ServerFnUrlError<I::Error>>>()
|
|
||||||
.and_then(|err| {
|
|
||||||
if err.path() == url {
|
|
||||||
Some(err.error().clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.0.update_value(|state| {
|
|
||||||
if let Some(err) = action_error {
|
|
||||||
state.value.set_untracked(Some(Err(err)));
|
|
||||||
}
|
|
||||||
state.url = Some(url.to_string());
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I, O> Clone for Action<I, O>
|
impl<S> Clone for ArcServerAction<S>
|
||||||
where
|
where
|
||||||
I: 'static,
|
S: ServerFn + 'static,
|
||||||
O: 'static,
|
S::Output: 'static,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: self.defined_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Default for ArcServerAction<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 ArcServerAction<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 ServerAction<S>
|
||||||
|
where
|
||||||
|
S: ServerFn + 'static,
|
||||||
|
S::Output: 'static,
|
||||||
|
{
|
||||||
|
inner: StoredValue<ArcServerAction<S>>,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: &'static Location<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> Clone for ServerAction<S>
|
||||||
|
where
|
||||||
|
S: ServerFn + 'static,
|
||||||
|
S::Output: 'static,
|
||||||
{
|
{
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
*self
|
*self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I, O> Copy for Action<I, O>
|
impl<S> Copy for ServerAction<S>
|
||||||
where
|
where
|
||||||
I: 'static,
|
S: ServerFn + 'static,
|
||||||
O: 'static,
|
S::Output: 'static,
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ActionState<I, O>
|
impl<S> ServerAction<S>
|
||||||
where
|
where
|
||||||
I: 'static,
|
S: ServerFn + Clone + Send + Sync + 'static,
|
||||||
O: 'static,
|
S::Output: Send + Sync + 'static,
|
||||||
|
S::Error: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
/// How many times the action has successfully resolved.
|
#[track_caller]
|
||||||
pub version: RwSignal<usize>,
|
pub fn new() -> Self {
|
||||||
/// The current argument that was dispatched to the `async` function.
|
Self {
|
||||||
/// `Some` while we are waiting for it to resolve, `None` if it has resolved.
|
inner: StoredValue::new(ArcServerAction::new()),
|
||||||
pub input: RwSignal<Option<I>>,
|
#[cfg(debug_assertions)]
|
||||||
/// The most recent return value of the `async` function.
|
defined_at: Location::caller(),
|
||||||
pub value: RwSignal<Option<O>>,
|
|
||||||
pending: RwSignal<bool>,
|
|
||||||
url: Option<String>,
|
|
||||||
/// How many dispatched actions are still pending.
|
|
||||||
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,
|
|
||||||
{
|
|
||||||
/// 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,
|
|
||||||
#[cfg(debug_assertions)] loc: &'static std::panic::Location<'static>,
|
|
||||||
) {
|
|
||||||
if !is_suppressing_resource_load() {
|
|
||||||
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().wrapping_add(1));
|
|
||||||
spawn_local(async move {
|
|
||||||
let new_value = fut.await;
|
|
||||||
let res = try_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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if res.is_err() {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
console_warn(&format!(
|
|
||||||
"At {loc}, you are dispatching an action in a runtime \
|
|
||||||
that has already been disposed. This may be because \
|
|
||||||
you are calling `.dispatch()` in the body of a \
|
|
||||||
component, during initial server-side rendering. If \
|
|
||||||
that's the case, you should probably be using \
|
|
||||||
`create_resource` instead of `create_action`."
|
|
||||||
));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn dispatch(&self, input: S) {
|
||||||
|
self.inner.with_value(|inner| inner.dispatch(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an [Action] to synchronize an imperative `async` call to the synchronous reactive system.
|
impl<S> Default for ServerAction<S>
|
||||||
///
|
|
||||||
/// If you’re trying to load data by running an `async` function reactively, you probably
|
|
||||||
/// want to use a [create_resource](leptos_reactive::create_resource) instead. If you’re 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 save_data = create_action(|task: &String| {
|
|
||||||
/// // `task` is given as `&String` because its value is available in `input`
|
|
||||||
/// send_new_todo_to_api(task.clone())
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// // the argument currently running
|
|
||||||
/// let input = save_data.input();
|
|
||||||
/// // the most recent returned result
|
|
||||||
/// let result_of_call = save_data.value();
|
|
||||||
/// // whether the call is pending
|
|
||||||
/// let pending = save_data.pending();
|
|
||||||
/// // how many times the action has run
|
|
||||||
/// // useful for reactively updating something else in response to a `dispatch` and response
|
|
||||||
/// let version = save_data.version();
|
|
||||||
///
|
|
||||||
/// // before we do anything
|
|
||||||
/// 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!(version.get(), 0);
|
|
||||||
/// # if false {
|
|
||||||
/// // 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!(pending.get(), true); // is pending
|
|
||||||
/// // assert_eq!(result_of_call.get(), None); // has not yet gotten a response
|
|
||||||
///
|
|
||||||
/// // after call has 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!(version.get(), 1);
|
|
||||||
/// # }
|
|
||||||
/// # 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 [Action::input] as well.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use leptos::*;
|
|
||||||
/// # let runtime = create_runtime();
|
|
||||||
/// // if there's a single argument, just use that
|
|
||||||
/// let action1 = create_action(|input: &String| {
|
|
||||||
/// let input = input.clone();
|
|
||||||
/// async move { todo!() }
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// // if there are no arguments, use the unit type `()`
|
|
||||||
/// let action2 = create_action(|input: &()| async { todo!() });
|
|
||||||
///
|
|
||||||
/// // if there are multiple arguments, use a tuple
|
|
||||||
/// let action3 = create_action(|input: &(usize, String)| async { todo!() });
|
|
||||||
/// # runtime.dispose();
|
|
||||||
/// ```
|
|
||||||
#[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
|
where
|
||||||
I: 'static,
|
S: ServerFn + Clone + Send + Sync + 'static,
|
||||||
O: 'static,
|
S::Output: Send + Sync + 'static,
|
||||||
F: Fn(&I) -> Fu + 'static,
|
S::Error: Send + Sync + 'static,
|
||||||
Fu: Future<Output = O> + 'static,
|
|
||||||
{
|
{
|
||||||
Action::new(action_fn)
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an [Action] that can be used to call a server function.
|
impl<S> DefinedAt for ServerAction<S>
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use leptos::*;
|
|
||||||
///
|
|
||||||
/// #[server(MyServerFn)]
|
|
||||||
/// async fn my_server_fn() -> Result<(), ServerFnError> {
|
|
||||||
/// todo!()
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// # let runtime = create_runtime();
|
|
||||||
/// let my_server_action = create_server_action::<MyServerFn>();
|
|
||||||
/// # runtime.dispose();
|
|
||||||
/// ```
|
|
||||||
#[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<S::Error>>>
|
|
||||||
where
|
where
|
||||||
S: Clone + ServerFn,
|
S: ServerFn + 'static,
|
||||||
S::Error: Clone + 'static,
|
S::Output: 'static,
|
||||||
{
|
{
|
||||||
Action::<S, _>::server()
|
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Some(self.defined_at)
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
//#![deny(missing_docs)]
|
//#![deny(missing_docs)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
mod action;
|
||||||
|
pub use action::*;
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
mod resource;
|
mod resource;
|
||||||
#[cfg(feature = "hydration")]
|
#[cfg(feature = "hydration")]
|
||||||
|
|
|
@ -9,13 +9,15 @@ use core::{fmt::Debug, marker::PhantomData};
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use hydration_context::SerializedDataId;
|
use hydration_context::SerializedDataId;
|
||||||
use reactive_graph::{
|
use reactive_graph::{
|
||||||
computed::{ArcAsyncDerived, AsyncDerived, AsyncDerivedFuture, AsyncState},
|
computed::{
|
||||||
|
ArcAsyncDerived, ArcMemo, AsyncDerived, AsyncDerivedFuture, AsyncState,
|
||||||
|
},
|
||||||
owner::Owner,
|
owner::Owner,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use std::{future::IntoFuture, ops::Deref};
|
use std::{future::IntoFuture, ops::Deref};
|
||||||
|
|
||||||
pub struct ArcResource<T, Ser> {
|
pub struct ArcResource<T, Ser = SerdeJson> {
|
||||||
ser: PhantomData<Ser>,
|
ser: PhantomData<Ser>,
|
||||||
data: ArcAsyncDerived<T>,
|
data: ArcAsyncDerived<T>,
|
||||||
}
|
}
|
||||||
|
@ -34,12 +36,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new<S, Fut>(
|
||||||
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
ArcResource::new_with_encoding(fun)
|
ArcResource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,12 +55,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_serde<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new<S, Fut>(
|
||||||
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
ArcResource::new_with_encoding(fun)
|
ArcResource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,14 +75,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_miniserde<Fut>(
|
pub fn new_miniserde<S, Fut>(
|
||||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
ArcResource::new_with_encoding(fun)
|
ArcResource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,14 +95,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_serde_lite<Fut>(
|
pub fn new_serde_lite<S, Fut>(
|
||||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
ArcResource::new_with_encoding(fun)
|
ArcResource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,12 +115,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_rkyv<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new_rkyv<S, Fut>(
|
||||||
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
ArcResource::new_with_encoding(fun)
|
ArcResource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,10 +135,12 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_with_encoding<Fut>(
|
pub fn new_with_encoding<S, Fut>(
|
||||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
) -> ArcResource<T, Ser>
|
) -> ArcResource<T, Ser>
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Debug + Send + Sync + 'static,
|
T: Debug + Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
|
@ -132,6 +152,9 @@ where
|
||||||
|
|
||||||
let initial = Self::initial_value(&id);
|
let initial = Self::initial_value(&id);
|
||||||
|
|
||||||
|
let source = ArcMemo::new(move |_| source());
|
||||||
|
let fun = move || fetcher(source.get());
|
||||||
|
|
||||||
let data = ArcAsyncDerived::new_with_initial(initial, fun);
|
let data = ArcAsyncDerived::new_with_initial(initial, fun);
|
||||||
|
|
||||||
if let Some(shared_context) = shared_context {
|
if let Some(shared_context) = shared_context {
|
||||||
|
@ -227,12 +250,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new<S, Fut>(
|
||||||
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Resource::new_with_encoding(fun)
|
Resource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,12 +269,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_serde<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new_serde<S, Fut>(
|
||||||
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Resource::new_with_encoding(fun)
|
Resource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,14 +289,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_miniserde<Fut>(
|
pub fn new_miniserde<S, Fut>(
|
||||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Resource::new_with_encoding(fun)
|
Resource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,14 +309,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_serde_lite<Fut>(
|
pub fn new_serde_lite<S, Fut>(
|
||||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Resource::new_with_encoding(fun)
|
Resource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,12 +329,16 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_rkyv<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new_rkyv<S, Fut>(
|
||||||
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
where
|
where
|
||||||
|
S: PartialEq + Clone + Send + Sync + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Resource::new_with_encoding(fun)
|
Resource::new_with_encoding(source, fetcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,69 +349,22 @@ where
|
||||||
T::SerErr: Debug,
|
T::SerErr: Debug,
|
||||||
T::DeErr: Debug,
|
T::DeErr: Debug,
|
||||||
{
|
{
|
||||||
pub fn new_with_encoding<Fut>(
|
pub fn new_with_encoding<S, Fut>(
|
||||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
source: impl Fn() -> S + Send + Sync + 'static,
|
||||||
|
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||||
) -> Resource<T, Ser>
|
) -> Resource<T, Ser>
|
||||||
where
|
where
|
||||||
|
S: Send + Sync + Clone + PartialEq + 'static,
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
let shared_context = Owner::current_shared_context();
|
let ArcResource { ser, data } =
|
||||||
let id = shared_context
|
ArcResource::new_with_encoding(source, fetcher);
|
||||||
.as_ref()
|
|
||||||
.map(|sc| sc.next_id())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let initial = Self::initial_value(&id);
|
|
||||||
|
|
||||||
let data = AsyncDerived::new_with_initial(initial, fun);
|
|
||||||
|
|
||||||
if let Some(shared_context) = shared_context {
|
|
||||||
let value = data;
|
|
||||||
let ready_fut = data.ready();
|
|
||||||
|
|
||||||
shared_context.write_async(
|
|
||||||
id,
|
|
||||||
Box::pin(async move {
|
|
||||||
ready_fut.await;
|
|
||||||
value
|
|
||||||
.with_untracked(|data| match &data {
|
|
||||||
AsyncState::Complete(val) => val.ser(),
|
|
||||||
_ => unreachable!(),
|
|
||||||
})
|
|
||||||
.unwrap() // TODO handle
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Resource {
|
Resource {
|
||||||
ser: PhantomData,
|
ser: PhantomData,
|
||||||
data,
|
data: data.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn initial_value(id: &SerializedDataId) -> AsyncState<T> {
|
|
||||||
#[cfg(feature = "hydration")]
|
|
||||||
{
|
|
||||||
let shared_context = Owner::current_shared_context();
|
|
||||||
if let Some(shared_context) = shared_context {
|
|
||||||
let value = shared_context.read_data(id);
|
|
||||||
if let Some(value) = value {
|
|
||||||
match T::de(&value) {
|
|
||||||
Ok(value) => return AsyncState::Complete(value),
|
|
||||||
Err(e) => {
|
|
||||||
#[cfg(feature = "tracing")]
|
|
||||||
tracing::error!(
|
|
||||||
"couldn't deserialize from {value:?}: {e:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AsyncState::Loading
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, Ser> IntoFuture for Resource<T, Ser>
|
impl<T, Ser> IntoFuture for Resource<T, Ser>
|
||||||
|
|
|
@ -1,19 +1,62 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
computed::AsyncState,
|
computed::AsyncState,
|
||||||
signal::ArcRwSignal,
|
diagnostics::is_suppressing_resource_load,
|
||||||
traits::{Set, Update},
|
owner::{Owner, StoredValue},
|
||||||
|
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||||
|
traits::{DefinedAt, GetUntracked, Set, Update, WithUntracked},
|
||||||
|
unwrap_signal,
|
||||||
|
};
|
||||||
|
use any_spawner::Executor;
|
||||||
|
use futures::{
|
||||||
|
channel::oneshot,
|
||||||
|
select,
|
||||||
|
stream::{AbortRegistration, Abortable},
|
||||||
|
FutureExt,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
future::Future,
|
future::Future,
|
||||||
|
mem::swap,
|
||||||
|
panic::Location,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::{atomic::AtomicUsize, Arc},
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum ActionState<I, O> {
|
/*enum ActionState<I, O> {
|
||||||
Idle,
|
Idle,
|
||||||
Loading(I),
|
Loading(I),
|
||||||
|
LoadingMultiple(I, usize),
|
||||||
Complete(O),
|
Complete(O),
|
||||||
Reloading(I, O),
|
Reloading(I, O),
|
||||||
|
ReloadingMultiple(I, O, usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, O> ActionState<I, O> {
|
||||||
|
fn currently_loading(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
ActionState::Idle => 0,
|
||||||
|
ActionState::Loading(_) => 1,
|
||||||
|
ActionState::LoadingMultiple(_, curr) => *curr,
|
||||||
|
ActionState::Complete(_) => 0,
|
||||||
|
ActionState::Reloading(_, _) => 1,
|
||||||
|
ActionState::ReloadingMultiple(_, _, curr) => *curr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
struct ActionInner<I, O> {
|
||||||
|
input: Option<I>,
|
||||||
|
value: Option<O>,
|
||||||
|
version: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, O> Default for ActionInner<I, O> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
input: Default::default(),
|
||||||
|
value: Default::default(),
|
||||||
|
version: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ArcAction<I, O>
|
pub struct ArcAction<I, O>
|
||||||
|
@ -21,31 +64,234 @@ where
|
||||||
I: 'static,
|
I: 'static,
|
||||||
O: 'static,
|
O: 'static,
|
||||||
{
|
{
|
||||||
|
in_flight: ArcRwSignal<usize>,
|
||||||
|
input: ArcRwSignal<Option<I>>,
|
||||||
|
value: ArcRwSignal<Option<O>>,
|
||||||
version: ArcRwSignal<usize>,
|
version: ArcRwSignal<usize>,
|
||||||
state: ArcRwSignal<ActionState<I, O>>,
|
|
||||||
pending_dispatches: Arc<AtomicUsize>,
|
|
||||||
#[allow(clippy::complexity)]
|
#[allow(clippy::complexity)]
|
||||||
action_fn: Arc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
|
action_fn: Arc<
|
||||||
#[cfg(debug_assertion)]
|
dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>> + Send + Sync,
|
||||||
|
>,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
defined_at: &'static Location<'static>,
|
defined_at: &'static Location<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I, O> ArcAction<I, O>
|
impl<I, O> Clone for ArcAction<I, O>
|
||||||
where
|
where
|
||||||
I: 'static,
|
I: 'static,
|
||||||
O: 'static,
|
O: 'static,
|
||||||
{
|
{
|
||||||
#[track_caller]
|
fn clone(&self) -> Self {
|
||||||
pub fn dispatch(&self, input: I) {
|
Self {
|
||||||
let fut = (self.action_fn)(&input);
|
in_flight: self.in_flight.clone(),
|
||||||
|
input: self.input.clone(),
|
||||||
self.state.update(|prev| {
|
value: self.value.clone(),
|
||||||
*prev = match prev {
|
version: self.version.clone(),
|
||||||
ActionState::Idle => ActionState::Loading(input),
|
action_fn: self.action_fn.clone(),
|
||||||
ActionState::Loading(_) => todo!(),
|
#[cfg(debug_assertions)]
|
||||||
ActionState::Complete(_) => todo!(),
|
defined_at: self.defined_at,
|
||||||
ActionState::Reloading(_, _) => todo!(),
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<I, O> ArcAction<I, O>
|
||||||
|
where
|
||||||
|
I: Send + Sync + 'static,
|
||||||
|
O: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
#[track_caller]
|
||||||
|
pub fn new<F, Fu>(action_fn: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&I) -> Fu + Send + Sync + 'static,
|
||||||
|
Fu: Future<Output = O> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcAction {
|
||||||
|
in_flight: ArcRwSignal::new(0),
|
||||||
|
input: Default::default(),
|
||||||
|
value: Default::default(),
|
||||||
|
version: Default::default(),
|
||||||
|
action_fn: Arc::new(move |input| Box::pin(action_fn(input))),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: Location::caller(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn dispatch(&self, input: I) {
|
||||||
|
if !is_suppressing_resource_load() {
|
||||||
|
let mut fut = (self.action_fn)(&input).fuse();
|
||||||
|
|
||||||
|
// abort this task if the owner is cleaned up
|
||||||
|
let (abort_tx, mut abort_rx) = oneshot::channel();
|
||||||
|
Owner::on_cleanup(move || {
|
||||||
|
abort_tx.send(()).expect(
|
||||||
|
"tried to cancel a future in ArcAction::dispatch(), but \
|
||||||
|
the channel has already closed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the state before loading
|
||||||
|
self.in_flight.update(|n| *n += 1);
|
||||||
|
let current_version =
|
||||||
|
self.version.try_get_untracked().unwrap_or_default();
|
||||||
|
self.input.try_update(|inp| *inp = Some(input));
|
||||||
|
|
||||||
|
// Spawn the task
|
||||||
|
Executor::spawn({
|
||||||
|
let input = self.input.clone();
|
||||||
|
let version = self.version.clone();
|
||||||
|
let value = self.value.clone();
|
||||||
|
let in_flight = self.in_flight.clone();
|
||||||
|
async move {
|
||||||
|
select! {
|
||||||
|
// if the abort message has been sent, bail and do nothing
|
||||||
|
_ = abort_rx => {
|
||||||
|
in_flight.update(|n| *n = n.saturating_sub(1));
|
||||||
|
},
|
||||||
|
// otherwise, update the value
|
||||||
|
result = fut => {
|
||||||
|
in_flight.update(|n| *n = n.saturating_sub(1));
|
||||||
|
let is_latest = version.get_untracked() <= current_version;
|
||||||
|
if is_latest {
|
||||||
|
version.update(|n| *n += 1);
|
||||||
|
value.update(|n| *n = Some(result));
|
||||||
|
}
|
||||||
|
if in_flight.get_untracked() == 0 {
|
||||||
|
input.update(|inp| *inp = None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, O> ArcAction<I, O> {
|
||||||
|
#[track_caller]
|
||||||
|
pub fn version(&self) -> ArcRwSignal<usize> {
|
||||||
|
self.version.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn input(&self) -> ArcRwSignal<Option<I>> {
|
||||||
|
self.input.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn value(&self) -> ArcRwSignal<Option<O>> {
|
||||||
|
self.value.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, O> DefinedAt for ArcAction<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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Action<I, O>
|
||||||
|
where
|
||||||
|
I: 'static,
|
||||||
|
O: 'static,
|
||||||
|
{
|
||||||
|
inner: StoredValue<ArcAction<I, O>>,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: &'static Location<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, O> Action<I, O>
|
||||||
|
where
|
||||||
|
I: Send + Sync + 'static,
|
||||||
|
O: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
#[track_caller]
|
||||||
|
pub fn new<F, Fu>(action_fn: F) -> Self
|
||||||
|
where
|
||||||
|
F: Fn(&I) -> Fu + Send + Sync + 'static,
|
||||||
|
Fu: Future<Output = O> + Send + 'static,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
inner: StoredValue::new(ArcAction::new(action_fn)),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: Location::caller(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn version(&self) -> RwSignal<usize> {
|
||||||
|
let inner = self
|
||||||
|
.inner
|
||||||
|
.with_value(|inner| inner.version())
|
||||||
|
.unwrap_or_else(unwrap_signal!(self));
|
||||||
|
inner.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn input(&self) -> RwSignal<Option<I>> {
|
||||||
|
let inner = self
|
||||||
|
.inner
|
||||||
|
.with_value(|inner| inner.input())
|
||||||
|
.unwrap_or_else(unwrap_signal!(self));
|
||||||
|
inner.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn value(&self) -> RwSignal<Option<O>> {
|
||||||
|
let inner = self
|
||||||
|
.inner
|
||||||
|
.with_value(|inner| inner.value())
|
||||||
|
.unwrap_or_else(unwrap_signal!(self));
|
||||||
|
inner.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
pub fn dispatch(&self, input: I) {
|
||||||
|
self.inner.with_value(|inner| inner.dispatch(input));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, O> DefinedAt for Action<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> 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,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,18 @@ pub struct AsyncDerived<T: Send + Sync + 'static> {
|
||||||
inner: StoredValue<ArcAsyncDerived<T>>,
|
inner: StoredValue<ArcAsyncDerived<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + 'static> From<ArcAsyncDerived<T>> for AsyncDerived<T> {
|
||||||
|
fn from(value: ArcAsyncDerived<T>) -> Self {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let defined_at = value.defined_at;
|
||||||
|
Self {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at,
|
||||||
|
inner: StoredValue::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> StoredData for AsyncDerived<T> {
|
impl<T: Send + Sync + 'static> StoredData for AsyncDerived<T> {
|
||||||
type Data = ArcAsyncDerived<T>;
|
type Data = ArcAsyncDerived<T>;
|
||||||
|
|
||||||
|
|
|
@ -71,3 +71,17 @@ macro_rules! diagnostics {
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static SUPPRESS_RESOURCE_LOAD: Cell<bool> = const { Cell::new(false) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn suppress_resource_load(suppress: bool) {
|
||||||
|
SUPPRESS_RESOURCE_LOAD.with(|w| w.set(suppress));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn is_suppressing_resource_load() -> bool {
|
||||||
|
SUPPRESS_RESOURCE_LOAD.with(|w| w.get())
|
||||||
|
}
|
||||||
|
|
|
@ -57,6 +57,16 @@ impl<T> Hash for ArcRwSignal<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Default for ArcRwSignal<T>
|
||||||
|
where
|
||||||
|
T: Default,
|
||||||
|
{
|
||||||
|
#[track_caller]
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(T::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> ArcRwSignal<T> {
|
impl<T> ArcRwSignal<T> {
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "tracing",
|
feature = "tracing",
|
||||||
|
|
|
@ -298,9 +298,23 @@ pub trait Update {
|
||||||
self.try_update(fun);
|
self.try_update(fun);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn maybe_update(&self, fun: impl FnOnce(&mut Self::Value) -> bool) {
|
||||||
|
self.try_maybe_update(|val| {
|
||||||
|
let did_update = fun(val);
|
||||||
|
(did_update, ())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn try_update<U>(
|
fn try_update<U>(
|
||||||
&self,
|
&self,
|
||||||
fun: impl FnOnce(&mut Self::Value) -> U,
|
fun: impl FnOnce(&mut Self::Value) -> U,
|
||||||
|
) -> Option<U> {
|
||||||
|
self.try_maybe_update(|val| (true, fun(val)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_maybe_update<U>(
|
||||||
|
&self,
|
||||||
|
fun: impl FnOnce(&mut Self::Value) -> (bool, U),
|
||||||
) -> Option<U>;
|
) -> Option<U>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,12 +324,14 @@ where
|
||||||
{
|
{
|
||||||
type Value = <Self as UpdateUntracked>::Value;
|
type Value = <Self as UpdateUntracked>::Value;
|
||||||
|
|
||||||
fn try_update<U>(
|
fn try_maybe_update<U>(
|
||||||
&self,
|
&self,
|
||||||
fun: impl FnOnce(&mut Self::Value) -> U,
|
fun: impl FnOnce(&mut Self::Value) -> (bool, U),
|
||||||
) -> Option<U> {
|
) -> Option<U> {
|
||||||
let val = self.try_update_untracked(fun)?;
|
let (did_update, val) = self.try_update_untracked(fun)?;
|
||||||
self.trigger();
|
if did_update {
|
||||||
|
self.trigger();
|
||||||
|
}
|
||||||
Some(val)
|
Some(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -345,7 +361,8 @@ pub trait DefinedAt {
|
||||||
fn defined_at(&self) -> Option<&'static Location<'static>>;
|
fn defined_at(&self) -> Option<&'static Location<'static>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn panic_getting_disposed_signal(
|
#[doc(hidden)]
|
||||||
|
pub fn panic_getting_disposed_signal(
|
||||||
defined_at: Option<&'static Location<'static>>,
|
defined_at: Option<&'static Location<'static>>,
|
||||||
location: &'static Location<'static>,
|
location: &'static Location<'static>,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
|
|
@ -17,6 +17,7 @@ xxhash-rust = { version = "0.8", features = ["const_xxh64"] }
|
||||||
# used across multiple features
|
# used across multiple features
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
send_wrapper = { version = "0.6", features = ["futures"], optional = true }
|
send_wrapper = { version = "0.6", features = ["futures"], optional = true }
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
# registration system
|
# registration system
|
||||||
inventory = { version = "0.3", optional = true }
|
inventory = { version = "0.3", optional = true }
|
||||||
|
@ -47,7 +48,6 @@ http = { version = "1" }
|
||||||
ciborium = { version = "0.2", optional = true }
|
ciborium = { version = "0.2", optional = true }
|
||||||
hyper = { version = "1", optional = true }
|
hyper = { version = "1", optional = true }
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
thiserror = "1"
|
|
||||||
http-body-util = { version = "0.1.0", optional = true }
|
http-body-util = { version = "0.1.0", optional = true }
|
||||||
rkyv = { version = "0.7", features = [
|
rkyv = { version = "0.7", features = [
|
||||||
"validation",
|
"validation",
|
||||||
|
|
|
@ -6,10 +6,11 @@ pub use gloo_net::http::Request;
|
||||||
use js_sys::{Reflect, Uint8Array};
|
use js_sys::{Reflect, Uint8Array};
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use wasm_bindgen::JsValue;
|
use thiserror::Error;
|
||||||
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
use wasm_streams::ReadableStream;
|
use wasm_streams::ReadableStream;
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
AbortController, AbortSignal, FormData, Headers, RequestInit,
|
AbortController, AbortSignal, Event, FormData, Headers, RequestInit,
|
||||||
UrlSearchParams,
|
UrlSearchParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue