work related to 0.7 blog port

This commit is contained in:
Greg Johnston 2024-03-18 11:25:15 -04:00
parent b41fde3ff9
commit 2bc04444e1
16 changed files with 888 additions and 613 deletions

View file

@ -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" }

View file

@ -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() {

View file

@ -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
View 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
}
}
}

View 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)
}
}
}

View file

@ -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 {

View file

@ -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 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 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 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 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
}
}
} }

View file

@ -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")]

View file

@ -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>

View file

@ -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,
{
}

View file

@ -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>;

View file

@ -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())
}

View file

@ -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",

View file

@ -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 {

View file

@ -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",

View file

@ -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,
}; };