Compare commits

..

9 Commits

Author SHA1 Message Date
Greg Johnston
3782a8ad70 chore: update for non-generic rendering 2024-09-30 20:32:24 -04:00
Greg Johnston
cb11ce0a5f feat: use new bind: syntax 2024-09-30 20:32:09 -04:00
Greg Johnston
37c794b283 chore: move Group into the reactive module, as that's the only place it has meaning 2024-09-30 20:20:41 -04:00
Greg Johnston
f5ff45863d chore: use ::leptos absolute path in macro 2024-09-30 20:15:48 -04:00
Greg Johnston
ca6f934039 Merge remote-tracking branch 'origin' into two-way-data-binding 2024-09-30 20:10:43 -04:00
Maccesch
6ea942f3c5 Merge branch 'main' into two-way-data-binding
# Conflicts:
#	leptos_macro/src/view/component_builder.rs
#	leptos_macro/src/view/slot_helper.rs
2024-09-20 12:43:23 +02:00
Maccesch
3a079ace46 moved bind into reactive_graph mod 2024-09-16 05:13:32 +02:00
Maccesch
dbf654aa86 added two-way data binding to radio groups 2024-09-16 04:47:14 +02:00
Maccesch
d16231aa3a added two-way data binding to dom elements 2024-09-15 20:50:17 +02:00
85 changed files with 2337 additions and 2171 deletions

View File

@@ -1,7 +1,7 @@
use counter::*;
use leptos::mount::mount_to;
use leptos::prelude::*;
use leptos::task::tick;
use leptos::spawn::tick;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -1,4 +1,4 @@
use leptos::prelude::*;
use leptos::{prelude::*, reactive_graph::actions::Action};
use leptos_router::{
components::{FlatRoutes, Route, Router, A},
StaticSegment,

View File

@@ -1,5 +1,5 @@
use counter_without_macros::counter;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::tick};
use pretty_assertions::assert_eq;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -4,7 +4,7 @@ use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use counters::Counters;
use leptos::prelude::*;
use leptos::task::tick;
use leptos::spawn::tick;
use web_sys::HtmlElement;
#[wasm_bindgen_test]

View File

@@ -1,5 +1,5 @@
use directives::App;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::tick};
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::HtmlElement;

View File

