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_server = { path = "./leptos_server", version = "0.6.5" }
|
||||
leptos_meta = { path = "./meta", version = "0.6.5" }
|
||||
next_tuple = { path = "./next_tuple" }
|
||||
oco = { path = "./oco" }
|
||||
or_poisoned = { path = "./or_poisoned" }
|
||||
reactive_graph = { path = "./reactive_graph" }
|
||||
routing = { path = "./routing" }
|
||||
routing_utils = { path = "./routing_utils" }
|
||||
server_fn = { path = "./server_fn", 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" }
|
||||
|
|
|
@ -16,6 +16,7 @@ pub fn App() -> impl IntoView {
|
|||
<Stylesheet id="leptos" href="/pkg/hackernews.css"/>
|
||||
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
|
||||
<Meta name="description" content="Leptos implementation of a HackerNews demo."/>
|
||||
<Example/>
|
||||
<Router>
|
||||
<Nav />
|
||||
<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")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
|
|
|
@ -24,10 +24,11 @@ paste = "1"
|
|||
rand = { version = "0.8", optional = true }
|
||||
reactive_graph = { workspace = true, features = ["serde"] }
|
||||
tachys = { workspace = true, features = ["reactive_graph"] }
|
||||
thiserror = "1"
|
||||
tracing = "0.1"
|
||||
typed-builder = "0.18"
|
||||
typed-builder-macro = "0.18"
|
||||
serde = { version = "1", optional = true }
|
||||
serde = "1"
|
||||
serde_json = { version = "1", optional = true }
|
||||
server_fn = { workspace = true, features = [
|
||||
"form-redirects",
|
||||
|
@ -41,6 +42,7 @@ web-sys = { version = "0.3.63", features = [
|
|||
"ShadowRootMode",
|
||||
] }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
serde_qs = "0.12.0"
|
||||
|
||||
[features]
|
||||
default = ["serde"]
|
||||
|
@ -78,7 +80,6 @@ experimental-islands = [
|
|||
"leptos_dom/experimental-islands",
|
||||
"leptos_macro/experimental-islands",
|
||||
"leptos_reactive/experimental-islands",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
]
|
||||
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::*;
|
||||
}
|
||||
|
||||
mod action_form;
|
||||
pub use action_form::*;
|
||||
pub mod callback;
|
||||
pub mod children;
|
||||
pub mod component;
|
||||
|
@ -184,15 +186,13 @@ pub use leptos_config as config;
|
|||
pub use mount::hydrate_body;
|
||||
pub use mount::mount_to_body;
|
||||
pub use oco;
|
||||
pub mod from_form_data;
|
||||
|
||||
pub mod context {
|
||||
pub use reactive_graph::owner::{provide_context, use_context};
|
||||
}
|
||||
|
||||
#[cfg(feature = "hydration")]
|
||||
pub mod server {
|
||||
pub use leptos_server::{ArcResource, Resource};
|
||||
}
|
||||
pub use leptos_server as server;
|
||||
|
||||
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||
pub mod logging {
|
||||
|
|
|
@ -1,537 +1,208 @@
|
|||
//use crate::{ServerFn, ServerFnError};
|
||||
#[cfg(debug_assertions)]
|
||||
use leptos_reactive::console_warn;
|
||||
use leptos_reactive::{
|
||||
is_suppressing_resource_load, signal_prelude::*, spawn_local, store_value,
|
||||
try_batch, use_context, StoredValue,
|
||||
use reactive_graph::{
|
||||
action::ArcAction,
|
||||
owner::StoredValue,
|
||||
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
|
||||
traits::DefinedAt,
|
||||
unwrap_signal,
|
||||
};
|
||||
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.
|
||||
///
|
||||
/// 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>>)
|
||||
pub struct ArcServerAction<S>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static;
|
||||
|
||||
impl<I, O> Action<I, O>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
inner: ArcAction<S, Result<S::Output, ServerFnError<S::Error>>>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'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]
|
||||
pub fn dispatch(&self, input: I) {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: ArcAction::new(|input: &S| S::run_on_client(input.clone())),
|
||||
#[cfg(debug_assertions)]
|
||||
let loc = std::panic::Location::caller();
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
self.0.with_value(|a| {
|
||||
a.dispatch(
|
||||
input,
|
||||
#[track_caller]
|
||||
pub fn dispatch(&self, input: S) {
|
||||
self.inner.dispatch(input);
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> ArcServerAction<S>
|
||||
where
|
||||
S: ServerFn + Clone + Send + Sync + 'static,
|
||||
S::Output: 'static,
|
||||
S::Error: 'static,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn version(&self) -> ArcRwSignal<usize> {
|
||||
self.inner.version()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn input(&self) -> ArcRwSignal<Option<S>> {
|
||||
self.inner.input()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn value(
|
||||
&self,
|
||||
) -> ArcRwSignal<Option<Result<S::Output, ServerFnError<S::Error>>>> {
|
||||
self.inner.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Clone for ArcServerAction<S>
|
||||
where
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
#[cfg(debug_assertions)]
|
||||
loc,
|
||||
)
|
||||
})
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an [Action].
|
||||
///
|
||||
/// [Action] is a type of [Signal] which represent imperative calls to
|
||||
/// 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
|
||||
impl<S> Default for ArcServerAction<S>
|
||||
where
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
S: ServerFn + Clone + Send + Sync + 'static,
|
||||
S::Output: Send + Sync + 'static,
|
||||
S::Error: Send + Sync + '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)
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Action<I, Result<I::Output, ServerFnError<I::Error>>>
|
||||
impl<S> DefinedAt for ArcServerAction<S>
|
||||
where
|
||||
I: ServerFn + 'static,
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
/// Create an [Action] to imperatively call a [server](leptos_macro::server) function.
|
||||
///
|
||||
/// The struct representing your server function's arguments should be
|
||||
/// 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,
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// 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()
|
||||
Some(self.defined_at)
|
||||
}
|
||||
|
||||
/// Associates the URL of the given server function with this action.
|
||||
/// This enables integration with the `ActionForm` component in `leptos_router`.
|
||||
#[cfg_attr(
|
||||
any(debug_assertions, feature = "ssr"),
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub fn using_server_fn(self) -> Self
|
||||
where
|
||||
I::Error: Clone + 'static,
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
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>
|
||||
pub struct ServerAction<S>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
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 {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> Copy for Action<I, O>
|
||||
impl<S> Copy for ServerAction<S>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
S: ServerFn + 'static,
|
||||
S::Output: 'static,
|
||||
{
|
||||
}
|
||||
|
||||
struct ActionState<I, O>
|
||||
impl<S> ServerAction<S>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
S: ServerFn + Clone + Send + Sync + 'static,
|
||||
S::Output: Send + Sync + 'static,
|
||||
S::Error: Send + Sync + 'static,
|
||||
{
|
||||
/// How many times the action has successfully resolved.
|
||||
pub version: RwSignal<usize>,
|
||||
/// The current argument that was dispatched to the `async` function.
|
||||
/// `Some` while we are waiting for it to resolve, `None` if it has resolved.
|
||||
pub input: RwSignal<Option<I>>,
|
||||
/// The most recent return value of the `async` function.
|
||||
pub value: RwSignal<Option<O>>,
|
||||
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() {
|
||||
#[track_caller]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: StoredValue::new(ArcServerAction::new()),
|
||||
#[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`."
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
defined_at: Location::caller(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an [Action] to synchronize an imperative `async` call to the synchronous reactive system.
|
||||
///
|
||||
/// 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
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
F: Fn(&I) -> Fu + 'static,
|
||||
Fu: Future<Output = O> + 'static,
|
||||
{
|
||||
Action::new(action_fn)
|
||||
#[track_caller]
|
||||
pub fn dispatch(&self, input: S) {
|
||||
self.inner.with_value(|inner| inner.dispatch(input));
|
||||
}
|
||||
|
||||
/// Creates an [Action] that can be used to call a server function.
|
||||
///
|
||||
/// ```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
|
||||
S: Clone + ServerFn,
|
||||
S::Error: Clone + 'static,
|
||||
{
|
||||
Action::<S, _>::server()
|
||||
#[track_caller]
|
||||
pub fn version(&self) -> RwSignal<usize> {
|
||||
self.inner
|
||||
.with_value(|inner| inner.version())
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
.into()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn input(&self) -> RwSignal<Option<S>> {
|
||||
self.inner
|
||||
.with_value(|inner| inner.input())
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
.into()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn value(
|
||||
&self,
|
||||
) -> RwSignal<Option<Result<S::Output, ServerFnError<S::Error>>>> {
|
||||
self.inner
|
||||
.with_value(|inner| inner.value())
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Default for ServerAction<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 ServerAction<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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
//#![deny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod action;
|
||||
pub use action::*;
|
||||
#[cfg(feature = "hydration")]
|
||||
mod resource;
|
||||
#[cfg(feature = "hydration")]
|
||||
|
|
|
@ -9,13 +9,15 @@ use core::{fmt::Debug, marker::PhantomData};
|
|||
use futures::Future;
|
||||
use hydration_context::SerializedDataId;
|
||||
use reactive_graph::{
|
||||
computed::{ArcAsyncDerived, AsyncDerived, AsyncDerivedFuture, AsyncState},
|
||||
computed::{
|
||||
ArcAsyncDerived, ArcMemo, AsyncDerived, AsyncDerivedFuture, AsyncState,
|
||||
},
|
||||
owner::Owner,
|
||||
prelude::*,
|
||||
};
|
||||
use std::{future::IntoFuture, ops::Deref};
|
||||
|
||||
pub struct ArcResource<T, Ser> {
|
||||
pub struct ArcResource<T, Ser = SerdeJson> {
|
||||
ser: PhantomData<Ser>,
|
||||
data: ArcAsyncDerived<T>,
|
||||
}
|
||||
|
@ -34,12 +36,16 @@ where
|
|||
T::SerErr: 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
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::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
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::DeErr: Debug,
|
||||
{
|
||||
pub fn new_miniserde<Fut>(
|
||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||
pub fn new_miniserde<S, Fut>(
|
||||
source: impl Fn() -> S + Send + Sync + 'static,
|
||||
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::DeErr: Debug,
|
||||
{
|
||||
pub fn new_serde_lite<Fut>(
|
||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||
pub fn new_serde_lite<S, Fut>(
|
||||
source: impl Fn() -> S + Send + Sync + 'static,
|
||||
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::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
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::DeErr: Debug,
|
||||
{
|
||||
pub fn new_with_encoding<Fut>(
|
||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||
pub fn new_with_encoding<S, Fut>(
|
||||
source: impl Fn() -> S + Send + Sync + 'static,
|
||||
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||
) -> ArcResource<T, Ser>
|
||||
where
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Debug + Send + Sync + 'static,
|
||||
Fut: Future<Output = T> + Send + 'static,
|
||||
{
|
||||
|
@ -132,6 +152,9 @@ where
|
|||
|
||||
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);
|
||||
|
||||
if let Some(shared_context) = shared_context {
|
||||
|
@ -227,12 +250,16 @@ where
|
|||
T::SerErr: 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
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::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
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::DeErr: Debug,
|
||||
{
|
||||
pub fn new_miniserde<Fut>(
|
||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||
pub fn new_miniserde<S, Fut>(
|
||||
source: impl Fn() -> S + Send + Sync + 'static,
|
||||
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::DeErr: Debug,
|
||||
{
|
||||
pub fn new_serde_lite<Fut>(
|
||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||
pub fn new_serde_lite<S, Fut>(
|
||||
source: impl Fn() -> S + Send + Sync + 'static,
|
||||
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::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
|
||||
S: PartialEq + Clone + Send + Sync + 'static,
|
||||
T: Send + Sync + '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::DeErr: Debug,
|
||||
{
|
||||
pub fn new_with_encoding<Fut>(
|
||||
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||
pub fn new_with_encoding<S, Fut>(
|
||||
source: impl Fn() -> S + Send + Sync + 'static,
|
||||
fetcher: impl Fn(S) -> Fut + Send + Sync + 'static,
|
||||
) -> Resource<T, Ser>
|
||||
where
|
||||
S: Send + Sync + Clone + PartialEq + 'static,
|
||||
T: Send + Sync + 'static,
|
||||
Fut: Future<Output = T> + Send + 'static,
|
||||
{
|
||||
let shared_context = Owner::current_shared_context();
|
||||
let id = shared_context
|
||||
.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
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let ArcResource { ser, data } =
|
||||
ArcResource::new_with_encoding(source, fetcher);
|
||||
Resource {
|
||||
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>
|
||||
|
|
|
@ -1,19 +1,62 @@
|
|||
use crate::{
|
||||
computed::AsyncState,
|
||||
signal::ArcRwSignal,
|
||||
traits::{Set, Update},
|
||||
diagnostics::is_suppressing_resource_load,
|
||||
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::{
|
||||
future::Future,
|
||||
mem::swap,
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
};
|
||||
|
||||
pub enum ActionState<I, O> {
|
||||
/*enum ActionState<I, O> {
|
||||
Idle,
|
||||
Loading(I),
|
||||
LoadingMultiple(I, usize),
|
||||
Complete(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>
|
||||
|
@ -21,31 +64,234 @@ where
|
|||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
in_flight: ArcRwSignal<usize>,
|
||||
input: ArcRwSignal<Option<I>>,
|
||||
value: ArcRwSignal<Option<O>>,
|
||||
version: ArcRwSignal<usize>,
|
||||
state: ArcRwSignal<ActionState<I, O>>,
|
||||
pending_dispatches: Arc<AtomicUsize>,
|
||||
#[allow(clippy::complexity)]
|
||||
action_fn: Arc<dyn Fn(&I) -> Pin<Box<dyn Future<Output = O>>>>,
|
||||
#[cfg(debug_assertion)]
|
||||
action_fn: Arc<
|
||||
dyn Fn(&I) -> Pin<Box<dyn Future<Output = O> + Send>> + Send + Sync,
|
||||
>,
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: &'static Location<'static>,
|
||||
}
|
||||
|
||||
impl<I, O> ArcAction<I, O>
|
||||
impl<I, O> Clone for ArcAction<I, O>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
in_flight: self.in_flight.clone(),
|
||||
input: self.input.clone(),
|
||||
value: self.value.clone(),
|
||||
version: self.version.clone(),
|
||||
action_fn: self.action_fn.clone(),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_at: self.defined_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
let fut = (self.action_fn)(&input);
|
||||
if !is_suppressing_resource_load() {
|
||||
let mut fut = (self.action_fn)(&input).fuse();
|
||||
|
||||
self.state.update(|prev| {
|
||||
*prev = match prev {
|
||||
ActionState::Idle => ActionState::Loading(input),
|
||||
ActionState::Loading(_) => todo!(),
|
||||
ActionState::Complete(_) => todo!(),
|
||||
ActionState::Reloading(_, _) => todo!(),
|
||||
// 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>>,
|
||||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
|
|
|
@ -298,9 +298,23 @@ pub trait Update {
|
|||
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>(
|
||||
&self,
|
||||
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>;
|
||||
}
|
||||
|
||||
|
@ -310,12 +324,14 @@ where
|
|||
{
|
||||
type Value = <Self as UpdateUntracked>::Value;
|
||||
|
||||
fn try_update<U>(
|
||||
fn try_maybe_update<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&mut Self::Value) -> U,
|
||||
fun: impl FnOnce(&mut Self::Value) -> (bool, U),
|
||||
) -> Option<U> {
|
||||
let val = self.try_update_untracked(fun)?;
|
||||
let (did_update, val) = self.try_update_untracked(fun)?;
|
||||
if did_update {
|
||||
self.trigger();
|
||||
}
|
||||
Some(val)
|
||||
}
|
||||
}
|
||||
|
@ -345,7 +361,8 @@ pub trait DefinedAt {
|
|||
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>>,
|
||||
location: &'static Location<'static>,
|
||||
) -> String {
|
||||
|
|
|
@ -17,6 +17,7 @@ xxhash-rust = { version = "0.8", features = ["const_xxh64"] }
|
|||
# used across multiple features
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
send_wrapper = { version = "0.6", features = ["futures"], optional = true }
|
||||
thiserror = "1"
|
||||
|
||||
# registration system
|
||||
inventory = { version = "0.3", optional = true }
|
||||
|
@ -47,7 +48,6 @@ http = { version = "1" }
|
|||
ciborium = { version = "0.2", optional = true }
|
||||
hyper = { version = "1", optional = true }
|
||||
bytes = "1"
|
||||
thiserror = "1"
|
||||
http-body-util = { version = "0.1.0", optional = true }
|
||||
rkyv = { version = "0.7", features = [
|
||||
"validation",
|
||||
|
|
|
@ -6,10 +6,11 @@ pub use gloo_net::http::Request;
|
|||
use js_sys::{Reflect, Uint8Array};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use wasm_bindgen::JsValue;
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use wasm_streams::ReadableStream;
|
||||
use web_sys::{
|
||||
AbortController, AbortSignal, FormData, Headers, RequestInit,
|
||||
AbortController, AbortSignal, Event, FormData, Headers, RequestInit,
|
||||
UrlSearchParams,
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue