work related to 0.7 blog port
This commit is contained in:
parent
c29081b12a
commit
b41fde3ff9
|
@ -25,7 +25,7 @@ use futures::Stream;
|
||||||
pub use hydrate::*;
|
pub use hydrate::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
pub use ssr::*;
|
pub use ssr::*;
|
||||||
use std::{fmt::Debug, future::Future, pin::Pin, sync::OnceLock};
|
use std::{fmt::Debug, future::Future, pin::Pin};
|
||||||
|
|
||||||
/// Type alias for a boxed [`Future`].
|
/// Type alias for a boxed [`Future`].
|
||||||
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;
|
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;
|
||||||
|
|
|
@ -98,7 +98,8 @@ impl SharedContext for SsrSharedContext {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, data)| async move {
|
.map(|(id, data)| async move {
|
||||||
let data = data.await;
|
let data = data.await;
|
||||||
format!("__RESOLVED_RESOURCES[{}] = {data:?};", id.0)
|
let data = data.replace('<', "\\u003c");
|
||||||
|
format!("__RESOLVED_RESOURCES[{}] = {:?};", id.0, data)
|
||||||
})
|
})
|
||||||
.collect::<FuturesUnordered<_>>();
|
.collect::<FuturesUnordered<_>>();
|
||||||
|
|
||||||
|
|
|
@ -9,34 +9,33 @@ description = "Axum integrations for the Leptos web framework."
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
any_spawner = { workspace = true, features = ["tokio"] }
|
||||||
|
hydration_context = { workspace = true }
|
||||||
axum = { version = "0.7", default-features = false, features = [
|
axum = { version = "0.7", default-features = false, features = [
|
||||||
"matched-path",
|
"matched-path",
|
||||||
] }
|
] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
http-body-util = "0.1"
|
http-body-util = "0.1"
|
||||||
leptos = { workspace = true, features = ["ssr"] }
|
leptos = { workspace = true, features = ["nonce", "hydration"] }
|
||||||
server_fn = { workspace = true, features = ["axum-no-default"] }
|
server_fn = { workspace = true, features = ["axum-no-default"] }
|
||||||
leptos_macro = { workspace = true, features = ["axum"] }
|
leptos_macro = { workspace = true, features = ["axum"] }
|
||||||
leptos_meta = { workspace = true }
|
leptos_meta = { workspace = true }
|
||||||
leptos_router = { workspace = true, features = ["ssr"] }
|
routing = { workspace = true }
|
||||||
leptos_integration_utils = { workspace = true }
|
#leptos_integration_utils = { workspace = true }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", default-features = false }
|
tokio = { version = "1", default-features = false }
|
||||||
tokio-util = { version = "0.7", features = ["rt"] }
|
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
once_cell = "1.18"
|
once_cell = "1.18"
|
||||||
cfg-if = "1.0"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
tokio = { version = "1", features = ["net"] }
|
tokio = { version = "1", features = ["net"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
nonce = ["leptos/nonce"]
|
|
||||||
wasm = []
|
wasm = []
|
||||||
default = ["tokio/fs", "tokio/sync"]
|
default = ["tokio/fs", "tokio/sync"]
|
||||||
experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
#experimental-islands = ["leptos_integration_utils/experimental-islands"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
//! - `default`: supports running in a typical native Tokio/Axum environment
|
//! - `default`: supports running in a typical native Tokio/Axum environment
|
||||||
//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
|
//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
|
||||||
//! environment
|
//! environment
|
||||||
//! - `nonce`: activates Leptos features that automatically provide a CSP [`Nonce`](leptos::nonce::Nonce) via context
|
|
||||||
//! - `experimental-islands`: activates Leptos [islands mode](https://leptos-rs.github.io/leptos/islands.html)
|
//! - `experimental-islands`: activates Leptos [islands mode](https://leptos-rs.github.io/leptos/islands.html)
|
||||||
//!
|
//!
|
||||||
//! ### Important Note
|
//! ### Important Note
|
||||||
|
@ -41,25 +40,38 @@ use axum::{
|
||||||
request::Parts,
|
request::Parts,
|
||||||
HeaderMap, Method, Request, Response, StatusCode,
|
HeaderMap, Method, Request, Response, StatusCode,
|
||||||
},
|
},
|
||||||
response::IntoResponse,
|
response::{Html, IntoResponse},
|
||||||
routing::{delete, get, patch, post, put},
|
routing::{delete, get, patch, post, put},
|
||||||
};
|
};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::mpsc::{Receiver, Sender},
|
channel::mpsc::{Receiver, Sender},
|
||||||
Future, SinkExt, Stream, StreamExt,
|
stream::once,
|
||||||
|
Future, FutureExt, SinkExt, Stream, StreamExt,
|
||||||
};
|
};
|
||||||
use leptos::{ssr::*, *};
|
use hydration_context::SsrSharedContext;
|
||||||
use leptos_integration_utils::{build_async_response, html_parts_separated};
|
use leptos::{
|
||||||
use leptos_meta::{generate_head_metadata_separated, MetaContext};
|
config::LeptosOptions,
|
||||||
use leptos_router::*;
|
context::{provide_context, use_context},
|
||||||
|
reactive_graph::{computed::ScopedFuture, owner::Owner},
|
||||||
|
IntoView,
|
||||||
|
};
|
||||||
|
use leptos_meta::{MetaContext, ServerMetaContext};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use server_fn::{
|
use routing::{
|
||||||
error::{NoCustomError, ServerFnErrorSerde},
|
location::RequestUrl, PathSegment, RouteList, RouteListing, SsrMode,
|
||||||
redirect::REDIRECT_HEADER,
|
StaticDataMap, StaticMode,
|
||||||
|
};
|
||||||
|
use server_fn::{
|
||||||
|
|
||||||
|
error::{NoCustomError, ServerFnErrorSerde},
|
||||||
|
redirect::REDIRECT_HEADER, ServerFnError,
|
||||||
|
,
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
collections::HashSet, fmt::Debug, io, pin::Pin, sync::Arc,
|
||||||
|
thread::available_parallelism,
|
||||||
};
|
};
|
||||||
use std::{fmt::Debug, io, pin::Pin, sync::Arc, thread::available_parallelism};
|
|
||||||
use tokio_util::task::LocalPoolHandle;
|
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
|
||||||
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
|
/// This struct lets you define headers and override the status of the Response from an Element or a Server Function
|
||||||
|
@ -217,23 +229,40 @@ pub async fn handle_server_fns(req: Request<Body>) -> impl IntoResponse {
|
||||||
handle_server_fns_inner(|| {}, req).await
|
handle_server_fns_inner(|| {}, req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_executor() {
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
let _ = leptos::Executor::init_wasm_bindgen();
|
||||||
|
#[cfg(all(not(feature = "wasm"), feature = "default"))]
|
||||||
|
let _ = leptos::Executor::init_tokio();
|
||||||
|
#[cfg(all(not(feature = "wasm"), not(feature = "default")))]
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"It appears you have set 'default-features = false' on \
|
||||||
|
'leptos_axum', but are not using the 'wasm' feature. Either \
|
||||||
|
remove 'default-features = false' or, if you are running in a \
|
||||||
|
JS-hosted WASM server environment, add the 'wasm' feature."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Leptos pool causes wasm to panic and leptos_reactive::spawn::spawn_local causes native
|
/// Leptos pool causes wasm to panic and leptos_reactive::spawn::spawn_local causes native
|
||||||
/// to panic so we define a macro to conditionally compile the correct code.
|
/// to panic so we define a macro to conditionally compile the correct code.
|
||||||
macro_rules! spawn_task {
|
macro_rules! spawn_task {
|
||||||
($block:expr) => {
|
($block:expr) => {
|
||||||
cfg_if::cfg_if! {
|
#[cfg(feature = "wasm")]
|
||||||
if #[cfg(feature = "wasm")] {
|
|
||||||
spawn_local($block);
|
spawn_local($block);
|
||||||
} else if #[cfg(feature = "default")] {
|
#[cfg(all(not(feature = "wasm"), feature = "default"))]
|
||||||
let pool_handle = get_leptos_pool();
|
spawn($block);
|
||||||
pool_handle.spawn_pinned(move || { $block });
|
#[cfg(all(not(feature = "wasm"), not(feature = "default")))]
|
||||||
} else {
|
{
|
||||||
eprintln!("It appears you have set 'default-features = false' on 'leptos_axum', \
|
eprintln!(
|
||||||
but are not using the 'wasm' feature. Either remove 'default-features = false' or, \
|
"It appears you have set 'default-features = false' on \
|
||||||
if you are running in a JS-hosted WASM server environment, add the 'wasm' feature.");
|
'leptos_axum', but are not using the 'wasm' feature. Either \
|
||||||
|
remove 'default-features = false' or, if you are running in \
|
||||||
|
a JS-hosted WASM server environment, add the 'wasm' feature."
|
||||||
|
);
|
||||||
spawn_local($block);
|
spawn_local($block);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,23 +301,16 @@ async fn handle_server_fns_inner(
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
use server_fn::middleware::Service;
|
use server_fn::middleware::Service;
|
||||||
|
|
||||||
let (tx, rx) = futures::channel::oneshot::channel();
|
|
||||||
|
|
||||||
// capture current span to enable trace context propagation
|
|
||||||
let current_span = tracing::Span::current();
|
|
||||||
|
|
||||||
spawn_task!(async move {
|
|
||||||
// enter captured span for trace context propagation in spawned task
|
|
||||||
let _guard = current_span.enter();
|
|
||||||
|
|
||||||
let path = req.uri().path().to_string();
|
let path = req.uri().path().to_string();
|
||||||
let (req, parts) = generate_request_and_parts(req);
|
let (req, parts) = generate_request_and_parts(req);
|
||||||
|
|
||||||
let res = if let Some(mut service) =
|
let res = if let Some(mut service) =
|
||||||
server_fn::axum::get_server_fn_service(&path)
|
server_fn::axum::get_server_fn_service(&path)
|
||||||
{
|
{
|
||||||
let runtime = create_runtime();
|
let owner = Owner::new();
|
||||||
|
owner
|
||||||
|
.with(|| {
|
||||||
|
ScopedFuture::new(async move {
|
||||||
additional_context();
|
additional_context();
|
||||||
provide_context(parts);
|
provide_context(parts);
|
||||||
provide_context(ResponseOptions::default());
|
provide_context(ResponseOptions::default());
|
||||||
|
@ -305,11 +327,22 @@ async fn handle_server_fns_inner(
|
||||||
// actually run the server fn
|
// actually run the server fn
|
||||||
let mut res = service.run(req).await;
|
let mut res = service.run(req).await;
|
||||||
|
|
||||||
// if it accepts text/html (i.e., is a plain form post) and doesn't already have a
|
// update response as needed
|
||||||
// Location set, then redirect to Referer
|
let res_options = use_context::<ResponseOptions>()
|
||||||
|
.expect("ResponseOptions not found")
|
||||||
|
.0;
|
||||||
|
let res_options_inner = res_options.read();
|
||||||
|
let (status, mut res_headers) = (
|
||||||
|
res_options_inner.status,
|
||||||
|
res_options_inner.headers.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// it it accepts text/html (i.e., is a plain form post) and doesn't already have a
|
||||||
|
// Location set, then redirect to to Referer
|
||||||
if accepts_html {
|
if accepts_html {
|
||||||
if let Some(referrer) = referrer {
|
if let Some(referrer) = referrer {
|
||||||
let has_location = res.headers().get(LOCATION).is_some();
|
let has_location =
|
||||||
|
res.headers().get(LOCATION).is_some();
|
||||||
if !has_location {
|
if !has_location {
|
||||||
*res.status_mut() = StatusCode::FOUND;
|
*res.status_mut() = StatusCode::FOUND;
|
||||||
res.headers_mut().insert(LOCATION, referrer);
|
res.headers_mut().insert(LOCATION, referrer);
|
||||||
|
@ -317,47 +350,34 @@ async fn handle_server_fns_inner(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update response as needed
|
|
||||||
if let Some(res_options) = use_context::<ResponseOptions>() {
|
|
||||||
let res_options_inner = res_options.0.read();
|
|
||||||
let (status, mut res_headers) = (
|
|
||||||
res_options_inner.status,
|
|
||||||
res_options_inner.headers.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// apply status code and headers if used changed them
|
// apply status code and headers if used changed them
|
||||||
if let Some(status) = status {
|
if let Some(status) = status {
|
||||||
*res.status_mut() = status;
|
*res.status_mut() = status;
|
||||||
}
|
}
|
||||||
res.headers_mut().extend(res_headers.drain());
|
res.headers_mut().extend(res_headers.drain());
|
||||||
} else {
|
|
||||||
eprintln!("Failed to find ResponseOptions for {path}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up the scope
|
|
||||||
runtime.dispose();
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
} else {
|
} else {
|
||||||
Response::builder().status(StatusCode::BAD_REQUEST).body(
|
Response::builder()
|
||||||
Body::from(format!(
|
.status(StatusCode::BAD_REQUEST)
|
||||||
|
.body(Body::from(format!(
|
||||||
"Could not find a server function at the route {path}. \
|
"Could not find a server function at the route {path}. \
|
||||||
\n\nIt's likely that either
|
\n\nIt's likely that either
|
||||||
1. The API prefix you specify in the `#[server]` \
|
1. The API prefix you specify in the `#[server]` \
|
||||||
macro doesn't match the prefix at which your server \
|
macro doesn't match the prefix at which your server function \
|
||||||
function handler is mounted, or \n2. You are on a \
|
handler is mounted, or \n2. You are on a platform that \
|
||||||
platform that doesn't support automatic server function \
|
doesn't support automatic server function registration and \
|
||||||
registration and you need to call \
|
you need to call ServerFn::register_explicit() on the server \
|
||||||
ServerFn::register_explicit() on the server function \
|
function type, somewhere in your `main` function.",
|
||||||
type, somewhere in your `main` function.",
|
)))
|
||||||
)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.expect("could not build Response");
|
.expect("could not build Response");
|
||||||
|
|
||||||
_ = tx.send(res);
|
res
|
||||||
});
|
|
||||||
|
|
||||||
rx.await.unwrap_or_else(|e| {
|
/*rx.await.unwrap_or_else(|e| {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ServerFnErrorSerde::ser(
|
ServerFnErrorSerde::ser(
|
||||||
|
@ -366,7 +386,7 @@ async fn handle_server_fns_inner(
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
.into_response()
|
.into_response()
|
||||||
})
|
})*/
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PinnedHtmlStream =
|
pub type PinnedHtmlStream =
|
||||||
|
@ -441,12 +461,12 @@ where
|
||||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||||
/// to route it using [leptos_router], serving an HTML stream of your application.
|
/// to route it using [leptos_router], serving an HTML stream of your application.
|
||||||
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
|
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
|
||||||
/// one respects the `SsrMode` on each Route and thus requires `Vec<RouteListing>` for route checking.
|
/// one respects the `SsrMode` on each Route and thus requires `Vec<AxumRouteListing>` for route checking.
|
||||||
/// This is useful if you are using `.leptos_routes_with_handler()`
|
/// This is useful if you are using `.leptos_routes_with_handler()`
|
||||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||||
pub fn render_route<IV>(
|
pub fn render_route<IV>(
|
||||||
options: LeptosOptions,
|
options: LeptosOptions,
|
||||||
paths: Vec<RouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
) -> impl Fn(
|
) -> impl Fn(
|
||||||
Request<Body>,
|
Request<Body>,
|
||||||
|
@ -579,12 +599,12 @@ where
|
||||||
/// to route it using [leptos_router], serving an HTML stream of your application. It allows you
|
/// to route it using [leptos_router], serving an HTML stream of your application. It allows you
|
||||||
/// to pass in a context function with additional info to be made available to the app
|
/// to pass in a context function with additional info to be made available to the app
|
||||||
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
|
/// The difference between calling this and `render_app_to_stream_with_context()` is that this
|
||||||
/// one respects the `SsrMode` on each Route, and thus requires `Vec<RouteListing>` for route checking.
|
/// one respects the `SsrMode` on each Route, and thus requires `Vec<AxumRouteListing>` for route checking.
|
||||||
/// This is useful if you are using `.leptos_routes_with_handler()`.
|
/// This is useful if you are using `.leptos_routes_with_handler()`.
|
||||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||||
pub fn render_route_with_context<IV>(
|
pub fn render_route_with_context<IV>(
|
||||||
options: LeptosOptions,
|
options: LeptosOptions,
|
||||||
paths: Vec<RouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
additional_context: impl Fn() + 'static + Clone + Send,
|
additional_context: impl Fn() + 'static + Clone + Send,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
) -> impl Fn(
|
) -> impl Fn(
|
||||||
|
@ -627,8 +647,11 @@ where
|
||||||
.as_str();
|
.as_str();
|
||||||
// 2. Find RouteListing in paths. This should probably be optimized, we probably don't want to
|
// 2. Find RouteListing in paths. This should probably be optimized, we probably don't want to
|
||||||
// search for this every time
|
// search for this every time
|
||||||
let listing: &RouteListing =
|
let listing: &AxumRouteListing = paths
|
||||||
paths.iter().find(|r| r.path() == path).unwrap_or_else(|| {
|
.iter()
|
||||||
|
// TODO this should be cached rather than recalculating the Axum version of the path
|
||||||
|
.find(|r| r.path() == path)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
panic!(
|
panic!(
|
||||||
"Failed to find the route {path} requested by the user. \
|
"Failed to find the route {path} requested by the user. \
|
||||||
This suggests that the routing rules in the Router that \
|
This suggests that the routing rules in the Router that \
|
||||||
|
@ -681,18 +704,17 @@ where
|
||||||
IV: IntoView,
|
IV: IntoView,
|
||||||
{
|
{
|
||||||
move |req: Request<Body>| {
|
move |req: Request<Body>| {
|
||||||
Box::pin({
|
|
||||||
let options = options.clone();
|
let options = options.clone();
|
||||||
let app_fn = app_fn.clone();
|
let app_fn = app_fn.clone();
|
||||||
let add_context = additional_context.clone();
|
let add_context = additional_context.clone();
|
||||||
let default_res_options = ResponseOptions::default();
|
let default_res_options = ResponseOptions::default();
|
||||||
let res_options2 = default_res_options.clone();
|
let res_options2 = default_res_options.clone();
|
||||||
let res_options3 = default_res_options.clone();
|
let res_options3 = default_res_options.clone();
|
||||||
let (tx, rx) = futures::channel::mpsc::channel(8);
|
|
||||||
|
|
||||||
let current_span = tracing::Span::current();
|
Box::pin(async move {
|
||||||
spawn_task!(async move {
|
let owner = Owner::new_root(Arc::new(SsrSharedContext::new()));
|
||||||
let app = {
|
let meta_context = ServerMetaContext::new();
|
||||||
|
let stream = owner.with(|| {
|
||||||
// Need to get the path and query string of the Request
|
// Need to get the path and query string of the Request
|
||||||
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
// For reasons that escape me, if the incoming URI protocol is https, it provides the absolute URI
|
||||||
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
|
// if http, it returns a relative path. Adding .path() seems to make it explicitly return the relative uri
|
||||||
|
@ -700,25 +722,44 @@ where
|
||||||
|
|
||||||
let full_path = format!("http://leptos.dev{path}");
|
let full_path = format!("http://leptos.dev{path}");
|
||||||
let (_, req_parts) = generate_request_and_parts(req);
|
let (_, req_parts) = generate_request_and_parts(req);
|
||||||
move || {
|
provide_contexts(
|
||||||
provide_contexts(full_path, req_parts, default_res_options);
|
&full_path,
|
||||||
app_fn().into_view()
|
&meta_context,
|
||||||
}
|
req_parts,
|
||||||
};
|
default_res_options,
|
||||||
let (bundle, runtime) =
|
|
||||||
leptos::leptos_dom::ssr::render_to_stream_with_prefix_undisposed_with_context_and_block_replacement(
|
|
||||||
app,
|
|
||||||
|| generate_head_metadata_separated().1.into(),
|
|
||||||
add_context,
|
|
||||||
replace_blocks
|
|
||||||
);
|
);
|
||||||
|
println!("providing additional contexts");
|
||||||
|
add_context();
|
||||||
|
|
||||||
forward_stream(&options, res_options2, bundle, tx).await;
|
// run app
|
||||||
|
let app = app_fn();
|
||||||
|
|
||||||
runtime.dispose();
|
// convert app to appropriate response type
|
||||||
}.instrument(current_span));
|
let app_stream = app.to_html_stream_out_of_order();
|
||||||
|
|
||||||
generate_response(res_options3, rx)
|
// TODO nonce
|
||||||
|
|
||||||
|
let shared_context = Owner::current_shared_context().unwrap();
|
||||||
|
let shared_context = shared_context
|
||||||
|
.pending_data()
|
||||||
|
.unwrap()
|
||||||
|
.map(|chunk| format!("<script>{chunk}</script>"));
|
||||||
|
futures::stream::select(app_stream, shared_context)
|
||||||
|
});
|
||||||
|
|
||||||
|
let stream = meta_context.inject_meta_context(stream).await;
|
||||||
|
|
||||||
|
Html(Body::from_stream(
|
||||||
|
stream
|
||||||
|
.map(|chunk| Ok(chunk) as Result<String, std::io::Error>)
|
||||||
|
// drop the owner, cleaning up the reactive runtime,
|
||||||
|
// once the stream is over
|
||||||
|
.chain(once(async move {
|
||||||
|
//drop(owner);
|
||||||
|
Ok(Default::default())
|
||||||
|
})),
|
||||||
|
))
|
||||||
|
.into_response()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -728,6 +769,7 @@ async fn generate_response(
|
||||||
res_options: ResponseOptions,
|
res_options: ResponseOptions,
|
||||||
rx: Receiver<String>,
|
rx: Receiver<String>,
|
||||||
) -> Response<Body> {
|
) -> Response<Body> {
|
||||||
|
todo!() /*
|
||||||
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
|
let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html))));
|
||||||
|
|
||||||
// Get the first and second chunks in the stream, which renders the app shell, and thus allows Resources to run
|
// Get the first and second chunks in the stream, which renders the app shell, and thus allows Resources to run
|
||||||
|
@ -763,7 +805,7 @@ async fn generate_response(
|
||||||
HeaderValue::from_str("text/html; charset=utf-8").unwrap(),
|
HeaderValue::from_str("text/html; charset=utf-8").unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
res
|
res*/
|
||||||
}
|
}
|
||||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||||
async fn forward_stream(
|
async fn forward_stream(
|
||||||
|
@ -772,7 +814,7 @@ async fn forward_stream(
|
||||||
bundle: impl Stream<Item = String> + 'static,
|
bundle: impl Stream<Item = String> + 'static,
|
||||||
mut tx: Sender<String>,
|
mut tx: Sender<String>,
|
||||||
) {
|
) {
|
||||||
let mut shell = Box::pin(bundle);
|
/*let mut shell = Box::pin(bundle);
|
||||||
let first_app_chunk = shell.next().await.unwrap_or_default();
|
let first_app_chunk = shell.next().await.unwrap_or_default();
|
||||||
|
|
||||||
let (head, tail) =
|
let (head, tail) =
|
||||||
|
@ -796,7 +838,7 @@ async fn forward_stream(
|
||||||
let mut writable = res_options2.0.write();
|
let mut writable = res_options2.0.write();
|
||||||
*writable = new_res_parts;
|
*writable = new_res_parts;
|
||||||
|
|
||||||
tx.close_channel();
|
tx.close_channel();*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||||
|
@ -840,6 +882,8 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
|
||||||
where
|
where
|
||||||
IV: IntoView,
|
IV: IntoView,
|
||||||
{
|
{
|
||||||
|
|req| todo!()
|
||||||
|
/*
|
||||||
move |req: Request<Body>| {
|
move |req: Request<Body>| {
|
||||||
Box::pin({
|
Box::pin({
|
||||||
let options = options.clone();
|
let options = options.clone();
|
||||||
|
@ -884,21 +928,22 @@ where
|
||||||
generate_response(res_options3, rx).await
|
generate_response(res_options3, rx).await
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||||
fn provide_contexts(
|
fn provide_contexts(
|
||||||
path: String,
|
path: &str,
|
||||||
|
meta_context: &ServerMetaContext,
|
||||||
parts: Parts,
|
parts: Parts,
|
||||||
default_res_options: ResponseOptions,
|
default_res_options: ResponseOptions,
|
||||||
) {
|
) {
|
||||||
let integration = ServerIntegration { path };
|
provide_context(RequestUrl::new(path));
|
||||||
provide_context(RouterIntegrationContext::new(integration));
|
provide_context(meta_context.clone());
|
||||||
provide_context(MetaContext::new());
|
|
||||||
provide_context(parts);
|
provide_context(parts);
|
||||||
provide_context(default_res_options);
|
provide_context(default_res_options);
|
||||||
provide_server_redirect(redirect);
|
// TODO server redirect
|
||||||
|
// provide_server_redirect(redirect);
|
||||||
#[cfg(feature = "nonce")]
|
#[cfg(feature = "nonce")]
|
||||||
leptos::nonce::provide_nonce();
|
leptos::nonce::provide_nonce();
|
||||||
}
|
}
|
||||||
|
@ -1013,7 +1058,8 @@ where
|
||||||
IV: IntoView,
|
IV: IntoView,
|
||||||
{
|
{
|
||||||
move |req: Request<Body>| {
|
move |req: Request<Body>| {
|
||||||
Box::pin({
|
todo!()
|
||||||
|
/*Box::pin({
|
||||||
let options = options.clone();
|
let options = options.clone();
|
||||||
let app_fn = app_fn.clone();
|
let app_fn = app_fn.clone();
|
||||||
let add_context = additional_context.clone();
|
let add_context = additional_context.clone();
|
||||||
|
@ -1097,7 +1143,7 @@ where
|
||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
})
|
})*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1141,7 +1187,8 @@ pub fn render_app_async_with_context<IV>(
|
||||||
where
|
where
|
||||||
IV: IntoView,
|
IV: IntoView,
|
||||||
{
|
{
|
||||||
move |req: Request<Body>| {
|
|_| todo!()
|
||||||
|
/* move |req: Request<Body>| {
|
||||||
Box::pin({
|
Box::pin({
|
||||||
let options = options.clone();
|
let options = options.clone();
|
||||||
let app_fn = app_fn.clone();
|
let app_fn = app_fn.clone();
|
||||||
|
@ -1210,7 +1257,7 @@ where
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||||
|
@ -1219,7 +1266,7 @@ where
|
||||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||||
pub fn generate_route_list<IV>(
|
pub fn generate_route_list<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||||
) -> Vec<RouteListing>
|
) -> Vec<AxumRouteListing>
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -1232,7 +1279,7 @@ where
|
||||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||||
pub fn generate_route_list_with_ssg<IV>(
|
pub fn generate_route_list_with_ssg<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||||
) -> (Vec<RouteListing>, StaticDataMap)
|
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -1247,7 +1294,7 @@ where
|
||||||
pub fn generate_route_list_with_exclusions<IV>(
|
pub fn generate_route_list_with_exclusions<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||||
excluded_routes: Option<Vec<String>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
) -> Vec<RouteListing>
|
) -> Vec<AxumRouteListing>
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -1263,6 +1310,8 @@ pub async fn build_static_routes<IV>(
|
||||||
) where
|
) where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
todo!()
|
||||||
|
/*
|
||||||
let options = options.clone();
|
let options = options.clone();
|
||||||
let routes = routes.to_owned();
|
let routes = routes.to_owned();
|
||||||
spawn_task!(async move {
|
spawn_task!(async move {
|
||||||
|
@ -1274,7 +1323,7 @@ pub async fn build_static_routes<IV>(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("could not build static routes")
|
.expect("could not build static routes")
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||||
|
@ -1285,7 +1334,7 @@ pub async fn build_static_routes<IV>(
|
||||||
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
|
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||||
excluded_routes: Option<Vec<String>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
) -> (Vec<RouteListing>, StaticDataMap)
|
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
@ -1296,6 +1345,73 @@ where
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
/// A route that this application can serve.
|
||||||
|
pub struct AxumRouteListing {
|
||||||
|
path: String,
|
||||||
|
mode: SsrMode,
|
||||||
|
methods: Vec<routing::Method>,
|
||||||
|
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RouteListing> for AxumRouteListing {
|
||||||
|
fn from(value: RouteListing) -> Self {
|
||||||
|
let path = value.path().to_axum_path();
|
||||||
|
let path = if path.is_empty() {
|
||||||
|
"/".to_string()
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
};
|
||||||
|
let mode = value.mode();
|
||||||
|
let methods = value.methods().collect();
|
||||||
|
let static_mode = value.into_static_parts();
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
mode,
|
||||||
|
methods,
|
||||||
|
static_mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxumRouteListing {
|
||||||
|
/// Create a route listing from its parts.
|
||||||
|
pub fn new(
|
||||||
|
path: String,
|
||||||
|
mode: SsrMode,
|
||||||
|
methods: impl IntoIterator<Item = routing::Method>,
|
||||||
|
static_mode: Option<(StaticMode, StaticDataMap)>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
mode,
|
||||||
|
methods: methods.into_iter().collect(),
|
||||||
|
static_mode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The path this route handles.
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rendering mode for this path.
|
||||||
|
pub fn mode(&self) -> SsrMode {
|
||||||
|
self.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The HTTP request methods this path can handle.
|
||||||
|
pub fn methods(&self) -> impl Iterator<Item = routing::Method> + '_ {
|
||||||
|
self.methods.iter().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this route is statically rendered.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn static_mode(&self) -> Option<StaticMode> {
|
||||||
|
self.static_mode.as_ref().map(|n| n.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
/// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically
|
||||||
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
/// create routes in Axum's Router without having to use wildcard matching or fallbacks. Takes in your root app Element
|
||||||
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
|
/// as an argument so it can walk you app tree. This version is tailored to generate Axum compatible paths. Adding excluded_routes
|
||||||
|
@ -1306,41 +1422,35 @@ pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
|
||||||
app_fn: impl Fn() -> IV + 'static + Clone,
|
app_fn: impl Fn() -> IV + 'static + Clone,
|
||||||
excluded_routes: Option<Vec<String>>,
|
excluded_routes: Option<Vec<String>>,
|
||||||
additional_context: impl Fn() + 'static + Clone,
|
additional_context: impl Fn() + 'static + Clone,
|
||||||
) -> (Vec<RouteListing>, StaticDataMap)
|
) -> (Vec<AxumRouteListing>, StaticDataMap)
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
let (routes, static_data_map) =
|
init_executor();
|
||||||
leptos_router::generate_route_list_inner_with_context(
|
|
||||||
app_fn,
|
let owner = Owner::new();
|
||||||
additional_context,
|
let routes = owner
|
||||||
);
|
.with(|| {
|
||||||
|
// stub out a path for now
|
||||||
|
provide_context(RequestUrl::new(""));
|
||||||
|
additional_context();
|
||||||
|
RouteList::generate(&app_fn)
|
||||||
|
})
|
||||||
|
.expect("could not generate routes");
|
||||||
|
|
||||||
// Axum's Router defines Root routes as "/" not ""
|
// Axum's Router defines Root routes as "/" not ""
|
||||||
let mut routes = routes
|
let mut routes = routes
|
||||||
|
.into_inner()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|listing| {
|
.map(AxumRouteListing::from)
|
||||||
let path = listing.path();
|
|
||||||
if path.is_empty() {
|
|
||||||
RouteListing::new(
|
|
||||||
"/".to_string(),
|
|
||||||
listing.path(),
|
|
||||||
listing.mode(),
|
|
||||||
listing.methods(),
|
|
||||||
listing.static_mode(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listing
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
(
|
(
|
||||||
if routes.is_empty() {
|
if routes.is_empty() {
|
||||||
vec![RouteListing::new(
|
vec![AxumRouteListing::new(
|
||||||
"/",
|
"/".to_string(),
|
||||||
"",
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
[leptos_router::Method::Get],
|
[routing::Method::Get],
|
||||||
None,
|
None,
|
||||||
)]
|
)]
|
||||||
} else {
|
} else {
|
||||||
|
@ -1351,7 +1461,8 @@ where
|
||||||
}
|
}
|
||||||
routes
|
routes
|
||||||
},
|
},
|
||||||
static_data_map,
|
StaticDataMap::new(), // TODO
|
||||||
|
//static_data_map,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1364,7 +1475,7 @@ where
|
||||||
fn leptos_routes<IV>(
|
fn leptos_routes<IV>(
|
||||||
self,
|
self,
|
||||||
options: &S,
|
options: &S,
|
||||||
paths: Vec<RouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
|
@ -1373,7 +1484,7 @@ where
|
||||||
fn leptos_routes_with_context<IV>(
|
fn leptos_routes_with_context<IV>(
|
||||||
self,
|
self,
|
||||||
options: &S,
|
options: &S,
|
||||||
paths: Vec<RouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
additional_context: impl Fn() + 'static + Clone + Send,
|
additional_context: impl Fn() + 'static + Clone + Send,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
|
@ -1382,14 +1493,14 @@ where
|
||||||
|
|
||||||
fn leptos_routes_with_handler<H, T>(
|
fn leptos_routes_with_handler<H, T>(
|
||||||
self,
|
self,
|
||||||
paths: Vec<RouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
handler: H,
|
handler: H,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
H: axum::handler::Handler<T, S>,
|
H: axum::handler::Handler<T, S>,
|
||||||
T: 'static;
|
T: 'static;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
fn handle_static_response<IV>(
|
fn handle_static_response<IV>(
|
||||||
path: String,
|
path: String,
|
||||||
|
@ -1486,7 +1597,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}*/
|
||||||
|
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
fn static_route<IV, S>(
|
fn static_route<IV, S>(
|
||||||
|
@ -1495,14 +1606,15 @@ fn static_route<IV, S>(
|
||||||
options: LeptosOptions,
|
options: LeptosOptions,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
additional_context: impl Fn() + Clone + Send + 'static,
|
additional_context: impl Fn() + Clone + Send + 'static,
|
||||||
method: leptos_router::Method,
|
method: routing::Method,
|
||||||
mode: StaticMode,
|
mode: StaticMode,
|
||||||
) -> axum::Router<S>
|
) -> axum::Router<S>
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
match mode {
|
todo!()
|
||||||
|
/*match mode {
|
||||||
StaticMode::Incremental => {
|
StaticMode::Incremental => {
|
||||||
let handler = move |req: Request<Body>| {
|
let handler = move |req: Request<Body>| {
|
||||||
Box::pin({
|
Box::pin({
|
||||||
|
@ -1538,11 +1650,11 @@ where
|
||||||
router.route(
|
router.route(
|
||||||
path,
|
path,
|
||||||
match method {
|
match method {
|
||||||
leptos_router::Method::Get => get(handler),
|
routing::Method::Get => get(handler),
|
||||||
leptos_router::Method::Post => post(handler),
|
routing::Method::Post => post(handler),
|
||||||
leptos_router::Method::Put => put(handler),
|
routing::Method::Put => put(handler),
|
||||||
leptos_router::Method::Delete => delete(handler),
|
routing::Method::Delete => delete(handler),
|
||||||
leptos_router::Method::Patch => patch(handler),
|
routing::Method::Patch => patch(handler),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1581,14 +1693,42 @@ where
|
||||||
router.route(
|
router.route(
|
||||||
path,
|
path,
|
||||||
match method {
|
match method {
|
||||||
leptos_router::Method::Get => get(handler),
|
routing::Method::Get => get(handler),
|
||||||
leptos_router::Method::Post => post(handler),
|
routing::Method::Post => post(handler),
|
||||||
leptos_router::Method::Put => put(handler),
|
routing::Method::Put => put(handler),
|
||||||
leptos_router::Method::Delete => delete(handler),
|
routing::Method::Delete => delete(handler),
|
||||||
leptos_router::Method::Patch => patch(handler),
|
routing::Method::Patch => patch(handler),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
trait AxumPath {
|
||||||
|
fn to_axum_path(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxumPath for &[PathSegment] {
|
||||||
|
fn to_axum_path(&self) -> String {
|
||||||
|
let mut path = String::new();
|
||||||
|
for segment in self.iter() {
|
||||||
|
if !segment.as_raw_str().starts_with('/') {
|
||||||
|
path.push('/');
|
||||||
|
}
|
||||||
|
match segment {
|
||||||
|
PathSegment::Static(s) => path.push_str(s),
|
||||||
|
PathSegment::Param(s) => {
|
||||||
|
path.push(':');
|
||||||
|
path.push_str(s);
|
||||||
|
}
|
||||||
|
PathSegment::Splat(s) => {
|
||||||
|
path.push('*');
|
||||||
|
path.push_str(s);
|
||||||
|
}
|
||||||
|
PathSegment::Unit => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1603,7 +1743,7 @@ where
|
||||||
fn leptos_routes<IV>(
|
fn leptos_routes<IV>(
|
||||||
self,
|
self,
|
||||||
options: &S,
|
options: &S,
|
||||||
paths: Vec<RouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
|
@ -1616,13 +1756,15 @@ where
|
||||||
fn leptos_routes_with_context<IV>(
|
fn leptos_routes_with_context<IV>(
|
||||||
self,
|
self,
|
||||||
options: &S,
|
options: &S,
|
||||||
paths: Vec<RouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
additional_context: impl Fn() + 'static + Clone + Send,
|
additional_context: impl Fn() + 'static + Clone + Send,
|
||||||
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
app_fn: impl Fn() -> IV + Clone + Send + 'static,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
IV: IntoView + 'static,
|
IV: IntoView + 'static,
|
||||||
{
|
{
|
||||||
|
init_executor();
|
||||||
|
|
||||||
// S represents the router's finished state allowing us to provide
|
// S represents the router's finished state allowing us to provide
|
||||||
// it to the user's server functions.
|
// it to the user's server functions.
|
||||||
let state = options.clone();
|
let state = options.clone();
|
||||||
|
@ -1672,7 +1814,7 @@ where
|
||||||
{
|
{
|
||||||
static_route(
|
static_route(
|
||||||
router,
|
router,
|
||||||
path,
|
&path,
|
||||||
LeptosOptions::from_ref(options),
|
LeptosOptions::from_ref(options),
|
||||||
app_fn.clone(),
|
app_fn.clone(),
|
||||||
cx_with_state_and_method.clone(),
|
cx_with_state_and_method.clone(),
|
||||||
|
@ -1690,7 +1832,7 @@ where
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
router.route(
|
router.route(
|
||||||
path,
|
&path,
|
||||||
match listing.mode() {
|
match listing.mode() {
|
||||||
SsrMode::OutOfOrder => {
|
SsrMode::OutOfOrder => {
|
||||||
let s = render_app_to_stream_with_context(
|
let s = render_app_to_stream_with_context(
|
||||||
|
@ -1699,11 +1841,11 @@ where
|
||||||
app_fn.clone(),
|
app_fn.clone(),
|
||||||
);
|
);
|
||||||
match method {
|
match method {
|
||||||
leptos_router::Method::Get => get(s),
|
routing::Method::Get => get(s),
|
||||||
leptos_router::Method::Post => post(s),
|
routing::Method::Post => post(s),
|
||||||
leptos_router::Method::Put => put(s),
|
routing::Method::Put => put(s),
|
||||||
leptos_router::Method::Delete => delete(s),
|
routing::Method::Delete => delete(s),
|
||||||
leptos_router::Method::Patch => patch(s),
|
routing::Method::Patch => patch(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SsrMode::PartiallyBlocked => {
|
SsrMode::PartiallyBlocked => {
|
||||||
|
@ -1714,11 +1856,11 @@ where
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
match method {
|
match method {
|
||||||
leptos_router::Method::Get => get(s),
|
routing::Method::Get => get(s),
|
||||||
leptos_router::Method::Post => post(s),
|
routing::Method::Post => post(s),
|
||||||
leptos_router::Method::Put => put(s),
|
routing::Method::Put => put(s),
|
||||||
leptos_router::Method::Delete => delete(s),
|
routing::Method::Delete => delete(s),
|
||||||
leptos_router::Method::Patch => patch(s),
|
routing::Method::Patch => patch(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SsrMode::InOrder => {
|
SsrMode::InOrder => {
|
||||||
|
@ -1728,11 +1870,11 @@ where
|
||||||
app_fn.clone(),
|
app_fn.clone(),
|
||||||
);
|
);
|
||||||
match method {
|
match method {
|
||||||
leptos_router::Method::Get => get(s),
|
routing::Method::Get => get(s),
|
||||||
leptos_router::Method::Post => post(s),
|
routing::Method::Post => post(s),
|
||||||
leptos_router::Method::Put => put(s),
|
routing::Method::Put => put(s),
|
||||||
leptos_router::Method::Delete => delete(s),
|
routing::Method::Delete => delete(s),
|
||||||
leptos_router::Method::Patch => patch(s),
|
routing::Method::Patch => patch(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SsrMode::Async => {
|
SsrMode::Async => {
|
||||||
|
@ -1742,11 +1884,11 @@ where
|
||||||
app_fn.clone(),
|
app_fn.clone(),
|
||||||
);
|
);
|
||||||
match method {
|
match method {
|
||||||
leptos_router::Method::Get => get(s),
|
routing::Method::Get => get(s),
|
||||||
leptos_router::Method::Post => post(s),
|
routing::Method::Post => post(s),
|
||||||
leptos_router::Method::Put => put(s),
|
routing::Method::Put => put(s),
|
||||||
leptos_router::Method::Delete => delete(s),
|
routing::Method::Delete => delete(s),
|
||||||
leptos_router::Method::Patch => patch(s),
|
routing::Method::Patch => patch(s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1761,7 +1903,7 @@ where
|
||||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||||
fn leptos_routes_with_handler<H, T>(
|
fn leptos_routes_with_handler<H, T>(
|
||||||
self,
|
self,
|
||||||
paths: Vec<RouteListing>,
|
paths: Vec<AxumRouteListing>,
|
||||||
handler: H,
|
handler: H,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
|
@ -1774,13 +1916,11 @@ where
|
||||||
router = router.route(
|
router = router.route(
|
||||||
listing.path(),
|
listing.path(),
|
||||||
match method {
|
match method {
|
||||||
leptos_router::Method::Get => get(handler.clone()),
|
routing::Method::Get => get(handler.clone()),
|
||||||
leptos_router::Method::Post => post(handler.clone()),
|
routing::Method::Post => post(handler.clone()),
|
||||||
leptos_router::Method::Put => put(handler.clone()),
|
routing::Method::Put => put(handler.clone()),
|
||||||
leptos_router::Method::Delete => {
|
routing::Method::Delete => delete(handler.clone()),
|
||||||
delete(handler.clone())
|
routing::Method::Patch => patch(handler.clone()),
|
||||||
}
|
|
||||||
leptos_router::Method::Patch => patch(handler.clone()),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1789,18 +1929,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
|
||||||
fn get_leptos_pool() -> LocalPoolHandle {
|
|
||||||
static LOCAL_POOL: OnceCell<LocalPoolHandle> = OnceCell::new();
|
|
||||||
LOCAL_POOL
|
|
||||||
.get_or_init(|| {
|
|
||||||
tokio_util::task::LocalPoolHandle::new(
|
|
||||||
available_parallelism().map(Into::into).unwrap_or(1),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A helper to make it easier to use Axum extractors in server functions.
|
/// A helper to make it easier to use Axum extractors in server functions.
|
||||||
///
|
///
|
||||||
/// It is generic over some type `T` that implements [`FromRequestParts`] and can
|
/// It is generic over some type `T` that implements [`FromRequestParts`] and can
|
||||||
|
|
|
@ -10,7 +10,7 @@ rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
leptos = { workspace = true, features = ["ssr"] }
|
leptos = { workspace = true }
|
||||||
leptos_hot_reload = { workspace = true }
|
leptos_hot_reload = { workspace = true }
|
||||||
leptos_meta = { workspace = true }
|
leptos_meta = { workspace = true }
|
||||||
leptos_config = { workspace = true }
|
leptos_config = { workspace = true }
|
||||||
|
|
|
@ -7,20 +7,23 @@ license = "MIT"
|
||||||
repository = "https://github.com/leptos-rs/leptos"
|
repository = "https://github.com/leptos-rs/leptos"
|
||||||
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
|
description = "Leptos is a full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces."
|
||||||
readme = "../README.md"
|
readme = "../README.md"
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
|
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
|
base64 = { version = "0.22", optional = true }
|
||||||
cfg-if = "1"
|
cfg-if = "1"
|
||||||
|
hydration_context = { workspace = true, optional = true }
|
||||||
leptos_dom = { workspace = true }
|
leptos_dom = { workspace = true }
|
||||||
leptos_macro = { workspace = true }
|
leptos_macro = { workspace = true }
|
||||||
leptos_reactive = { workspace = true }
|
leptos_reactive = { workspace = true }
|
||||||
leptos_server = { workspace = true }
|
leptos_server = { workspace = true }
|
||||||
leptos_config = { workspace = true }
|
leptos_config = { workspace = true }
|
||||||
leptos-spin-macro = { git = "https://github.com/fermyon/leptos-spin", optional = true }
|
leptos-spin-macro = { version = "0.1", optional = true }
|
||||||
oco = { workspace = true }
|
|
||||||
paste = "1"
|
paste = "1"
|
||||||
|
rand = { version = "0.8", optional = true }
|
||||||
reactive_graph = { workspace = true, features = ["serde"] }
|
reactive_graph = { workspace = true, features = ["serde"] }
|
||||||
tachys = { workspace = true, features = ["oco", "reactive_graph"] }
|
tachys = { workspace = true, features = ["reactive_graph"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
typed-builder = "0.18"
|
typed-builder = "0.18"
|
||||||
typed-builder-macro = "0.18"
|
typed-builder-macro = "0.18"
|
||||||
|
@ -37,7 +40,7 @@ web-sys = { version = "0.3.63", features = [
|
||||||
"ShadowRootInit",
|
"ShadowRootInit",
|
||||||
"ShadowRootMode",
|
"ShadowRootMode",
|
||||||
] }
|
] }
|
||||||
wasm-bindgen = { version = "0.2" }
|
wasm-bindgen = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serde"]
|
default = ["serde"]
|
||||||
|
@ -94,6 +97,8 @@ denylist = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"rkyv", # was causing clippy issues on nightly
|
"rkyv", # was causing clippy issues on nightly
|
||||||
"trace-component-props",
|
"trace-component-props",
|
||||||
|
"spin",
|
||||||
|
"experimental-islands",
|
||||||
]
|
]
|
||||||
skip_feature_sets = [
|
skip_feature_sets = [
|
||||||
[
|
[
|
||||||
|
|
|
@ -156,6 +156,8 @@ pub mod children;
|
||||||
pub mod component;
|
pub mod component;
|
||||||
mod for_loop;
|
mod for_loop;
|
||||||
mod hydration_scripts;
|
mod hydration_scripts;
|
||||||
|
#[cfg(feature = "nonce")]
|
||||||
|
pub mod nonce;
|
||||||
mod show;
|
mod show;
|
||||||
pub mod text_prop;
|
pub mod text_prop;
|
||||||
pub use for_loop::*;
|
pub use for_loop::*;
|
||||||
|
@ -165,7 +167,7 @@ pub use reactive_graph::{
|
||||||
self,
|
self,
|
||||||
signal::{arc_signal, create_signal, signal},
|
signal::{arc_signal, create_signal, signal},
|
||||||
};
|
};
|
||||||
pub use server_fn::error;
|
pub use server_fn::{self, error};
|
||||||
pub use show::*;
|
pub use show::*;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use typed_builder;
|
pub use typed_builder;
|
||||||
|
@ -175,12 +177,28 @@ mod into_view;
|
||||||
pub use into_view::IntoView;
|
pub use into_view::IntoView;
|
||||||
pub use leptos_dom;
|
pub use leptos_dom;
|
||||||
pub use tachys;
|
pub use tachys;
|
||||||
pub mod logging;
|
pub mod mount;
|
||||||
mod mount;
|
|
||||||
pub use any_spawner::Executor;
|
pub use any_spawner::Executor;
|
||||||
pub use mount::*;
|
pub use leptos_config as config;
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
pub use mount::hydrate_body;
|
||||||
|
pub use mount::mount_to_body;
|
||||||
pub use oco;
|
pub use oco;
|
||||||
|
|
||||||
|
pub mod context {
|
||||||
|
pub use reactive_graph::owner::{provide_context, use_context};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub mod server {
|
||||||
|
pub use leptos_server::{ArcResource, Resource};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||||
|
pub mod logging {
|
||||||
|
pub use leptos_dom::{debug_warn, error, log, warn};
|
||||||
|
}
|
||||||
|
|
||||||
/*mod additional_attributes;
|
/*mod additional_attributes;
|
||||||
pub use additional_attributes::*;
|
pub use additional_attributes::*;
|
||||||
mod await_;
|
mod await_;
|
||||||
|
|
|
@ -4,11 +4,59 @@ use reactive_graph::owner::Owner;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use tachys::{
|
use tachys::{
|
||||||
dom::body,
|
dom::body,
|
||||||
|
hydration::Cursor,
|
||||||
renderer::{dom::Dom, Renderer},
|
renderer::{dom::Dom, Renderer},
|
||||||
view::{Mountable, Render},
|
view::{Mountable, PositionState, Render, RenderHtml},
|
||||||
};
|
};
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::HtmlElement;
|
use web_sys::HtmlElement;
|
||||||
|
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
/// Hydrates the app described by the provided function, starting at `<body>`.
|
||||||
|
pub fn hydrate_body<F, N>(f: F)
|
||||||
|
where
|
||||||
|
F: FnOnce() -> N + 'static,
|
||||||
|
N: IntoView,
|
||||||
|
{
|
||||||
|
let owner = hydrate_from(body(), f);
|
||||||
|
owner.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
/// Runs the provided closure and mounts the result to the provided element.
|
||||||
|
pub fn hydrate_from<F, N>(
|
||||||
|
parent: HtmlElement,
|
||||||
|
f: F,
|
||||||
|
) -> UnmountHandle<N::State, Dom>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> N + 'static,
|
||||||
|
N: IntoView,
|
||||||
|
{
|
||||||
|
use hydration_context::HydrateSharedContext;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
// use wasm-bindgen-futures to drive the reactive system
|
||||||
|
Executor::init_wasm_bindgen();
|
||||||
|
|
||||||
|
// create a new reactive owner and use it as the root node to run the app
|
||||||
|
let owner = Owner::new_root(Arc::new(HydrateSharedContext::new()));
|
||||||
|
let mountable = owner.with(move || {
|
||||||
|
let view = f().into_view();
|
||||||
|
view.hydrate::<true>(
|
||||||
|
&Cursor::new(parent.unchecked_into()),
|
||||||
|
&PositionState::default(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// returns a handle that owns the owner
|
||||||
|
// when this is dropped, it will clean up the reactive system and unmount the view
|
||||||
|
UnmountHandle {
|
||||||
|
owner,
|
||||||
|
mountable,
|
||||||
|
rndr: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs the provided closure and mounts the result to the `<body>`.
|
/// Runs the provided closure and mounts the result to the `<body>`.
|
||||||
pub fn mount_to_body<F, N>(f: F)
|
pub fn mount_to_body<F, N>(f: F)
|
||||||
where
|
where
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
use crate::context::{provide_context, use_context};
|
||||||
|
use base64::{
|
||||||
|
alphabet,
|
||||||
|
engine::{self, general_purpose},
|
||||||
|
Engine,
|
||||||
|
};
|
||||||
|
use rand::{thread_rng, RngCore};
|
||||||
|
use std::{fmt::Display, ops::Deref, sync::Arc};
|
||||||
|
|
||||||
|
/// A cryptographic nonce ("number used once") which can be
|
||||||
|
/// used by Content Security Policy to determine whether or not a given
|
||||||
|
/// resource will be allowed to load.
|
||||||
|
///
|
||||||
|
/// When the `nonce` feature is enabled on one of the server integrations,
|
||||||
|
/// a nonce is generated during server rendering and added to all inline
|
||||||
|
/// scripts used for HTML streaming and resource loading.
|
||||||
|
///
|
||||||
|
/// The nonce being used during the current server response can be
|
||||||
|
/// accessed using [`use_nonce`].
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// #[component]
|
||||||
|
/// pub fn App() -> impl IntoView {
|
||||||
|
/// provide_meta_context;
|
||||||
|
///
|
||||||
|
/// view! {
|
||||||
|
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||||
|
/// <Meta
|
||||||
|
/// http_equiv="Content-Security-Policy"
|
||||||
|
/// content=move || {
|
||||||
|
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||||
|
/// use_nonce()
|
||||||
|
/// .map(|nonce| {
|
||||||
|
/// format!(
|
||||||
|
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||||
|
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||||
|
/// )
|
||||||
|
/// })
|
||||||
|
/// .unwrap_or_default()
|
||||||
|
/// }
|
||||||
|
/// />
|
||||||
|
/// // manually insert nonce during SSR on inline script
|
||||||
|
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
|
||||||
|
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||||
|
/// <Style>"body { color: blue; }"</Style>
|
||||||
|
/// <p>"Test"</p>
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Nonce(pub(crate) Arc<str>);
|
||||||
|
|
||||||
|
impl Deref for Nonce {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Nonce {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO implement Attribute
|
||||||
|
|
||||||
|
/// Accesses the nonce that has been generated during the current
|
||||||
|
/// server response. This can be added to inline `<script>` and
|
||||||
|
/// `<style>` tags for compatibility with a Content Security Policy.
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// #[component]
|
||||||
|
/// pub fn App() -> impl IntoView {
|
||||||
|
/// provide_meta_context;
|
||||||
|
///
|
||||||
|
/// view! {
|
||||||
|
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||||
|
/// <Meta
|
||||||
|
/// http_equiv="Content-Security-Policy"
|
||||||
|
/// content=move || {
|
||||||
|
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||||
|
/// use_nonce()
|
||||||
|
/// .map(|nonce| {
|
||||||
|
/// format!(
|
||||||
|
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||||
|
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||||
|
/// )
|
||||||
|
/// })
|
||||||
|
/// .unwrap_or_default()
|
||||||
|
/// }
|
||||||
|
/// />
|
||||||
|
/// // manually insert nonce during SSR on inline script
|
||||||
|
/// <script nonce=use_nonce()>"console.log('Hello, world!');"</script>
|
||||||
|
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||||
|
/// <Style>"body { color: blue; }"</Style>
|
||||||
|
/// <p>"Test"</p>
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn use_nonce() -> Option<Nonce> {
|
||||||
|
use_context::<Nonce>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a nonce and provides it via context.
|
||||||
|
pub fn provide_nonce() {
|
||||||
|
provide_context(Nonce::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
const NONCE_ENGINE: engine::GeneralPurpose =
|
||||||
|
engine::GeneralPurpose::new(&alphabet::URL_SAFE, general_purpose::NO_PAD);
|
||||||
|
|
||||||
|
impl Nonce {
|
||||||
|
/// Generates a new nonce from 16 bytes (128 bits) of random data.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut thread_rng = thread_rng();
|
||||||
|
let mut bytes = [0; 16];
|
||||||
|
thread_rng.fill_bytes(&mut bytes);
|
||||||
|
Nonce(NONCE_ENGINE.encode(bytes).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Nonce {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,10 @@ use web_sys::HtmlElement;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub use tachys::html::event as events;
|
pub use tachys::html::event as events;
|
||||||
|
|
||||||
|
/// Utilities for simple isomorphic logging to the console or terminal.
|
||||||
|
#[macro_use]
|
||||||
|
pub mod logging;
|
||||||
|
|
||||||
/*#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
/*#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||||
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
#![cfg_attr(feature = "nightly", feature(unboxed_closures))]
|
||||||
// to prevent warnings from popping up when a nightly feature is stabilized
|
// to prevent warnings from popping up when a nightly feature is stabilized
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::is_server;
|
//! Utilities for simple isomorphic logging to the console or terminal.
|
||||||
use cfg_if::cfg_if;
|
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
/// Uses `println!()`-style formatting to log something to the console (in the browser)
|
/// Uses `println!()`-style formatting to log something to the console (in the browser)
|
||||||
|
@ -41,11 +41,18 @@ macro_rules! debug_warn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn log_to_stdout() -> bool {
|
||||||
|
cfg!(not(all(
|
||||||
|
target_arch = "wasm32",
|
||||||
|
not(any(target_os = "emscripten", target_os = "wasi"))
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Log a string to the console (in the browser)
|
/// Log a string to the console (in the browser)
|
||||||
/// or via `println!()` (if not in the browser).
|
/// or via `println!()` (if not in the browser).
|
||||||
pub fn console_log(s: &str) {
|
pub fn console_log(s: &str) {
|
||||||
#[allow(clippy::print_stdout)]
|
#[allow(clippy::print_stdout)]
|
||||||
if is_server() {
|
if log_to_stdout() {
|
||||||
println!("{s}");
|
println!("{s}");
|
||||||
} else {
|
} else {
|
||||||
web_sys::console::log_1(&JsValue::from_str(s));
|
web_sys::console::log_1(&JsValue::from_str(s));
|
||||||
|
@ -55,7 +62,7 @@ pub fn console_log(s: &str) {
|
||||||
/// Log a warning to the console (in the browser)
|
/// Log a warning to the console (in the browser)
|
||||||
/// or via `println!()` (if not in the browser).
|
/// or via `println!()` (if not in the browser).
|
||||||
pub fn console_warn(s: &str) {
|
pub fn console_warn(s: &str) {
|
||||||
if is_server() {
|
if log_to_stdout() {
|
||||||
eprintln!("{s}");
|
eprintln!("{s}");
|
||||||
} else {
|
} else {
|
||||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||||
|
@ -64,8 +71,9 @@ pub fn console_warn(s: &str) {
|
||||||
|
|
||||||
/// Log an error to the console (in the browser)
|
/// Log an error to the console (in the browser)
|
||||||
/// or via `println!()` (if not in the browser).
|
/// or via `println!()` (if not in the browser).
|
||||||
|
#[inline(always)]
|
||||||
pub fn console_error(s: &str) {
|
pub fn console_error(s: &str) {
|
||||||
if is_server() {
|
if log_to_stdout() {
|
||||||
eprintln!("{s}");
|
eprintln!("{s}");
|
||||||
} else {
|
} else {
|
||||||
web_sys::console::error_1(&JsValue::from_str(s));
|
web_sys::console::error_1(&JsValue::from_str(s));
|
||||||
|
@ -74,16 +82,19 @@ pub fn console_error(s: &str) {
|
||||||
|
|
||||||
/// Log an error to the console (in the browser)
|
/// Log an error to the console (in the browser)
|
||||||
/// or via `println!()` (if not in the browser), but only in a debug build.
|
/// or via `println!()` (if not in the browser), but only in a debug build.
|
||||||
|
#[inline(always)]
|
||||||
pub fn console_debug_warn(s: &str) {
|
pub fn console_debug_warn(s: &str) {
|
||||||
cfg_if! {
|
#[cfg(debug_assertions)]
|
||||||
if #[cfg(debug_assertions)] {
|
{
|
||||||
if is_server() {
|
if log_to_stdout() {
|
||||||
eprintln!("{s}");
|
eprintln!("{s}");
|
||||||
} else {
|
} else {
|
||||||
web_sys::console::warn_1(&JsValue::from_str(s));
|
web_sys::console::warn_1(&JsValue::from_str(s));
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
let _ = s;
|
let _ = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -10,14 +10,27 @@ readme = "../README.md"
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reactive_graph = { workspace = true }
|
hydration_context = { workspace = true }
|
||||||
leptos_macro = { workspace = true }
|
reactive_graph = { workspace = true, features = ["hydration"] }
|
||||||
|
#leptos_macro = { workspace = true }
|
||||||
server_fn = { workspace = true }
|
server_fn = { workspace = true }
|
||||||
lazy_static = "1"
|
#lazy_static = "1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tracing = "0.1"
|
tracing = { version = "0.1", optional = true }
|
||||||
inventory = "0.3"
|
#inventory = "0.3"
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
# serialization formats
|
||||||
|
serde = { version = "1", optional = true }
|
||||||
|
serde_json = { version = "1", optional = true }
|
||||||
|
miniserde = { version = "0.1", optional = true }
|
||||||
|
rkyv = { version = "0.7", optional = true, features = [
|
||||||
|
"validation",
|
||||||
|
"uuid",
|
||||||
|
"strict",
|
||||||
|
] }
|
||||||
|
serde-lite = { version = "0.5", optional = true }
|
||||||
|
base64 = { version = "0.22", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
leptos = { path = "../leptos" }
|
leptos = { path = "../leptos" }
|
||||||
|
@ -25,6 +38,9 @@ leptos = { path = "../leptos" }
|
||||||
[features]
|
[features]
|
||||||
default-tls = ["server_fn/default-tls"]
|
default-tls = ["server_fn/default-tls"]
|
||||||
rustls = ["server_fn/rustls"]
|
rustls = ["server_fn/rustls"]
|
||||||
|
hydration = ["reactive_graph/hydration", "dep:serde", "dep:serde_json"]
|
||||||
|
rkyv = ["dep:rkyv", "dep:base64"]
|
||||||
|
tracing = ["dep:tracing"]
|
||||||
|
|
||||||
[package.metadata.cargo-all-features]
|
[package.metadata.cargo-all-features]
|
||||||
denylist = ["nightly"]
|
denylist = ["nightly"]
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
//#![deny(missing_docs)]
|
//#![deny(missing_docs)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
mod resource;
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub mod serializers;
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub use resource::*;
|
||||||
|
|
||||||
////! # Leptos Server Functions
|
////! # Leptos Server Functions
|
||||||
////!
|
////!
|
||||||
////! This package is based on a simple idea: sometimes it’s useful to write functions
|
////! This package is based on a simple idea: sometimes it’s useful to write functions
|
||||||
|
|
|
@ -0,0 +1,388 @@
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
use crate::serializers::Miniserde;
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
use crate::serializers::Rkyv;
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
use crate::serializers::SerdeLite;
|
||||||
|
use crate::serializers::{SerdeJson, SerializableData, Serializer, Str};
|
||||||
|
use core::{fmt::Debug, marker::PhantomData};
|
||||||
|
use futures::Future;
|
||||||
|
use hydration_context::SerializedDataId;
|
||||||
|
use reactive_graph::{
|
||||||
|
computed::{ArcAsyncDerived, AsyncDerived, AsyncDerivedFuture, AsyncState},
|
||||||
|
owner::Owner,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use std::{future::IntoFuture, ops::Deref};
|
||||||
|
|
||||||
|
pub struct ArcResource<T, Ser> {
|
||||||
|
ser: PhantomData<Ser>,
|
||||||
|
data: ArcAsyncDerived<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> Deref for ArcResource<T, Ser> {
|
||||||
|
type Target = ArcAsyncDerived<T>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ArcResource<T, Str>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Str>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ArcResource<T, SerdeJson>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeJson>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_serde<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
impl<T> ArcResource<T, Miniserde>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Miniserde>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_miniserde<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
impl<T> ArcResource<T, SerdeLite>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeLite>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_serde_lite<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
impl<T> ArcResource<T, SerdeLite>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeLite>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_rkyv<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
ArcResource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> ArcResource<T, Ser>
|
||||||
|
where
|
||||||
|
Ser: Serializer,
|
||||||
|
T: Debug + SerializableData<Ser>,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_with_encoding<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> ArcResource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Debug + Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
let shared_context = Owner::current_shared_context();
|
||||||
|
let id = shared_context
|
||||||
|
.as_ref()
|
||||||
|
.map(|sc| sc.next_id())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let initial = Self::initial_value(&id);
|
||||||
|
|
||||||
|
let data = ArcAsyncDerived::new_with_initial(initial, fun);
|
||||||
|
|
||||||
|
if let Some(shared_context) = shared_context {
|
||||||
|
let value = data.clone();
|
||||||
|
let ready_fut = data.ready();
|
||||||
|
|
||||||
|
shared_context.write_async(
|
||||||
|
id,
|
||||||
|
Box::pin(async move {
|
||||||
|
ready_fut.await;
|
||||||
|
value
|
||||||
|
.with_untracked(|data| match &data {
|
||||||
|
AsyncState::Complete(val) => val.ser(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.unwrap() // TODO handle
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArcResource {
|
||||||
|
ser: PhantomData,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn initial_value(id: &SerializedDataId) -> AsyncState<T> {
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
{
|
||||||
|
let shared_context = Owner::current_shared_context();
|
||||||
|
if let Some(shared_context) = shared_context {
|
||||||
|
let value = shared_context.read_data(id);
|
||||||
|
if let Some(value) = value {
|
||||||
|
match T::de(&value) {
|
||||||
|
Ok(value) => return AsyncState::Complete(value),
|
||||||
|
Err(e) => {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::error!(
|
||||||
|
"couldn't deserialize from {value:?}: {e:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AsyncState::Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> IntoFuture for ArcResource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Clone + 'static,
|
||||||
|
{
|
||||||
|
type Output = T;
|
||||||
|
type IntoFuture = AsyncDerivedFuture<T>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
self.data.into_future()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Resource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
ser: PhantomData<Ser>,
|
||||||
|
data: AsyncDerived<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + 'static, Ser> Copy for Resource<T, Ser> {}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + 'static, Ser> Clone for Resource<T, Ser> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> Deref for Resource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Target = AsyncDerived<T>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Resource<T, Str>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Str> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Resource<T, SerdeJson>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeJson> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_serde<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
impl<T> Resource<T, Miniserde>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Miniserde> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_miniserde<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
impl<T> Resource<T, SerdeLite>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<SerdeLite> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_serde_lite<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
impl<T> Resource<T, Rkyv>
|
||||||
|
where
|
||||||
|
T: Debug + SerializableData<Rkyv> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_rkyv<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
Resource::new_with_encoding(fun)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> Resource<T, Ser>
|
||||||
|
where
|
||||||
|
Ser: Serializer,
|
||||||
|
T: Debug + SerializableData<Ser> + Send + Sync + 'static,
|
||||||
|
T::SerErr: Debug,
|
||||||
|
T::DeErr: Debug,
|
||||||
|
{
|
||||||
|
pub fn new_with_encoding<Fut>(
|
||||||
|
fun: impl Fn() -> Fut + Send + Sync + 'static,
|
||||||
|
) -> Resource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
|
{
|
||||||
|
let shared_context = Owner::current_shared_context();
|
||||||
|
let id = shared_context
|
||||||
|
.as_ref()
|
||||||
|
.map(|sc| sc.next_id())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let initial = Self::initial_value(&id);
|
||||||
|
|
||||||
|
let data = AsyncDerived::new_with_initial(initial, fun);
|
||||||
|
|
||||||
|
if let Some(shared_context) = shared_context {
|
||||||
|
let value = data;
|
||||||
|
let ready_fut = data.ready();
|
||||||
|
|
||||||
|
shared_context.write_async(
|
||||||
|
id,
|
||||||
|
Box::pin(async move {
|
||||||
|
ready_fut.await;
|
||||||
|
value
|
||||||
|
.with_untracked(|data| match &data {
|
||||||
|
AsyncState::Complete(val) => val.ser(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
})
|
||||||
|
.unwrap() // TODO handle
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource {
|
||||||
|
ser: PhantomData,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn initial_value(id: &SerializedDataId) -> AsyncState<T> {
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
{
|
||||||
|
let shared_context = Owner::current_shared_context();
|
||||||
|
if let Some(shared_context) = shared_context {
|
||||||
|
let value = shared_context.read_data(id);
|
||||||
|
if let Some(value) = value {
|
||||||
|
match T::de(&value) {
|
||||||
|
Ok(value) => return AsyncState::Complete(value),
|
||||||
|
Err(e) => {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::error!(
|
||||||
|
"couldn't deserialize from {value:?}: {e:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AsyncState::Loading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, Ser> IntoFuture for Resource<T, Ser>
|
||||||
|
where
|
||||||
|
T: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Output = T;
|
||||||
|
type IntoFuture = AsyncDerivedFuture<T>;
|
||||||
|
|
||||||
|
fn into_future(self) -> Self::IntoFuture {
|
||||||
|
self.data.into_future()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
use core::str::FromStr;
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
|
pub trait SerializableData<Ser: Serializer>: Sized {
|
||||||
|
type SerErr;
|
||||||
|
type DeErr;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr>;
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Serializer {}
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes using [`ToString`] and deserializes
|
||||||
|
/// using [`FromStr`](core::str::FromStr).
|
||||||
|
pub struct Str;
|
||||||
|
|
||||||
|
impl Serializer for Str {}
|
||||||
|
|
||||||
|
impl<T> SerializableData<Str> for T
|
||||||
|
where
|
||||||
|
T: ToString + FromStr,
|
||||||
|
{
|
||||||
|
type SerErr = ();
|
||||||
|
type DeErr = <T as FromStr>::Err;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
Ok(self.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
T::from_str(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes using [`serde_json`].
|
||||||
|
pub struct SerdeJson;
|
||||||
|
|
||||||
|
impl Serializer for SerdeJson {}
|
||||||
|
|
||||||
|
impl<T> SerializableData<SerdeJson> for T
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Serialize,
|
||||||
|
{
|
||||||
|
type SerErr = serde_json::Error;
|
||||||
|
type DeErr = serde_json::Error;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
serde_json::to_string(&self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
serde_json::from_str(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
mod miniserde {
|
||||||
|
use super::{SerializableData, Serializer};
|
||||||
|
use miniserde::{json, Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes and deserializes using [`miniserde`].
|
||||||
|
pub struct Miniserde;
|
||||||
|
|
||||||
|
impl Serializer for Miniserde {}
|
||||||
|
|
||||||
|
impl<T> SerializableData<Miniserde> for T
|
||||||
|
where
|
||||||
|
T: Deserialize + Serialize,
|
||||||
|
{
|
||||||
|
type SerErr = ();
|
||||||
|
type DeErr = miniserde::Error;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
Ok(json::to_string(&self))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
json::from_str(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "miniserde")]
|
||||||
|
pub use miniserde::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
mod serde_lite {
|
||||||
|
use super::{SerializableData, Serializer};
|
||||||
|
use serde_lite::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SerdeLiteError {
|
||||||
|
#[error("serde_lite error {0:?}")]
|
||||||
|
SerdeLite(serde_lite::Error),
|
||||||
|
#[error("serde_json error {0:?}")]
|
||||||
|
SerdeJson(serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_lite::Error> for SerdeLiteError {
|
||||||
|
fn from(value: serde_lite::Error) -> Self {
|
||||||
|
SerdeLiteError::SerdeLite(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for SerdeLiteError {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
SerdeLiteError::SerdeJson(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes and deserializes using [`serde_lite`].
|
||||||
|
pub struct SerdeLite;
|
||||||
|
|
||||||
|
impl Serializer for SerdeLite {}
|
||||||
|
|
||||||
|
impl<T> SerializableData<SerdeLite> for T
|
||||||
|
where
|
||||||
|
T: Deserialize + Serialize,
|
||||||
|
{
|
||||||
|
type SerErr = SerdeLiteError;
|
||||||
|
type DeErr = SerdeLiteError;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
let intermediate = self.serialize()?;
|
||||||
|
Ok(serde_json::to_string(&intermediate)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
let intermediate = serde_json::from_str(data)?;
|
||||||
|
Ok(Self::deserialize(&intermediate)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "serde-lite")]
|
||||||
|
pub use serde_lite::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
mod rkyv {
|
||||||
|
use super::{SerializableData, Serializer};
|
||||||
|
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
|
||||||
|
use rkyv::{
|
||||||
|
de::deserializers::SharedDeserializeMap,
|
||||||
|
ser::serializers::AllocSerializer,
|
||||||
|
validation::validators::DefaultValidator, Archive, CheckBytes,
|
||||||
|
Deserialize, Serialize,
|
||||||
|
};
|
||||||
|
use std::{error::Error, sync::Arc};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// A [`Serializer`] that serializes and deserializes using [`rkyv`].
|
||||||
|
pub struct Rkyv;
|
||||||
|
|
||||||
|
impl Serializer for Rkyv {}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum RkyvError {
|
||||||
|
#[error("rkyv error {0:?}")]
|
||||||
|
Rkyv(Arc<dyn Error>),
|
||||||
|
#[error("base64 error {0:?}")]
|
||||||
|
Base64Decode(base64::DecodeError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arc<dyn Error>> for RkyvError {
|
||||||
|
fn from(value: Arc<dyn Error>) -> Self {
|
||||||
|
RkyvError::Rkyv(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<base64::DecodeError> for RkyvError {
|
||||||
|
fn from(value: base64::DecodeError) -> Self {
|
||||||
|
RkyvError::Base64Decode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SerializableData<Rkyv> for T
|
||||||
|
where
|
||||||
|
T: Serialize<AllocSerializer<1024>>,
|
||||||
|
T: Archive,
|
||||||
|
T::Archived: for<'b> CheckBytes<DefaultValidator<'b>>
|
||||||
|
+ Deserialize<T, SharedDeserializeMap>,
|
||||||
|
{
|
||||||
|
type SerErr = RkyvError;
|
||||||
|
type DeErr = RkyvError;
|
||||||
|
|
||||||
|
fn ser(&self) -> Result<String, Self::SerErr> {
|
||||||
|
let bytes = rkyv::to_bytes::<T, 1024>(self)
|
||||||
|
.map_err(|e| Arc::new(e) as Arc<dyn Error>)?;
|
||||||
|
Ok(STANDARD_NO_PAD.encode(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de(data: &str) -> Result<Self, Self::DeErr> {
|
||||||
|
let bytes = STANDARD_NO_PAD.decode(data.as_bytes())?;
|
||||||
|
Ok(rkyv::from_bytes::<T>(&bytes)
|
||||||
|
.map_err(|e| Arc::new(e) as Arc<dyn Error>)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rkyv")]
|
||||||
|
pub use rkyv::*;
|
|
@ -16,6 +16,7 @@ indexmap = "2"
|
||||||
send_wrapper = "0.6.0"
|
send_wrapper = "0.6.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
futures = "0.3.30"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
|
|
|
@ -7,10 +7,15 @@ use leptos::{
|
||||||
tachys::{
|
tachys::{
|
||||||
dom::document,
|
dom::document,
|
||||||
error::Result,
|
error::Result,
|
||||||
html::attribute::{
|
html::{
|
||||||
any_attribute::{AnyAttribute, AnyAttributeState},
|
attribute::{
|
||||||
|
any_attribute::{
|
||||||
|
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
|
||||||
|
},
|
||||||
Attribute,
|
Attribute,
|
||||||
},
|
},
|
||||||
|
class,
|
||||||
|
},
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
reactive_graph::RenderEffectState,
|
reactive_graph::RenderEffectState,
|
||||||
renderer::{dom::Dom, Renderer},
|
renderer::{dom::Dom, Renderer},
|
||||||
|
@ -57,10 +62,17 @@ use web_sys::HtmlElement;
|
||||||
/// ```
|
/// ```
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Body(
|
pub fn Body(
|
||||||
|
/// The `class` attribute on the `<body>`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
mut class: Option<TextProp>,
|
||||||
/// Arbitrary attributes to add to the `<body>`.
|
/// Arbitrary attributes to add to the `<body>`.
|
||||||
#[prop(attrs)]
|
#[prop(attrs)]
|
||||||
mut attributes: Vec<AnyAttribute<Dom>>,
|
mut attributes: Vec<AnyAttribute<Dom>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
if let Some(value) = class.take() {
|
||||||
|
let value = class::class(move || value.get());
|
||||||
|
attributes.push(value.into_any_attr());
|
||||||
|
}
|
||||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||||
let mut meta = meta.inner.write().or_poisoned();
|
let mut meta = meta.inner.write().or_poisoned();
|
||||||
// if we are server rendering, we will not actually use these values via RenderHtml
|
// if we are server rendering, we will not actually use these values via RenderHtml
|
||||||
|
|
|
@ -7,10 +7,16 @@ use leptos::{
|
||||||
tachys::{
|
tachys::{
|
||||||
dom::document,
|
dom::document,
|
||||||
error::Result,
|
error::Result,
|
||||||
html::attribute::{
|
html::{
|
||||||
any_attribute::{AnyAttribute, AnyAttributeState},
|
attribute::{
|
||||||
|
self,
|
||||||
|
any_attribute::{
|
||||||
|
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
|
||||||
|
},
|
||||||
Attribute,
|
Attribute,
|
||||||
},
|
},
|
||||||
|
class,
|
||||||
|
},
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
reactive_graph::RenderEffectState,
|
reactive_graph::RenderEffectState,
|
||||||
renderer::{dom::Dom, Renderer},
|
renderer::{dom::Dom, Renderer},
|
||||||
|
@ -54,10 +60,30 @@ use web_sys::{Element, HtmlElement};
|
||||||
/// ```
|
/// ```
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Html(
|
pub fn Html(
|
||||||
|
/// The `lang` attribute on the `<html>`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
mut lang: Option<TextProp>,
|
||||||
|
/// The `dir` attribute on the `<html>`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
mut dir: Option<TextProp>,
|
||||||
|
/// The `class` attribute on the `<html>`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
mut class: Option<TextProp>,
|
||||||
/// Arbitrary attributes to add to the `<html>`
|
/// Arbitrary attributes to add to the `<html>`
|
||||||
#[prop(attrs)]
|
#[prop(attrs)]
|
||||||
mut attributes: Vec<AnyAttribute<Dom>>,
|
mut attributes: Vec<AnyAttribute<Dom>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
attributes.extend(
|
||||||
|
lang.take()
|
||||||
|
.map(|value| attribute::lang(move || value.get()).into_any_attr())
|
||||||
|
.into_iter()
|
||||||
|
.chain(dir.take().map(|value| {
|
||||||
|
attribute::dir(move || value.get()).into_any_attr()
|
||||||
|
}))
|
||||||
|
.chain(class.take().map(|value| {
|
||||||
|
class::class(move || value.get()).into_any_attr()
|
||||||
|
})),
|
||||||
|
);
|
||||||
if let Some(meta) = use_context::<ServerMetaContext>() {
|
if let Some(meta) = use_context::<ServerMetaContext>() {
|
||||||
let mut meta = meta.inner.write().or_poisoned();
|
let mut meta = meta.inner.write().or_poisoned();
|
||||||
// if we are server rendering, we will not actually use these values via RenderHtml
|
// if we are server rendering, we will not actually use these values via RenderHtml
|
||||||
|
|
|
@ -47,9 +47,10 @@
|
||||||
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
|
||||||
//! which mode your app is operating in.
|
//! which mode your app is operating in.
|
||||||
|
|
||||||
use indexmap::IndexMap;
|
use futures::{Stream, StreamExt};
|
||||||
use leptos::{
|
use leptos::{
|
||||||
component, debug_warn,
|
component,
|
||||||
|
logging::debug_warn,
|
||||||
reactive_graph::owner::{provide_context, use_context},
|
reactive_graph::owner::{provide_context, use_context},
|
||||||
tachys::{
|
tachys::{
|
||||||
dom::document,
|
dom::document,
|
||||||
|
@ -67,13 +68,12 @@ use once_cell::sync::Lazy;
|
||||||
use or_poisoned::OrPoisoned;
|
use or_poisoned::OrPoisoned;
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
rc::Rc,
|
pin::Pin,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{HtmlHeadElement, Node};
|
use web_sys::HtmlHeadElement;
|
||||||
|
|
||||||
mod body;
|
mod body;
|
||||||
mod html;
|
mod html;
|
||||||
|
@ -157,7 +157,61 @@ pub struct ServerMetaContext {
|
||||||
pub(crate) title: TitleContext,
|
pub(crate) title: TitleContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
impl ServerMetaContext {
|
||||||
|
/// Consumes the metadata, injecting it into the the first chunk of an HTML stream in the
|
||||||
|
/// appropriate place.
|
||||||
|
///
|
||||||
|
/// This means that only meta tags rendered during the first chunk of the stream will be
|
||||||
|
/// included.
|
||||||
|
pub async fn inject_meta_context(
|
||||||
|
self,
|
||||||
|
mut stream: impl Stream<Item = String> + Send + Sync + Unpin,
|
||||||
|
) -> impl Stream<Item = String> + Send + Sync {
|
||||||
|
let mut first_chunk = stream.next().await.unwrap_or_default();
|
||||||
|
|
||||||
|
let meta_buf =
|
||||||
|
std::mem::take(&mut self.inner.write().or_poisoned().head_html);
|
||||||
|
|
||||||
|
let title = self.title.as_string();
|
||||||
|
let title_len = title
|
||||||
|
.as_ref()
|
||||||
|
.map(|n| "<title>".len() + n.len() + "</title>".len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let modified_chunk = if title_len == 0 && meta_buf.is_empty() {
|
||||||
|
first_chunk
|
||||||
|
} else {
|
||||||
|
let mut buf = String::with_capacity(
|
||||||
|
first_chunk.len() + title_len + meta_buf.len(),
|
||||||
|
);
|
||||||
|
let head_loc = first_chunk
|
||||||
|
.find("</head>")
|
||||||
|
.expect("you are using leptos_meta without a </head> tag");
|
||||||
|
let marker_loc =
|
||||||
|
first_chunk.find("<!--HEAD-->").unwrap_or_else(|| {
|
||||||
|
first_chunk.find("</head>").unwrap_or(head_loc)
|
||||||
|
});
|
||||||
|
let (before_marker, after_marker) =
|
||||||
|
first_chunk.split_at_mut(marker_loc);
|
||||||
|
let (before_head_close, after_head) =
|
||||||
|
after_marker.split_at_mut(head_loc - marker_loc);
|
||||||
|
buf.push_str(before_marker);
|
||||||
|
if let Some(title) = title {
|
||||||
|
buf.push_str("<title>");
|
||||||
|
buf.push_str(&title);
|
||||||
|
buf.push_str("</title>");
|
||||||
|
}
|
||||||
|
buf.push_str(before_head_close);
|
||||||
|
buf.push_str(&meta_buf);
|
||||||
|
buf.push_str(after_head);
|
||||||
|
buf
|
||||||
|
};
|
||||||
|
|
||||||
|
futures::stream::once(async move { modified_chunk }).chain(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
struct ServerMetaContextInner {
|
struct ServerMetaContextInner {
|
||||||
/*/// Metadata associated with the `<html>` element
|
/*/// Metadata associated with the `<html>` element
|
||||||
pub html: HtmlContext,
|
pub html: HtmlContext,
|
||||||
|
@ -173,7 +227,9 @@ struct ServerMetaContextInner {
|
||||||
|
|
||||||
impl Debug for ServerMetaContext {
|
impl Debug for ServerMetaContext {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("ServerMetaContext").finish_non_exhaustive()
|
f.debug_struct("ServerMetaContext")
|
||||||
|
.field("inner", &self.inner)
|
||||||
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,6 +423,7 @@ pub fn MetaTags() -> impl IntoView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
struct MetaTagsView {
|
struct MetaTagsView {
|
||||||
context: ServerMetaContext,
|
context: ServerMetaContext,
|
||||||
}
|
}
|
||||||
|
@ -399,14 +456,7 @@ impl RenderHtml<Dom> for MetaTagsView {
|
||||||
const MIN_LENGTH: usize = 0;
|
const MIN_LENGTH: usize = 0;
|
||||||
|
|
||||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||||
if let Some(title) = self.context.title.as_string() {
|
buf.push_str("<!--HEAD-->");
|
||||||
buf.reserve(15 + title.len());
|
|
||||||
buf.push_str("<title>");
|
|
||||||
buf.push_str(&title);
|
|
||||||
buf.push_str("</title>");
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.push_str(&self.context.inner.write().or_poisoned().head_html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
|
|
@ -7,6 +7,7 @@ version.workspace = true
|
||||||
any_spawner = { workspace = true }
|
any_spawner = { workspace = true }
|
||||||
or_poisoned = { workspace = true }
|
or_poisoned = { workspace = true }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
hydration_context = { workspace = true, optional = true }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
rustc-hash = "1.1.0"
|
rustc-hash = "1.1.0"
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
@ -23,6 +24,7 @@ any_spawner = { workspace = true, features = ["tokio"] }
|
||||||
nightly = []
|
nightly = []
|
||||||
serde = ["dep:serde"]
|
serde = ["dep:serde"]
|
||||||
tracing = ["dep:tracing"]
|
tracing = ["dep:tracing"]
|
||||||
|
hydration = ["dep:hydration_context"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|
|
@ -71,7 +71,7 @@ impl<T> DefinedAt for ArcAsyncDerived<T> {
|
||||||
|
|
||||||
// This helps create a derived async signal.
|
// This helps create a derived async signal.
|
||||||
// It needs to be implemented as a macro because it needs to be flexible over
|
// It needs to be implemented as a macro because it needs to be flexible over
|
||||||
// whether `fun` returns a `Future` that is `Send + Sync`. Doing it as a function would,
|
// whether `fun` returns a `Future` that is `Send`. Doing it as a function would,
|
||||||
// as far as I can tell, require repeating most of the function body.
|
// as far as I can tell, require repeating most of the function body.
|
||||||
macro_rules! spawn_derived {
|
macro_rules! spawn_derived {
|
||||||
($spawner:expr, $initial:ident, $fun:ident) => {{
|
($spawner:expr, $initial:ident, $fun:ident) => {{
|
||||||
|
@ -174,7 +174,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||||
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
pub fn new<Fut>(fun: impl Fn() -> Fut + Send + Sync + 'static) -> Self
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + Sync + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Self::new_with_initial(AsyncState::Loading, fun)
|
Self::new_with_initial(AsyncState::Loading, fun)
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + Sync + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
spawn_derived!(Executor::spawn, initial_value, fun)
|
spawn_derived!(Executor::spawn, initial_value, fun)
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
T: Send + Sync + 'static,
|
T: Send + Sync + 'static,
|
||||||
Fut: Future<Output = T> + Send + Sync + 'static,
|
Fut: Future<Output = T> + Send + 'static,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl<T> Debug for MemoInner<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> MemoInner<T> {
|
impl<T: 'static> MemoInner<T> {
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
fun: Arc<dyn Fn(Option<&T>) -> T + Send + Sync>,
|
fun: Arc<dyn Fn(Option<&T>) -> T + Send + Sync>,
|
||||||
|
@ -49,7 +49,7 @@ impl<T: Send + Sync + 'static> MemoInner<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> ReactiveNode for RwLock<MemoInner<T>> {
|
impl<T: 'static> ReactiveNode for RwLock<MemoInner<T>> {
|
||||||
fn mark_dirty(&self) {
|
fn mark_dirty(&self) {
|
||||||
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
|
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
|
||||||
self.mark_subscribers_check();
|
self.mark_subscribers_check();
|
||||||
|
@ -133,7 +133,7 @@ impl<T: Send + Sync + 'static> ReactiveNode for RwLock<MemoInner<T>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> Source for RwLock<MemoInner<T>> {
|
impl<T: 'static> Source for RwLock<MemoInner<T>> {
|
||||||
fn add_subscriber(&self, subscriber: AnySubscriber) {
|
fn add_subscriber(&self, subscriber: AnySubscriber) {
|
||||||
self.write().or_poisoned().subscribers.subscribe(subscriber);
|
self.write().or_poisoned().subscribers.subscribe(subscriber);
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ impl<T: Send + Sync + 'static> Source for RwLock<MemoInner<T>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> Subscriber for RwLock<MemoInner<T>> {
|
impl<T: 'static> Subscriber for RwLock<MemoInner<T>> {
|
||||||
fn add_source(&self, source: AnySource) {
|
fn add_source(&self, source: AnySource) {
|
||||||
self.write().or_poisoned().sources.insert(source);
|
self.write().or_poisoned().sources.insert(source);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,17 @@ pub struct Memo<T: Send + Sync + 'static> {
|
||||||
inner: StoredValue<ArcMemo<T>>,
|
inner: StoredValue<ArcMemo<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Sync + 'static> From<ArcMemo<T>> for Memo<T> {
|
||||||
|
#[track_caller]
|
||||||
|
fn from(value: ArcMemo<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: Location::caller(),
|
||||||
|
inner: StoredValue::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T: Send + Sync + 'static> Memo<T> {
|
impl<T: Send + Sync + 'static> Memo<T> {
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
|
|
|
@ -84,6 +84,7 @@ pub mod selector;
|
||||||
mod serde;
|
mod serde;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
|
pub mod wrappers;
|
||||||
|
|
||||||
pub use graph::untrack;
|
pub use graph::untrack;
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,11 @@ use crate::{
|
||||||
ArcReadSignal, ArcRwSignal, ArcWriteSignal, ReadSignal, RwSignal,
|
ArcReadSignal, ArcRwSignal, ArcWriteSignal, ReadSignal, RwSignal,
|
||||||
WriteSignal,
|
WriteSignal,
|
||||||
},
|
},
|
||||||
traits::{Read, Set},
|
traits::{Get, Read, Set},
|
||||||
|
wrappers::read::{ArcSignal, Signal},
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! impl_get_fn_traits {
|
macro_rules! impl_get_fn_traits_read {
|
||||||
($($ty:ident $(($method_name:ident))?),*) => {
|
($($ty:ident $(($method_name:ident))?),*) => {
|
||||||
$(
|
$(
|
||||||
#[cfg(feature = "nightly")]
|
#[cfg(feature = "nightly")]
|
||||||
|
@ -16,7 +17,7 @@ macro_rules! impl_get_fn_traits {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ macro_rules! impl_get_fn_traits {
|
||||||
impl<T: 'static> FnMut<()> for $ty<T> {
|
impl<T: 'static> FnMut<()> for $ty<T> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ macro_rules! impl_get_fn_traits {
|
||||||
impl<T: 'static> Fn<()> for $ty<T> {
|
impl<T: 'static> Fn<()> for $ty<T> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
@ -45,6 +46,44 @@ macro_rules! impl_get_fn_traits {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_get_fn_traits_get {
|
||||||
|
($($ty:ident $(($method_name:ident))?),*) => {
|
||||||
|
$(
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: 'static> FnOnce<()> for $ty<T> {
|
||||||
|
type Output = <Self as Get>::Value;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: 'static> FnMut<()> for $ty<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: 'static> Fn<()> for $ty<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_read!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
(@method_name $self:ident) => {
|
||||||
|
$self.get()
|
||||||
|
};
|
||||||
|
(@method_name $self:ident $ident:ident) => {
|
||||||
|
$self.$ident()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! impl_set_fn_traits {
|
macro_rules! impl_set_fn_traits {
|
||||||
($($ty:ident $($method_name:ident)?),*) => {
|
($($ty:ident $($method_name:ident)?),*) => {
|
||||||
$(
|
$(
|
||||||
|
@ -83,7 +122,7 @@ macro_rules! impl_set_fn_traits {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! impl_get_fn_traits_send {
|
macro_rules! impl_get_fn_traits_read_send {
|
||||||
($($ty:ident $(($method_name:ident))?),*) => {
|
($($ty:ident $(($method_name:ident))?),*) => {
|
||||||
$(
|
$(
|
||||||
#[cfg(feature = "nightly")]
|
#[cfg(feature = "nightly")]
|
||||||
|
@ -92,7 +131,7 @@ macro_rules! impl_get_fn_traits_send {
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits_send!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read_send!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +139,7 @@ macro_rules! impl_get_fn_traits_send {
|
||||||
impl<T: Send + Sync + 'static> FnMut<()> for $ty<T> {
|
impl<T: Send + Sync + 'static> FnMut<()> for $ty<T> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits_send!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read_send!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +147,7 @@ macro_rules! impl_get_fn_traits_send {
|
||||||
impl<T: Send + Sync + 'static> Fn<()> for $ty<T> {
|
impl<T: Send + Sync + 'static> Fn<()> for $ty<T> {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||||
impl_get_fn_traits_send!(@method_name self $($method_name)?)
|
impl_get_fn_traits_read_send!(@method_name self $($method_name)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
@ -121,6 +160,43 @@ macro_rules! impl_get_fn_traits_send {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_get_fn_traits_get_send {
|
||||||
|
($($ty:ident $(($method_name:ident))?),*) => {
|
||||||
|
$(
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: Send + Sync + Clone + 'static> FnOnce<()> for $ty<T> {
|
||||||
|
type Output = <Self as Get>::Value;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_get_send!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: Send + Sync + Clone + 'static> FnMut<()> for $ty<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_get_send!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "nightly")]
|
||||||
|
impl<T: Send + Sync + Clone + 'static> Fn<()> for $ty<T> {
|
||||||
|
#[inline(always)]
|
||||||
|
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
|
||||||
|
impl_get_fn_traits_get_send!(@method_name self $($method_name)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
(@method_name $self:ident) => {
|
||||||
|
$self.get()
|
||||||
|
};
|
||||||
|
(@method_name $self:ident $ident:ident) => {
|
||||||
|
$self.$ident()
|
||||||
|
};
|
||||||
|
}
|
||||||
macro_rules! impl_set_fn_traits_send {
|
macro_rules! impl_set_fn_traits_send {
|
||||||
($($ty:ident $($method_name:ident)?),*) => {
|
($($ty:ident $($method_name:ident)?),*) => {
|
||||||
$(
|
$(
|
||||||
|
@ -159,7 +235,8 @@ macro_rules! impl_set_fn_traits_send {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_get_fn_traits![ArcReadSignal, ArcRwSignal];
|
impl_get_fn_traits_read![ArcReadSignal, ArcRwSignal];
|
||||||
impl_get_fn_traits_send![ReadSignal, RwSignal, Memo, ArcMemo];
|
impl_get_fn_traits_get_send![ArcSignal, Signal];
|
||||||
|
impl_get_fn_traits_read_send![ReadSignal, RwSignal, Memo, ArcMemo];
|
||||||
impl_set_fn_traits![ArcWriteSignal];
|
impl_set_fn_traits![ArcWriteSignal];
|
||||||
impl_set_fn_traits_send![WriteSignal];
|
impl_set_fn_traits_send![WriteSignal];
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
use hydration_context::SharedContext;
|
||||||
use or_poisoned::OrPoisoned;
|
use or_poisoned::OrPoisoned;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -18,6 +20,8 @@ pub use context::*;
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct Owner {
|
pub struct Owner {
|
||||||
pub(crate) inner: Arc<RwLock<OwnerInner>>,
|
pub(crate) inner: Arc<RwLock<OwnerInner>>,
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub(crate) shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
|
@ -25,9 +29,21 @@ thread_local! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Owner {
|
impl Owner {
|
||||||
|
pub fn debug_id(&self) -> usize {
|
||||||
|
Arc::as_ptr(&self.inner) as usize
|
||||||
|
}
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
#[cfg(not(feature = "hydration"))]
|
||||||
let parent = OWNER
|
let parent = OWNER
|
||||||
.with(|o| o.borrow().as_ref().map(|o| Arc::downgrade(&o.inner)));
|
.with(|o| o.borrow().as_ref().map(|o| Arc::downgrade(&o.inner)));
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
let (parent, shared_context) = OWNER
|
||||||
|
.with(|o| {
|
||||||
|
o.borrow().as_ref().map(|o| {
|
||||||
|
(Some(Arc::clone(&o.inner)), o.shared_context.clone())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or((None, None));
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(RwLock::new(OwnerInner {
|
inner: Arc::new(RwLock::new(OwnerInner {
|
||||||
parent,
|
parent,
|
||||||
|
@ -35,11 +51,29 @@ impl Owner {
|
||||||
contexts: Default::default(),
|
contexts: Default::default(),
|
||||||
cleanups: Default::default(),
|
cleanups: Default::default(),
|
||||||
})),
|
})),
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
shared_context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub fn new_root(
|
||||||
|
shared_context: Arc<dyn SharedContext + Send + Sync>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(RwLock::new(OwnerInner {
|
||||||
|
parent: None,
|
||||||
|
nodes: Default::default(),
|
||||||
|
contexts: Default::default(),
|
||||||
|
cleanups: Default::default(),
|
||||||
|
})),
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
shared_context: Some(shared_context),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn child(&self) -> Self {
|
pub fn child(&self) -> Self {
|
||||||
let parent = Some(Arc::downgrade(&self.inner));
|
let parent = Some(Arc::clone(&self.inner));
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(RwLock::new(OwnerInner {
|
inner: Arc::new(RwLock::new(OwnerInner {
|
||||||
parent,
|
parent,
|
||||||
|
@ -47,6 +81,8 @@ impl Owner {
|
||||||
contexts: Default::default(),
|
contexts: Default::default(),
|
||||||
cleanups: Default::default(),
|
cleanups: Default::default(),
|
||||||
})),
|
})),
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
shared_context: self.shared_context.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,11 +133,21 @@ impl Owner {
|
||||||
pub fn current() -> Option<Owner> {
|
pub fn current() -> Option<Owner> {
|
||||||
OWNER.with(|o| o.borrow().clone())
|
OWNER.with(|o| o.borrow().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hydration")]
|
||||||
|
pub fn current_shared_context(
|
||||||
|
) -> Option<Arc<dyn SharedContext + Send + Sync>> {
|
||||||
|
OWNER.with(|o| {
|
||||||
|
o.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|current| current.shared_context.clone())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct OwnerInner {
|
pub(crate) struct OwnerInner {
|
||||||
pub parent: Option<Weak<RwLock<OwnerInner>>>,
|
pub parent: Option<Arc<RwLock<OwnerInner>>>,
|
||||||
nodes: Vec<NodeId>,
|
nodes: Vec<NodeId>,
|
||||||
pub contexts: FxHashMap<TypeId, Box<dyn Any + Send + Sync>>,
|
pub contexts: FxHashMap<TypeId, Box<dyn Any + Send + Sync>>,
|
||||||
pub cleanups: Vec<Box<dyn FnOnce() + Send + Sync>>,
|
pub cleanups: Vec<Box<dyn FnOnce() + Send + Sync>>,
|
||||||
|
|
|
@ -14,7 +14,7 @@ impl Owner {
|
||||||
fn use_context<T: Clone + 'static>(&self) -> Option<T> {
|
fn use_context<T: Clone + 'static>(&self) -> Option<T> {
|
||||||
let ty = TypeId::of::<T>();
|
let ty = TypeId::of::<T>();
|
||||||
let inner = self.inner.read().or_poisoned();
|
let inner = self.inner.read().or_poisoned();
|
||||||
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
|
let mut parent = inner.parent.as_ref().map(|p| p.clone());
|
||||||
let contexts = &self.inner.read().or_poisoned().contexts;
|
let contexts = &self.inner.read().or_poisoned().contexts;
|
||||||
if let Some(context) = contexts.get(&ty) {
|
if let Some(context) = contexts.get(&ty) {
|
||||||
context.downcast_ref::<T>().cloned()
|
context.downcast_ref::<T>().cloned()
|
||||||
|
@ -28,8 +28,7 @@ impl Owner {
|
||||||
if let Some(value) = downcast {
|
if let Some(value) = downcast {
|
||||||
return Some(value);
|
return Some(value);
|
||||||
} else {
|
} else {
|
||||||
parent =
|
parent = this_parent.parent.as_ref().map(|p| p.clone());
|
||||||
this_parent.parent.as_ref().and_then(|p| p.upgrade());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
|
@ -184,12 +184,10 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait With: WithUntracked + Track {
|
pub trait With: DefinedAt {
|
||||||
#[track_caller]
|
type Value: ?Sized;
|
||||||
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
|
|
||||||
self.track();
|
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U>;
|
||||||
self.try_with_untracked(fun)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
|
fn with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> U {
|
||||||
|
@ -197,15 +195,23 @@ pub trait With: WithUntracked + Track {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> With for T where T: WithUntracked + Track {}
|
impl<T> With for T
|
||||||
|
|
||||||
pub trait GetUntracked: WithUntracked
|
|
||||||
where
|
where
|
||||||
Self::Value: Clone,
|
T: WithUntracked + Track,
|
||||||
{
|
{
|
||||||
fn try_get_untracked(&self) -> Option<Self::Value> {
|
type Value = <T as WithUntracked>::Value;
|
||||||
self.try_with_untracked(Self::Value::clone)
|
|
||||||
|
#[track_caller]
|
||||||
|
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
|
||||||
|
self.track();
|
||||||
|
self.try_with_untracked(fun)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait GetUntracked: DefinedAt {
|
||||||
|
type Value;
|
||||||
|
|
||||||
|
fn try_get_untracked(&self) -> Option<Self::Value>;
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn get_untracked(&self) -> Self::Value {
|
fn get_untracked(&self) -> Self::Value {
|
||||||
|
@ -219,20 +225,21 @@ where
|
||||||
T: WithUntracked,
|
T: WithUntracked,
|
||||||
T::Value: Clone,
|
T::Value: Clone,
|
||||||
{
|
{
|
||||||
|
type Value = <Self as WithUntracked>::Value;
|
||||||
|
|
||||||
|
fn try_get_untracked(&self) -> Option<Self::Value> {
|
||||||
|
self.try_with_untracked(Self::Value::clone)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Get: With
|
pub trait Get: DefinedAt {
|
||||||
where
|
type Value: Clone;
|
||||||
Self::Value: Clone,
|
|
||||||
{
|
fn try_get(&self) -> Option<Self::Value>;
|
||||||
fn try_get(&self) -> Option<Self::Value> {
|
|
||||||
self.try_with(Self::Value::clone)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn get(&self) -> Self::Value {
|
fn get(&self) -> Self::Value {
|
||||||
self.try_with(Self::Value::clone)
|
self.try_get().unwrap_or_else(unwrap_signal!(self))
|
||||||
.unwrap_or_else(unwrap_signal!(self))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,6 +248,11 @@ where
|
||||||
T: With,
|
T: With,
|
||||||
T::Value: Clone,
|
T::Value: Clone,
|
||||||
{
|
{
|
||||||
|
type Value = <T as With>::Value;
|
||||||
|
|
||||||
|
fn try_get(&self) -> Option<Self::Value> {
|
||||||
|
self.try_with(Self::Value::clone)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Trigger {
|
pub trait Trigger {
|
||||||
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
pub mod read {
|
||||||
|
use crate::{
|
||||||
|
computed::ArcMemo,
|
||||||
|
owner::StoredValue,
|
||||||
|
signal::ArcReadSignal,
|
||||||
|
traits::{DefinedAt, Get, GetUntracked},
|
||||||
|
untrack,
|
||||||
|
};
|
||||||
|
use std::{panic::Location, sync::Arc};
|
||||||
|
|
||||||
|
enum SignalTypes<T: 'static> {
|
||||||
|
ReadSignal(ArcReadSignal<T>),
|
||||||
|
Memo(ArcMemo<T>),
|
||||||
|
DerivedSignal(Arc<dyn Fn() -> T + Send + Sync>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for SignalTypes<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::ReadSignal(arg0) => Self::ReadSignal(arg0.clone()),
|
||||||
|
Self::Memo(arg0) => Self::Memo(arg0.clone()),
|
||||||
|
Self::DerivedSignal(arg0) => Self::DerivedSignal(arg0.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> core::fmt::Debug for SignalTypes<T> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ReadSignal(arg0) => {
|
||||||
|
f.debug_tuple("ReadSignal").field(arg0).finish()
|
||||||
|
}
|
||||||
|
Self::Memo(arg0) => f.debug_tuple("Memo").field(arg0).finish(),
|
||||||
|
Self::DerivedSignal(_) => {
|
||||||
|
f.debug_tuple("DerivedSignal").finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq for SignalTypes<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::ReadSignal(l0), Self::ReadSignal(r0)) => l0 == r0,
|
||||||
|
(Self::Memo(l0), Self::Memo(r0)) => l0 == r0,
|
||||||
|
(Self::DerivedSignal(l0), Self::DerivedSignal(r0)) => {
|
||||||
|
std::ptr::eq(l0, r0)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArcSignal<T: 'static> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: &'static Location<'static>,
|
||||||
|
inner: SignalTypes<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for ArcSignal<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: self.defined_at,
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> core::fmt::Debug for ArcSignal<T> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let mut s = f.debug_struct("ArcSignal");
|
||||||
|
s.field("inner", &self.inner);
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
s.field("defined_at", &self.defined_at);
|
||||||
|
s.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Eq for ArcSignal<T> {}
|
||||||
|
|
||||||
|
impl<T> PartialEq for ArcSignal<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner == other.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DefinedAt for ArcSignal<T> {
|
||||||
|
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Some(self.defined_at)
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GetUntracked for ArcSignal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn try_get_untracked(&self) -> Option<Self::Value> {
|
||||||
|
match &self.inner {
|
||||||
|
SignalTypes::ReadSignal(i) => i.try_get_untracked(),
|
||||||
|
SignalTypes::Memo(i) => i.try_get_untracked(),
|
||||||
|
SignalTypes::DerivedSignal(i) => Some(untrack(|| i())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Get for ArcSignal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn try_get(&self) -> Option<Self::Value> {
|
||||||
|
match &self.inner {
|
||||||
|
SignalTypes::ReadSignal(i) => i.try_get(),
|
||||||
|
SignalTypes::Memo(i) => i.try_get(),
|
||||||
|
SignalTypes::DerivedSignal(i) => Some(i()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Signal<T: 'static> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: &'static Location<'static>,
|
||||||
|
inner: StoredValue<SignalTypes<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Clone for Signal<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Copy for Signal<T> {}
|
||||||
|
|
||||||
|
impl<T> core::fmt::Debug for Signal<T> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
let mut s = f.debug_struct("Signal");
|
||||||
|
s.field("inner", &self.inner);
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
s.field("defined_at", &self.defined_at);
|
||||||
|
s.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Eq for Signal<T> {}
|
||||||
|
|
||||||
|
impl<T> PartialEq for Signal<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner == other.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DefinedAt for Signal<T> {
|
||||||
|
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Some(self.defined_at)
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> GetUntracked for Signal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn try_get_untracked(&self) -> Option<Self::Value> {
|
||||||
|
self.inner
|
||||||
|
.with_value(|inner| match &inner {
|
||||||
|
SignalTypes::ReadSignal(i) => i.try_get_untracked(),
|
||||||
|
SignalTypes::Memo(i) => i.try_get_untracked(),
|
||||||
|
SignalTypes::DerivedSignal(i) => Some(untrack(|| i())),
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Get for Signal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + Clone,
|
||||||
|
{
|
||||||
|
type Value = T;
|
||||||
|
|
||||||
|
fn try_get(&self) -> Option<Self::Value> {
|
||||||
|
self.inner
|
||||||
|
.with_value(|inner| match &inner {
|
||||||
|
SignalTypes::ReadSignal(i) => i.try_get(),
|
||||||
|
SignalTypes::Memo(i) => i.try_get(),
|
||||||
|
SignalTypes::DerivedSignal(i) => Some(i()),
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Signal<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
/// Wraps a derived signal, i.e., any computation that accesses one or more
|
||||||
|
/// reactive signals.
|
||||||
|
/// ```rust
|
||||||
|
/// # use leptos_reactive::*;
|
||||||
|
/// # let runtime = create_runtime();
|
||||||
|
/// let (count, set_count) = create_signal(2);
|
||||||
|
/// let double_count = Signal::derive(move || count.get() * 2);
|
||||||
|
///
|
||||||
|
/// // this function takes any kind of wrapped signal
|
||||||
|
/// fn above_3(arg: &Signal<i32>) -> bool {
|
||||||
|
/// arg.get() > 3
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// assert_eq!(above_3(&count.into()), false);
|
||||||
|
/// assert_eq!(above_3(&double_count), true);
|
||||||
|
/// # runtime.dispose();
|
||||||
|
/// ```
|
||||||
|
#[track_caller]
|
||||||
|
pub fn derive(
|
||||||
|
derived_signal: impl Fn() -> T + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
let span = ::tracing::Span::current();
|
||||||
|
|
||||||
|
let derived_signal = move || {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
let _guard = span.enter();
|
||||||
|
derived_signal()
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner: StoredValue::new(SignalTypes::DerivedSignal(Arc::new(
|
||||||
|
derived_signal,
|
||||||
|
))),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
defined_at: std::panic::Location::caller(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for Signal<T>
|
||||||
|
where
|
||||||
|
T: Default + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::derive(|| Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use tachys::{renderer::Renderer, view::RenderHtml};
|
use tachys::{renderer::Renderer, view::RenderHtml};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
/// A route that this application can serve.
|
/// A route that this application can serve.
|
||||||
pub struct RouteListing {
|
pub struct RouteListing {
|
||||||
path: Vec<PathSegment>,
|
path: Vec<PathSegment>,
|
||||||
|
@ -64,6 +64,10 @@ impl RouteListing {
|
||||||
self.static_mode.as_ref().map(|n| &n.1)
|
self.static_mode.as_ref().map(|n| &n.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_static_parts(self) -> Option<(StaticMode, StaticDataMap)> {
|
||||||
|
self.static_mode
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
|
/// Build a route statically, will return `Ok(true)` on success or `Ok(false)` when the route
|
||||||
/// is not marked as statically rendered. All route parameters to use when resolving all paths
|
/// is not marked as statically rendered. All route parameters to use when resolving all paths
|
||||||
|
@ -100,6 +104,12 @@ impl RouteListing {
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct RouteList(Vec<RouteListing>);
|
pub struct RouteList(Vec<RouteListing>);
|
||||||
|
|
||||||
|
impl From<Vec<RouteListing>> for RouteList {
|
||||||
|
fn from(value: Vec<RouteListing>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RouteList {
|
impl RouteList {
|
||||||
pub fn push(&mut self, data: RouteListing) {
|
pub fn push(&mut self, data: RouteListing) {
|
||||||
self.0.push(data);
|
self.0.push(data);
|
||||||
|
|
|
@ -20,11 +20,6 @@ impl fmt::Debug for BrowserUrl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserUrl {
|
impl BrowserUrl {
|
||||||
pub fn new() -> Result<Self, JsValue> {
|
|
||||||
let url = ArcRwSignal::new(Self::current()?);
|
|
||||||
Ok(Self { url })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scroll_to_el(loc_scroll: bool) {
|
fn scroll_to_el(loc_scroll: bool) {
|
||||||
if let Ok(hash) = window().location().hash() {
|
if let Ok(hash) = window().location().hash() {
|
||||||
if !hash.is_empty() {
|
if !hash.is_empty() {
|
||||||
|
@ -50,6 +45,11 @@ impl BrowserUrl {
|
||||||
impl Location for BrowserUrl {
|
impl Location for BrowserUrl {
|
||||||
type Error = JsValue;
|
type Error = JsValue;
|
||||||
|
|
||||||
|
fn new() -> Result<Self, JsValue> {
|
||||||
|
let url = ArcRwSignal::new(Self::current()?);
|
||||||
|
Ok(Self { url })
|
||||||
|
}
|
||||||
|
|
||||||
fn as_url(&self) -> &ArcRwSignal<Url> {
|
fn as_url(&self) -> &ArcRwSignal<Url> {
|
||||||
&self.url
|
&self.url
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,9 +71,11 @@ impl Default for LocationChange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Location {
|
pub trait Location: Sized {
|
||||||
type Error: Debug;
|
type Error: Debug;
|
||||||
|
|
||||||
|
fn new() -> Result<Self, Self::Error>;
|
||||||
|
|
||||||
fn as_url(&self) -> &ArcRwSignal<Url>;
|
fn as_url(&self) -> &ArcRwSignal<Url>;
|
||||||
|
|
||||||
fn current() -> Result<Url, Self::Error>;
|
fn current() -> Result<Url, Self::Error>;
|
||||||
|
|
|
@ -11,6 +11,12 @@ impl RequestUrl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for RequestUrl {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for RequestUrl {
|
impl Default for RequestUrl {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new("/")
|
Self::new("/")
|
||||||
|
@ -18,7 +24,7 @@ impl Default for RequestUrl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestUrl {
|
impl RequestUrl {
|
||||||
fn parse(url: &str) -> Result<Url, url::ParseError> {
|
pub fn parse(url: &str) -> Result<Url, url::ParseError> {
|
||||||
Self::parse_with_base(url, BASE)
|
Self::parse_with_base(url, BASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -214,14 +214,12 @@ where
|
||||||
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
|
||||||
let mut segment_routes = Vec::new();
|
let mut segment_routes = Vec::new();
|
||||||
self.segments.generate_path(&mut segment_routes);
|
self.segments.generate_path(&mut segment_routes);
|
||||||
let segment_routes = segment_routes.into_iter();
|
let children = self.children.as_ref();
|
||||||
let children_routes = self.children.as_ref().into_iter().flat_map(|child| child.generate_routes().into_iter());
|
match children {
|
||||||
children_routes.map(move |child_routes| {
|
None => Either::Left(iter::once(segment_routes)),
|
||||||
segment_routes
|
Some(children) => {
|
||||||
.clone()
|
Either::Right(children.generate_routes().into_iter())
|
||||||
.chain(child_routes)
|
}
|
||||||
.filter(|seg| seg != &PathSegment::Unit)
|
}
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -316,7 +316,7 @@ macro_rules! tuples {
|
||||||
}
|
}
|
||||||
|
|
||||||
tuples!(EitherOf3 => A = 0, B = 1, C = 2);
|
tuples!(EitherOf3 => A = 0, B = 1, C = 2);
|
||||||
/*tuples!(EitherOf4 => A = 0, B = 1, C = 2, D = 3);
|
tuples!(EitherOf4 => A = 0, B = 1, C = 2, D = 3);
|
||||||
tuples!(EitherOf5 => A = 0, B = 1, C = 2, D = 3, E = 4);
|
tuples!(EitherOf5 => A = 0, B = 1, C = 2, D = 3, E = 4);
|
||||||
tuples!(EitherOf6 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5);
|
tuples!(EitherOf6 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5);
|
||||||
tuples!(EitherOf7 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6);
|
tuples!(EitherOf7 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6);
|
||||||
|
@ -329,4 +329,3 @@ tuples!(EitherOf13 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I
|
||||||
tuples!(EitherOf14 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13);
|
tuples!(EitherOf14 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13);
|
||||||
tuples!(EitherOf15 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14);
|
tuples!(EitherOf15 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14);
|
||||||
tuples!(EitherOf16 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15);
|
tuples!(EitherOf16 => A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, I = 8, J = 9, K = 10, L = 11, M = 12, N = 13, O = 14, P = 15);
|
||||||
*/
|
|
||||||
|
|
|
@ -7,3 +7,14 @@ pub enum PathSegment {
|
||||||
Param(Cow<'static, str>),
|
Param(Cow<'static, str>),
|
||||||
Splat(Cow<'static, str>),
|
Splat(Cow<'static, str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PathSegment {
|
||||||
|
pub fn as_raw_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
PathSegment::Unit => "",
|
||||||
|
PathSegment::Static(i) => i,
|
||||||
|
PathSegment::Param(i) => i,
|
||||||
|
PathSegment::Splat(i) => i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,12 @@ impl Params {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, key: &str) -> Option<&str> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.find_map(|(k, v)| (k == key).then_some(v.as_str()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K, V> FromIterator<(K, V)> for Params
|
impl<K, V> FromIterator<(K, V)> for Params
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
generate_route_list::RouteList,
|
generate_route_list::RouteList,
|
||||||
location::Location,
|
location::{Location, RequestUrl},
|
||||||
matching::{
|
matching::{
|
||||||
MatchInterface, MatchNestedRoutes, PossibleRouteMatch, RouteMatchId,
|
MatchInterface, MatchNestedRoutes, PossibleRouteMatch, RouteMatchId,
|
||||||
Routes,
|
Routes,
|
||||||
},
|
},
|
||||||
ChooseView, MatchParams, Params,
|
ChooseView, MatchParams, Method, Params, PathSegment, RouteListing,
|
||||||
|
SsrMode,
|
||||||
};
|
};
|
||||||
use core::marker::PhantomData;
|
use core::marker::PhantomData;
|
||||||
use either_of::*;
|
use either_of::*;
|
||||||
|
@ -13,17 +14,23 @@ use once_cell::unsync::Lazy;
|
||||||
use reactive_graph::{
|
use reactive_graph::{
|
||||||
computed::{ArcMemo, Memo},
|
computed::{ArcMemo, Memo},
|
||||||
effect::RenderEffect,
|
effect::RenderEffect,
|
||||||
owner::Owner,
|
owner::{use_context, Owner},
|
||||||
signal::ArcRwSignal,
|
signal::ArcRwSignal,
|
||||||
traits::{Get, Read, Set, Track},
|
traits::{Get, Read, Set, Track},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any, borrow::Cow, cell::RefCell, collections::VecDeque, rc::Rc,
|
any::Any,
|
||||||
|
borrow::Cow,
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
collections::VecDeque,
|
||||||
|
fmt::Debug,
|
||||||
|
iter,
|
||||||
|
rc::Rc,
|
||||||
};
|
};
|
||||||
use tachys::{
|
use tachys::{
|
||||||
html::attribute::Attribute,
|
html::attribute::Attribute,
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
renderer::Renderer,
|
renderer::{dom::Dom, Renderer},
|
||||||
ssr::StreamBuilder,
|
ssr::StreamBuilder,
|
||||||
view::{
|
view::{
|
||||||
add_attr::AddAnyAttr,
|
add_attr::AddAnyAttr,
|
||||||
|
@ -36,7 +43,7 @@ use tachys::{
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Router<Rndr, Loc, Children, FallbackFn> {
|
pub struct Router<Rndr, Loc, Children, FallbackFn> {
|
||||||
base: Option<Cow<'static, str>>,
|
base: Option<Cow<'static, str>>,
|
||||||
location: Loc,
|
location: PhantomData<Loc>,
|
||||||
pub routes: Routes<Children, Rndr>,
|
pub routes: Routes<Children, Rndr>,
|
||||||
fallback: FallbackFn,
|
fallback: FallbackFn,
|
||||||
}
|
}
|
||||||
|
@ -49,13 +56,12 @@ where
|
||||||
FallbackFn: Fn() -> Fallback,
|
FallbackFn: Fn() -> Fallback,
|
||||||
{
|
{
|
||||||
pub fn new(
|
pub fn new(
|
||||||
location: Loc,
|
|
||||||
routes: Routes<Children, Rndr>,
|
routes: Routes<Children, Rndr>,
|
||||||
fallback: FallbackFn,
|
fallback: FallbackFn,
|
||||||
) -> Router<Rndr, Loc, Children, FallbackFn> {
|
) -> Router<Rndr, Loc, Children, FallbackFn> {
|
||||||
Self {
|
Self {
|
||||||
base: None,
|
base: None,
|
||||||
location,
|
location: PhantomData,
|
||||||
routes,
|
routes,
|
||||||
fallback,
|
fallback,
|
||||||
}
|
}
|
||||||
|
@ -63,13 +69,12 @@ where
|
||||||
|
|
||||||
pub fn new_with_base(
|
pub fn new_with_base(
|
||||||
base: impl Into<Cow<'static, str>>,
|
base: impl Into<Cow<'static, str>>,
|
||||||
location: Loc,
|
|
||||||
routes: Routes<Children, Rndr>,
|
routes: Routes<Children, Rndr>,
|
||||||
fallback: FallbackFn,
|
fallback: FallbackFn,
|
||||||
) -> Router<Rndr, Loc, Children, FallbackFn> {
|
) -> Router<Rndr, Loc, Children, FallbackFn> {
|
||||||
Self {
|
Self {
|
||||||
base: Some(base.into()),
|
base: Some(base.into()),
|
||||||
location,
|
location: PhantomData,
|
||||||
routes,
|
routes,
|
||||||
fallback,
|
fallback,
|
||||||
}
|
}
|
||||||
|
@ -87,7 +92,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RouteData<R>
|
pub struct RouteData<R = Dom>
|
||||||
where
|
where
|
||||||
R: Renderer + 'static,
|
R: Renderer + 'static,
|
||||||
{
|
{
|
||||||
|
@ -120,8 +125,9 @@ where
|
||||||
type FallibleState = (); // TODO
|
type FallibleState = (); // TODO
|
||||||
|
|
||||||
fn build(self) -> Self::State {
|
fn build(self) -> Self::State {
|
||||||
self.location.init(self.base);
|
let location = Loc::new().unwrap(); // TODO
|
||||||
let url = self.location.as_url().clone();
|
location.init(self.base);
|
||||||
|
let url = location.as_url().clone();
|
||||||
let path = ArcMemo::new({
|
let path = ArcMemo::new({
|
||||||
let url = url.clone();
|
let url = url.clone();
|
||||||
move |_| url.read().path().to_string()
|
move |_| url.read().path().to_string()
|
||||||
|
@ -185,6 +191,178 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Rndr, Loc, FallbackFn, Fallback, Children> RenderHtml<Rndr>
|
||||||
|
for Router<Rndr, Loc, Children, FallbackFn>
|
||||||
|
where
|
||||||
|
Loc: Location,
|
||||||
|
FallbackFn: Fn() -> Fallback + 'static,
|
||||||
|
Fallback: RenderHtml<Rndr>,
|
||||||
|
Children: MatchNestedRoutes<Rndr> + 'static,
|
||||||
|
Children::View: RenderHtml<Rndr>,
|
||||||
|
/*View: Render<Rndr> + IntoAny<Rndr> + 'static,
|
||||||
|
View::State: 'static,*/
|
||||||
|
Fallback: RenderHtml<Rndr>,
|
||||||
|
Fallback::State: 'static,
|
||||||
|
Rndr: Renderer + 'static,
|
||||||
|
Children::Match: std::fmt::Debug,
|
||||||
|
<Children::Match as MatchInterface<Rndr>>::Child: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
// TODO probably pick a max length here
|
||||||
|
const MIN_LENGTH: usize = Fallback::MIN_LENGTH;
|
||||||
|
|
||||||
|
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||||
|
// if this is being run on the server for the first time, generating all possible routes
|
||||||
|
if RouteList::is_generating() {
|
||||||
|
// add routes
|
||||||
|
let (base, routes) = self.routes.generate_routes();
|
||||||
|
let mut routes = routes
|
||||||
|
.into_iter()
|
||||||
|
.map(|segments| {
|
||||||
|
let path = base
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|base| {
|
||||||
|
iter::once(PathSegment::Static(
|
||||||
|
base.to_string().into(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.chain(segments)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
// TODO add non-defaults for mode, etc.
|
||||||
|
RouteListing::new(
|
||||||
|
path,
|
||||||
|
SsrMode::OutOfOrder,
|
||||||
|
[Method::Get],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// add fallback
|
||||||
|
// TODO fix: causes overlapping route issues on Axum
|
||||||
|
/*routes.push(RouteListing::new(
|
||||||
|
[PathSegment::Static(
|
||||||
|
base.unwrap_or_default().to_string().into(),
|
||||||
|
)],
|
||||||
|
SsrMode::Async,
|
||||||
|
[
|
||||||
|
Method::Get,
|
||||||
|
Method::Post,
|
||||||
|
Method::Put,
|
||||||
|
Method::Patch,
|
||||||
|
Method::Delete,
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
));*/
|
||||||
|
|
||||||
|
RouteList::register(RouteList::from(routes));
|
||||||
|
} else {
|
||||||
|
let outer_owner = Owner::current()
|
||||||
|
.expect("creating Router, but no Owner was found");
|
||||||
|
let url = use_context::<RequestUrl>()
|
||||||
|
.expect("could not find request URL in context");
|
||||||
|
// TODO base
|
||||||
|
let url =
|
||||||
|
RequestUrl::parse(url.as_ref()).expect("could not parse URL");
|
||||||
|
// TODO query params
|
||||||
|
let new_match = self.routes.match_route(url.path());
|
||||||
|
match new_match {
|
||||||
|
Some(matched) => {
|
||||||
|
Either::Left(NestedRouteView::new(&outer_owner, matched))
|
||||||
|
}
|
||||||
|
_ => Either::Right((self.fallback)()),
|
||||||
|
}
|
||||||
|
.to_html_with_buf(buf, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||||
|
self,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let outer_owner =
|
||||||
|
Owner::current().expect("creating Router, but no Owner was found");
|
||||||
|
let url = use_context::<RequestUrl>()
|
||||||
|
.expect("could not find request URL in context");
|
||||||
|
// TODO base
|
||||||
|
let url = RequestUrl::parse(url.as_ref()).expect("could not parse URL");
|
||||||
|
// TODO query params
|
||||||
|
let new_match = self.routes.match_route(url.path());
|
||||||
|
match new_match {
|
||||||
|
Some(matched) => {
|
||||||
|
Either::Left(NestedRouteView::new(&outer_owner, matched))
|
||||||
|
}
|
||||||
|
_ => Either::Right((self.fallback)()),
|
||||||
|
}
|
||||||
|
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
cursor: &Cursor<Rndr>,
|
||||||
|
position: &PositionState,
|
||||||
|
) -> Self::State {
|
||||||
|
let location = Loc::new().unwrap(); // TODO
|
||||||
|
location.init(self.base);
|
||||||
|
let url = location.as_url().clone();
|
||||||
|
let path = ArcMemo::new({
|
||||||
|
let url = url.clone();
|
||||||
|
move |_| url.read().path().to_string()
|
||||||
|
});
|
||||||
|
let search_params = ArcMemo::new({
|
||||||
|
let url = url.clone();
|
||||||
|
move |_| url.read().search_params().clone()
|
||||||
|
});
|
||||||
|
let outer_owner =
|
||||||
|
Owner::current().expect("creating Router, but no Owner was found");
|
||||||
|
|
||||||
|
let cursor = cursor.clone();
|
||||||
|
let position = position.clone();
|
||||||
|
RenderEffect::new(move |prev: Option<EitherState<_, _, _>>| {
|
||||||
|
let path = path.read();
|
||||||
|
let new_match = self.routes.match_route(&path);
|
||||||
|
|
||||||
|
if let Some(mut prev) = prev {
|
||||||
|
if let Some(new_match) = new_match {
|
||||||
|
match &mut prev.state {
|
||||||
|
Either::Left(prev) => {
|
||||||
|
rebuild_nested(&outer_owner, prev, new_match);
|
||||||
|
}
|
||||||
|
Either::Right(_) => {
|
||||||
|
Either::<_, Fallback>::Left(NestedRouteView::new(
|
||||||
|
&outer_owner,
|
||||||
|
new_match,
|
||||||
|
))
|
||||||
|
.rebuild(&mut prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Either::<NestedRouteView<Children::Match, Rndr>, _>::Right(
|
||||||
|
(self.fallback)(),
|
||||||
|
)
|
||||||
|
.rebuild(&mut prev);
|
||||||
|
}
|
||||||
|
prev
|
||||||
|
} else {
|
||||||
|
match new_match {
|
||||||
|
Some(matched) => {
|
||||||
|
Either::Left(NestedRouteView::new_hydrate(
|
||||||
|
&outer_owner,
|
||||||
|
matched,
|
||||||
|
&cursor,
|
||||||
|
&position,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => Either::Right((self.fallback)()),
|
||||||
|
}
|
||||||
|
.hydrate::<true>(&cursor, &position)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct NestedRouteView<Matcher, R>
|
pub struct NestedRouteView<Matcher, R>
|
||||||
where
|
where
|
||||||
Matcher: MatchInterface<R>,
|
Matcher: MatchInterface<R>,
|
||||||
|
@ -238,6 +416,53 @@ where
|
||||||
ty: PhantomData,
|
ty: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_hydrate(
|
||||||
|
outer_owner: &Owner,
|
||||||
|
route_match: Matcher,
|
||||||
|
cursor: &Cursor<Rndr>,
|
||||||
|
position: &PositionState,
|
||||||
|
) -> Self {
|
||||||
|
// keep track of all outlets, for diffing
|
||||||
|
let mut outlets = VecDeque::new();
|
||||||
|
|
||||||
|
// build this view
|
||||||
|
let owner = outer_owner.child();
|
||||||
|
let id = route_match.as_id();
|
||||||
|
let params =
|
||||||
|
ArcRwSignal::new(route_match.to_params().into_iter().collect());
|
||||||
|
let (view, child) = route_match.into_view_and_child();
|
||||||
|
|
||||||
|
let outlet = child
|
||||||
|
.map(|child| {
|
||||||
|
get_inner_view_hydrate(
|
||||||
|
&mut outlets,
|
||||||
|
&owner,
|
||||||
|
child,
|
||||||
|
cursor,
|
||||||
|
position,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let route_data = RouteData {
|
||||||
|
params: ArcMemo::new({
|
||||||
|
let params = params.clone();
|
||||||
|
move |_| params.get()
|
||||||
|
}),
|
||||||
|
outlet,
|
||||||
|
};
|
||||||
|
let view = owner.with(|| view.choose(route_data));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
params,
|
||||||
|
outlets,
|
||||||
|
view,
|
||||||
|
ty: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NestedRouteState<Matcher, Rndr>
|
pub struct NestedRouteState<Matcher, Rndr>
|
||||||
|
@ -270,11 +495,11 @@ where
|
||||||
.map(|child| get_inner_view(outlets, &owner, child))
|
.map(|child| get_inner_view(outlets, &owner, child))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let inner = Rc::new(RefCell::new(OutletStateInner {
|
let view = Rc::new(Lazy::new({
|
||||||
state: Lazy::new({
|
|
||||||
let params = params.clone();
|
|
||||||
let owner = owner.clone();
|
let owner = owner.clone();
|
||||||
|
let params = params.clone();
|
||||||
Box::new(move || {
|
Box::new(move || {
|
||||||
|
RefCell::new(Some(
|
||||||
owner
|
owner
|
||||||
.with(|| {
|
.with(|| {
|
||||||
view.choose(RouteData {
|
view.choose(RouteData {
|
||||||
|
@ -282,10 +507,68 @@ where
|
||||||
outlet,
|
outlet,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.into_any()
|
.into_any(),
|
||||||
.build()
|
))
|
||||||
|
}) as Box<dyn FnOnce() -> RefCell<Option<AnyView<R>>>>
|
||||||
|
}));
|
||||||
|
let inner = Rc::new(RefCell::new(OutletStateInner {
|
||||||
|
view: Rc::clone(&view),
|
||||||
|
state: Lazy::new(Box::new(move || view.take().unwrap().build())),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let outlet = Outlet {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
params,
|
||||||
|
inner,
|
||||||
|
};
|
||||||
|
outlets.push_back(outlet.clone());
|
||||||
|
outlet
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_inner_view_hydrate<Match, R>(
|
||||||
|
outlets: &mut VecDeque<Outlet<R>>,
|
||||||
|
parent: &Owner,
|
||||||
|
route_match: Match,
|
||||||
|
cursor: &Cursor<R>,
|
||||||
|
position: &PositionState,
|
||||||
|
) -> Outlet<R>
|
||||||
|
where
|
||||||
|
Match: MatchInterface<R> + MatchParams,
|
||||||
|
R: Renderer + 'static,
|
||||||
|
{
|
||||||
|
let owner = parent.child();
|
||||||
|
let id = route_match.as_id();
|
||||||
|
let params =
|
||||||
|
ArcRwSignal::new(route_match.to_params().into_iter().collect());
|
||||||
|
let (view, child) = route_match.into_view_and_child();
|
||||||
|
let outlet = child
|
||||||
|
.map(|child| get_inner_view(outlets, &owner, child))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let view = Rc::new(Lazy::new({
|
||||||
|
let owner = owner.clone();
|
||||||
|
let params = params.clone();
|
||||||
|
Box::new(move || {
|
||||||
|
RefCell::new(Some(
|
||||||
|
owner
|
||||||
|
.with(|| {
|
||||||
|
view.choose(RouteData {
|
||||||
|
params: ArcMemo::new(move |_| params.get()),
|
||||||
|
outlet,
|
||||||
})
|
})
|
||||||
}),
|
})
|
||||||
|
.into_any(),
|
||||||
|
))
|
||||||
|
}) as Box<dyn FnOnce() -> RefCell<Option<AnyView<R>>>>
|
||||||
|
}));
|
||||||
|
let inner = Rc::new(RefCell::new(OutletStateInner {
|
||||||
|
view: Rc::clone(&view),
|
||||||
|
state: Lazy::new(Box::new({
|
||||||
|
let cursor = cursor.clone();
|
||||||
|
let position = position.clone();
|
||||||
|
move || view.take().unwrap().hydrate::<true>(&cursor, &position)
|
||||||
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let outlet = Outlet {
|
let outlet = Outlet {
|
||||||
|
@ -332,9 +615,7 @@ where
|
||||||
id: RouteMatchId(0),
|
id: RouteMatchId(0),
|
||||||
owner: Owner::current().unwrap(),
|
owner: Owner::current().unwrap(),
|
||||||
params: ArcRwSignal::new(Params::new()),
|
params: ArcRwSignal::new(Params::new()),
|
||||||
inner: Rc::new(RefCell::new(OutletStateInner {
|
inner: Default::default(),
|
||||||
state: Lazy::new(Box::new(|| ().into_any().build())),
|
|
||||||
})),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,7 +654,19 @@ where
|
||||||
const MIN_LENGTH: usize = 0; // TODO
|
const MIN_LENGTH: usize = 0; // TODO
|
||||||
|
|
||||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||||
todo!()
|
let view = self.inner.borrow().view.take().unwrap();
|
||||||
|
view.to_html_with_buf(buf, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||||
|
self,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let view = self.inner.borrow().view.take().unwrap();
|
||||||
|
view.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
@ -381,24 +674,41 @@ where
|
||||||
cursor: &Cursor<R>,
|
cursor: &Cursor<R>,
|
||||||
position: &PositionState,
|
position: &PositionState,
|
||||||
) -> Self::State {
|
) -> Self::State {
|
||||||
todo!()
|
let view = self.inner.borrow().view.take().unwrap();
|
||||||
|
let state = view.hydrate::<FROM_SERVER>(cursor, position);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct OutletStateInner<R>
|
pub struct OutletStateInner<R>
|
||||||
where
|
where
|
||||||
R: Renderer + 'static,
|
R: Renderer + 'static,
|
||||||
{
|
{
|
||||||
|
view: Rc<
|
||||||
|
Lazy<
|
||||||
|
RefCell<Option<AnyView<R>>>,
|
||||||
|
Box<dyn FnOnce() -> RefCell<Option<AnyView<R>>>>,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
state: Lazy<AnyViewState<R>, Box<dyn FnOnce() -> AnyViewState<R>>>,
|
state: Lazy<AnyViewState<R>, Box<dyn FnOnce() -> AnyViewState<R>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R: Renderer> Debug for OutletStateInner<R> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("OutletStateInner").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<R> Default for OutletStateInner<R>
|
impl<R> Default for OutletStateInner<R>
|
||||||
where
|
where
|
||||||
R: Renderer + 'static,
|
R: Renderer + 'static,
|
||||||
{
|
{
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
let view =
|
||||||
|
Rc::new(Lazy::new(Box::new(|| RefCell::new(Some(().into_any())))
|
||||||
|
as Box<dyn FnOnce() -> RefCell<Option<AnyView<R>>>>));
|
||||||
Self {
|
Self {
|
||||||
|
view,
|
||||||
state: Lazy::new(Box::new(|| ().into_any().build())),
|
state: Lazy::new(Box::new(|| ().into_any().build())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -583,6 +893,52 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Matcher, R> RenderHtml<R> for NestedRouteView<Matcher, R>
|
||||||
|
where
|
||||||
|
Matcher: MatchInterface<R>,
|
||||||
|
Matcher::View: Sized + 'static,
|
||||||
|
R: Renderer + 'static,
|
||||||
|
{
|
||||||
|
const MIN_LENGTH: usize = Matcher::View::MIN_LENGTH;
|
||||||
|
|
||||||
|
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
||||||
|
self.view.to_html_with_buf(buf, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||||
|
self,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.view
|
||||||
|
.to_html_async_with_buf::<OUT_OF_ORDER>(buf, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
cursor: &Cursor<R>,
|
||||||
|
position: &PositionState,
|
||||||
|
) -> Self::State {
|
||||||
|
let NestedRouteView {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
params,
|
||||||
|
outlets,
|
||||||
|
view,
|
||||||
|
ty,
|
||||||
|
} = self;
|
||||||
|
NestedRouteState {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
outlets,
|
||||||
|
params,
|
||||||
|
view: view.hydrate::<FROM_SERVER>(cursor, position),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<Matcher, R> Mountable<R> for NestedRouteState<Matcher, R>
|
impl<Matcher, R> Mountable<R> for NestedRouteState<Matcher, R>
|
||||||
where
|
where
|
||||||
Matcher: MatchInterface<R>,
|
Matcher: MatchInterface<R>,
|
||||||
|
@ -605,37 +961,6 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Rndr, Loc, FallbackFn, Fallback, Children> RenderHtml<Rndr>
|
|
||||||
for Router<Rndr, Loc, Children, FallbackFn>
|
|
||||||
where
|
|
||||||
Loc: Location,
|
|
||||||
FallbackFn: Fn() -> Fallback + 'static,
|
|
||||||
Fallback: RenderHtml<Rndr>,
|
|
||||||
Children: MatchNestedRoutes<Rndr> + 'static,
|
|
||||||
Children::View: RenderHtml<Rndr>,
|
|
||||||
/*View: Render<Rndr> + IntoAny<Rndr> + 'static,
|
|
||||||
View::State: 'static,*/
|
|
||||||
Fallback::State: 'static,
|
|
||||||
Rndr: Renderer + 'static,
|
|
||||||
Children::Match: std::fmt::Debug,
|
|
||||||
<Children::Match as MatchInterface<Rndr>>::Child: std::fmt::Debug,
|
|
||||||
{
|
|
||||||
// TODO probably pick a max length here
|
|
||||||
const MIN_LENGTH: usize = Fallback::MIN_LENGTH;
|
|
||||||
|
|
||||||
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
|
||||||
self,
|
|
||||||
cursor: &Cursor<Rndr>,
|
|
||||||
position: &PositionState,
|
|
||||||
) -> Self::State {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Rndr, Loc, FallbackFn, Fallback, Children, View> AddAnyAttr<Rndr>
|
impl<Rndr, Loc, FallbackFn, Fallback, Children, View> AddAnyAttr<Rndr>
|
||||||
for Router<Rndr, Loc, Children, FallbackFn>
|
for Router<Rndr, Loc, Children, FallbackFn>
|
||||||
where
|
where
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub enum StaticMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StaticDataMap;
|
pub struct StaticDataMap;
|
||||||
|
|
||||||
impl StaticDataMap {
|
impl StaticDataMap {
|
||||||
|
|
|
@ -66,6 +66,8 @@ web-sys = { version = "0.3", optional = true, features = [
|
||||||
"console",
|
"console",
|
||||||
"ReadableStream",
|
"ReadableStream",
|
||||||
"ReadableStreamDefaultReader",
|
"ReadableStreamDefaultReader",
|
||||||
|
"AbortController",
|
||||||
|
"AbortSignal"
|
||||||
] }
|
] }
|
||||||
|
|
||||||
# reqwest client
|
# reqwest client
|
||||||
|
@ -74,6 +76,7 @@ reqwest = { version = "0.12", default-features = false, optional = true, feature
|
||||||
"stream",
|
"stream",
|
||||||
] }
|
] }
|
||||||
url = "2"
|
url = "2"
|
||||||
|
pin-project-lite = "0.2.13"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["json", "cbor"]
|
default = ["json", "cbor"]
|
||||||
|
|
|
@ -38,11 +38,19 @@ pub trait Client<CustErr> {
|
||||||
pub mod browser {
|
pub mod browser {
|
||||||
use super::Client;
|
use super::Client;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ServerFnError, request::browser::BrowserRequest,
|
error::ServerFnError,
|
||||||
|
request::browser::{AbortOnDrop, BrowserRequest, RequestInner},
|
||||||
response::browser::BrowserResponse,
|
response::browser::BrowserResponse,
|
||||||
};
|
};
|
||||||
|
use gloo_net::{http::Response, Error};
|
||||||
use send_wrapper::SendWrapper;
|
use send_wrapper::SendWrapper;
|
||||||
use std::future::Future;
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
marker::PhantomData,
|
||||||
|
pin::Pin,
|
||||||
|
task::{Context, Poll},
|
||||||
|
};
|
||||||
|
use web_sys::AbortController;
|
||||||
|
|
||||||
/// Implements [`Client`] for a `fetch` request in the browser.
|
/// Implements [`Client`] for a `fetch` request in the browser.
|
||||||
pub struct BrowserClient;
|
pub struct BrowserClient;
|
||||||
|
@ -56,8 +64,17 @@ pub mod browser {
|
||||||
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
|
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
|
||||||
+ Send {
|
+ Send {
|
||||||
SendWrapper::new(async move {
|
SendWrapper::new(async move {
|
||||||
req.0
|
let mut req = req.0.take();
|
||||||
.take()
|
let RequestInner {
|
||||||
|
request,
|
||||||
|
abort_ctrl,
|
||||||
|
} = req;
|
||||||
|
/*BrowserRequestFuture {
|
||||||
|
request_fut: request.send(),
|
||||||
|
abort_ctrl,
|
||||||
|
cust_err: PhantomData,
|
||||||
|
}*/
|
||||||
|
request
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map(|res| BrowserResponse(SendWrapper::new(res)))
|
.map(|res| BrowserResponse(SendWrapper::new(res)))
|
||||||
|
@ -65,6 +82,36 @@ pub mod browser {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*pin_project_lite::pin_project! {
|
||||||
|
struct BrowserRequestFuture<Fut, CustErr>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = Result<Response, Error>>,
|
||||||
|
{
|
||||||
|
#[pin]
|
||||||
|
request_fut: Fut,
|
||||||
|
abort_ctrl: Option<AbortOnDrop>,
|
||||||
|
cust_err: PhantomData<CustErr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Fut, CustErr> Future for BrowserRequestFuture<Fut, CustErr>
|
||||||
|
where
|
||||||
|
Fut: Future<Output = Result<Response, Error>>,
|
||||||
|
{
|
||||||
|
type Output = Result<BrowserResponse, ServerFnError<CustErr>>;
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Self::Output> {
|
||||||
|
let this = self.project();
|
||||||
|
match this.request_fut.poll(cx) {
|
||||||
|
Poll::Ready(value) => todo!(),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "reqwest")]
|
#[cfg(feature = "reqwest")]
|
||||||
|
|
|
@ -8,27 +8,39 @@ use send_wrapper::SendWrapper;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use wasm_streams::ReadableStream;
|
use wasm_streams::ReadableStream;
|
||||||
use web_sys::{FormData, Headers, RequestInit, UrlSearchParams};
|
use web_sys::{
|
||||||
|
AbortController, AbortSignal, FormData, Headers, RequestInit,
|
||||||
|
UrlSearchParams,
|
||||||
|
};
|
||||||
|
|
||||||
/// A `fetch` request made in the browser.
|
/// A `fetch` request made in the browser.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BrowserRequest(pub(crate) SendWrapper<Request>);
|
pub struct BrowserRequest(pub(crate) SendWrapper<RequestInner>);
|
||||||
|
|
||||||
impl From<Request> for BrowserRequest {
|
#[derive(Debug)]
|
||||||
fn from(value: Request) -> Self {
|
pub(crate) struct RequestInner {
|
||||||
Self(SendWrapper::new(value))
|
pub(crate) request: Request,
|
||||||
|
pub(crate) abort_ctrl: Option<AbortOnDrop>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct AbortOnDrop(AbortController);
|
||||||
|
|
||||||
|
impl Drop for AbortOnDrop {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BrowserRequest> for Request {
|
impl From<BrowserRequest> for Request {
|
||||||
fn from(value: BrowserRequest) -> Self {
|
fn from(value: BrowserRequest) -> Self {
|
||||||
value.0.take()
|
value.0.take().request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BrowserRequest> for web_sys::Request {
|
impl From<BrowserRequest> for web_sys::Request {
|
||||||
fn from(value: BrowserRequest) -> Self {
|
fn from(value: BrowserRequest) -> Self {
|
||||||
value.0.take().into()
|
value.0.take().request.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,13 +48,13 @@ impl Deref for BrowserRequest {
|
||||||
type Target = Request;
|
type Target = Request;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
self.0.deref()
|
&self.0.deref().request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for BrowserRequest {
|
impl DerefMut for BrowserRequest {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
self.0.deref_mut()
|
&mut self.0.deref_mut().request
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +68,12 @@ impl From<FormData> for BrowserFormData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn abort_signal() -> (Option<AbortOnDrop>, Option<AbortSignal>) {
|
||||||
|
let ctrl = AbortController::new().ok();
|
||||||
|
let signal = ctrl.as_ref().map(|ctrl| ctrl.signal());
|
||||||
|
(ctrl.map(AbortOnDrop), signal)
|
||||||
|
}
|
||||||
|
|
||||||
impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
type FormData = BrowserFormData;
|
type FormData = BrowserFormData;
|
||||||
|
|
||||||
|
@ -65,6 +83,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let server_url = get_server_url();
|
let server_url = get_server_url();
|
||||||
let mut url = String::with_capacity(
|
let mut url = String::with_capacity(
|
||||||
server_url.len() + path.len() + 1 + query.len(),
|
server_url.len() + path.len() + 1 + query.len(),
|
||||||
|
@ -73,13 +92,15 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
url.push_str(path);
|
url.push_str(path);
|
||||||
url.push('?');
|
url.push('?');
|
||||||
url.push_str(query);
|
url.push_str(query);
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::get(&url)
|
request: Request::get(&url)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", content_type)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_post(
|
fn try_new_post(
|
||||||
|
@ -88,17 +109,20 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: String,
|
body: String,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let server_url = get_server_url();
|
let server_url = get_server_url();
|
||||||
let mut url = String::with_capacity(server_url.len() + path.len());
|
let mut url = String::with_capacity(server_url.len() + path.len());
|
||||||
url.push_str(server_url);
|
url.push_str(server_url);
|
||||||
url.push_str(path);
|
url.push_str(path);
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::post(&url)
|
request: Request::post(&url)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", content_type)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.body(body)
|
.body(body)
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_post_bytes(
|
fn try_new_post_bytes(
|
||||||
|
@ -107,19 +131,22 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: Bytes,
|
body: Bytes,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let server_url = get_server_url();
|
let server_url = get_server_url();
|
||||||
let mut url = String::with_capacity(server_url.len() + path.len());
|
let mut url = String::with_capacity(server_url.len() + path.len());
|
||||||
url.push_str(server_url);
|
url.push_str(server_url);
|
||||||
url.push_str(path);
|
url.push_str(path);
|
||||||
let body: &[u8] = &body;
|
let body: &[u8] = &body;
|
||||||
let body = Uint8Array::from(body).buffer();
|
let body = Uint8Array::from(body).buffer();
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::post(&url)
|
request: Request::post(&url)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", content_type)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.body(body)
|
.body(body)
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_multipart(
|
fn try_new_multipart(
|
||||||
|
@ -127,16 +154,19 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
accepts: &str,
|
accepts: &str,
|
||||||
body: Self::FormData,
|
body: Self::FormData,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let server_url = get_server_url();
|
let server_url = get_server_url();
|
||||||
let mut url = String::with_capacity(server_url.len() + path.len());
|
let mut url = String::with_capacity(server_url.len() + path.len());
|
||||||
url.push_str(server_url);
|
url.push_str(server_url);
|
||||||
url.push_str(path);
|
url.push_str(path);
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::post(&url)
|
request: Request::post(&url)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.body(body.0.take())
|
.body(body.0.take())
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_post_form_data(
|
fn try_new_post_form_data(
|
||||||
|
@ -145,6 +175,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: Self::FormData,
|
body: Self::FormData,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let form_data = body.0.take();
|
let form_data = body.0.take();
|
||||||
let url_params =
|
let url_params =
|
||||||
UrlSearchParams::new_with_str_sequence_sequence(&form_data)
|
UrlSearchParams::new_with_str_sequence_sequence(&form_data)
|
||||||
|
@ -156,13 +187,15 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(Self(SendWrapper::new(
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
Request::post(path)
|
request: Request::post(path)
|
||||||
.header("Content-Type", content_type)
|
.header("Content-Type", content_type)
|
||||||
.header("Accept", accepts)
|
.header("Accept", accepts)
|
||||||
|
.abort_signal(abort_signal.as_ref())
|
||||||
.body(url_params)
|
.body(url_params)
|
||||||
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
.map_err(|e| ServerFnError::Request(e.to_string()))?,
|
||||||
)))
|
abort_ctrl,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_new_streaming(
|
fn try_new_streaming(
|
||||||
|
@ -171,9 +204,13 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: impl Stream<Item = Bytes> + 'static,
|
body: impl Stream<Item = Bytes> + 'static,
|
||||||
) -> Result<Self, ServerFnError<CustErr>> {
|
) -> Result<Self, ServerFnError<CustErr>> {
|
||||||
|
// TODO abort signal
|
||||||
let req = streaming_request(path, accepts, content_type, body)
|
let req = streaming_request(path, accepts, content_type, body)
|
||||||
.map_err(|e| ServerFnError::Request(format!("{e:?}")))?;
|
.map_err(|e| ServerFnError::Request(format!("{e:?}")))?;
|
||||||
Ok(Self(SendWrapper::new(req)))
|
Ok(Self(SendWrapper::new(RequestInner {
|
||||||
|
request: req,
|
||||||
|
abort_ctrl: None,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +220,7 @@ fn streaming_request(
|
||||||
content_type: &str,
|
content_type: &str,
|
||||||
body: impl Stream<Item = Bytes> + 'static,
|
body: impl Stream<Item = Bytes> + 'static,
|
||||||
) -> Result<Request, JsValue> {
|
) -> Result<Request, JsValue> {
|
||||||
|
let (abort_ctrl, abort_signal) = abort_signal();
|
||||||
let stream = ReadableStream::from_stream(body.map(|bytes| {
|
let stream = ReadableStream::from_stream(body.map(|bytes| {
|
||||||
let data = Uint8Array::from(bytes.as_ref());
|
let data = Uint8Array::from(bytes.as_ref());
|
||||||
let data = JsValue::from(data);
|
let data = JsValue::from(data);
|
||||||
|
|
|
@ -2,6 +2,7 @@ use super::{Attribute, NextAttribute};
|
||||||
use crate::renderer::Renderer;
|
use crate::renderer::Renderer;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
|
fmt::Debug,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,6 +18,15 @@ pub struct AnyAttribute<R: Renderer> {
|
||||||
fn(Box<dyn Any>, &R::Element) -> AnyAttributeState<R>,
|
fn(Box<dyn Any>, &R::Element) -> AnyAttributeState<R>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R> Debug for AnyAttribute<R>
|
||||||
|
where
|
||||||
|
R: Renderer,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("AnyAttribute").finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AnyAttributeState<R>
|
pub struct AnyAttributeState<R>
|
||||||
where
|
where
|
||||||
R: Renderer,
|
R: Renderer,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
renderer::DomRenderer,
|
renderer::DomRenderer,
|
||||||
view::{Position, ToTemplate},
|
view::{Position, ToTemplate},
|
||||||
};
|
};
|
||||||
use std::marker::PhantomData;
|
use std::{marker::PhantomData, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn class<C, R>(class: C) -> Class<C, R>
|
pub fn class<C, R>(class: C) -> Class<C, R>
|
||||||
|
@ -114,7 +114,7 @@ impl<'a, R> IntoClass<R> for &'a str
|
||||||
where
|
where
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
type State = (R::Element, &'a str);
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
fn to_html(self, class: &mut String) {
|
fn to_html(self, class: &mut String) {
|
||||||
class.push_str(self);
|
class.push_str(self);
|
||||||
|
@ -145,7 +145,7 @@ impl<R> IntoClass<R> for String
|
||||||
where
|
where
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
type State = (R::Element, String);
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
fn to_html(self, class: &mut String) {
|
fn to_html(self, class: &mut String) {
|
||||||
IntoClass::<R>::to_html(self.as_str(), class);
|
IntoClass::<R>::to_html(self.as_str(), class);
|
||||||
|
@ -172,6 +172,68 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R> IntoClass<R> for Rc<str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, class: &mut String) {
|
||||||
|
IntoClass::<R>::to_html(self.as_ref(), class);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &R::Element) -> Self::State {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
let (el, prev) = state;
|
||||||
|
if !Rc::ptr_eq(&self, prev) {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
*prev = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> IntoClass<R> for Arc<str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, class: &mut String) {
|
||||||
|
IntoClass::<R>::to_html(self.as_ref(), class);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &R::Element) -> Self::State {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
let (el, prev) = state;
|
||||||
|
if !Arc::ptr_eq(&self, prev) {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
*prev = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<R> IntoClass<R> for (&'static str, bool)
|
impl<R> IntoClass<R> for (&'static str, bool)
|
||||||
where
|
where
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
|
|
|
@ -4,12 +4,12 @@ use crate::{
|
||||||
renderer::{DomRenderer, Renderer},
|
renderer::{DomRenderer, Renderer},
|
||||||
view::add_attr::AddAnyAttr,
|
view::add_attr::AddAnyAttr,
|
||||||
};
|
};
|
||||||
use std::marker::PhantomData;
|
use std::{marker::PhantomData, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn inner_html<T, R>(value: T) -> InnerHtml<T, R>
|
pub fn inner_html<T, R>(value: T) -> InnerHtml<T, R>
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: InnerHtmlValue<R>,
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
InnerHtml {
|
InnerHtml {
|
||||||
|
@ -21,8 +21,8 @@ where
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct InnerHtml<T, R>
|
pub struct InnerHtml<T, R>
|
||||||
where
|
where
|
||||||
T: AsRef<str>,
|
T: InnerHtmlValue<R>,
|
||||||
R: Renderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
value: T,
|
value: T,
|
||||||
rndr: PhantomData<R>,
|
rndr: PhantomData<R>,
|
||||||
|
@ -30,12 +30,12 @@ where
|
||||||
|
|
||||||
impl<T, R> Attribute<R> for InnerHtml<T, R>
|
impl<T, R> Attribute<R> for InnerHtml<T, R>
|
||||||
where
|
where
|
||||||
T: AsRef<str> + PartialEq,
|
T: InnerHtmlValue<R>,
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
const MIN_LENGTH: usize = 0;
|
const MIN_LENGTH: usize = 0;
|
||||||
|
|
||||||
type State = (R::Element, T);
|
type State = T::State;
|
||||||
|
|
||||||
fn to_html(
|
fn to_html(
|
||||||
self,
|
self,
|
||||||
|
@ -44,33 +44,28 @@ where
|
||||||
_style: &mut String,
|
_style: &mut String,
|
||||||
inner_html: &mut String,
|
inner_html: &mut String,
|
||||||
) {
|
) {
|
||||||
inner_html.push_str(self.value.as_ref());
|
self.value.to_html(inner_html);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
self,
|
self,
|
||||||
el: &<R as Renderer>::Element,
|
el: &<R as Renderer>::Element,
|
||||||
) -> Self::State {
|
) -> Self::State {
|
||||||
(el.clone(), self.value)
|
self.value.hydrate::<FROM_SERVER>(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
R::set_inner_html(el, self.value.as_ref());
|
self.value.build(el)
|
||||||
(el.clone(), self.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild(self, state: &mut Self::State) {
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
let (el, prev) = state;
|
self.value.rebuild(state);
|
||||||
if self.value != *prev {
|
|
||||||
R::set_inner_html(el, self.value.as_ref());
|
|
||||||
*prev = self.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, R> NextAttribute<R> for InnerHtml<T, R>
|
impl<T, R> NextAttribute<R> for InnerHtml<T, R>
|
||||||
where
|
where
|
||||||
T: AsRef<str> + PartialEq,
|
T: InnerHtmlValue<R>,
|
||||||
R: DomRenderer,
|
R: DomRenderer,
|
||||||
{
|
{
|
||||||
type Output<NewAttr: Attribute<R>> = (Self, NewAttr);
|
type Output<NewAttr: Attribute<R>> = (Self, NewAttr);
|
||||||
|
@ -85,7 +80,7 @@ where
|
||||||
|
|
||||||
pub trait InnerHtmlAttribute<T, Rndr>
|
pub trait InnerHtmlAttribute<T, Rndr>
|
||||||
where
|
where
|
||||||
T: AsRef<str> + PartialEq,
|
T: InnerHtmlValue<Rndr>,
|
||||||
Rndr: DomRenderer,
|
Rndr: DomRenderer,
|
||||||
Self: Sized + AddAnyAttr<Rndr>,
|
Self: Sized + AddAnyAttr<Rndr>,
|
||||||
{
|
{
|
||||||
|
@ -103,7 +98,7 @@ where
|
||||||
Self: AddAnyAttr<Rndr>,
|
Self: AddAnyAttr<Rndr>,
|
||||||
E: ElementWithChildren,
|
E: ElementWithChildren,
|
||||||
At: Attribute<Rndr>,
|
At: Attribute<Rndr>,
|
||||||
T: AsRef<str> + PartialEq,
|
T: InnerHtmlValue<Rndr>,
|
||||||
Rndr: DomRenderer,
|
Rndr: DomRenderer,
|
||||||
{
|
{
|
||||||
fn inner_html(
|
fn inner_html(
|
||||||
|
@ -113,3 +108,188 @@ where
|
||||||
self.add_any_attr(inner_html(value))
|
self.add_any_attr(inner_html(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait InnerHtmlValue<R: DomRenderer> {
|
||||||
|
type State;
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String);
|
||||||
|
|
||||||
|
fn to_template(buf: &mut String);
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State;
|
||||||
|
|
||||||
|
fn build(self, el: &R::Element) -> Self::State;
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> InnerHtmlValue<R> for String
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
buf.push_str(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
if self != state.1 {
|
||||||
|
R::set_inner_html(&state.0, &self);
|
||||||
|
state.1 = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> InnerHtmlValue<R> for Rc<str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
buf.push_str(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
if !Rc::ptr_eq(&self, &state.1) {
|
||||||
|
R::set_inner_html(&state.0, &self);
|
||||||
|
state.1 = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> InnerHtmlValue<R> for Arc<str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
buf.push_str(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
R::set_inner_html(el, &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
if !Arc::ptr_eq(&self, &state.1) {
|
||||||
|
R::set_inner_html(&state.0, &self);
|
||||||
|
state.1 = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, R> InnerHtmlValue<R> for &'a str
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
buf.push_str(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_inner_html(el, self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
R::set_inner_html(el, self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
if self != state.1 {
|
||||||
|
R::set_inner_html(&state.0, self);
|
||||||
|
state.1 = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, R> InnerHtmlValue<R> for Option<T>
|
||||||
|
where
|
||||||
|
T: InnerHtmlValue<R>,
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = Option<T::State>;
|
||||||
|
|
||||||
|
fn to_html(self, buf: &mut String) {
|
||||||
|
if let Some(value) = self {
|
||||||
|
value.to_html(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
self.map(|n| n.hydrate::<FROM_SERVER>(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
self.map(|n| n.build(el))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
html::attribute::AttributeValue,
|
html::{attribute::AttributeValue, class::IntoClass},
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
prelude::{Mountable, Render, RenderHtml},
|
prelude::{Mountable, Render, RenderHtml},
|
||||||
renderer::Renderer,
|
renderer::{DomRenderer, Renderer},
|
||||||
view::{strings::StrState, Position, PositionState, ToTemplate},
|
view::{strings::StrState, Position, PositionState, ToTemplate},
|
||||||
};
|
};
|
||||||
use oco::Oco;
|
use oco::Oco;
|
||||||
|
@ -142,3 +142,34 @@ where
|
||||||
*prev_value = self;
|
*prev_value = self;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<R> IntoClass<R> for Oco<'static, str>
|
||||||
|
where
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = (R::Element, Self);
|
||||||
|
|
||||||
|
fn to_html(self, class: &mut String) {
|
||||||
|
IntoClass::<R>::to_html(self.as_str(), class);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(self, el: &R::Element) -> Self::State {
|
||||||
|
if !FROM_SERVER {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self, el: &R::Element) -> Self::State {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
(el.clone(), self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, state: &mut Self::State) {
|
||||||
|
let (el, prev) = state;
|
||||||
|
if self != *prev {
|
||||||
|
R::set_attribute(el, "class", &self);
|
||||||
|
}
|
||||||
|
*prev = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use crate::{
|
||||||
error::AnyError,
|
error::AnyError,
|
||||||
html::{
|
html::{
|
||||||
attribute::{Attribute, AttributeValue},
|
attribute::{Attribute, AttributeValue},
|
||||||
|
element::InnerHtmlValue,
|
||||||
property::IntoProperty,
|
property::IntoProperty,
|
||||||
},
|
},
|
||||||
hydration::Cursor,
|
hydration::Cursor,
|
||||||
|
@ -554,6 +555,56 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<F, V, R> InnerHtmlValue<R> for F
|
||||||
|
where
|
||||||
|
F: FnMut() -> V + 'static,
|
||||||
|
V: InnerHtmlValue<R>,
|
||||||
|
V::State: 'static,
|
||||||
|
R: DomRenderer,
|
||||||
|
{
|
||||||
|
type State = RenderEffectState<V::State>;
|
||||||
|
|
||||||
|
fn to_html(mut self, buf: &mut String) {
|
||||||
|
let value = self();
|
||||||
|
value.to_html(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_template(_buf: &mut String) {}
|
||||||
|
|
||||||
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
|
mut self,
|
||||||
|
el: &<R as Renderer>::Element,
|
||||||
|
) -> Self::State {
|
||||||
|
let el = el.to_owned();
|
||||||
|
RenderEffect::new(move |prev| {
|
||||||
|
let value = self();
|
||||||
|
if let Some(mut state) = prev {
|
||||||
|
value.rebuild(&mut state);
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
value.hydrate::<FROM_SERVER>(&el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(mut self, el: &<R as Renderer>::Element) -> Self::State {
|
||||||
|
let el = el.to_owned();
|
||||||
|
RenderEffect::new(move |prev| {
|
||||||
|
let value = self();
|
||||||
|
if let Some(mut state) = prev {
|
||||||
|
value.rebuild(&mut state);
|
||||||
|
state
|
||||||
|
} else {
|
||||||
|
value.build(&el)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild(self, _state: &mut Self::State) {}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::{Mountable, Position, PositionState, Render, RenderHtml};
|
use super::{Mountable, Position, PositionState, Render, RenderHtml};
|
||||||
use crate::{hydration::Cursor, renderer::Renderer};
|
use crate::{hydration::Cursor, renderer::Renderer, ssr::StreamBuilder};
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
fmt::Debug,
|
fmt::Debug,
|
||||||
|
@ -12,8 +12,9 @@ where
|
||||||
{
|
{
|
||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
value: Box<dyn Any>,
|
value: Box<dyn Any>,
|
||||||
// TODO add async HTML rendering for AnyView
|
|
||||||
to_html: fn(Box<dyn Any>, &mut String, &mut Position),
|
to_html: fn(Box<dyn Any>, &mut String, &mut Position),
|
||||||
|
to_html_async: fn(Box<dyn Any>, &mut StreamBuilder, &mut Position),
|
||||||
|
to_html_async_ooo: fn(Box<dyn Any>, &mut StreamBuilder, &mut Position),
|
||||||
build: fn(Box<dyn Any>) -> AnyViewState<R>,
|
build: fn(Box<dyn Any>) -> AnyViewState<R>,
|
||||||
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState<R>),
|
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState<R>),
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
|
@ -122,6 +123,24 @@ where
|
||||||
.expect("AnyView::to_html could not be downcast");
|
.expect("AnyView::to_html could not be downcast");
|
||||||
value.to_html_with_buf(buf, position);
|
value.to_html_with_buf(buf, position);
|
||||||
};
|
};
|
||||||
|
let to_html_async =
|
||||||
|
|value: Box<dyn Any>,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position| {
|
||||||
|
let value = value
|
||||||
|
.downcast::<T>()
|
||||||
|
.expect("AnyView::to_html could not be downcast");
|
||||||
|
value.to_html_async_with_buf::<false>(buf, position);
|
||||||
|
};
|
||||||
|
let to_html_async_ooo =
|
||||||
|
|value: Box<dyn Any>,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position| {
|
||||||
|
let value = value
|
||||||
|
.downcast::<T>()
|
||||||
|
.expect("AnyView::to_html could not be downcast");
|
||||||
|
value.to_html_async_with_buf::<true>(buf, position);
|
||||||
|
};
|
||||||
let build = |value: Box<dyn Any>| {
|
let build = |value: Box<dyn Any>| {
|
||||||
let value = value
|
let value = value
|
||||||
.downcast::<T>()
|
.downcast::<T>()
|
||||||
|
@ -198,6 +217,8 @@ where
|
||||||
type_id: TypeId::of::<T>(),
|
type_id: TypeId::of::<T>(),
|
||||||
value,
|
value,
|
||||||
to_html,
|
to_html,
|
||||||
|
to_html_async,
|
||||||
|
to_html_async_ooo,
|
||||||
build,
|
build,
|
||||||
rebuild,
|
rebuild,
|
||||||
hydrate_from_server,
|
hydrate_from_server,
|
||||||
|
@ -243,6 +264,20 @@ where
|
||||||
(self.to_html)(self.value, buf, position);
|
(self.to_html)(self.value, buf, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
|
||||||
|
self,
|
||||||
|
buf: &mut StreamBuilder,
|
||||||
|
position: &mut Position,
|
||||||
|
) where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
if OUT_OF_ORDER {
|
||||||
|
(self.to_html_async_ooo)(self.value, buf, position);
|
||||||
|
} else {
|
||||||
|
(self.to_html_async)(self.value, buf, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn hydrate<const FROM_SERVER: bool>(
|
fn hydrate<const FROM_SERVER: bool>(
|
||||||
self,
|
self,
|
||||||
cursor: &Cursor<R>,
|
cursor: &Cursor<R>,
|
||||||
|
|
Loading…
Reference in New Issue