@@ -17,7 +17,7 @@ leptos = { path = "../../leptos", features = [
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [
"dont-use-islands-router",
"islands-router",
], optional = true }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,14 +1,19 @@
# Work in Progress
# Leptos Todo App Sqlite with Axum
This example is something I wrote on a long layover in the Orlando airport in July. (It was really hot!)
This example creates a basic todo app with an Axum backend that uses Leptos' server functions to call sqlx from the client and seamlessly run it on the server.
It is the culmination of a couple years of thinking and working toward being able to do this, which you can see
described pretty well in the pinned roadmap issue (#1830) and its discussion of different modes of client-side
routing when you use islands.
## Getting Started
This uses *only* server rendering, with no actual islands, but still maintains client-side state across page navigations.
It does this by building on the fact that we now have a statically-typed view tree to do pretty smart updates with
new HTML from the client, with extremely minimal diffing.
See the [Examples README](../README.md) for setup and run instructions.
The demo itself works, but the feature that supports it is incomplete. A couple people have accidentally
used it and broken their applications in ways they don't understand, so I've renamed the feature to `dont-use-islands-router`.
## E2E Testing
See the [E2E README](./e2e/README.md) for more information about the testing strategy.
## Rendering
See the [SSR Notes](../SSR_NOTES.md) for more information about Server Side Rendering.
## Quick Start
Run `cargo leptos watch` to run this example.

View File

@@ -1,5 +1,5 @@
use js_framework_benchmark_leptos::*;
use leptos::{prelude::*, task::tick};
use leptos::{prelude::*, spawn::tick};
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;

View File

@@ -3,7 +3,7 @@ use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
use leptos::task::tick;
use leptos::spawn::tick;
use leptos::{leptos_dom::helpers::document, mount::mount_to};
use web_sys::HtmlButtonElement;

View File

@@ -40,8 +40,6 @@ pin-project-lite = "0.2.14"
dashmap = { version = "6.0", optional = true }
once_cell = { version = "1.19", optional = true }
async-broadcast = { version = "0.7.1", optional = true }
bytecheck = "0.8.0"
rkyv = { version = "0.8.8" }
[features]
hydrate = ["leptos/hydrate"]

View File

@@ -1,6 +1,6 @@
use futures::StreamExt;
use http::Method;
use leptos::{html::Input, prelude::*, task::spawn_local};
use leptos::{html::Input, prelude::*, spawn::spawn_local};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use server_fn::{
client::{browser::BrowserClient, Client},
@@ -417,6 +417,7 @@ pub fn FileUploadWithProgress() -> impl IntoView {
/// This requires us to store some global state of all the uploads. In a real app, you probably
/// shouldn't do exactly what I'm doing here in the demo. For example, this map just
/// distinguishes between files by filename, not by user.
#[cfg(feature = "ssr")]
mod progress {
use async_broadcast::{broadcast, Receiver, Sender};

View File

@@ -4,8 +4,6 @@ use leptos::{config::get_configuration, logging};
use leptos_axum::{generate_route_list, LeptosRoutes};
use server_fns_axum::*;
// cargo make cli: error: unneeded `return` statement
#[allow(clippy::needless_return)]
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Error)

58
flake.lock generated
View File

@@ -5,11 +5,29 @@
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
@@ -20,11 +38,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1727634051,
"narHash": "sha256-S5kVU7U82LfpEukbn/ihcyNt2+EvG7Z5unsKW9H/yFA=",
"lastModified": 1703961334,
"narHash": "sha256-M1mV/Cq+pgjk0rt6VxoyyD+O8cOUiai8t9Q6Yyq4noY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "06cf0e1da4208d3766d898b7fdab6513366d45b9",
"rev": "b0d36bd0a420ecee3bc916c91886caca87c894e9",
"type": "github"
},
"original": {
@@ -36,11 +54,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1718428119,
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
@@ -59,14 +77,15 @@
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1727749966,
"narHash": "sha256-DUS8ehzqB1DQzfZ4bRXVSollJhu+y7cvh1DJ9mbWebE=",
"lastModified": 1704075545,
"narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "00decf1b4f9886d25030b9ee4aed7bfddccb5f66",
"rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
"type": "github"
},
"original": {
@@ -89,6 +108,21 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -33,7 +33,7 @@ once_cell = "1"
rustdoc-args = ["--generate-link-to-definition"]
[features]
dont-use-islands-router = []
islands-router = []
tracing = ["dep:tracing"]
[package.metadata.cargo-all-features]

View File

@@ -24,7 +24,7 @@ use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::expect_context,
reactive::{computed::ScopedFuture, owner::Owner},
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
@@ -749,7 +749,7 @@ where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()

View File

@@ -28,7 +28,7 @@ once_cell = "1"
parking_lot = "0.12.3"
serde_json = "1.0"
tokio = { version = "1.39", default-features = false }
tower = { version = "0.4.13", features = ["util"] }
tower = "0.4.13"
tower-http = "0.5.2"
tracing = { version = "0.1.40", optional = true }
@@ -39,7 +39,7 @@ tokio = { version = "1.39", features = ["net", "rt-multi-thread"] }
[features]
wasm = []
default = ["tokio/fs", "tokio/sync", "tower-http/fs", "tower/util"]
dont-use-islands-router = []
islands-router = []
tracing = ["dep:tracing"]
[package.metadata.docs.rs]

View File

@@ -53,7 +53,7 @@ use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
prelude::*,
reactive::{computed::ScopedFuture, owner::Owner},
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_integration_utils::{
@@ -76,7 +76,7 @@ use server_fn::{redirect::REDIRECT_HEADER, ServerFnError};
use std::path::Path;
use std::{fmt::Debug, io, pin::Pin, sync::Arc};
#[cfg(feature = "default")]
use tower::util::ServiceExt;
use tower::ServiceExt;
#[cfg(feature = "default")]
use tower_http::services::ServeDir;
// use tracing::Instrument; // TODO check tracing span -- was this used in 0.6 for a missing link?
@@ -784,7 +784,7 @@ where
_ = replace_blocks; // TODO
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_out_of_order_branching()
} else {
app.to_html_stream_out_of_order()
@@ -849,7 +849,7 @@ where
IV: IntoView + 'static,
{
handle_response(additional_context, app_fn, |app, chunks| {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1069,7 +1069,7 @@ where
{
handle_response(additional_context, app_fn, |app, chunks| {
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()
@@ -1146,7 +1146,7 @@ where
IV: IntoView + 'static,
{
Box::pin(async move {
let app = if cfg!(feature = "dont-use-islands-router") {
let app = if cfg!(feature = "islands-router") {
app.to_html_stream_in_order_branching()
} else {
app.to_html_stream_in_order()

View File

@@ -2,7 +2,7 @@ use futures::{stream::once, Stream, StreamExt};
use hydration_context::{SharedContext, SsrSharedContext};
use leptos::{
nonce::use_nonce,
reactive::owner::{Owner, Sandboxed},
reactive_graph::owner::{Owner, Sandboxed},
IntoView,
};
use leptos_config::LeptosOptions;

View File

@@ -1,8 +1,10 @@
use crate::{prelude::Suspend, suspense_component::Suspense, IntoView};
use crate::Suspense;
use leptos_dom::IntoView;
use leptos_macro::{component, view};
use leptos_server::ArcOnceResource;
use reactive_graph::prelude::ReadUntracked;
use serde::{de::DeserializeOwned, Serialize};
use leptos_reactive::{
create_blocking_resource, create_local_resource, create_resource,
store_value, Serializable,
};
#[component]
/// Allows you to inline the data loading for an `async` block or
@@ -13,8 +15,11 @@ use serde::{de::DeserializeOwned, Serialize};
/// Adding `let:{variable name}` to the props makes the data available in the children
/// that variable name, when resolved.
/// ```
/// # use leptos::prelude::*;
/// # use leptos_reactive::*;
/// # use leptos_macro::*;
/// # use leptos_dom::*; use leptos::*;
/// # if false {
/// # let runtime = create_runtime();
/// async fn fetch_monkeys(monkey: i32) -> i32 {
/// // do some expensive work
/// 3
@@ -22,23 +27,29 @@ use serde::{de::DeserializeOwned, Serialize};
///
/// view! {
/// <Await
/// future=fetch_monkeys(3)
/// future=|| fetch_monkeys(3)
/// let:data
/// >
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// </Await>
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
pub fn Await<T, Fut, Chil, V>(
/// A [`Future`](std::future::Future) that will the component will `.await`
/// before rendering.
future: Fut,
/// If `true`, the component will create a blocking resource, preventing
pub fn Await<T, Fut, FF, VF, V>(
/// A function that returns the [`Future`](std::future::Future) that
/// will the component will `.await` before rendering.
future: FF,
/// If `true`, the component will use [`create_blocking_resource`], preventing
/// the HTML stream from returning anything before `future` has resolved.
#[prop(optional)]
blocking: bool,
/// If `true`, the component will use [`create_local_resource`], this will
/// always run on the local system and therefore its result type does not
/// need to be `Serializable`.
#[prop(optional)]
local: bool,
/// A function that takes a reference to the resolved data from the `future`
/// renders a view.
///
@@ -47,58 +58,65 @@ pub fn Await<T, Fut, Chil, V>(
/// `let:` syntax to specify the name for the data variable.
///
/// ```rust
/// # use leptos::prelude::*;
/// # use leptos::*;
/// # if false {
/// # let runtime = create_runtime();
/// # async fn fetch_monkeys(monkey: i32) -> i32 {
/// # 3
/// # }
/// view! {
/// <Await
/// future=fetch_monkeys(3)
/// future=|| fetch_monkeys(3)
/// let:data
/// >
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// </Await>
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
/// is the same as
/// ```rust
/// # use leptos::prelude::*;
/// # use leptos::*;
/// # if false {
/// # let runtime = create_runtime();
/// # async fn fetch_monkeys(monkey: i32) -> i32 {
/// # 3
/// # }
/// view! {
/// <Await
/// future=fetch_monkeys(3)
/// future=|| fetch_monkeys(3)
/// children=|data| view! {
/// <p>{*data} " little monkeys, jumping on the bed."</p>
/// }
/// />
/// }
/// # ;
/// # runtime.dispose();
/// # }
/// ```
children: Chil,
children: VF,
) -> impl IntoView
where
T: Send + Sync + Serialize + DeserializeOwned + 'static,
Fut: std::future::Future<Output = T> + Send + 'static,
Chil: FnOnce(&T) -> V + Send + 'static,
Fut: std::future::Future<Output = T> + 'static,
FF: Fn() -> Fut + 'static,
V: IntoView,
VF: Fn(&T) -> V + 'static,
T: Serializable + 'static,
{
let res = ArcOnceResource::<T>::new_with_options(future, blocking);
let ready = res.ready();
let res = if blocking {
create_blocking_resource(|| (), move |_| future())
} else if local {
create_local_resource(|| (), move |_| future())
} else {
create_resource(|| (), move |_| future())
};
let view = store_value(children);
view! {
<Suspense fallback=|| ()>
{Suspend::new(async move {
ready.await;
children(res.read_untracked().as_ref().unwrap())
})}
{move || res.map(|data| view.with_value(|view| view(data)))}
</Suspense>
}
}

View File

@@ -41,10 +41,7 @@
//!
//! Use `SyncCallback` if the function is not `Sync` and `Send`.
use reactive_graph::{
owner::{LocalStorage, StoredValue},
traits::WithValue,
};
use reactive_graph::owner::{LocalStorage, StoredValue};
use std::{fmt, rc::Rc, sync::Arc};
/// A wrapper trait for calling callbacks.

View File

@@ -168,11 +168,12 @@ pub mod prelude {
pub use leptos_server::*;
pub use oco_ref::*;
pub use reactive_graph::{
actions::*, computed::*, effect::*, graph::untrack, owner::*,
signal::*, wrappers::read::*,
actions::*, computed::*, effect::*, owner::*, signal::*, untrack,
wrappers::read::*,
};
pub use server_fn::{self, ServerFnError};
pub use tachys::{
self,
reactive_graph::{bind::BindAttribute, node_ref::*, Suspend},
view::template::ViewTemplate,
};
@@ -200,11 +201,10 @@ pub mod error {
pub use throw_error::*;
}
/// Control-flow components like `<Show>`, `<For>`, and `<Await>`.
/// Control-flow components like `<Show>` and `<For>`.
pub mod control_flow {
pub use crate::{await_::*, for_loop::*, show::*};
pub use crate::{for_loop::*, show::*};
}
mod await_;
mod for_loop;
mod show;
@@ -230,7 +230,6 @@ mod suspense_component;
pub mod text_prop;
mod transition;
pub use leptos_macro::*;
#[doc(inline)]
pub use server_fn;
#[doc(hidden)]
pub use typed_builder;
@@ -238,22 +237,16 @@ pub use typed_builder;
pub use typed_builder_macro;
mod into_view;
pub use into_view::IntoView;
#[doc(inline)]
pub use leptos_dom;
mod provider;
#[doc(inline)]
pub use tachys;
/// Tools to mount an application to the DOM, or to hydrate it from server-rendered HTML.
pub mod mount;
#[doc(inline)]
pub use leptos_config as config;
#[doc(inline)]
pub use oco_ref as oco;
mod from_form_data;
#[doc(inline)]
pub use either_of as either;
#[doc(inline)]
pub use reactive_graph as reactive;
pub use reactive_graph;
/// Provide and access data along the reactive graph, sharing data without directly passing arguments.
pub mod context {
@@ -261,22 +254,17 @@ pub mod context {
pub use reactive_graph::owner::{provide_context, use_context};
}
#[doc(inline)]
pub use leptos_server as server;
/// HTML attribute types.
#[doc(inline)]
pub use tachys::html::attribute as attr;
/// HTML element types.
#[doc(inline)]
pub use tachys::html::element as html;
/// HTML event types.
#[doc(no_inline)]
pub use tachys::html::event as ev;
/// MathML element types.
#[doc(inline)]
pub use tachys::mathml as math;
/// SVG element types.
#[doc(inline)]
pub use tachys::svg;
/// Utilities for simple isomorphic logging to the console or terminal.
@@ -284,7 +272,7 @@ pub mod logging {
pub use leptos_dom::{debug_warn, error, log, warn};
}
pub mod task {
pub mod spawn {
pub use any_spawner::Executor;
use std::future::Future;
@@ -302,7 +290,6 @@ pub mod task {
Executor::spawn_local(fut)
}
/// Waits until the next "tick" of the current async executor.
pub async fn tick() {
Executor::tick().await
}
@@ -329,6 +316,7 @@ pub use web_sys;
/*mod additional_attributes;
pub use additional_attributes::*;
mod await_;
pub use await_::*;
pub use leptos_config::{self, get_configuration, LeptosOptions};
#[cfg(not(all(

View File

@@ -1,7 +1,7 @@
use crate::{children::TypedChildrenFn, mount, IntoView};
use leptos_dom::helpers::document;
use leptos_macro::component;
use reactive_graph::{effect::Effect, graph::untrack, owner::Owner};
use reactive_graph::{effect::Effect, owner::Owner, untrack};
use std::sync::Arc;
/// Renders components somewhere else in the DOM.

View File

@@ -255,7 +255,7 @@ impl ToTokens for Model {
let body_name = unmodified_fn_name_from_fn_name(&body_name);
let body_expr = if is_island {
quote! {
::leptos::reactive::owner::Owner::with_hydration(move || {
::leptos::reactive_graph::owner::Owner::with_hydration(move || {
#body_name(#prop_names)
})
}
@@ -266,7 +266,7 @@ impl ToTokens for Model {
};
let component = quote! {
::leptos::prelude::untrack(
::leptos::reactive_graph::untrack(
move || {
#tracing_guard_expr
#tracing_props_expr
@@ -280,7 +280,7 @@ impl ToTokens for Model {
let hydrate_fn_name = hydrate_fn_name.as_ref().unwrap();
quote! {
{
if ::leptos::reactive::owner::Owner::current_shared_context()
if ::leptos::reactive_graph::owner::Owner::current_shared_context()
.map(|sc| sc.get_is_hydrating())
.unwrap_or(false) {
::leptos::either::Either::Left(
@@ -316,9 +316,9 @@ impl ToTokens for Model {
quote! {
use leptos::tachys::view::any_view::IntoAny;
let children = Box::new(|| {
let sc = ::leptos::reactive::owner::Owner::current_shared_context().unwrap();
let sc = ::leptos::reactive_graph::owner::Owner::current_shared_context().unwrap();
let prev = sc.get_is_hydrating();
let value = ::leptos::reactive::owner::Owner::with_no_hydration(||
let value = ::leptos::reactive_graph::owner::Owner::with_no_hydration(||
::leptos::tachys::html::islands::IslandChildren::new(children()).into_any()
);
sc.set_is_hydrating(prev);

View File

@@ -40,9 +40,9 @@ impl ToTokens for MemoMacroInput {
let path = &self.path;
tokens.extend(quote! {
::leptos::reactive::computed::Memo::new(
::leptos::reactive_graph::computed::Memo::new(
move |_| {
use ::leptos::reactive::traits::With;
use ::leptos::reactive_graph::traits::With;
#root.with(|st: _| st.#path.clone())
}
)

View File

@@ -40,7 +40,7 @@ impl ToTokens for SliceMacroInput {
let path = &self.path;
tokens.extend(quote! {
::leptos::reactive::computed::create_slice(
::leptos::reactive_graph::computed::create_slice(
#root,
|st: &_| st.#path.clone(),
|st: &mut _, n| st.#path = n

View File

@@ -307,12 +307,10 @@ fn inert_element_to_tokens(
match current {
Node::RawText(raw) => {
let text = raw.to_string_best();
let text = html_escape::encode_text(&text);
html.push_str(&text);
}
Node::Text(text) => {
let text = text.value_string();
let text = html_escape::encode_text(&text);
html.push_str(&text);
}
Node::Element(node) => {
@@ -326,12 +324,9 @@ fn inert_element_to_tokens(
for attr in node.attributes() {
if let NodeAttribute::Attribute(attr) = attr {
let attr_name = attr.key.to_string();
// trim r# from raw identifiers like r#as
let attr_name =
attr_name.trim_start_matches("r#");
if attr_name != "class" {
html.push(' ');
html.push_str(attr_name);
html.push_str(&attr_name);
}
if let Some(value) =
@@ -342,13 +337,11 @@ fn inert_element_to_tokens(
)) = &value.value
{
if let Lit::Str(txt) = &lit.lit {
let value = txt.value();
let value = html_escape::encode_double_quoted_attribute(&value);
if attr_name == "class" {
html.push_class(&value);
html.push_class(&txt.value());
} else {
html.push_str("=\"");
html.push_str(&value);
html.push_str(&txt.value());
html.push('"');
}
}
@@ -548,9 +541,7 @@ fn node_to_tokens(
view_marker,
disable_inert_html,
),
Node::Block(block) => {
Some(quote! { ::leptos::prelude::IntoRender::into_render(#block) })
}
Node::Block(block) => Some(quote! { #block }),
Node::Text(text) => Some(text_to_tokens(&text.value)),
Node::RawText(raw) => {
let text = raw.to_string_best();
@@ -727,11 +718,6 @@ pub(crate) fn element_to_tokens(
quote! { ::leptos::tachys::html::element::#custom(#name) }
} else if is_svg_element(&tag) {
parent_type = TagType::Svg;
let name = if tag == "use" || tag == "use_" {
Ident::new_raw("use", name.span()).to_token_stream()
} else {
name.to_token_stream()
};
quote! { ::leptos::tachys::svg::#name() }
} else if is_math_ml_element(&tag) {
parent_type = TagType::Math;
@@ -885,7 +871,7 @@ fn attribute_to_tokens(
NodeName::Path(path) => path.path.get_ident(),
_ => unreachable!(),
};
let value = attribute_value(node, false);
let value = attribute_value(node);
quote! {
.#node_ref(#value)
}
@@ -935,13 +921,13 @@ fn attribute_to_tokens(
// we don't provide statically-checked methods for SVG attributes
|| (tag_type == TagType::Svg && name != "inner_html")
{
let value = attribute_value(node, true);
let value = attribute_value(node);
quote! {
.attr(#name, #value)
}
} else {
let key = attribute_name(&node.key);
let value = attribute_value(node, true);
let value = attribute_value(node);
// special case of global_class and class attribute
if &node.key.to_string() == "class"
@@ -978,11 +964,11 @@ pub(crate) fn attribute_absolute(
let id = &parts[0];
match id {
NodeNameFragment::Ident(id) => {
let value = attribute_value(node);
// ignore `let:` and `clone:`
if id == "let" || id == "clone" {
None
} else if id == "attr" {
let value = attribute_value(node, true);
let key = &parts[1];
let key_name = key.to_string();
if key_name == "class" || key_name == "style" {
@@ -990,7 +976,6 @@ pub(crate) fn attribute_absolute(
quote! { ::leptos::tachys::html::#key::#key(#value) },
)
} else if key_name == "aria" {
let value = attribute_value(node, true);
let mut parts_iter = parts.iter();
parts_iter.next();
let fn_name = parts_iter.map(|p| p.to_string()).collect::<Vec<String>>().join("_");
@@ -1019,7 +1004,6 @@ pub(crate) fn attribute_absolute(
},
)
} else if id == "style" || id == "class" {
let value = attribute_value(node, false);
let key = &node.key.to_string();
let key = key
.replacen("style:", "", 1)
@@ -1028,7 +1012,6 @@ pub(crate) fn attribute_absolute(
quote! { ::leptos::tachys::html::#id::#id((#key, #value)) },
)
} else if id == "prop" {
let value = attribute_value(node, false);
let key = &node.key.to_string();
let key = key.replacen("prop:", "", 1);
Some(
@@ -1085,7 +1068,7 @@ pub(crate) fn two_way_binding_to_tokens(
name: &str,
node: &KeyedAttribute,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
let ident =
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
@@ -1110,7 +1093,7 @@ pub(crate) fn event_type_and_handler(
name: &str,
node: &KeyedAttribute,
) -> (TokenStream, TokenStream, TokenStream) {
let handler = attribute_value(node, false);
let handler = attribute_value(node);
let (event_type, is_custom, is_force_undelegated, is_targeted) =
parse_event_name(name);
@@ -1167,7 +1150,7 @@ fn class_to_tokens(
class: TokenStream,
class_name: Option<&str>,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
if let Some(class_name) = class_name {
quote! {
.#class((#class_name, #value))
@@ -1184,7 +1167,7 @@ fn style_to_tokens(
style: TokenStream,
style_name: Option<&str>,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
if let Some(style_name) = style_name {
quote! {
.#style((#style_name, #value))
@@ -1201,7 +1184,7 @@ fn prop_to_tokens(
prop: TokenStream,
key: &str,
) -> TokenStream {
let value = attribute_value(node, false);
let value = attribute_value(node);
quote! {
.#prop(#key, #value)
}
@@ -1358,10 +1341,7 @@ fn attribute_name(name: &NodeName) -> TokenStream {
}
}
fn attribute_value(
attr: &KeyedAttribute,
is_attribute_proper: bool,
) -> TokenStream {
fn attribute_value(attr: &KeyedAttribute) -> TokenStream {
match attr.possible_value.to_value() {
None => quote! { true },
Some(value) => match &value.value {
@@ -1376,26 +1356,14 @@ fn attribute_value(
}
}
if matches!(expr, Expr::Lit(_)) || !is_attribute_proper {
quote! {
#expr
}
} else {
quote! {
::leptos::prelude::IntoAttributeValue::into_attribute_value(#expr)
}
quote! {
{#expr}
}
}
// any value in braces: expand as-is to give proper r-a support
KVAttributeValue::InvalidBraced(block) => {
if is_attribute_proper {
quote! {
::leptos::prelude::IntoAttributeValue::into_attribute_value(#block)
}
} else {
quote! {
#block
}
quote! {
#block
}
}
},

View File

@@ -19,7 +19,6 @@ tracing = { version = "0.1.40", optional = true }
futures = "0.3.30"
any_spawner = { workspace = true }
or_poisoned = { workspace = true }
tachys = { workspace = true, optional = true, features = ["reactive_graph"] }
send_wrapper = "0.6"

View File

@@ -8,8 +8,6 @@ mod local_resource;
pub use local_resource::*;
mod multi_action;
pub use multi_action::*;
mod once_resource;
pub use once_resource::*;
mod resource;
pub use resource::*;
mod shared;

View File

@@ -35,10 +35,10 @@ impl<T> Clone for ArcLocalResource<T> {
impl<T> ArcLocalResource<T> {
#[track_caller]
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
pub fn new<Fut>(fetcher: impl Fn() -> Fut + Send + Sync + 'static) -> Self
where
T: 'static,
Fut: Future<Output = T> + 'static,
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
let fetcher = move || {
let fut = fetcher();
@@ -60,7 +60,7 @@ impl<T> ArcLocalResource<T> {
}
};
Self {
data: ArcAsyncDerived::new_unsync(fetcher),
data: ArcAsyncDerived::new(fetcher),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
@@ -103,7 +103,7 @@ impl<T> DefinedAt for ArcLocalResource<T> {
impl<T> ReadUntracked for ArcLocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
type Value = ReadGuard<Option<T>, AsyncPlain<Option<T>>>;
@@ -201,7 +201,7 @@ impl<T> LocalResource<T> {
pub fn new<Fut>(fetcher: impl Fn() -> Fut + 'static) -> Self
where
T: 'static,
Fut: Future<Output = T> + 'static,
Fut: Future<Output = T> + Send + 'static,
{
let fetcher = move || {
let fut = fetcher();
@@ -230,7 +230,7 @@ impl<T> LocalResource<T> {
let fetcher = SendWrapper::new(fetcher);
AsyncDerived::new(move || {
let fut = fetcher();
SendWrapper::new(async move { SendWrapper::new(fut.await) })
async move { SendWrapper::new(fut.await) }
})
},
#[cfg(debug_assertions)]
@@ -280,7 +280,7 @@ impl<T> DefinedAt for LocalResource<T> {
impl<T> ReadUntracked for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
type Value =
ReadGuard<Option<SendWrapper<T>>, AsyncPlain<Option<SendWrapper<T>>>>;
@@ -307,7 +307,7 @@ impl<T: 'static> IsDisposed for LocalResource<T> {
impl<T: 'static> ToAnySource for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn to_any_source(&self) -> AnySource {
self.data.to_any_source()
@@ -316,7 +316,7 @@ where
impl<T: 'static> ToAnySubscriber for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn to_any_subscriber(&self) -> AnySubscriber {
self.data.to_any_subscriber()
@@ -325,7 +325,7 @@ where
impl<T> Source for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn add_subscriber(&self, subscriber: AnySubscriber) {
self.data.add_subscriber(subscriber)
@@ -342,7 +342,7 @@ where
impl<T> ReactiveNode for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn mark_dirty(&self) {
self.data.mark_dirty();
@@ -363,7 +363,7 @@ where
impl<T> Subscriber for LocalResource<T>
where
T: 'static,
T: Send + Sync + 'static,
{
fn add_source(&self, source: AnySource) {
self.data.add_source(source);

View File

@@ -1,700 +0,0 @@
use crate::{
initial_value, FromEncodedStr, IntoEncodedString,
IS_SUPPRESSING_RESOURCE_LOAD,
};
#[cfg(feature = "rkyv")]
use codee::binary::RkyvCodec;
#[cfg(feature = "serde-wasm-bindgen")]
use codee::string::JsonSerdeWasmCodec;
#[cfg(feature = "miniserde")]
use codee::string::MiniserdeCodec;
#[cfg(feature = "serde-lite")]
use codee::SerdeLite;
use codee::{
string::{FromToStringCodec, JsonSerdeCodec},
Decoder, Encoder,
};
use core::{fmt::Debug, marker::PhantomData};
use futures::Future;
use or_poisoned::OrPoisoned;
use reactive_graph::{
computed::{
suspense::SuspenseContext, AsyncDerivedReadyFuture, ScopedFuture,
},
diagnostics::{SpecialNonReactiveFuture, SpecialNonReactiveZone},
graph::{AnySource, ToAnySource},
owner::{use_context, ArenaItem, Owner},
prelude::*,
signal::{
guards::{Plain, ReadGuard},
ArcTrigger,
},
unwrap_signal,
};
use std::{
future::IntoFuture,
mem,
panic::Location,
pin::Pin,
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
},
task::{Context, Poll, Waker},
};
#[derive(Debug)]
pub struct ArcOnceResource<T, Ser = JsonSerdeCodec> {
trigger: ArcTrigger,
value: Arc<RwLock<Option<T>>>,
wakers: Arc<RwLock<Vec<Waker>>>,
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
loading: Arc<AtomicBool>,
ser: PhantomData<fn() -> Ser>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<T, Ser> Clone for ArcOnceResource<T, Ser> {
fn clone(&self) -> Self {
Self {
trigger: self.trigger.clone(),
value: self.value.clone(),
wakers: self.wakers.clone(),
suspenses: self.suspenses.clone(),
loading: self.loading.clone(),
ser: self.ser,
#[cfg(debug_assertions)]
defined_at: self.defined_at,
}
}
}
impl<T, Ser> ArcOnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: Encoder<T> + Decoder<T>,
<Ser as Encoder<T>>::Error: Debug,
<Ser as Decoder<T>>::Error: Debug,
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_with_options(
fut: impl Future<Output = T> + Send + 'static,
#[allow(unused)] // this is used with `feature = "ssr"`
blocking: bool,
) -> Self {
let shared_context = Owner::current_shared_context();
let id = shared_context
.as_ref()
.map(|sc| sc.next_id())
.unwrap_or_default();
let initial = initial_value::<T, Ser>(&id, shared_context.as_ref());
let is_ready = initial.is_some();
let value = Arc::new(RwLock::new(initial));
let wakers = Arc::new(RwLock::new(Vec::<Waker>::new()));
let suspenses = Arc::new(RwLock::new(Vec::<SuspenseContext>::new()));
let loading = Arc::new(AtomicBool::new(!is_ready));
let trigger = ArcTrigger::new();
let fut = ScopedFuture::new(fut);
if !is_ready && !IS_SUPPRESSING_RESOURCE_LOAD.load(Ordering::Relaxed) {
let value = Arc::clone(&value);
let wakers = Arc::clone(&wakers);
let loading = Arc::clone(&loading);
let trigger = trigger.clone();
reactive_graph::spawn(async move {
let loaded = fut.await;
*value.write().or_poisoned() = Some(loaded);
loading.store(false, Ordering::Relaxed);
for waker in mem::take(&mut *wakers.write().or_poisoned()) {
waker.wake();
}
trigger.notify();
});
}
let data = Self {
trigger,
value: value.clone(),
loading,
wakers,
suspenses,
ser: PhantomData,
#[cfg(debug_assertions)]
defined_at: Location::caller(),
};
#[cfg(feature = "ssr")]
if let Some(shared_context) = shared_context {
let value = Arc::clone(&value);
let ready_fut = data.ready();
if blocking {
shared_context.defer_stream(Box::pin(data.ready()));
}
if shared_context.get_is_hydrating() {
shared_context.write_async(
id,
Box::pin(async move {
ready_fut.await;
let value = value.read().or_poisoned();
let value = value.as_ref().unwrap();
Ser::encode(value).unwrap().into_encoded_string()
}),
);
}
}
data
}
}
impl<T, Ser> ArcOnceResource<T, Ser> {
/// Returns a `Future` that is ready when this resource has next finished loading.
pub fn ready(&self) -> AsyncDerivedReadyFuture {
AsyncDerivedReadyFuture::new(
self.to_any_source(),
&self.loading,
&self.wakers,
)
}
}
impl<T, Ser> DefinedAt for ArcOnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(debug_assertions))]
{
None
}
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
}
}
impl<T, Ser> IsDisposed for ArcOnceResource<T, Ser> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T, Ser> ToAnySource for ArcOnceResource<T, Ser> {
fn to_any_source(&self) -> AnySource {
self.trigger.to_any_source()
}
}
impl<T, Ser> Track for ArcOnceResource<T, Ser> {
fn track(&self) {
self.trigger.track();
}
}
impl<T, Ser> ReadUntracked for ArcOnceResource<T, Ser>
where
T: 'static,
{
type Value = ReadGuard<Option<T>, Plain<Option<T>>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
if let Some(suspense_context) = use_context::<SuspenseContext>() {
if self.value.read().or_poisoned().is_none() {
let handle = suspense_context.task_id();
let ready = SpecialNonReactiveFuture::new(self.ready());
reactive_graph::spawn(async move {
ready.await;
drop(handle);
});
self.suspenses.write().or_poisoned().push(suspense_context);
}
}
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
}
}
impl<T, Ser> IntoFuture for ArcOnceResource<T, Ser>
where
T: Clone + 'static,
{
type Output = T;
type IntoFuture = OnceResourceFuture<T>;
fn into_future(self) -> Self::IntoFuture {
OnceResourceFuture {
source: self.to_any_source(),
value: Arc::clone(&self.value),
loading: Arc::clone(&self.loading),
wakers: Arc::clone(&self.wakers),
suspenses: Arc::clone(&self.suspenses),
}
}
}
/// A [`Future`] that is ready when an [`ArcAsyncDerived`] is finished loading or reloading,
/// and contains its value. `.await`ing this clones the value `T`.
pub struct OnceResourceFuture<T> {
source: AnySource,
value: Arc<RwLock<Option<T>>>,
loading: Arc<AtomicBool>,
wakers: Arc<RwLock<Vec<Waker>>>,
suspenses: Arc<RwLock<Vec<SuspenseContext>>>,
}
impl<T> Future for OnceResourceFuture<T>
where
T: Clone + 'static,
{
type Output = T;
#[track_caller]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
#[cfg(debug_assertions)]
let _guard = SpecialNonReactiveZone::enter();
let waker = cx.waker();
self.source.track();
if let Some(suspense_context) = use_context::<SuspenseContext>() {
self.suspenses.write().or_poisoned().push(suspense_context);
}
if self.loading.load(Ordering::Relaxed) {
self.wakers.write().or_poisoned().push(waker.clone());
Poll::Pending
} else {
Poll::Ready(
self.value.read().or_poisoned().as_ref().unwrap().clone(),
)
}
}
}
impl<T> ArcOnceResource<T, JsonSerdeCodec>
where
T: Send + Sync + 'static,
JsonSerdeCodec: Encoder<T> + Decoder<T>,
<JsonSerdeCodec as Encoder<T>>::Error: Debug,
<JsonSerdeCodec as Decoder<T>>::Error: Debug,
<<JsonSerdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_blocking(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, true)
}
}
impl<T> ArcOnceResource<T, FromToStringCodec>
where
T: Send + Sync + 'static,
FromToStringCodec: Encoder<T> + Decoder<T>,
<FromToStringCodec as Encoder<T>>::Error: Debug, <FromToStringCodec as Decoder<T>>::Error: Debug,
<<FromToStringCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
pub fn new_str(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, false)
}
pub fn new_str_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "serde-wasm-bindgen")]
impl<T> ArcOnceResource<T, JsonSerdeWasmCodec>
where
T: Send + Sync + 'static,
JsonSerdeWasmCodec: Encoder<T> + Decoder<T>,
<JsonSerdeWasmCodec as Encoder<T>>::Error: Debug, <JsonSerdeWasmCodec as Decoder<T>>::Error: Debug,
<<JsonSerdeWasmCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_wb(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_serde_wb_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "miniserde")]
impl<T> ArcOnceResource<T, MiniserdeCodec>
where
T: Send + Sync + 'static,
MiniserdeCodec: Encoder<T> + Decoder<T>,
<MiniserdeCodec as Encoder<T>>::Error: Debug,
<MiniserdeCodec as Decoder<T>>::Error: Debug,
<<MiniserdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_miniserde(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_miniserde_blocking(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
ArcOnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "serde-lite")]
impl<T> ArcOnceResource<T, SerdeLite<JsonSerdeCodec>>
where
T: Send + Sync + 'static,
SerdeLite<JsonSerdeCodec>: Encoder<T> + Decoder<T>,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Error: Debug, <SerdeLite<JsonSerdeCodec> as Decoder<T>>::Error: Debug,
<<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_lite(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_serde_lite_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
ArcOnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "rkyv")]
impl<T> ArcOnceResource<T, RkyvCodec>
where
T: Send + Sync + 'static,
RkyvCodec: Encoder<T> + Decoder<T>,
<RkyvCodec as Encoder<T>>::Error: Debug,
<RkyvCodec as Decoder<T>>::Error: Debug,
<<RkyvCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_rkyv(fut: impl Future<Output = T> + Send + 'static) -> Self {
ArcOnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_rkyv_blocking(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
ArcOnceResource::new_with_options(fut, true)
}
}
#[derive(Debug)]
pub struct OnceResource<T, Ser = JsonSerdeCodec> {
inner: ArenaItem<ArcOnceResource<T, Ser>>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
impl<T, Ser> OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: Encoder<T> + Decoder<T>,
<Ser as Encoder<T>>::Error: Debug,
<Ser as Decoder<T>>::Error: Debug,
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_with_options(
fut: impl Future<Output = T> + Send + 'static,
blocking: bool,
) -> Self {
#[cfg(debug_assertions)]
let defined_at = Location::caller();
Self {
inner: ArenaItem::new(ArcOnceResource::new_with_options(
fut, blocking,
)),
#[cfg(debug_assertions)]
defined_at,
}
}
}
impl<T, Ser> OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: 'static,
{
/// Returns a `Future` that is ready when this resource has next finished loading.
pub fn ready(&self) -> AsyncDerivedReadyFuture {
self.inner
.try_with_value(|inner| inner.ready())
.unwrap_or_else(unwrap_signal!(self))
}
}
impl<T, Ser> DefinedAt for OnceResource<T, Ser> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(not(debug_assertions))]
{
None
}
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
}
}
impl<T, Ser> IsDisposed for OnceResource<T, Ser> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T, Ser> ToAnySource for OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: 'static,
{
fn to_any_source(&self) -> AnySource {
self.inner
.try_with_value(|inner| inner.to_any_source())
.unwrap_or_else(unwrap_signal!(self))
}
}
impl<T, Ser> Track for OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: 'static,
{
fn track(&self) {
if let Some(inner) = self.inner.try_get_value() {
inner.track();
}
}
}
impl<T, Ser> ReadUntracked for OnceResource<T, Ser>
where
T: Send + Sync + 'static,
Ser: 'static,
{
type Value = ReadGuard<Option<T>, Plain<Option<T>>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.inner
.try_with_value(|inner| inner.try_read_untracked())
.flatten()
}
}
impl<T, Ser> IntoFuture for OnceResource<T, Ser>
where
T: Clone + Send + Sync + 'static,
Ser: 'static,
{
type Output = T;
type IntoFuture = OnceResourceFuture<T>;
fn into_future(self) -> Self::IntoFuture {
self.inner
.try_get_value()
.unwrap_or_else(unwrap_signal!(self))
.into_future()
}
}
impl<T> OnceResource<T, JsonSerdeCodec>
where
T: Send + Sync + 'static,
JsonSerdeCodec: Encoder<T> + Decoder<T>,
<JsonSerdeCodec as Encoder<T>>::Error: Debug,
<JsonSerdeCodec as Decoder<T>>::Error: Debug,
<<JsonSerdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<JsonSerdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_blocking(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, true)
}
}
impl<T> OnceResource<T, FromToStringCodec>
where
T: Send + Sync + 'static,
FromToStringCodec: Encoder<T> + Decoder<T>,
<FromToStringCodec as Encoder<T>>::Error: Debug, <FromToStringCodec as Decoder<T>>::Error: Debug,
<<FromToStringCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<FromToStringCodec as Encoder<T>>::Encoded: IntoEncodedString,
<FromToStringCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
pub fn new_str(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, false)
}
pub fn new_str_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "serde-wasm-bindgen")]
impl<T> OnceResource<T, JsonSerdeWasmCodec>
where
T: Send + Sync + 'static,
JsonSerdeWasmCodec: Encoder<T> + Decoder<T>,
<JsonSerdeWasmCodec as Encoder<T>>::Error: Debug, <JsonSerdeWasmCodec as Decoder<T>>::Error: Debug,
<<JsonSerdeWasmCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<JsonSerdeWasmCodec as Encoder<T>>::Encoded: IntoEncodedString,
<JsonSerdeWasmCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_wb(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_serde_wb_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "miniserde")]
impl<T> OnceResource<T, MiniserdeCodec>
where
T: Send + Sync + 'static,
MiniserdeCodec: Encoder<T> + Decoder<T>,
<MiniserdeCodec as Encoder<T>>::Error: Debug,
<MiniserdeCodec as Decoder<T>>::Error: Debug,
<<MiniserdeCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<MiniserdeCodec as Encoder<T>>::Encoded: IntoEncodedString,
<MiniserdeCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_miniserde(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_miniserde_blocking(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
OnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "serde-lite")]
impl<T> OnceResource<T, SerdeLite<JsonSerdeCodec>>
where
T: Send + Sync + 'static,
SerdeLite<JsonSerdeCodec>: Encoder<T> + Decoder<T>,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Error: Debug, <SerdeLite<JsonSerdeCodec> as Decoder<T>>::Error: Debug,
<<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<SerdeLite<JsonSerdeCodec> as Encoder<T>>::Encoded: IntoEncodedString,
<SerdeLite<JsonSerdeCodec> as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_serde_lite(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_serde_lite_blocking(
fut: impl Future<Output = T> + Send + 'static
) -> Self
{
OnceResource::new_with_options(fut, true)
}
}
#[cfg(feature = "rkyv")]
impl<T> OnceResource<T, RkyvCodec>
where
T: Send + Sync + 'static,
RkyvCodec: Encoder<T> + Decoder<T>,
<RkyvCodec as Encoder<T>>::Error: Debug,
<RkyvCodec as Decoder<T>>::Error: Debug,
<<RkyvCodec as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError:
Debug,
<RkyvCodec as Encoder<T>>::Encoded: IntoEncodedString,
<RkyvCodec as Decoder<T>>::Encoded: FromEncodedStr,
{
#[track_caller]
pub fn new_rkyv(fut: impl Future<Output = T> + Send + 'static) -> Self {
OnceResource::new_with_options(fut, false)
}
#[track_caller]
pub fn new_rkyv_blocking(
fut: impl Future<Output = T> + Send + 'static,
) -> Self {
OnceResource::new_with_options(fut, true)
}
}

View File

@@ -13,7 +13,7 @@ use codee::{
};
use core::{fmt::Debug, marker::PhantomData};
use futures::Future;
use hydration_context::{SerializedDataId, SharedContext};
use hydration_context::SerializedDataId;
use reactive_graph::{
computed::{
ArcAsyncDerived, ArcMemo, AsyncDerived, AsyncDerivedFuture,
@@ -28,14 +28,10 @@ use std::{
future::{pending, IntoFuture},
ops::Deref,
panic::Location,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
sync::atomic::{AtomicBool, Ordering},
};
pub(crate) static IS_SUPPRESSING_RESOURCE_LOAD: AtomicBool =
AtomicBool::new(false);
static IS_SUPPRESSING_RESOURCE_LOAD: AtomicBool = AtomicBool::new(false);
pub struct SuppressResourceLoad;
@@ -179,7 +175,7 @@ where
.map(|sc| sc.next_id())
.unwrap_or_default();
let initial = initial_value::<T, Ser>(&id, shared_context.as_ref());
let initial = Self::initial_value(&id);
let is_ready = initial.is_some();
let refetch = ArcRwSignal::new(0);
@@ -257,53 +253,43 @@ where
pub fn refetch(&self) {
*self.refetch.write() += 1;
}
}
#[inline(always)]
#[allow(unused)]
pub(crate) fn initial_value<T, Ser>(
id: &SerializedDataId,
shared_context: Option<&Arc<dyn SharedContext + Send + Sync>>,
) -> Option<T>
where
Ser: Encoder<T> + Decoder<T>,
<Ser as Encoder<T>>::Error: Debug,
<Ser as Decoder<T>>::Error: Debug,
<<Ser as Decoder<T>>::Encoded as FromEncodedStr>::DecodingError: Debug,
<Ser as Encoder<T>>::Encoded: IntoEncodedString,
<Ser as Decoder<T>>::Encoded: FromEncodedStr,
{
#[cfg(feature = "hydration")]
{
use std::borrow::Borrow;
#[inline(always)]
#[allow(unused)]
fn initial_value(id: &SerializedDataId) -> Option<T> {
#[cfg(feature = "hydration")]
{
use std::borrow::Borrow;
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 {
let encoded =
match <Ser as Decoder<T>>::Encoded::from_encoded_str(&value)
{
Ok(value) => value,
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 {
let encoded =
match <Ser as Decoder<T>>::Encoded::from_encoded_str(
&value,
) {
Ok(value) => value,
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("couldn't deserialize: {e:?}");
return None;
}
};
let encoded = encoded.borrow();
match Ser::decode(encoded) {
Ok(value) => return Some(value),
#[allow(unused)]
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("couldn't deserialize: {e:?}");
return None;
}
};
let encoded = encoded.borrow();
match Ser::decode(encoded) {
Ok(value) => return Some(value),
#[allow(unused)]
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!("couldn't deserialize: {e:?}");
}
}
}
}
None
}
None
}
impl<T, E, Ser> ArcResource<Result<T, E>, Ser>

View File

@@ -2,7 +2,7 @@ use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
component, html,
reactive::owner::use_context,
reactive_graph::owner::use_context,
tachys::{
dom::document,
html::attribute::Attribute,

View File

@@ -2,7 +2,7 @@ use crate::ServerMetaContext;
use leptos::{
attr::NextAttribute,
component, html,
reactive::owner::use_context,
reactive_graph::owner::use_context,
tachys::{
dom::document,
html::attribute::Attribute,

View File

@@ -52,7 +52,7 @@ use leptos::{
attr::NextAttribute,
component,
logging::debug_warn,
reactive::owner::{provide_context, use_context},
reactive_graph::owner::{provide_context, use_context},
tachys::{
dom::document,
html::{

View File

@@ -3,7 +3,7 @@ use leptos::{
attr::Attribute,
component,
oco::Oco,
reactive::{
reactive_graph::{
effect::RenderEffect,
owner::{use_context, Owner},
},

View File

@@ -30,21 +30,23 @@ sqlx = { version = "0.8.0", features = [
], optional = true }
thiserror = "1.0"
wasm-bindgen = "0.2.0"
axum_session_auth = { version = "0.14.0", features = [], optional = true }
axum_session = { version = "0.14.0", features = [], optional = true }
axum_session_sqlx = { version = "0.3.0", features = [ "sqlite", "tls-rustls"], optional = true }
axum_session_auth = { version = "0.14.0", features = [
"sqlite-rustls",
], optional = true }
axum_session = { version = "0.14.0", features = [
"sqlite-rustls",
], optional = true }
bcrypt = { version = "0.15.0", optional = true }
async-trait = { version = "0.1.0", optional = true }
[features]
default = ["ssr"]
hydrate = ["leptos/hydrate"]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [
"dep:axum",
"dep:tower",
"dep:tower-http",
"dep:tokio",
"dep:axum_session_sqlx",
"dep:axum_session_auth",
"dep:axum_session",
"dep:async-trait",

View File

@@ -28,8 +28,9 @@ impl Default for User {
#[cfg(feature = "ssr")]
pub mod ssr {
pub use super::{User, UserPasshash};
pub use axum_session_auth::{Authentication, HasPermission};
use axum_session_sqlx::SessionSqlitePool;
pub use axum_session_auth::{
Authentication, HasPermission, SessionSqlitePool,
};
pub use sqlx::SqlitePool;
pub use std::collections::HashSet;
pub type AuthSession = axum_session_auth::AuthSession<

View File

@@ -8,10 +8,10 @@ use leptos_axum::ResponseOptions;
#[component]
pub fn ErrorTemplate(
#[prop(optional)] outside_errors: Option<Errors>,
#[prop(optional)] errors: Option<ArcRwSignal<Errors>>,
#[prop(optional)] errors: Option<RwSignal<Errors>>,
) -> impl IntoView {
let errors = match outside_errors {
Some(e) => ArcRwSignal::new(e),
Some(e) => RwSignal::new(e),
None => match errors {
Some(e) => e,
None => panic!("No Errors found and we expected errors!"),

View File

@@ -0,0 +1,50 @@
use crate::{error_template::ErrorTemplate, errors::TodoAppError};
use axum::{
body::Body,
extract::State,
http::{Request, Response, StatusCode, Uri},
response::{IntoResponse, Response as AxumResponse},
};
use leptos::{view, Errors, LeptosOptions};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub async fn file_and_error_handler(
uri: Uri,
State(options): State<LeptosOptions>,
req: Request<Body>,
) -> AxumResponse {
let root = options.site_root.clone();
let res = get_static_file(uri.clone(), &root).await.unwrap();
if res.status() == StatusCode::OK {
res.into_response()
} else {
let mut errors = Errors::default();
errors.insert_with_default_key(TodoAppError::NotFound);
let handler = leptos_axum::render_app_to_stream(
options.to_owned(),
move || view! {<ErrorTemplate outside_errors=errors.clone()/>},
);
handler(req).await.into_response()
}
}
async fn get_static_file(
uri: Uri,
root: &str,
) -> Result<Response<Body>, (StatusCode, String)> {
let req = Request::builder()
.uri(uri.clone())
.body(Body::empty())
.unwrap();
// `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot`
// This path is relative to the cargo root
match ServeDir::new(root).oneshot(req).await {
Ok(res) => Ok(res.into_response()),
Err(err) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)),
}
}

View File

@@ -2,6 +2,8 @@ pub mod auth;
pub mod error_template;
pub mod errors;
#[cfg(feature = "ssr")]
pub mod fallback;
#[cfg(feature = "ssr")]
pub mod state;
pub mod todo;
@@ -12,5 +14,5 @@ pub fn hydrate() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(TodoApp);
leptos::mount_to_body(TodoApp);
}

View File

@@ -7,16 +7,14 @@ use axum::{
Router,
};
use axum_session::{SessionConfig, SessionLayer, SessionStore};
use axum_session_auth::{AuthConfig, AuthSessionLayer};
use axum_session_sqlx::SessionSqlitePool;
use leptos::{
config::get_configuration, logging::log, prelude::provide_context,
};
use axum_session_auth::{AuthConfig, AuthSessionLayer, SessionSqlitePool};
use leptos::{get_configuration, logging::log, provide_context};
use leptos_axum::{
generate_route_list, handle_server_fns_with_context, LeptosRoutes,
};
use session_auth_axum::{
auth::{ssr::AuthSession, User},
fallback::file_and_error_handler,
state::AppState,
todo::*,
};
@@ -42,19 +40,19 @@ async fn server_fn_handler(
async fn leptos_routes_handler(
auth_session: AuthSession,
state: State<AppState>,
State(app_state): State<AppState>,
req: Request<AxumBody>,
) -> Response {
let State(app_state) = state.clone();
let handler = leptos_axum::render_route_with_context(
app_state.leptos_options.clone(),
app_state.routes.clone(),
move || {
provide_context(auth_session.clone());
provide_context(app_state.pool.clone());
},
move || shell(app_state.leptos_options.clone()),
TodoApp,
);
handler(state, req).await.into_response()
handler(req).await.into_response()
}
#[tokio::main]
@@ -113,7 +111,7 @@ async fn main() {
get(server_fn_handler).post(server_fn_handler),
)
.leptos_routes_with_handler(routes, get(leptos_routes_handler))
.fallback(leptos_axum::file_and_error_handler::<AppState, _>(shell))
.fallback(file_and_error_handler)
.layer(
AuthSessionLayer::<User, i64, SessionSqlitePool, SqlitePool>::new(
Some(pool.clone()),

View File

@@ -1,6 +1,6 @@
use axum::extract::FromRef;
use leptos::prelude::LeptosOptions;
use leptos_axum::AxumRouteListing;
use leptos::LeptosOptions;
use leptos_router::RouteListing;
use sqlx::SqlitePool;
/// This takes advantage of Axum's SubStates feature by deriving FromRef. This is the only way to have more than one
@@ -9,5 +9,5 @@ use sqlx::SqlitePool;
pub struct AppState {
pub leptos_options: LeptosOptions,
pub pool: SqlitePool,
pub routes: Vec<AxumRouteListing>,
pub routes: Vec<RouteListing>,
}

View File

@@ -1,7 +1,7 @@
use crate::{auth::*, error_template::ErrorTemplate};
use leptos::prelude::*;
use leptos_meta::*;
use leptos_router::{components::*, *};
use leptos_router::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -109,33 +109,13 @@ pub async fn delete_todo(id: u16) -> Result<(), ServerFnError> {
.map(|_| ())?)
}
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<link rel="stylesheet" id="leptos" href="/pkg/session_auth_axum.css"/>
<link rel="shortcut icon" type="image/ico" href="/favicon.ico"/>
<MetaTags/>
</head>
<body>
<TodoApp/>
</body>
</html>
}
}
#[component]
pub fn TodoApp() -> impl IntoView {
let login = ServerAction::<Login>::new();
let logout = ServerAction::<Logout>::new();
let signup = ServerAction::<Signup>::new();
let login = create_server_action::<Login>();
let logout = create_server_action::<Logout>();
let signup = create_server_action::<Signup>();
let user = Resource::new(
let user = create_resource(
move || {
(
login.version().get(),
@@ -148,6 +128,8 @@ pub fn TodoApp() -> impl IntoView {
provide_meta_context();
view! {
<Link rel="shortcut icon" type_="image/ico" href="/favicon.ico"/>
<Stylesheet id="leptos" href="/pkg/session_auth_axum.css"/>
<Router>
<header>
<A href="/">
@@ -167,7 +149,7 @@ pub fn TodoApp() -> impl IntoView {
", "
<span>{format!("Login error: {}", e)}</span>
}
.into_any()
.into_view()
}
Ok(None) => {
view! {
@@ -177,7 +159,7 @@ pub fn TodoApp() -> impl IntoView {
", "
<span>"Logged out."</span>
}
.into_any()
.into_view()
}
Ok(Some(user)) => {
view! {
@@ -187,7 +169,7 @@ pub fn TodoApp() -> impl IntoView {
{format!("Logged in as: {} ({})", user.username, user.id)}
</span>
}
.into_any()
.into_view()
}
})
}}
@@ -196,15 +178,13 @@ pub fn TodoApp() -> impl IntoView {
</header>
<hr/>
<main>
<FlatRoutes fallback=|| "Not found.">
<Routes>
// Route
<Route path=path!("") view=Todos/>
<Route path=path!("signup") view=move || view! { <Signup action=signup/> }/>
<Route path=path!("login") view=move || view! { <Login action=login/> }/>
<ProtectedRoute
path=path!("settings")
condition=move || user.get().map(|r| r.ok().flatten().is_some())
redirect_path=|| "/"
<Route path="" view=Todos/>
<Route path="signup" view=move || view! { <Signup action=signup/> }/>
<Route path="login" view=move || view! { <Login action=login/> }/>
<Route
path="settings"
view=move || {
view! {
<h1>"Settings"</h1>
@@ -213,7 +193,7 @@ pub fn TodoApp() -> impl IntoView {
}
/>
</FlatRoutes>
</Routes>
</main>
</Router>
}
@@ -221,12 +201,12 @@ pub fn TodoApp() -> impl IntoView {
#[component]
pub fn Todos() -> impl IntoView {
let add_todo = ServerMultiAction::<AddTodo>::new();
let delete_todo = ServerAction::<DeleteTodo>::new();
let add_todo = create_server_multi_action::<AddTodo>();
let delete_todo = create_server_action::<DeleteTodo>();
let submissions = add_todo.submissions();
// list of todos is loaded from the server in reaction to changes
let todos = Resource::new(
let todos = create_resource(
move || (add_todo.version().get(), delete_todo.version().get()),
move |_| get_todos(),
);
@@ -251,11 +231,11 @@ pub fn Todos() -> impl IntoView {
view! {
<pre class="error">"Server Error: " {e.to_string()}</pre>
}
.into_any()
.into_view()
}
Ok(todos) => {
if todos.is_empty() {
view! { <p>"No tasks were found."</p> }.into_any()
view! { <p>"No tasks were found."</p> }.into_view()
} else {
todos
.into_iter()
@@ -272,11 +252,10 @@ pub fn Todos() -> impl IntoView {
}
})
.collect_view()
.into_any()
}
}
})
.unwrap_or(().into_any())
.unwrap_or_default()
}
};
let pending_todos = move || {
@@ -287,7 +266,7 @@ pub fn Todos() -> impl IntoView {
.map(|submission| {
view! {
<li class="pending">
{move || submission.input().get().map(|data| data.title)}
{move || submission.input.get().map(|data| data.title)}
</li>
}
})
@@ -303,7 +282,9 @@ pub fn Todos() -> impl IntoView {
}
#[component]
pub fn Login(action: ServerAction<Login>) -> impl IntoView {
pub fn Login(
action: Action<Login, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<ActionForm action=action>
<h1>"Log In"</h1>
@@ -336,7 +317,9 @@ pub fn Login(action: ServerAction<Login>) -> impl IntoView {
}
#[component]
pub fn Signup(action: ServerAction<Signup>) -> impl IntoView {
pub fn Signup(
action: Action<Signup, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<ActionForm action=action>
<h1>"Sign Up"</h1>
@@ -379,7 +362,9 @@ pub fn Signup(action: ServerAction<Signup>) -> impl IntoView {
}
#[component]
pub fn Logout(action: ServerAction<Logout>) -> impl IntoView {
pub fn Logout(
action: Action<Logout, Result<(), ServerFnError>>,
) -> impl IntoView {
view! {
<div id="loginbox">
<ActionForm action=action>

View File

@@ -15,7 +15,6 @@ use crate::{
};
pub use arc_memo::*;
pub use async_derived::*;
pub(crate) use inner::MemoInner;
pub use memo::*;
pub use selector::*;

View File

@@ -19,7 +19,7 @@ use crate::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
Writeable,
},
transition::AsyncTransition,
};
@@ -534,11 +534,11 @@ impl<T: 'static> ArcAsyncDerived<T> {
/// Returns a `Future` that is ready when this resource has next finished loading.
pub fn ready(&self) -> AsyncDerivedReadyFuture {
AsyncDerivedReadyFuture::new(
self.to_any_source(),
&self.loading,
&self.wakers,
)
AsyncDerivedReadyFuture {
source: self.to_any_source(),
loading: Arc::clone(&self.loading),
wakers: Arc::clone(&self.wakers),
}
}
}
@@ -600,7 +600,7 @@ impl<T: 'static> Notify for ArcAsyncDerived<T> {
}
}
impl<T: 'static> Write for ArcAsyncDerived<T> {
impl<T: 'static> Writeable for ArcAsyncDerived<T> {
type Value = Option<T>;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {

View File

@@ -8,7 +8,7 @@ use crate::{
signal::guards::{AsyncPlain, ReadGuard, WriteGuard},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
UntrackableGuard, Write,
UntrackableGuard, Writeable,
},
unwrap_signal,
};
@@ -300,7 +300,7 @@ where
}
}
impl<T, S> Write for AsyncDerived<T, S>
impl<T, S> Writeable for AsyncDerived<T, S>
where
T: 'static,
S: Storage<ArcAsyncDerived<T>>,

View File

@@ -34,21 +34,6 @@ pub struct AsyncDerivedReadyFuture {
pub(crate) wakers: Arc<RwLock<Vec<Waker>>>,
}
impl AsyncDerivedReadyFuture {
/// Creates a new [`Future`] that will be ready when the given resource is ready.
pub fn new(
source: AnySource,
loading: &Arc<AtomicBool>,
wakers: &Arc<RwLock<Vec<Waker>>>,
) -> Self {
AsyncDerivedReadyFuture {
source,
loading: Arc::clone(loading),
wakers: Arc::clone(wakers),
}
}
}
impl Future for AsyncDerivedReadyFuture {
type Output = ();

View File

@@ -58,7 +58,7 @@ impl<Fut: Future> Future for ScopedFuture<Fut> {
pub mod suspense {
use crate::{
signal::ArcRwSignal,
traits::{Update, Write},
traits::{Update, Writeable},
};
use futures::channel::oneshot::Sender;
use or_poisoned::OrPoisoned;

View File

@@ -53,8 +53,7 @@ impl Drop for SpecialNonReactiveZoneGuard {
}
pin_project! {
#[doc(hidden)]
pub struct SpecialNonReactiveFuture<Fut> {
pub(crate) struct SpecialNonReactiveFuture<Fut> {
#[pin]
inner: Fut
}

View File

@@ -106,7 +106,7 @@ impl Observer {
/// # use reactive_graph::computed::*;
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::prelude::*;
/// # use reactive_graph::graph::untrack;
/// # use reactive_graph::untrack;
/// # tokio_test::block_on(async move {
/// # any_spawner::Executor::init_tokio(); let owner = reactive_graph::owner::Owner::new(); owner.set();
/// let (a, set_a) = signal(0);

View File

@@ -89,6 +89,7 @@ pub mod transition;
pub mod wrappers;
use computed::ScopedFuture;
pub use graph::untrack;
#[cfg(feature = "nightly")]
mod nightly;
@@ -125,7 +126,7 @@ pub fn log_warning(text: Arguments) {
/// Calls [`Executor::spawn`], but ensures that the task also runs in the current arena, if
/// multithreaded arena sandboxing is enabled.
pub fn spawn(task: impl Future<Output = ()> + Send + 'static) {
pub(crate) fn spawn(task: impl Future<Output = ()> + Send + 'static) {
#[cfg(feature = "sandboxed-arenas")]
let task = owner::Sandboxed::new(task);

View File

@@ -12,14 +12,12 @@ use std::{
sync::{Arc, RwLock, Weak},
};
mod arc_stored_value;
mod arena;
mod arena_item;
mod context;
mod storage;
mod stored_value;
use self::arena::Arena;
pub use arc_stored_value::ArcStoredValue;
#[cfg(feature = "sandboxed-arenas")]
pub use arena::sandboxed::Sandboxed;
#[cfg(feature = "sandboxed-arenas")]

View File

@@ -1,126 +0,0 @@
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, IsDisposed, ReadValue, WriteValue},
};
use std::{
fmt::{Debug, Formatter},
hash::Hash,
panic::Location,
sync::{Arc, RwLock},
};
/// A reference-counted getter for any value non-reactively.
///
/// This is a reference-counted value, which is `Clone` but not `Copy`.
/// For arena-allocated `Copy` values, use [`StoredValue`](super::StoredValue).
///
/// This allows you to create a stable reference for any value by storing it within
/// the reactive system. Unlike e.g. [`ArcRwSignal`](crate::signal::ArcRwSignal), it is not reactive;
/// accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
pub struct ArcStoredValue<T> {
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
value: Arc<RwLock<T>>,
}
impl<T> Clone for ArcStoredValue<T> {
fn clone(&self) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
}
}
}
impl<T> Debug for ArcStoredValue<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ArcStoredValue")
.field("type", &std::any::type_name::<T>())
.field("value", &Arc::as_ptr(&self.value))
.finish()
}
}
impl<T: Default> Default for ArcStoredValue<T> {
#[track_caller]
fn default() -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(T::default())),
}
}
}
impl<T> PartialEq for ArcStoredValue<T> {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.value, &other.value)
}
}
impl<T> Eq for ArcStoredValue<T> {}
impl<T> Hash for ArcStoredValue<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::ptr::hash(&Arc::as_ptr(&self.value), state);
}
}
impl<T> DefinedAt for ArcStoredValue<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
{
Some(self.defined_at)
}
#[cfg(not(debug_assertions))]
{
None
}
}
}
impl<T> ArcStoredValue<T> {
/// Creates a new stored value, taking the initial value as its argument.
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "trace", skip_all)
)]
#[track_caller]
pub fn new(value: T) -> Self {
Self {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(value)),
}
}
}
impl<T> ReadValue for ArcStoredValue<T>
where
T: 'static,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
}
}
impl<T> WriteValue for ArcStoredValue<T>
where
T: 'static,
{
type Value = T;
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
UntrackedWriteGuard::try_new(self.value.clone())
}
}
impl<T> IsDisposed for ArcStoredValue<T> {
fn is_disposed(&self) -> bool {
false
}
}

View File

@@ -1,16 +1,14 @@
use super::{
arc_stored_value::ArcStoredValue, ArenaItem, LocalStorage, Storage,
SyncStorage,
};
use super::{ArenaItem, LocalStorage, Storage, SyncStorage};
use crate::{
signal::guards::{Plain, ReadGuard, UntrackedWriteGuard},
traits::{DefinedAt, Dispose, IsDisposed, ReadValue, WriteValue},
traits::{DefinedAt, Dispose, IsDisposed},
unwrap_signal,
};
use or_poisoned::OrPoisoned;
use std::{
fmt::{Debug, Formatter},
hash::Hash,
panic::Location,
sync::{Arc, RwLock},
};
/// A **non-reactive**, `Copy` handle for any value.
@@ -20,8 +18,9 @@ use std::{
/// and [`RwSignal`](crate::signal::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
#[derive(Debug)]
pub struct StoredValue<T, S = SyncStorage> {
value: ArenaItem<ArcStoredValue<T>, S>,
value: ArenaItem<Arc<RwLock<T>>, S>,
#[cfg(debug_assertions)]
defined_at: &'static Location<'static>,
}
@@ -34,18 +33,6 @@ impl<T, S> Clone for StoredValue<T, S> {
}
}
impl<T, S> Debug for StoredValue<T, S>
where
S: Debug,
{
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StoredValue")
.field("type", &std::any::type_name::<T>())
.field("value", &self.value)
.finish()
}
}
impl<T, S> PartialEq for StoredValue<T, S> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
@@ -76,13 +63,13 @@ impl<T, S> DefinedAt for StoredValue<T, S> {
impl<T, S> StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
S: Storage<Arc<RwLock<T>>>,
{
/// Stores the given value in the arena allocator.
#[track_caller]
pub fn new_with_storage(value: T) -> Self {
Self {
value: ArenaItem::new_with_storage(ArcStoredValue::new(value)),
value: ArenaItem::new_with_storage(Arc::new(RwLock::new(value))),
#[cfg(debug_assertions)]
defined_at: Location::caller(),
}
@@ -92,7 +79,7 @@ where
impl<T, S> Default for StoredValue<T, S>
where
T: Default + 'static,
S: Storage<ArcStoredValue<T>>,
S: Storage<Arc<RwLock<T>>>,
{
#[track_caller] // Default trait is not annotated with #[track_caller]
fn default() -> Self {
@@ -122,31 +109,268 @@ where
}
}
impl<T, S> ReadValue for StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
impl<T, S: Storage<Arc<RwLock<T>>>> StoredValue<T, S> {
/// Returns an [`Option`] of applying a function to the value within the [`StoredValue`].
///
/// If the owner of the reactive node has not been disposed [`Some`] is returned. Calling this
/// function after the owner has been disposed will always return [`None`].
///
/// See [`StoredValue::with_value`] for a version that panics in the case of the owner being
/// disposed.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data {
/// rows: vec![0, 1, 2, 3, 4],
/// });
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let length_fn = move || data.try_with_value(|inner| inner.rows.len());
///
/// let sum = data.try_with_value(|inner| inner.rows.iter().sum::<u8>());
///
/// assert_eq!(sum, Some(10));
/// assert_eq!(length_fn(), Some(5));
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// let last = data.try_with_value(|inner| inner.rows.last().cloned());
///
/// assert_eq!(last, None);
/// assert_eq!(length_fn(), None);
/// ```
#[track_caller]
pub fn try_with_value<U>(&self, fun: impl FnOnce(&T) -> U) -> Option<U> {
self.value
.try_get_value()
.and_then(|inner| inner.try_read_value())
.map(|inner| fun(&*inner.read().or_poisoned()))
}
}
impl<T, S> WriteValue for StoredValue<T, S>
where
T: 'static,
S: Storage<ArcStoredValue<T>>,
{
type Value = T;
/// Returns the output of applying a function to the value within the [`StoredValue`].
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_with_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data {
/// rows: vec![1, 2, 3],
/// });
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let length_fn = move || data.with_value(|inner| inner.rows.len());
///
/// let sum = data.with_value(|inner| inner.rows.iter().sum::<u8>());
///
/// assert_eq!(sum, 6);
/// assert_eq!(length_fn(), 3);
/// ```
#[track_caller]
pub fn with_value<U>(&self, fun: impl FnOnce(&T) -> U) -> U {
self.try_with_value(fun)
.unwrap_or_else(unwrap_signal!(self))
}
fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
/// Returns a read guard to the stored data, or `None` if the owner of the reactive node has been disposed.
#[track_caller]
pub fn try_read_value(&self) -> Option<ReadGuard<T, Plain<T>>> {
self.value
.try_get_value()
.and_then(|inner| inner.try_write_value())
.and_then(|inner| Plain::try_new(inner).map(ReadGuard::new))
}
/// Returns a read guard to the stored data.
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_read_value`] for a version without panic.
#[track_caller]
pub fn read_value(&self) -> ReadGuard<T, Plain<T>> {
self.try_read_value().unwrap_or_else(unwrap_signal!(self))
}
/// Returns a write guard to the stored data, or `None` if the owner of the reactive node has been disposed.
#[track_caller]
pub fn try_write_value(&self) -> Option<UntrackedWriteGuard<T>> {
self.value
.try_get_value()
.and_then(|inner| UntrackedWriteGuard::try_new(inner))
}
/// Returns a write guard to the stored data.
///
/// # Panics
///
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_write_value`] for a version without panic.
#[track_caller]
pub fn write_value(&self) -> UntrackedWriteGuard<T> {
self.try_write_value().unwrap_or_else(unwrap_signal!(self))
}
/// Updates the current value by applying the given closure, returning the return value of the
/// closure, or `None` if the value has already been disposed.
pub fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut T) -> U,
) -> Option<U> {
self.value
.try_get_value()
.map(|inner| fun(&mut *inner.write().or_poisoned()))
}
/// Updates the value within [`StoredValue`] by applying a function to it.
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_update_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// #[derive(Default)] // Does not implement Clone
/// struct Data {
/// rows: Vec<u8>,
/// }
///
/// let data = StoredValue::new(Data::default());
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let push_next = move || {
/// data.update_value(|inner| match inner.rows.last().as_deref() {
/// Some(n) => inner.rows.push(n + 1),
/// None => inner.rows.push(0),
/// })
/// };
///
/// data.update_value(|inner| inner.rows = vec![5, 6, 7]);
/// data.with_value(|inner| assert_eq!(inner.rows.last(), Some(&7)));
///
/// push_next();
/// data.with_value(|inner| assert_eq!(inner.rows.last(), Some(&8)));
///
/// data.update_value(|inner| {
/// std::mem::take(inner) // sets Data back to default
/// });
/// data.with_value(|inner| assert!(inner.rows.is_empty()));
/// ```
pub fn update_value<U>(&self, fun: impl FnOnce(&mut T) -> U) {
self.try_update_value(fun);
}
/// Sets the value within [`StoredValue`].
///
/// Returns [`Some`] containing the passed value if the owner of the reactive node has been
/// disposed.
///
/// For types that do not implement [`Clone`], or in cases where allocating the entire object
/// would be too expensive, prefer [`StoredValue::try_update_value`].
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// let data = StoredValue::new(String::default());
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let say_hello = move || {
/// // Note that using the `update` methods would be more efficient here.
/// data.try_set_value("Hello, World!".into())
/// };
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let reset = move || {
/// // Note that using the `update` methods would be more efficient here.
/// data.try_set_value(Default::default())
/// };
/// assert_eq!(data.get_value(), "");
///
/// // None is returned because the value was able to be updated
/// assert_eq!(say_hello(), None);
///
/// assert_eq!(data.get_value(), "Hello, World!");
///
/// reset();
/// assert_eq!(data.get_value(), "");
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// // The reactive owner is disposed, so the value we intended to set is now
/// // returned as some.
/// assert_eq!(say_hello().as_deref(), Some("Hello, World!"));
/// assert_eq!(reset().as_deref(), Some(""));
/// ```
pub fn try_set_value(&self, value: T) -> Option<T> {
match self.value.try_get_value() {
Some(inner) => {
*inner.write().or_poisoned() = value;
None
}
None => Some(value),
}
}
/// Sets the value within [`StoredValue`].
///
/// For types that do not implement [`Clone`], or in cases where allocating the entire object
/// would be too expensive, prefer [`StoredValue::update_value`].
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_set_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// let data = StoredValue::new(10);
///
/// // Easy to move into closures because StoredValue is Copy + 'static.
/// // *NOTE* this is not the same thing as a derived signal!
/// // *NOTE* this will not be automatically rerun as StoredValue is NOT reactive!
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.get_value(), u8::MAX);
///
/// zero();
/// assert_eq!(data.get_value(), u8::MIN);
/// ```
pub fn set_value(&self, value: T) {
self.update_value(|n| *n = value);
}
}
@@ -156,39 +380,90 @@ impl<T, S> IsDisposed for StoredValue<T, S> {
}
}
impl<T, S: Storage<Arc<RwLock<T>>>> StoredValue<T, S>
where
T: Clone + 'static,
{
/// Returns the value within [`StoredValue`] by cloning.
///
/// Returns [`Some`] containing the value if the owner of the reactive node has not been
/// disposed. When disposed, returns [`None`].
///
/// See [`StoredValue::try_with_value`] for a version that avoids cloning. See
/// [`StoredValue::get_value`] for a version that clones, but panics if the node is disposed.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
/// # use reactive_graph::traits::Dispose;
///
/// // u8 is practically free to clone.
/// let data: StoredValue<u8> = StoredValue::new(10);
///
/// // Larger data structures can become very expensive to clone.
/// // You may prefer to use StoredValue::try_with_value.
/// let _expensive: StoredValue<Vec<String>> = StoredValue::new(vec![]);
///
/// // Easy to move into closures because StoredValue is Copy + 'static
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.try_get_value(), Some(u8::MAX));
///
/// zero();
/// assert_eq!(data.try_get_value(), Some(u8::MIN));
///
/// // You should not call dispose yourself in normal user code.
/// // This is shown here for the sake of the example.
/// data.dispose();
///
/// assert_eq!(data.try_get_value(), None);
/// ```
pub fn try_get_value(&self) -> Option<T> {
self.try_with_value(T::clone)
}
/// Returns the value within [`StoredValue`] by cloning.
///
/// See [`StoredValue::with_value`] for a version that avoids cloning.
///
/// # Panics
/// This function panics when called after the owner of the reactive node has been disposed.
/// See [`StoredValue::try_get_value`] for a version without panic.
///
/// # Examples
/// ```rust
/// # use reactive_graph::owner::StoredValue; let owner = reactive_graph::owner::Owner::new(); owner.set();
///
/// // u8 is practically free to clone.
/// let data: StoredValue<u8> = StoredValue::new(10);
///
/// // Larger data structures can become very expensive to clone.
/// // You may prefer to use StoredValue::try_with_value.
/// let _expensive: StoredValue<Vec<String>> = StoredValue::new(vec![]);
///
/// // Easy to move into closures because StoredValue is Copy + 'static
/// let maxout = move || data.set_value(u8::MAX);
/// let zero = move || data.set_value(u8::MIN);
///
/// maxout();
/// assert_eq!(data.get_value(), u8::MAX);
///
/// zero();
/// assert_eq!(data.get_value(), u8::MIN);
/// ```
pub fn get_value(&self) -> T {
self.with_value(T::clone)
}
}
impl<T, S> Dispose for StoredValue<T, S> {
fn dispose(self) {
self.value.dispose();
}
}
impl<T> From<ArcStoredValue<T>> for StoredValue<T>
where
T: Send + Sync + 'static,
{
#[track_caller]
fn from(value: ArcStoredValue<T>) -> Self {
StoredValue {
#[cfg(debug_assertions)]
defined_at: Location::caller(),
value: ArenaItem::new(value),
}
}
}
impl<T, S> From<StoredValue<T, S>> for ArcStoredValue<T>
where
S: Storage<ArcStoredValue<T>>,
{
#[track_caller]
fn from(value: StoredValue<T, S>) -> Self {
value
.value
.try_get_value()
.unwrap_or_else(unwrap_signal!(value))
}
}
/// Creates a new [`StoredValue`].
#[inline(always)]
#[track_caller]

View File

@@ -6,7 +6,7 @@ use super::{
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Notify},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Write},
traits::{DefinedAt, ReadUntracked, UntrackableGuard, Writeable},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
@@ -50,7 +50,7 @@ use std::{
/// - [`.set()`](crate::traits::Set) sets the signal to a new value.
/// - [`.update()`](crate::traits::Update) updates the value of the signal by
/// applying a closure that takes a mutable reference.
/// - [`.write()`](crate::traits::Write) returns a guard through which the signal
/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > Each of these has a related `_untracked()` method, which updates the signal
@@ -253,7 +253,7 @@ impl<T> Notify for ArcRwSignal<T> {
}
}
impl<T: 'static> Write for ArcRwSignal<T> {
impl<T: 'static> Writeable for ArcRwSignal<T> {
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {

View File

@@ -2,7 +2,7 @@ use super::guards::{UntrackedWriteGuard, WriteGuard};
use crate::{
graph::{ReactiveNode, SubscriberSet},
prelude::{IsDisposed, Notify},
traits::{DefinedAt, UntrackableGuard, Write},
traits::{DefinedAt, UntrackableGuard, Writeable},
};
use core::fmt::{Debug, Formatter, Result};
use std::{
@@ -23,7 +23,7 @@ use std::{
/// - [`.set()`](crate::traits::Set) sets the signal to a new value.
/// - [`.update()`](crate::traits::Update) updates the value of the signal by
/// applying a closure that takes a mutable reference.
/// - [`.write()`](crate::traits::Write) returns a guard through which the signal
/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > Each of these has a related `_untracked()` method, which updates the signal
@@ -122,7 +122,7 @@ impl<T> Notify for ArcWriteSignal<T> {
}
}
impl<T: 'static> Write for ArcWriteSignal<T> {
impl<T: 'static> Writeable for ArcWriteSignal<T> {
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {

View File

@@ -9,7 +9,7 @@ use crate::{
signal::guards::{UntrackedWriteGuard, WriteGuard},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked,
UntrackableGuard, Write,
UntrackableGuard, Writeable,
},
unwrap_signal,
};
@@ -57,7 +57,7 @@ use std::{
/// - [`.set()`](crate::traits::Set) sets the signal to a new value.
/// - [`.update()`](crate::traits::Update) updates the value of the signal by
/// applying a closure that takes a mutable reference.
/// - [`.write()`](crate::traits::Write) returns a guard through which the signal
/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > Each of these has a related `_untracked()` method, which updates the signal
@@ -349,7 +349,7 @@ where
}
}
impl<T, S> Write for RwSignal<T, S>
impl<T, S> Writeable for RwSignal<T, S>
where
T: 'static,
S: Storage<ArcRwSignal<T>>,

View File

@@ -1,7 +1,9 @@
use super::{guards::WriteGuard, ArcWriteSignal};
use crate::{
owner::{ArenaItem, Storage, SyncStorage},
traits::{DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Write},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, UntrackableGuard, Writeable,
},
};
use core::fmt::Debug;
use guardian::ArcRwLockWriteGuardian;
@@ -20,7 +22,7 @@ use std::{hash::Hash, ops::DerefMut, panic::Location, sync::Arc};
/// - [`.set()`](crate::traits::Set) sets the signal to a new value.
/// - [`.update()`](crate::traits::Update) updates the value of the signal by
/// applying a closure that takes a mutable reference.
/// - [`.write()`](crate::traits::Write) returns a guard through which the signal
/// - [`.write()`](crate::traits::Writeable) returns a guard through which the signal
/// can be mutated, and which notifies subscribers when it is dropped.
///
/// > Each of these has a related `_untracked()` method, which updates the signal
@@ -126,7 +128,7 @@ where
}
}
impl<T, S> Write for WriteSignal<T, S>
impl<T, S> Writeable for WriteSignal<T, S>
where
T: 'static,
S: Storage<ArcWriteSignal<T>>,

View File

@@ -18,7 +18,7 @@
//! | [`Track`] | — | Tracks changes to this value, adding it as a source of the current reactive observer. |
//! | [`Trigger`] | — | Notifies subscribers that this value has changed. |
//! | [`ReadUntracked`] | Guard | Gives immutable access to the value of this signal. |
//! | [`Write`] | Guard | Gives mutable access to the value of this signal.
//! | [`Writeable`] | Guard | Gives mutable access to the value of this signal.
//!
//! ## Derived Traits
//!
@@ -33,7 +33,7 @@
//! ### Update
//! | Trait | Mode | Composition | Description
//! |---------------------|---------------|-----------------------------------|------------
//! | [`UpdateUntracked`] | `fn(&mut T)` | [`Write`] | Applies closure to the current value to update it, but doesn't notify subscribers.
//! | [`UpdateUntracked`] | `fn(&mut T)` | [`Writeable`] | Applies closure to the current value to update it, but doesn't notify subscribers.
//! | [`Update`] | `fn(&mut T)` | [`UpdateUntracked`] + [`Trigger`] | Applies closure to the current value to update it, and notifies subscribers.
//! | [`Set`] | `T` | [`Update`] | Sets the value to a new value, and notifies subscribers.
//!
@@ -52,7 +52,7 @@ use crate::{
effect::Effect,
graph::{Observer, Source, Subscriber, ToAnySource},
owner::Owner,
signal::{arc_signal, guards::UntrackedWriteGuard, ArcReadSignal},
signal::{arc_signal, ArcReadSignal},
};
use any_spawner::Executor;
use futures::{Stream, StreamExt};
@@ -61,7 +61,6 @@ use std::{
panic::Location,
};
#[doc(hidden)]
/// Provides a sensible panic message for accessing disposed signals.
#[macro_export]
macro_rules! unwrap_signal {
@@ -214,7 +213,7 @@ pub trait UntrackableGuard: DerefMut {
/// Gives mutable access to a signal's value through a guard type. When the guard is dropped, the
/// signal's subscribers will be notified.
pub trait Write: Sized + DefinedAt + Notify {
pub trait Writeable: Sized + DefinedAt + Notify {
/// The type of the signal's value.
type Value: Sized + 'static;
@@ -422,9 +421,9 @@ pub trait UpdateUntracked: DefinedAt {
impl<T> UpdateUntracked for T
where
T: Write,
T: Writeable,
{
type Value = <Self as Write>::Value;
type Value = <Self as Writeable>::Value;
#[track_caller]
fn try_update_untracked<U>(
@@ -479,9 +478,9 @@ pub trait Update {
impl<T> Update for T
where
T: Write,
T: Writeable,
{
type Value = <Self as Write>::Value;
type Value = <Self as Writeable>::Value;
#[track_caller]
fn try_maybe_update<U>(
@@ -639,189 +638,3 @@ pub fn panic_getting_disposed_signal(
)
}
}
/// A variation of the [`Read`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait ReadValue: Sized + DefinedAt {
/// The guard type that will be returned, which can be dereferenced to the value.
type Value: Deref;
/// Returns the non-reactive guard, or `None` if the value has already been disposed.
#[track_caller]
fn try_read_value(&self) -> Option<Self::Value>;
/// Returns the non-reactive guard.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn read_value(&self) -> Self::Value {
self.try_read_value().unwrap_or_else(unwrap_signal!(self))
}
}
/// A variation of the [`With`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait WithValue: DefinedAt {
/// The type of the value contained in the value.
type Value: ?Sized;
/// Applies the closure to the value, non-reactively, and returns the result,
/// or `None` if the value has already been disposed.
#[track_caller]
fn try_with_value<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U>;
/// Applies the closure to the value, non-reactively, and returns the result.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn with_value<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
self.try_with_value(fun)
.unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> WithValue for T
where
T: DefinedAt + ReadValue,
{
type Value = <<Self as ReadValue>::Value as Deref>::Target;
fn try_with_value<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
self.try_read_value().map(|value| fun(&value))
}
}
/// A variation of the [`Get`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait GetValue: DefinedAt {
/// The type of the value contained in the value.
type Value: Clone;
/// Clones and returns the value of the value, non-reactively,
/// or `None` if the value has already been disposed.
#[track_caller]
fn try_get_value(&self) -> Option<Self::Value>;
/// Clones and returns the value of the value, non-reactively.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn get_value(&self) -> Self::Value {
self.try_get_value().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> GetValue for T
where
T: WithValue,
T::Value: Clone,
{
type Value = <Self as WithValue>::Value;
fn try_get_value(&self) -> Option<Self::Value> {
self.try_with_value(Self::Value::clone)
}
}
/// A variation of the [`Write`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait WriteValue: Sized + DefinedAt {
/// The type of the value's value.
type Value: Sized + 'static;
/// Returns a non-reactive write guard, or `None` if the value has already been disposed.
#[track_caller]
fn try_write_value(&self) -> Option<UntrackedWriteGuard<Self::Value>>;
/// Returns a non-reactive write guard.
///
/// # Panics
/// Panics if you try to access a value that has been disposed.
#[track_caller]
fn write_value(&self) -> UntrackedWriteGuard<Self::Value> {
self.try_write_value().unwrap_or_else(unwrap_signal!(self))
}
}
/// A variation of the [`Update`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait UpdateValue: DefinedAt {
/// The type of the value contained in the value.
type Value;
/// Updates the value, returning the value that is
/// returned by the update function, or `None` if the value has already been disposed.
#[track_caller]
fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
) -> Option<U>;
/// Updates the value.
#[track_caller]
fn update_value(&self, fun: impl FnOnce(&mut Self::Value)) {
self.try_update_value(fun);
}
}
impl<T> UpdateValue for T
where
T: WriteValue,
{
type Value = <Self as WriteValue>::Value;
#[track_caller]
fn try_update_value<U>(
&self,
fun: impl FnOnce(&mut Self::Value) -> U,
) -> Option<U> {
let mut guard = self.try_write_value()?;
Some(fun(&mut *guard))
}
}
/// A variation of the [`Set`] trait that provides a signposted "always-non-reactive" API.
/// E.g. for [`StoredValue`](`crate::owner::StoredValue`).
pub trait SetValue: DefinedAt {
/// The type of the value contained in the value.
type Value;
/// Updates the value by replacing it, non-reactively.
///
/// If the value has already been disposed, returns `Some(value)` with the value that was
/// passed in. Otherwise, returns `None`.
#[track_caller]
fn try_set_value(&self, value: Self::Value) -> Option<Self::Value>;
/// Updates the value by replacing it, non-reactively.
#[track_caller]
fn set_value(&self, value: Self::Value) {
self.try_set_value(value);
}
}
impl<T> SetValue for T
where
T: WriteValue,
{
type Value = <Self as WriteValue>::Value;
fn try_set_value(&self, value: Self::Value) -> Option<Self::Value> {
// Unlike most other traits, for these None actually means success:
if let Some(mut guard) = self.try_write_value() {
*guard = value;
None
} else {
Some(value)
}
}
}

View File

@@ -3,30 +3,14 @@
/// Types that abstract over signals with values that can be read.
pub mod read {
use crate::{
computed::{ArcMemo, Memo, MemoInner},
graph::untrack,
owner::{
ArcStoredValue, ArenaItem, FromLocal, LocalStorage, Storage,
SyncStorage,
},
signal::{
guards::{Mapped, Plain, ReadGuard},
ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal,
},
traits::{
DefinedAt, Dispose, Get, Read, ReadUntracked, ReadValue, Track,
With, WithValue,
},
unwrap_signal,
computed::{ArcMemo, Memo},
owner::{ArenaItem, FromLocal, LocalStorage, Storage, SyncStorage},
signal::{ArcReadSignal, ArcRwSignal, ReadSignal, RwSignal},
traits::{DefinedAt, Dispose, Get, With, WithUntracked},
untrack, unwrap_signal,
};
use send_wrapper::SendWrapper;
use std::{
borrow::Borrow,
fmt::Display,
ops::Deref,
panic::Location,
sync::{Arc, RwLock},
};
use std::{panic::Location, sync::Arc};
/// Possibilities for the inner type of a [`Signal`].
pub enum SignalTypes<T, S = SyncStorage>
@@ -39,8 +23,6 @@ pub mod read {
Memo(ArcMemo<T, S>),
/// A derived signal.
DerivedSignal(Arc<dyn Fn() -> T + Send + Sync>),
/// A static, stored value.
Stored(ArcStoredValue<T>),
}
impl<T, S> Clone for SignalTypes<T, S>
@@ -52,7 +34,6 @@ pub mod read {
Self::ReadSignal(arg0) => Self::ReadSignal(arg0.clone()),
Self::Memo(arg0) => Self::Memo(arg0.clone()),
Self::DerivedSignal(arg0) => Self::DerivedSignal(arg0.clone()),
Self::Stored(arg0) => Self::Stored(arg0.clone()),
}
}
}
@@ -70,9 +51,6 @@ pub mod read {
Self::DerivedSignal(_) => {
f.debug_tuple("DerivedSignal").finish()
}
Self::Stored(arg0) => {
f.debug_tuple("Static").field(arg0).finish()
}
}
}
}
@@ -189,39 +167,6 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
#[track_caller]
pub fn stored(value: T) -> Self {
Self {
inner: SignalTypes::Stored(ArcStoredValue::new(value)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T, S> ArcSignal<T, S>
where
S: Storage<T>,
{
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
pub fn track(&self) {
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
}
}
}
impl<T> Default for ArcSignal<T, SyncStorage>
@@ -229,7 +174,7 @@ pub mod read {
T: Default + Send + Sync + 'static,
{
fn default() -> Self {
Self::stored(Default::default())
Self::derive(|| Default::default())
}
}
@@ -285,6 +230,24 @@ pub mod read {
}
}
impl<T, S> WithUntracked for ArcSignal<T, S>
where
S: Storage<T>,
{
type Value = T;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match &self.inner {
SignalTypes::ReadSignal(i) => i.try_with_untracked(fun),
SignalTypes::Memo(i) => i.try_with_untracked(fun),
SignalTypes::DerivedSignal(i) => Some(untrack(|| fun(&i()))),
}
}
}
impl<T, S> With for ArcSignal<T, S>
where
S: Storage<T>,
@@ -300,63 +263,10 @@ pub mod read {
SignalTypes::ReadSignal(i) => i.try_with(fun),
SignalTypes::Memo(i) => i.try_with(fun),
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
SignalTypes::Stored(i) => i.try_with_value(fun),
}
}
}
impl<T, S> ReadUntracked for ArcSignal<T, S>
where
S: Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.try_read_untracked().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read_untracked().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(untrack(|| i())))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
}
}
impl<T, S> Read for ArcSignal<T, S>
where
S: Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read(&self) -> Option<Self::Value> {
match &self.inner {
SignalTypes::ReadSignal(i) => {
i.try_read().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => i.try_read().map(SignalReadGuard::Memo),
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(i()))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
/// A wrapper for any kind of arena-allocated reactive signal:
/// an [`ReadSignal`], [`Memo`], [`RwSignal`], or derived signal closure.
///
@@ -432,6 +342,31 @@ pub mod read {
}
}
impl<T, S> WithUntracked for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = T;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
self.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| match &inner {
SignalTypes::ReadSignal(i) => i.try_with_untracked(fun),
SignalTypes::Memo(i) => i.try_with_untracked(fun),
SignalTypes::DerivedSignal(i) => {
Some(untrack(|| fun(&i())))
}
})
}
}
impl<T, S> With for Signal<T, S>
where
T: 'static,
@@ -451,79 +386,10 @@ pub mod read {
SignalTypes::ReadSignal(i) => i.try_with(fun),
SignalTypes::Memo(i) => i.try_with(fun),
SignalTypes::DerivedSignal(i) => Some(fun(&i())),
SignalTypes::Stored(i) => i.try_with_value(fun),
})
}
}
impl<T, S> ReadUntracked for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| {
match &inner {
SignalTypes::ReadSignal(i) => {
i.try_read_untracked().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read_untracked().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(untrack(|| i())))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
})
}
}
impl<T, S> Read for Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read(&self) -> Option<Self::Value> {
self.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.and_then(|inner| {
match &inner {
SignalTypes::ReadSignal(i) => {
i.try_read().map(SignalReadGuard::Read)
}
SignalTypes::Memo(i) => {
i.try_read().map(SignalReadGuard::Memo)
}
SignalTypes::DerivedSignal(i) => {
Some(SignalReadGuard::Owned(i()))
}
SignalTypes::Stored(i) => {
i.try_read_value().map(SignalReadGuard::Read)
}
}
.map(ReadGuard::new)
})
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> Signal<T>
where
T: Send + Sync + 'static,
@@ -566,18 +432,6 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
#[track_caller]
pub fn stored(value: T) -> Self {
Self {
inner: ArenaItem::new_with_storage(SignalTypes::Stored(
ArcStoredValue::new(value),
)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T> Signal<T, LocalStorage>
@@ -605,49 +459,6 @@ pub mod read {
defined_at: std::panic::Location::caller(),
}
}
/// Moves a static, nonreactive value into a signal, backed by [`ArcStoredValue`].
/// Works like [`Signal::stored`] but uses [`LocalStorage`].
#[track_caller]
pub fn stored_local(value: T) -> Self {
Self {
inner: ArenaItem::new_local(SignalTypes::Stored(
ArcStoredValue::new(value),
)),
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
}
impl<T, S> Signal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
pub fn track(&self) {
let inner = self
.inner
// clone the inner Arc type and release the lock
// prevents deadlocking if the derived value includes taking a lock on the arena
.try_with_value(Clone::clone)
.unwrap_or_else(unwrap_signal!(self));
match inner {
SignalTypes::ReadSignal(i) => {
i.track();
}
SignalTypes::Memo(i) => {
i.track();
}
SignalTypes::DerivedSignal(i) => {
i();
}
// Doesn't change.
SignalTypes::Stored(_) => {}
}
}
}
impl<T> Default for Signal<T>
@@ -655,7 +466,7 @@ pub mod read {
T: Send + Sync + Default + 'static,
{
fn default() -> Self {
Self::stored(Default::default())
Self::derive(|| Default::default())
}
}
@@ -664,34 +475,34 @@ pub mod read {
T: Default + 'static,
{
fn default() -> Self {
Self::stored_local(Default::default())
Self::derive_local(|| Default::default())
}
}
impl<T: Send + Sync + 'static> From<T> for ArcSignal<T, SyncStorage> {
impl<T: Clone + Send + Sync + 'static> From<T> for ArcSignal<T, SyncStorage> {
#[track_caller]
fn from(value: T) -> Self {
ArcSignal::stored(value)
Self::derive(move || value.clone())
}
}
impl<T> From<T> for Signal<T>
where
T: Send + Sync + 'static,
T: Clone + Send + Sync + 'static,
{
#[track_caller]
fn from(value: T) -> Self {
Self::stored(value)
Self::derive(move || value.clone())
}
}
impl<T> From<T> for Signal<T, LocalStorage>
where
T: 'static,
T: Clone + 'static,
{
#[track_caller]
fn from(value: T) -> Self {
Self::stored_local(value)
Self::derive_local(move || value.clone())
}
}
@@ -903,6 +714,23 @@ pub mod read {
}
}
impl<T, S> WithUntracked for MaybeSignal<T, S>
where
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = T;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match self {
Self::Static(t) => Some(fun(t)),
Self::Dynamic(s) => s.try_with_untracked(fun),
}
}
}
impl<T, S> With for MaybeSignal<T, S>
where
T: Send + Sync + 'static,
@@ -921,44 +749,6 @@ pub mod read {
}
}
impl<T, S> ReadUntracked for MaybeSignal<T, S>
where
T: Clone,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
match self {
Self::Static(t) => {
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
}
Self::Dynamic(s) => s.try_read_untracked(),
}
}
}
impl<T, S> Read for MaybeSignal<T, S>
where
T: Clone,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
type Value = ReadGuard<T, SignalReadGuard<T, S>>;
fn try_read(&self) -> Option<Self::Value> {
match self {
Self::Static(t) => {
Some(ReadGuard::new(SignalReadGuard::Owned(t.clone())))
}
Self::Dynamic(s) => s.try_read(),
}
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> MaybeSignal<T>
where
T: Send + Sync,
@@ -980,21 +770,6 @@ pub mod read {
}
}
impl<T, S> MaybeSignal<T, S>
where
T: 'static,
S: Storage<SignalTypes<T, S>> + Storage<T>,
{
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
pub fn track(&self) {
match self {
Self::Static(_) => {}
Self::Dynamic(signal) => signal.track(),
}
}
}
impl<T> From<T> for MaybeSignal<T, SyncStorage>
where
SyncStorage: Storage<T>,
@@ -1117,7 +892,7 @@ pub mod read {
impl<S> From<&str> for MaybeSignal<String, S>
where
S: Storage<String> + Storage<Arc<RwLock<String>>>,
S: Storage<String>,
{
fn from(value: &str) -> Self {
Self::Static(value.to_string())
@@ -1192,6 +967,23 @@ pub mod read {
}
}
impl<T, S> WithUntracked for MaybeProp<T, S>
where
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
type Value = Option<T>;
fn try_with_untracked<U>(
&self,
fun: impl FnOnce(&Self::Value) -> U,
) -> Option<U> {
match &self.0 {
None => Some(fun(&None)),
Some(inner) => inner.try_with_untracked(fun),
}
}
}
impl<T, S> With for MaybeProp<T, S>
where
T: Send + Sync + 'static,
@@ -1210,40 +1002,6 @@ pub mod read {
}
}
impl<T, S> ReadUntracked for MaybeProp<T, S>
where
T: Clone,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
match &self.0 {
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
Some(inner) => inner.try_read_untracked(),
}
}
}
impl<T, S> Read for MaybeProp<T, S>
where
T: Clone,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
type Value = ReadGuard<Option<T>, SignalReadGuard<Option<T>, S>>;
fn try_read(&self) -> Option<Self::Value> {
match &self.0 {
None => Some(ReadGuard::new(SignalReadGuard::Owned(None))),
Some(inner) => inner.try_read(),
}
}
fn read(&self) -> Self::Value {
self.try_read().unwrap_or_else(unwrap_signal!(self))
}
}
impl<T> MaybeProp<T>
where
T: Send + Sync,
@@ -1257,21 +1015,6 @@ pub mod read {
}
}
impl<T, S> MaybeProp<T, S>
where
T: 'static,
S: Storage<SignalTypes<Option<T>, S>> + Storage<Option<T>>,
{
/// Subscribes to this signal in the current reactive scope without doing anything with its value.
#[track_caller]
pub fn track(&self) {
match &self.0 {
None => {}
Some(signal) => signal.track(),
}
}
}
impl<T> From<T> for MaybeProp<T>
where
SyncStorage: Storage<Option<T>>,
@@ -1498,76 +1241,6 @@ pub mod read {
Self(Some(MaybeSignal::from_local(Some(value.to_string()))))
}
}
/// The content of a [`Signal`] wrapper read guard, variable depending on the signal type.
#[derive(Debug)]
pub enum SignalReadGuard<T: 'static, S: Storage<T>> {
/// A read signal guard.
Read(ReadGuard<T, Plain<T>>),
/// A memo guard.
Memo(ReadGuard<T, Mapped<Plain<MemoInner<T, S>>, T>>),
/// A fake guard for derived signals, the content had to actually be cloned, so it's not a guard but we pretend it is.
Owned(T),
}
impl<T, S> Clone for SignalReadGuard<T, S>
where
S: Storage<T>,
T: Clone,
Plain<T>: Clone,
Mapped<Plain<MemoInner<T, S>>, T>: Clone,
{
fn clone(&self) -> Self {
match self {
SignalReadGuard::Read(i) => SignalReadGuard::Read(i.clone()),
SignalReadGuard::Memo(i) => SignalReadGuard::Memo(i.clone()),
SignalReadGuard::Owned(i) => SignalReadGuard::Owned(i.clone()),
}
}
}
impl<T, S> Deref for SignalReadGuard<T, S>
where
S: Storage<T>,
{
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
SignalReadGuard::Read(i) => i,
SignalReadGuard::Memo(i) => i,
SignalReadGuard::Owned(i) => i,
}
}
}
impl<T, S> Borrow<T> for SignalReadGuard<T, S>
where
S: Storage<T>,
{
fn borrow(&self) -> &T {
self.deref()
}
}
impl<T, S> PartialEq<T> for SignalReadGuard<T, S>
where
S: Storage<T>,
T: PartialEq,
{
fn eq(&self, other: &T) -> bool {
self.deref() == other
}
}
impl<T, S> Display for SignalReadGuard<T, S>
where
S: Storage<T>,
T: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&**self, f)
}
}
}
/// Types that abstract over the ability to update a signal.

View File

@@ -1,88 +0,0 @@
use reactive_graph::{
computed::Memo,
owner::{on_cleanup, Owner},
signal::{RwSignal, Trigger},
traits::{Dispose, GetUntracked, Track},
};
use std::sync::Arc;
#[test]
fn cleanup_on_dispose() {
let owner = Owner::new();
owner.set();
struct ExecuteOnDrop(Option<Box<dyn FnOnce() + Send + Sync>>);
impl ExecuteOnDrop {
fn new(f: impl FnOnce() + Send + Sync + 'static) -> Self {
Self(Some(Box::new(f)))
}
}
impl Drop for ExecuteOnDrop {
fn drop(&mut self) {
self.0.take().unwrap()();
}
}
let trigger = Trigger::new();
println!("STARTING");
let memo = Memo::new(move |_| {
trigger.track();
// An example of why you might want to do this is that
// when something goes out of reactive scope you want it to be cleaned up.
// The cleaning up might have side effects, and those side effects might cause
// re-renders where new `on_cleanup` are registered.
let on_drop = ExecuteOnDrop::new(|| {
on_cleanup(|| println!("Nested cleanup in progress."))
});
on_cleanup(move || {
println!("Cleanup in progress.");
drop(on_drop)
});
});
println!("Memo 1: {:?}", memo);
memo.get_untracked(); // First cleanup registered.
memo.dispose(); // Cleanup not run here.
println!("Cleanup should have been executed.");
let memo = Memo::new(move |_| {
// New cleanup registered. It'll panic here.
on_cleanup(move || println!("Test passed."));
});
println!("Memo 2: {:?}", memo);
println!("^ Note how the memos have the same key (different versions).");
memo.get_untracked(); // First cleanup registered.
println!("Test passed.");
memo.dispose();
}
#[test]
fn leak_on_dispose() {
let owner = Owner::new();
owner.set();
let trigger = Trigger::new();
let value = Arc::new(());
let weak = Arc::downgrade(&value);
let memo = Memo::new(move |_| {
trigger.track();
RwSignal::new(value.clone());
});
memo.get_untracked();
memo.dispose();
assert!(weak.upgrade().is_none()); // Should have been dropped.
}

View File

@@ -3,7 +3,7 @@ use reactive_graph::{
signal::{arc_signal, signal, ArcRwSignal, RwSignal},
traits::{
Get, GetUntracked, Read, Set, Update, UpdateUntracked, With,
WithUntracked, Write,
WithUntracked, Writeable,
},
};

View File

@@ -10,7 +10,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
Writeable,
},
};
use std::{
@@ -171,7 +171,7 @@ where
}
}
impl<Inner, Prev> Write for AtIndex<Inner, Prev>
impl<Inner, Prev> Writeable for AtIndex<Inner, Prev>
where
Inner: StoreField<Value = Prev>,
Prev: IndexMut<usize> + 'static,
@@ -258,20 +258,3 @@ where
}
}
}
impl<Inner, Prev> DoubleEndedIterator for StoreFieldIter<Inner, Prev>
where
Inner: StoreField<Value = Prev> + Clone + 'static,
Prev: IndexMut<usize> + 'static,
Prev::Output: Sized + 'static,
{
fn next_back(&mut self) -> Option<Self::Item> {
if self.len > self.idx {
self.len -= 1;
let field = AtIndex::new(self.inner.clone(), self.len);
Some(field)
} else {
None
}
}
}

View File

@@ -10,7 +10,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
Writeable,
},
};
use std::{
@@ -312,7 +312,7 @@ where
}
}
impl<Inner, Prev, K, T> Write for KeyedSubfield<Inner, Prev, K, T>
impl<Inner, Prev, K, T> Writeable for KeyedSubfield<Inner, Prev, K, T>
where
Self: Clone,
for<'a> &'a T: IntoIterator,
@@ -574,7 +574,7 @@ where
}
}
impl<Inner, Prev, K, T> Write for AtKeyed<Inner, Prev, K, T>
impl<Inner, Prev, K, T> Writeable for AtKeyed<Inner, Prev, K, T>
where
K: Debug + Send + Sync + PartialEq + Eq + Hash + 'static,
KeyedSubfield<Inner, Prev, K, T>: Clone,
@@ -682,18 +682,3 @@ where
.map(|key| AtKeyed::new(self.inner.clone(), key))
}
}
impl<Inner, Prev, K, T> DoubleEndedIterator
for StoreFieldKeyedIter<Inner, Prev, K, T>
where
Inner: StoreField<Value = Prev> + Clone + 'static,
T: IndexMut<usize> + 'static,
T::Output: Sized + 'static,
for<'a> &'a T: IntoIterator,
{
fn next_back(&mut self) -> Option<Self::Item> {
self.keys
.pop_back()
.map(|key| AtKeyed::new(self.inner.clone(), key))
}
}

View File

@@ -7,7 +7,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
Writeable,
},
};
use rustc_hash::FxHashMap;
@@ -254,7 +254,7 @@ where
}
}
impl<T> Write for ArcStore<T>
impl<T> Writeable for ArcStore<T>
where
T: 'static,
{
@@ -379,7 +379,7 @@ where
}
}
impl<T, S> Write for Store<T, S>
impl<T, S> Writeable for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
@@ -428,7 +428,7 @@ mod tests {
use crate::{self as reactive_stores, Patch, Store, StoreFieldIterator};
use reactive_graph::{
effect::Effect,
traits::{Read, ReadUntracked, Set, Update, Write},
traits::{Read, ReadUntracked, Set, Update, Writeable},
};
use reactive_stores_macro::{Patch, Store};
use std::sync::{

View File

@@ -49,7 +49,7 @@ mod tests {
use crate::{self as reactive_stores, Store};
use reactive_graph::{
effect::Effect,
traits::{Get, Read, ReadUntracked, Set, Write},
traits::{Get, Read, ReadUntracked, Set, Writeable},
};
use reactive_stores_macro::Store;
use std::sync::{

View File

@@ -11,7 +11,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
Writeable,
},
unwrap_signal,
};
@@ -251,7 +251,7 @@ where
}
}
impl<T, S> Write for Then<T, S>
impl<T, S> Writeable for Then<T, S>
where
T: 'static,
S: StoreField,

View File

@@ -10,7 +10,7 @@ use reactive_graph::{
},
traits::{
DefinedAt, IsDisposed, Notify, ReadUntracked, Track, UntrackableGuard,
Write,
Writeable,
},
};
use std::{iter, marker::PhantomData, ops::DerefMut, panic::Location};
@@ -168,7 +168,7 @@ where
}
}
impl<Inner, Prev, T> Write for Subfield<Inner, Prev, T>
impl<Inner, Prev, T> Writeable for Subfield<Inner, Prev, T>
where
T: 'static,
Inner: StoreField<Value = Prev>,

View File

@@ -4,7 +4,7 @@ use crate::{
location::{BrowserUrl, LocationProvider},
NavigateOptions,
};
use leptos::{ev, html::form, prelude::*, task::spawn_local};
use leptos::{ev, html::form, prelude::*, spawn::spawn_local};
use std::{error::Error, sync::Arc};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use web_sys::{FormData, RequestRedirect, Response};

View File

@@ -101,9 +101,8 @@ where
let get = Memo::new({
let key = key.clone_inplace();
move |_| {
query_map.with(|map| {
map.get_str(&key).and_then(|value| value.parse().ok())
})
query_map
.with(|map| map.get(&key).and_then(|value| value.parse().ok()))
}
});

View File

@@ -14,6 +14,7 @@ mod method;
mod navigate;
pub mod nested_router;
pub mod params;
//mod router;
mod ssr_mode;
pub mod static_routes;
@@ -23,4 +24,5 @@ pub use leptos_router_macro::path;
pub use matching::*;
pub use method::*;
pub use navigate::*;
//pub use router::*;
pub use ssr_mode::*;

View File

@@ -1,5 +1,5 @@
use crate::{components::RouterContext, hooks::use_resolved_path};
use leptos::{children::Children, oco::Oco, prelude::*};
use leptos::{children::Children, oco::Oco, prelude::*, *};
use reactive_graph::{computed::ArcMemo, owner::use_context};
use std::{borrow::Cow, rc::Rc};

View File

@@ -1,8 +1,8 @@
use crate::location::{unescape, Url};
use std::{borrow::Cow, str::FromStr, sync::Arc};
use std::{borrow::Cow, mem, str::FromStr, sync::Arc};
use thiserror::Error;
type ParamsMapInner = Vec<(Cow<'static, str>, Vec<String>)>;
type ParamsMapInner = Vec<(Cow<'static, str>, String)>;
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct ParamsMap(ParamsMapInner);
@@ -21,43 +21,34 @@ impl ParamsMap {
}
/// Inserts a value into the map.
pub fn insert(&mut self, key: impl Into<Cow<'static, str>>, value: String) {
pub fn insert(&mut self, key: String, value: String) -> Option<String> {
let value = unescape(&value);
let key = key.into();
if let Some(prev) = self.0.iter_mut().find(|(k, _)| k == &key) {
prev.1.push(value);
} else {
self.0.push((key, vec![value]));
if let Some(prev) = self.0.iter().position(|(k, _)| k == &key) {
return Some(mem::replace(&mut self.0[prev].1, value));
}
self.0.push((key.into(), value));
None
}
/// Gets the most-recently-added value of this param from the map.
/// Gets an owned value from the map.
pub fn get(&self, key: &str) -> Option<String> {
self.get_str(key).map(ToOwned::to_owned)
}
/// Gets all references to a param of this name from the map.
pub fn get_all(&self, key: &str) -> Option<Vec<String>> {
self.0
.iter()
.find_map(|(k, v)| if k == key { Some(v.clone()) } else { None })
.find_map(|(k, v)| (k == key).then_some(v.to_owned()))
}
/// Gets a reference to the most-recently-added value of this param from the map.
/// Gets a referenc to a value from the map.
pub fn get_str(&self, key: &str) -> Option<&str> {
self.0.iter().find_map(|(k, v)| {
if k == key {
v.last().map(|i| i.as_str())
} else {
None
}
})
self.0
.iter()
.find_map(|(k, v)| (k == key).then_some(v.as_str()))
}
/// Removes a value from the map.
#[inline(always)]
pub fn remove(&mut self, key: &str) -> Option<Vec<String>> {
pub fn remove(&mut self, key: &str) -> Option<String> {
for i in 0..self.0.len() {
if self.0[i].0 == key {
return Some(self.0.swap_remove(i).1);
@@ -71,13 +62,11 @@ impl ParamsMap {
let mut buf = String::new();
if !self.0.is_empty() {
buf.push('?');
for (k, vs) in &self.0 {
for v in vs {
buf.push_str(&Url::escape(k));
buf.push('=');
buf.push_str(&Url::escape(v));
buf.push('&');
}
for (k, v) in &self.0 {
buf.push_str(&Url::escape(k));
buf.push('=');
buf.push_str(&Url::escape(v));
buf.push('&');
}
if buf.len() > 1 {
buf.pop();
@@ -93,12 +82,11 @@ where
V: Into<String>,
{
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
let mut map = Self::new();
for (key, value) in iter {
map.insert(key, value.into());
}
map
Self(
iter.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
)
}
}
@@ -107,21 +95,13 @@ impl IntoIterator for ParamsMap {
type IntoIter = ParamsMapIter;
fn into_iter(self) -> Self::IntoIter {
let inner = self.0.into_iter().fold(vec![], |mut c, (k, vs)| {
for v in vs {
c.push((k.clone(), v));
}
c
});
ParamsMapIter(inner.into_iter())
ParamsMapIter(self.0.into_iter())
}
}
/// An iterator over the keys and values of a [`ParamsMap`].
#[derive(Debug)]
pub struct ParamsMapIter(
<Vec<(Cow<'static, str>, String)> as IntoIterator>::IntoIter,
);
pub struct ParamsMapIter(<ParamsMapInner as IntoIterator>::IntoIter);
impl Iterator for ParamsMapIter {
type Item = (Cow<'static, str>, String);
@@ -221,20 +201,3 @@ impl PartialEq for ParamsError {
}
}
}
#[cfg(all(test, feature = "ssr"))]
mod tests {
use super::*;
#[test]
fn paramsmap_to_query_string() {
let mut map = ParamsMap::new();
let key = "param".to_string();
let value1 = "a".to_string();
let value2 = "b".to_string();
map.insert(key.clone(), value1);
map.insert(key, value2);
let query_string = map.to_query_string();
assert_eq!(&query_string, "?param=a&param=b")
}
}

1443
router/src/router.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use crate::{hooks::RawParamsMap, params::ParamsMap, PathSegment};
use futures::{channel::oneshot, stream, Stream, StreamExt};
use leptos::task::spawn;
use leptos::spawn::spawn;
use reactive_graph::{owner::Owner, traits::GetUntracked};
use std::{
fmt::{Debug, Display},

View File

@@ -25,8 +25,8 @@ inventory = { version = "0.3.15", optional = true }
dashmap = "6.0"
once_cell = "1.19"
## servers
# actix
## servers
# actix
actix-web = { version = "4.8", optional = true }
# axum
@@ -36,12 +36,12 @@ axum = { version = "0.7.5", optional = true, default-features = false, features
tower = { version = "0.4.13", optional = true }
tower-layer = { version = "0.3.2", optional = true }
## input encodings
## input encodings
serde_qs = { version = "0.13.0", optional = true }
multer = { version = "3.1", optional = true }
## output encodings
# serde
## output encodings
# serde
serde_json = "1.0"
serde-lite = { version = "0.5.0", features = ["derive"], optional = true }
futures = "0.3.30"
@@ -51,7 +51,11 @@ postcard = { version = "1", features = ["alloc"], optional = true }
hyper = { version = "1.4", optional = true }
bytes = "1.7"
http-body-util = { version = "0.1.2", optional = true }
rkyv = { version = "0.8.8", optional = true }
rkyv = { version = "0.7.44", features = [
"validation",
"uuid",
"strict",
], optional = true }
rmp-serde = { version = "1.3.0", optional = true }
# client
@@ -68,7 +72,7 @@ web-sys = { version = "0.3.70", optional = true, features = [
"AbortSignal",
] }
# reqwest client
# reqwest client
reqwest = { version = "0.12.5", default-features = false, optional = true, features = [
"multipart",
"stream",

View File

@@ -8,19 +8,11 @@ use bytes::Bytes;
use futures::StreamExt;
use http::Method;
use rkyv::{
api::high::{HighDeserializer, HighSerializer, HighValidator},
bytecheck::CheckBytes,
rancor,
ser::allocator::ArenaHandle,
util::AlignedVec,
Archive, Deserialize, Serialize,
de::deserializers::SharedDeserializeMap, ser::serializers::AllocSerializer,
validation::validators::DefaultValidator, AlignedVec, Archive, CheckBytes,
Deserialize, Serialize,
};
type RkyvSerializer<'a> =
HighSerializer<AlignedVec, ArenaHandle<'a>, rancor::Error>;
type RkyvDeserializer = HighDeserializer<rancor::Error>;
type RkyvValidator<'a> = HighValidator<'a, rancor::Error>;
/// Pass arguments and receive responses using `rkyv` in a `POST` request.
pub struct Rkyv;
@@ -32,16 +24,17 @@ impl Encoding for Rkyv {
impl<CustErr, T, Request> IntoReq<Rkyv, Request, CustErr> for T
where
Request: ClientReq<CustErr>,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
T: Serialize<AllocSerializer<1024>> + Send,
T: Archive,
T::Archived: for<'a> CheckBytes<DefaultValidator<'a>>
+ Deserialize<T, SharedDeserializeMap>,
{
fn into_req(
self,
path: &str,
accepts: &str,
) -> Result<Request, ServerFnError<CustErr>> {
let encoded = rkyv::to_bytes::<rancor::Error>(&self)
let encoded = rkyv::to_bytes::<T, 1024>(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
let bytes = Bytes::copy_from_slice(encoded.as_ref());
Request::try_new_post_bytes(path, accepts, Rkyv::CONTENT_TYPE, bytes)
@@ -51,12 +44,13 @@ where
impl<CustErr, T, Request> FromReq<Rkyv, Request, CustErr> for T
where
Request: Req<CustErr> + Send + 'static,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
T: Serialize<AllocSerializer<1024>> + Send,
T: Archive,
T::Archived: for<'a> CheckBytes<DefaultValidator<'a>>
+ Deserialize<T, SharedDeserializeMap>,
{
async fn from_req(req: Request) -> Result<Self, ServerFnError<CustErr>> {
let mut aligned = AlignedVec::<1024>::new();
let mut aligned = AlignedVec::new();
let mut body_stream = Box::pin(req.try_into_stream()?);
while let Some(chunk) = body_stream.next().await {
match chunk {
@@ -70,7 +64,7 @@ where
}
}
}
rkyv::from_bytes::<T, rancor::Error>(aligned.as_ref())
rkyv::from_bytes::<T>(aligned.as_ref())
.map_err(|e| ServerFnError::Args(e.to_string()))
}
}
@@ -78,14 +72,14 @@ where
impl<CustErr, T, Response> IntoRes<Rkyv, Response, CustErr> for T
where
Response: Res<CustErr>,
T: Send,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
T: Serialize<AllocSerializer<1024>> + Send,
T: Archive,
T::Archived: for<'a> CheckBytes<DefaultValidator<'a>>
+ Deserialize<T, SharedDeserializeMap>,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
let encoded = rkyv::to_bytes::<rancor::Error>(&self)
.map_err(|e| ServerFnError::Serialization(format!("{e:?}")))?;
let encoded = rkyv::to_bytes::<T, 1024>(&self)
.map_err(|e| ServerFnError::Serialization(e.to_string()))?;
let bytes = Bytes::copy_from_slice(encoded.as_ref());
Response::try_from_bytes(Rkyv::CONTENT_TYPE, bytes)
}
@@ -94,13 +88,14 @@ where
impl<CustErr, T, Response> FromRes<Rkyv, Response, CustErr> for T
where
Response: ClientRes<CustErr> + Send,
T: Archive + for<'a> Serialize<RkyvSerializer<'a>>,
T::Archived: Deserialize<T, RkyvDeserializer>
+ for<'a> CheckBytes<RkyvValidator<'a>>,
T: Serialize<AllocSerializer<1024>> + Send,
T: Archive,
T::Archived: for<'a> CheckBytes<DefaultValidator<'a>>
+ Deserialize<T, SharedDeserializeMap>,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<CustErr>> {
let data = res.try_into_bytes().await?;
rkyv::from_bytes::<T, rancor::Error>(&data)
rkyv::from_bytes::<T>(&data)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}

View File

@@ -505,7 +505,12 @@ pub fn server_macro_impl(
PathInfo::Serde => quote! {
#[serde(crate = #serde_path)]
},
PathInfo::Rkyv => quote! {},
PathInfo::Rkyv => {
let rkyv_path = format!("{server_fn_path}::rkyv");
quote! {
#[archive(crate = #rkyv_path, check_bytes)]
}
}
PathInfo::None => quote! {},
};

View File

@@ -11,26 +11,6 @@ use std::{
sync::Arc,
};
/// Declares that this type can be converted into some other type, which is a valid attribute value.
pub trait IntoAttributeValue {
/// The attribute value into which this type can be converted.
type Output;
/// Consumes this value, transforming it into an attribute value.
fn into_attribute_value(self) -> Self::Output;
}
impl<T> IntoAttributeValue for T
where
T: AttributeValue,
{
type Output = Self;
fn into_attribute_value(self) -> Self::Output {
self
}
}
/// A possible value for an HTML attribute.
pub trait AttributeValue: Send {
/// The state that should be retained between building and rebuilding.

View File

@@ -390,7 +390,7 @@ html_elements! {
/// The `<template>` HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.
template HtmlTemplateElement [] true,
/// The `<textarea>` HTML element represents a multi-line plain-text editing control, useful when you want to allow users to enter a sizeable amount of free-form text, for example a comment on a review or feedback form.
textarea HtmlTextAreaElement [autocomplete, cols, dirname, disabled, form, maxlength, minlength, name, placeholder, readonly, required, rows, wrap] false,
textarea HtmlTextAreaElement [autocomplete, cols, dirname, disabled, form, maxlength, minlength, name, placeholder, readonly, required, rows, wrap] true,
/// The `<tfoot>` HTML element defines a set of rows summarizing the columns of the table.
tfoot HtmlTableSectionElement [] true,
/// The `<th>` HTML element defines a cell as header of a group of table cells. The exact nature of this group is defined by the scope and headers attributes.

View File

@@ -4,8 +4,8 @@ use crate::{
renderer::{CastFrom, Rndr},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr, IntoRender, Mountable, Position, PositionState,
Render, RenderHtml, ToTemplate,
add_attr::AddAnyAttr, Mountable, Position, PositionState, Render,
RenderHtml, ToTemplate,
},
};
use const_str_slice_concat::{
@@ -65,13 +65,11 @@ impl<E, At, Ch, NewChild> ElementChild<NewChild> for HtmlElement<E, At, Ch>
where
E: ElementWithChildren,
Ch: Render + NextTuple,
<Ch as NextTuple>::Output<NewChild::Output>: Render,
<Ch as NextTuple>::Output<NewChild>: Render,
NewChild: IntoRender,
NewChild::Output: Render,
NewChild: Render,
{
type Output =
HtmlElement<E, At, <Ch as NextTuple>::Output<NewChild::Output>>;
type Output = HtmlElement<E, At, <Ch as NextTuple>::Output<NewChild>>;
fn child(self, child: NewChild) -> Self::Output {
let HtmlElement {
@@ -84,7 +82,7 @@ where
tag,
attributes,
children: children.next_tuple(child.into_render()),
children: children.next_tuple(child),
}
}
}
@@ -118,7 +116,7 @@ where
/// Adds a child to the element.
pub trait ElementChild<NewChild>
where
NewChild: IntoRender,
NewChild: Render,
{
/// The type of the element, with the child added.
type Output;

View File

@@ -19,7 +19,6 @@ pub mod prelude {
OnAttribute, OnTargetAttribute, PropAttribute,
StyleAttribute,
},
IntoAttributeValue,
},
directive::DirectiveAttribute,
element::{ElementChild, ElementExt, InnerHtmlAttribute},
@@ -27,8 +26,8 @@ pub mod prelude {
},
renderer::{dom::Dom, Renderer},
view::{
add_attr::AddAnyAttr, any_view::IntoAny, IntoRender, Mountable,
Render, RenderHtml,
add_attr::AddAnyAttr, any_view::IntoAny, Mountable, Render,
RenderHtml,
},
};
}

View File

@@ -41,7 +41,7 @@ where
///
/// Example:
///
/// ```ignore
/// ```
/// // You can use `RwSignal`s
/// let is_awesome = RwSignal::new(true);
///

View File

@@ -11,7 +11,7 @@ macro_rules! svg_elements {
($($tag:ident [$($attr:ty),*]),* $(,)?) => {
paste::paste! {
$(
/// An SVG element.
/// An SVG attribute.
// `tag()` function
#[allow(non_snake_case)]
pub fn $tag() -> HtmlElement<[<$tag:camel>], (), ()>
@@ -151,33 +151,4 @@ svg_elements![
view [],
];
/// An SVG element.
#[allow(non_snake_case)]
pub fn r#use() -> HtmlElement<Use, (), ()>
where {
HtmlElement {
tag: Use,
attributes: (),
children: (),
}
}
/// An SVG element.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Use;
impl ElementType for Use {
type Output = web_sys::SvgElement;
const TAG: &'static str = "use";
const SELF_CLOSING: bool = false;
const ESCAPE_CHILDREN: bool = true;
const NAMESPACE: Option<&'static str> = Some("http://www.w3.org/2000/svg");
#[inline(always)]
fn tag(&self) -> &str {
Self::TAG
}
}
impl ElementWithChildren for Use {}
// TODO <use>

View File

@@ -432,23 +432,3 @@ pub enum Position {
/// This is the last child of its parent.
LastChild,
}
/// Declares that this type can be converted into some other type, which can be renderered.
pub trait IntoRender {
/// The renderable type into which this type can be converted.
type Output;
/// Consumes this value, transforming it into the renderable type.
fn into_render(self) -> Self::Output;
}
impl<T> IntoRender for T
where
T: Render,
{
type Output = Self;
fn into_render(self) -> Self::Output {
self
}
}