work related to 0.7 blog port

This commit is contained in:
Greg Johnston 2024-03-16 16:33:15 -04:00
parent c29081b12a
commit b41fde3ff9
49 changed files with 2775 additions and 507 deletions

View File

@ -25,7 +25,7 @@ use futures::Stream;
pub use hydrate::*;
use serde::{Deserialize, Serialize};
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`].
pub type PinnedFuture<T> = Pin<Box<dyn Future<Output = T> + Send + Sync>>;

View File

@ -98,7 +98,8 @@ impl SharedContext for SsrSharedContext {
.into_iter()
.map(|(id, data)| async move {
let data = data.await;
format!("__RESOLVED_RESOURCES[{}] = {data:?};", id.0)
let data = data.replace('<', "\\u003c");
format!("__RESOLVED_RESOURCES[{}] = {:?};", id.0, data)
})
.collect::<FuturesUnordered<_>>();

View File

@ -9,34 +9,33 @@ description = "Axum integrations for the Leptos web framework."
rust-version.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["tokio"] }
hydration_context = { workspace = true }
axum = { version = "0.7", default-features = false, features = [
"matched-path",
] }
futures = "0.3"
http-body-util = "0.1"
leptos = { workspace = true, features = ["ssr"] }
leptos = { workspace = true, features = ["nonce", "hydration"] }
server_fn = { workspace = true, features = ["axum-no-default"] }
leptos_macro = { workspace = true, features = ["axum"] }
leptos_meta = { workspace = true }
leptos_router = { workspace = true, features = ["ssr"] }
leptos_integration_utils = { workspace = true }
routing = { workspace = true }
#leptos_integration_utils = { workspace = true }
parking_lot = "0.12"
serde_json = "1"
tokio = { version = "1", default-features = false }
tokio-util = { version = "0.7", features = ["rt"] }
tracing = "0.1"
once_cell = "1.18"
cfg-if = "1.0"
[dev-dependencies]
axum = "0.7"
tokio = { version = "1", features = ["net"] }
[features]
nonce = ["leptos/nonce"]
wasm = []
default = ["tokio/fs", "tokio/sync"]
experimental-islands = ["leptos_integration_utils/experimental-islands"]
#experimental-islands = ["leptos_integration_utils/experimental-islands"]
[package.metadata.docs.rs]
rustdoc-args = ["--generate-link-to-definition"]

View File

@ -14,7 +14,6 @@
//! - `default`: supports running in a typical native Tokio/Axum environment
//! - `wasm`: with `default-features = false`, supports running in a JS Fetch-based
//! 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)
//!
//! ### Important Note
@ -41,25 +40,38 @@ use axum::{
request::Parts,
HeaderMap, Method, Request, Response, StatusCode,
},
response::IntoResponse,
response::{Html, IntoResponse},
routing::{delete, get, patch, post, put},
};
use futures::{
channel::mpsc::{Receiver, Sender},
Future, SinkExt, Stream, StreamExt,
stream::once,
Future, FutureExt, SinkExt, Stream, StreamExt,
};
use leptos::{ssr::*, *};
use leptos_integration_utils::{build_async_response, html_parts_separated};
use leptos_meta::{generate_head_metadata_separated, MetaContext};
use leptos_router::*;
use hydration_context::SsrSharedContext;
use leptos::{
config::LeptosOptions,
context::{provide_context, use_context},
reactive_graph::{computed::ScopedFuture, owner::Owner},
IntoView,
};
use leptos_meta::{MetaContext, ServerMetaContext};
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use server_fn::{
error::{NoCustomError, ServerFnErrorSerde},
redirect::REDIRECT_HEADER,
use routing::{
location::RequestUrl, PathSegment, RouteList, RouteListing, SsrMode,
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;
/// 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
}
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
/// to panic so we define a macro to conditionally compile the correct code.
macro_rules! spawn_task {
($block:expr) => {
cfg_if::cfg_if! {
if #[cfg(feature = "wasm")] {
#[cfg(feature = "wasm")]
spawn_local($block);
} else if #[cfg(feature = "default")] {
let pool_handle = get_leptos_pool();
pool_handle.spawn_pinned(move || { $block });
} else {
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.");
#[cfg(all(not(feature = "wasm"), feature = "default"))]
spawn($block);
#[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."
);
spawn_local($block);
}
}
};
}
@ -272,23 +301,16 @@ async fn handle_server_fns_inner(
) -> impl IntoResponse {
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 (req, parts) = generate_request_and_parts(req);
let res = if let Some(mut service) =
server_fn::axum::get_server_fn_service(&path)
{
let runtime = create_runtime();
let owner = Owner::new();
owner
.with(|| {
ScopedFuture::new(async move {
additional_context();
provide_context(parts);
provide_context(ResponseOptions::default());
@ -305,11 +327,22 @@ async fn handle_server_fns_inner(
// actually run the server fn
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
// Location set, then redirect to Referer
// update response as needed
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 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 {
*res.status_mut() = StatusCode::FOUND;
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
if let Some(status) = status {
*res.status_mut() = status;
}
res.headers_mut().extend(res_headers.drain());
} else {
eprintln!("Failed to find ResponseOptions for {path}");
}
// clean up the scope
runtime.dispose();
Ok(res)
})
})
.await
} else {
Response::builder().status(StatusCode::BAD_REQUEST).body(
Body::from(format!(
Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::from(format!(
"Could not find a server function at the route {path}. \
\n\nIt's likely that either
1. The API prefix you specify in the `#[server]` \
macro doesn't match the prefix at which your server \
function handler is mounted, or \n2. You are on a \
platform that doesn't support automatic server function \
registration and you need to call \
ServerFn::register_explicit() on the server function \
type, somewhere in your `main` function.",
)),
)
macro doesn't match the prefix at which your server function \
handler is mounted, or \n2. You are on a platform that \
doesn't support automatic server function registration and \
you need to call ServerFn::register_explicit() on the server \
function type, somewhere in your `main` function.",
)))
}
.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,
ServerFnErrorSerde::ser(
@ -366,7 +386,7 @@ async fn handle_server_fns_inner(
.unwrap_or_default(),
)
.into_response()
})
})*/
}
pub type PinnedHtmlStream =
@ -441,12 +461,12 @@ where
/// 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.
/// 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()`
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_route<IV>(
options: LeptosOptions,
paths: Vec<RouteListing>,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
Request<Body>,
@ -579,12 +599,12 @@ where
/// 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
/// 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()`.
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn render_route_with_context<IV>(
options: LeptosOptions,
paths: Vec<RouteListing>,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> impl Fn(
@ -627,8 +647,11 @@ where
.as_str();
// 2. Find RouteListing in paths. This should probably be optimized, we probably don't want to
// search for this every time
let listing: &RouteListing =
paths.iter().find(|r| r.path() == path).unwrap_or_else(|| {
let listing: &AxumRouteListing = paths
.iter()
// TODO this should be cached rather than recalculating the Axum version of the path
.find(|r| r.path() == path)
.unwrap_or_else(|| {
panic!(
"Failed to find the route {path} requested by the user. \
This suggests that the routing rules in the Router that \
@ -681,18 +704,17 @@ where
IV: IntoView,
{
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
let default_res_options = ResponseOptions::default();
let res_options2 = 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();
spawn_task!(async move {
let app = {
Box::pin(async move {
let owner = Owner::new_root(Arc::new(SsrSharedContext::new()));
let meta_context = ServerMetaContext::new();
let stream = owner.with(|| {
// 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
// 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 (_, req_parts) = generate_request_and_parts(req);
move || {
provide_contexts(full_path, req_parts, default_res_options);
app_fn().into_view()
}
};
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
provide_contexts(
&full_path,
&meta_context,
req_parts,
default_res_options,
);
println!("providing additional contexts");
add_context();
forward_stream(&options, res_options2, bundle, tx).await;
// run app
let app = app_fn();
runtime.dispose();
}.instrument(current_span));
// convert app to appropriate response type
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,
rx: Receiver<String>,
) -> Response<Body> {
todo!() /*
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
@ -763,7 +805,7 @@ async fn generate_response(
HeaderValue::from_str("text/html; charset=utf-8").unwrap(),
);
}
res
res*/
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
async fn forward_stream(
@ -772,7 +814,7 @@ async fn forward_stream(
bundle: impl Stream<Item = String> + 'static,
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 (head, tail) =
@ -796,7 +838,7 @@ async fn forward_stream(
let mut writable = res_options2.0.write();
*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
@ -840,6 +882,8 @@ pub fn render_app_to_stream_in_order_with_context<IV>(
where
IV: IntoView,
{
|req| todo!()
/*
move |req: Request<Body>| {
Box::pin({
let options = options.clone();
@ -884,21 +928,22 @@ where
generate_response(res_options3, rx).await
}
})
}
}*/
}
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn provide_contexts(
path: String,
path: &str,
meta_context: &ServerMetaContext,
parts: Parts,
default_res_options: ResponseOptions,
) {
let integration = ServerIntegration { path };
provide_context(RouterIntegrationContext::new(integration));
provide_context(MetaContext::new());
provide_context(RequestUrl::new(path));
provide_context(meta_context.clone());
provide_context(parts);
provide_context(default_res_options);
provide_server_redirect(redirect);
// TODO server redirect
// provide_server_redirect(redirect);
#[cfg(feature = "nonce")]
leptos::nonce::provide_nonce();
}
@ -1013,7 +1058,8 @@ where
IV: IntoView,
{
move |req: Request<Body>| {
Box::pin({
todo!()
/*Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
let add_context = additional_context.clone();
@ -1097,7 +1143,7 @@ where
res
}
})
})*/
}
}
@ -1141,7 +1187,8 @@ pub fn render_app_async_with_context<IV>(
where
IV: IntoView,
{
move |req: Request<Body>| {
|_| todo!()
/* move |req: Request<Body>| {
Box::pin({
let options = options.clone();
let app_fn = app_fn.clone();
@ -1210,7 +1257,7 @@ where
res
}
})
}
}*/
}
/// 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)]
pub fn generate_route_list<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> Vec<RouteListing>
) -> Vec<AxumRouteListing>
where
IV: IntoView + 'static,
{
@ -1232,7 +1279,7 @@ where
#[tracing::instrument(level = "trace", fields(error), skip_all)]
pub fn generate_route_list_with_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
) -> (Vec<AxumRouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
@ -1247,7 +1294,7 @@ where
pub fn generate_route_list_with_exclusions<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> Vec<RouteListing>
) -> Vec<AxumRouteListing>
where
IV: IntoView + 'static,
{
@ -1263,6 +1310,8 @@ pub async fn build_static_routes<IV>(
) where
IV: IntoView + 'static,
{
todo!()
/*
let options = options.clone();
let routes = routes.to_owned();
spawn_task!(async move {
@ -1274,7 +1323,7 @@ pub async fn build_static_routes<IV>(
)
.await
.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
@ -1285,7 +1334,7 @@ pub async fn build_static_routes<IV>(
pub fn generate_route_list_with_exclusions_and_ssg<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
) -> (Vec<RouteListing>, StaticDataMap)
) -> (Vec<AxumRouteListing>, StaticDataMap)
where
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
/// 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
@ -1306,41 +1422,35 @@ pub fn generate_route_list_with_exclusions_and_ssg_and_context<IV>(
app_fn: impl Fn() -> IV + 'static + Clone,
excluded_routes: Option<Vec<String>>,
additional_context: impl Fn() + 'static + Clone,
) -> (Vec<RouteListing>, StaticDataMap)
) -> (Vec<AxumRouteListing>, StaticDataMap)
where
IV: IntoView + 'static,
{
let (routes, static_data_map) =
leptos_router::generate_route_list_inner_with_context(
app_fn,
additional_context,
);
init_executor();
let owner = Owner::new();
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 ""
let mut routes = routes
.into_inner()
.into_iter()
.map(|listing| {
let path = listing.path();
if path.is_empty() {
RouteListing::new(
"/".to_string(),
listing.path(),
listing.mode(),
listing.methods(),
listing.static_mode(),
)
} else {
listing
}
})
.map(AxumRouteListing::from)
.collect::<Vec<_>>();
(
if routes.is_empty() {
vec![RouteListing::new(
"/",
"",
vec![AxumRouteListing::new(
"/".to_string(),
Default::default(),
[leptos_router::Method::Get],
[routing::Method::Get],
None,
)]
} else {
@ -1351,7 +1461,8 @@ where
}
routes
},
static_data_map,
StaticDataMap::new(), // TODO
//static_data_map,
)
}
@ -1364,7 +1475,7 @@ where
fn leptos_routes<IV>(
self,
options: &S,
paths: Vec<RouteListing>,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
@ -1373,7 +1484,7 @@ where
fn leptos_routes_with_context<IV>(
self,
options: &S,
paths: Vec<RouteListing>,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
@ -1382,14 +1493,14 @@ where
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<RouteListing>,
paths: Vec<AxumRouteListing>,
handler: H,
) -> Self
where
H: axum::handler::Handler<T, S>,
T: 'static;
}
/*
#[cfg(feature = "default")]
fn handle_static_response<IV>(
path: String,
@ -1486,7 +1597,7 @@ where
}
}
})
}
}*/
#[cfg(feature = "default")]
fn static_route<IV, S>(
@ -1495,14 +1606,15 @@ fn static_route<IV, S>(
options: LeptosOptions,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
additional_context: impl Fn() + Clone + Send + 'static,
method: leptos_router::Method,
method: routing::Method,
mode: StaticMode,
) -> axum::Router<S>
where
IV: IntoView + 'static,
S: Clone + Send + Sync + 'static,
{
match mode {
todo!()
/*match mode {
StaticMode::Incremental => {
let handler = move |req: Request<Body>| {
Box::pin({
@ -1538,11 +1650,11 @@ where
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
routing::Method::Get => get(handler),
routing::Method::Post => post(handler),
routing::Method::Put => put(handler),
routing::Method::Delete => delete(handler),
routing::Method::Patch => patch(handler),
},
)
}
@ -1581,14 +1693,42 @@ where
router.route(
path,
match method {
leptos_router::Method::Get => get(handler),
leptos_router::Method::Post => post(handler),
leptos_router::Method::Put => put(handler),
leptos_router::Method::Delete => delete(handler),
leptos_router::Method::Patch => patch(handler),
routing::Method::Get => get(handler),
routing::Method::Post => post(handler),
routing::Method::Put => put(handler),
routing::Method::Delete => delete(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>(
self,
options: &S,
paths: Vec<RouteListing>,
paths: Vec<AxumRouteListing>,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
@ -1616,13 +1756,15 @@ where
fn leptos_routes_with_context<IV>(
self,
options: &S,
paths: Vec<RouteListing>,
paths: Vec<AxumRouteListing>,
additional_context: impl Fn() + 'static + Clone + Send,
app_fn: impl Fn() -> IV + Clone + Send + 'static,
) -> Self
where
IV: IntoView + 'static,
{
init_executor();
// S represents the router's finished state allowing us to provide
// it to the user's server functions.
let state = options.clone();
@ -1672,7 +1814,7 @@ where
{
static_route(
router,
path,
&path,
LeptosOptions::from_ref(options),
app_fn.clone(),
cx_with_state_and_method.clone(),
@ -1690,7 +1832,7 @@ where
}
} else {
router.route(
path,
&path,
match listing.mode() {
SsrMode::OutOfOrder => {
let s = render_app_to_stream_with_context(
@ -1699,11 +1841,11 @@ where
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
routing::Method::Get => get(s),
routing::Method::Post => post(s),
routing::Method::Put => put(s),
routing::Method::Delete => delete(s),
routing::Method::Patch => patch(s),
}
}
SsrMode::PartiallyBlocked => {
@ -1714,11 +1856,11 @@ where
true
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
routing::Method::Get => get(s),
routing::Method::Post => post(s),
routing::Method::Put => put(s),
routing::Method::Delete => delete(s),
routing::Method::Patch => patch(s),
}
}
SsrMode::InOrder => {
@ -1728,11 +1870,11 @@ where
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
routing::Method::Get => get(s),
routing::Method::Post => post(s),
routing::Method::Put => put(s),
routing::Method::Delete => delete(s),
routing::Method::Patch => patch(s),
}
}
SsrMode::Async => {
@ -1742,11 +1884,11 @@ where
app_fn.clone(),
);
match method {
leptos_router::Method::Get => get(s),
leptos_router::Method::Post => post(s),
leptos_router::Method::Put => put(s),
leptos_router::Method::Delete => delete(s),
leptos_router::Method::Patch => patch(s),
routing::Method::Get => get(s),
routing::Method::Post => post(s),
routing::Method::Put => put(s),
routing::Method::Delete => delete(s),
routing::Method::Patch => patch(s),
}
}
},
@ -1761,7 +1903,7 @@ where
#[tracing::instrument(level = "trace", fields(error), skip_all)]
fn leptos_routes_with_handler<H, T>(
self,
paths: Vec<RouteListing>,
paths: Vec<AxumRouteListing>,
handler: H,
) -> Self
where
@ -1774,13 +1916,11 @@ where
router = router.route(
listing.path(),
match method {
leptos_router::Method::Get => get(handler.clone()),
leptos_router::Method::Post => post(handler.clone()),
leptos_router::Method::Put => put(handler.clone()),
leptos_router::Method::Delete => {
delete(handler.clone())
}
leptos_router::Method::Patch => patch(handler.clone()),
routing::Method::Get => get(handler.clone()),
routing::Method::Post => post(handler.clone()),
routing::Method::Put => put(handler.clone()),
routing::Method::Delete => delete(handler.clone()),
routing::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.
///
/// It is generic over some type `T` that implements [`FromRequestParts`] and can

View File

@ -10,7 +10,7 @@ rust-version.workspace = true
[dependencies]
futures = "0.3"
leptos = { workspace = true, features = ["ssr"] }
leptos = { workspace = true }
leptos_hot_reload = { workspace = true }
leptos_meta = { workspace = true }
leptos_config = { workspace = true }

View File

@ -7,20 +7,23 @@ license = "MIT"
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."
readme = "../README.md"
rust-version.workspace = true
[dependencies]
any_spawner = { workspace = true, features = ["wasm-bindgen"] }
base64 = { version = "0.22", optional = true }
cfg-if = "1"
hydration_context = { workspace = true, optional = true }
leptos_dom = { workspace = true }
leptos_macro = { workspace = true }
leptos_reactive = { workspace = true }
leptos_server = { workspace = true }
leptos_config = { workspace = true }
leptos-spin-macro = { git = "https://github.com/fermyon/leptos-spin", optional = true }
oco = { workspace = true }
leptos-spin-macro = { version = "0.1", optional = true }
paste = "1"
rand = { version = "0.8", optional = true }
reactive_graph = { workspace = true, features = ["serde"] }
tachys = { workspace = true, features = ["oco", "reactive_graph"] }
tachys = { workspace = true, features = ["reactive_graph"] }
tracing = "0.1"
typed-builder = "0.18"
typed-builder-macro = "0.18"
@ -37,7 +40,7 @@ web-sys = { version = "0.3.63", features = [
"ShadowRootInit",
"ShadowRootMode",
] }
wasm-bindgen = { version = "0.2" }
wasm-bindgen = { version = "0.2", optional = true }
[features]
default = ["serde"]
@ -94,6 +97,8 @@ denylist = [
"wasm-bindgen",
"rkyv", # was causing clippy issues on nightly
"trace-component-props",
"spin",
"experimental-islands",
]
skip_feature_sets = [
[

View File

@ -156,6 +156,8 @@ pub mod children;
pub mod component;
mod for_loop;
mod hydration_scripts;
#[cfg(feature = "nonce")]
pub mod nonce;
mod show;
pub mod text_prop;
pub use for_loop::*;
@ -165,7 +167,7 @@ pub use reactive_graph::{
self,
signal::{arc_signal, create_signal, signal},
};
pub use server_fn::error;
pub use server_fn::{self, error};
pub use show::*;
#[doc(hidden)]
pub use typed_builder;
@ -175,12 +177,28 @@ mod into_view;
pub use into_view::IntoView;
pub use leptos_dom;
pub use tachys;
pub mod logging;
mod mount;
pub mod mount;
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 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;
pub use additional_attributes::*;
mod await_;

View File

@ -4,11 +4,59 @@ use reactive_graph::owner::Owner;
use std::marker::PhantomData;
use tachys::{
dom::body,
hydration::Cursor,
renderer::{dom::Dom, Renderer},
view::{Mountable, Render},
view::{Mountable, PositionState, Render, RenderHtml},
};
use wasm_bindgen::JsCast;
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>`.
pub fn mount_to_body<F, N>(f: F)
where

128
leptos/src/nonce.rs Normal file
View File

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

View File

@ -13,6 +13,10 @@ use web_sys::HtmlElement;
pub mod helpers;
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(unboxed_closures))]
// to prevent warnings from popping up when a nightly feature is stabilized

View File

@ -1,5 +1,5 @@
use crate::is_server;
use cfg_if::cfg_if;
//! Utilities for simple isomorphic logging to the console or terminal.
use wasm_bindgen::JsValue;
/// 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)
/// or via `println!()` (if not in the browser).
pub fn console_log(s: &str) {
#[allow(clippy::print_stdout)]
if is_server() {
if log_to_stdout() {
println!("{s}");
} else {
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)
/// or via `println!()` (if not in the browser).
pub fn console_warn(s: &str) {
if is_server() {
if log_to_stdout() {
eprintln!("{s}");
} else {
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)
/// or via `println!()` (if not in the browser).
#[inline(always)]
pub fn console_error(s: &str) {
if is_server() {
if log_to_stdout() {
eprintln!("{s}");
} else {
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)
/// or via `println!()` (if not in the browser), but only in a debug build.
#[inline(always)]
pub fn console_debug_warn(s: &str) {
cfg_if! {
if #[cfg(debug_assertions)] {
if is_server() {
#[cfg(debug_assertions)]
{
if log_to_stdout() {
eprintln!("{s}");
} else {
web_sys::console::warn_1(&JsValue::from_str(s));
}
} else {
}
#[cfg(not(debug_assertions))]
{
let _ = s;
}
}
}

View File

@ -10,14 +10,27 @@ readme = "../README.md"
rust-version.workspace = true
[dependencies]
reactive_graph = { workspace = true }
leptos_macro = { workspace = true }
hydration_context = { workspace = true }
reactive_graph = { workspace = true, features = ["hydration"] }
#leptos_macro = { workspace = true }
server_fn = { workspace = true }
lazy_static = "1"
serde = { version = "1", features = ["derive"] }
#lazy_static = "1"
thiserror = "1"
tracing = "0.1"
inventory = "0.3"
tracing = { version = "0.1", optional = true }
#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]
leptos = { path = "../leptos" }
@ -25,6 +38,9 @@ leptos = { path = "../leptos" }
[features]
default-tls = ["server_fn/default-tls"]
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]
denylist = ["nightly"]

View File

@ -1,6 +1,13 @@
//#![deny(missing_docs)]
#![forbid(unsafe_code)]
#[cfg(feature = "hydration")]
mod resource;
#[cfg(feature = "hydration")]
pub mod serializers;
#[cfg(feature = "hydration")]
pub use resource::*;
////! # Leptos Server Functions
////!
////! This package is based on a simple idea: sometimes its useful to write functions

View File

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

View File

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

View File

@ -16,6 +16,7 @@ indexmap = "2"
send_wrapper = "0.6.0"
tracing = "0.1"
wasm-bindgen = "0.2"
futures = "0.3.30"
[dependencies.web-sys]
version = "0.3"

View File

@ -7,10 +7,15 @@ use leptos::{
tachys::{
dom::document,
error::Result,
html::attribute::{
any_attribute::{AnyAttribute, AnyAttributeState},
html::{
attribute::{
any_attribute::{
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
},
Attribute,
},
class,
},
hydration::Cursor,
reactive_graph::RenderEffectState,
renderer::{dom::Dom, Renderer},
@ -57,10 +62,17 @@ use web_sys::HtmlElement;
/// ```
#[component]
pub fn Body(
/// The `class` attribute on the `<body>`.
#[prop(optional, into)]
mut class: Option<TextProp>,
/// Arbitrary attributes to add to the `<body>`.
#[prop(attrs)]
mut attributes: Vec<AnyAttribute<Dom>>,
) -> 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>() {
let mut meta = meta.inner.write().or_poisoned();
// if we are server rendering, we will not actually use these values via RenderHtml

View File

@ -7,10 +7,16 @@ use leptos::{
tachys::{
dom::document,
error::Result,
html::attribute::{
any_attribute::{AnyAttribute, AnyAttributeState},
html::{
attribute::{
self,
any_attribute::{
AnyAttribute, AnyAttributeState, IntoAnyAttribute,
},
Attribute,
},
class,
},
hydration::Cursor,
reactive_graph::RenderEffectState,
renderer::{dom::Dom, Renderer},
@ -54,10 +60,30 @@ use web_sys::{Element, HtmlElement};
/// ```
#[component]
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>`
#[prop(attrs)]
mut attributes: Vec<AnyAttribute<Dom>>,
) -> 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>() {
let mut meta = meta.inner.write().or_poisoned();
// if we are server rendering, we will not actually use these values via RenderHtml

View File

@ -47,9 +47,10 @@
//! **Important Note:** You must enable one of `csr`, `hydrate`, or `ssr` to tell Leptos
//! which mode your app is operating in.
use indexmap::IndexMap;
use futures::{Stream, StreamExt};
use leptos::{
component, debug_warn,
component,
logging::debug_warn,
reactive_graph::owner::{provide_context, use_context},
tachys::{
dom::document,
@ -67,13 +68,12 @@ use once_cell::sync::Lazy;
use or_poisoned::OrPoisoned;
use send_wrapper::SendWrapper;
use std::{
cell::{Cell, RefCell},
fmt::Debug,
rc::Rc,
pin::Pin,
sync::{Arc, RwLock},
};
use wasm_bindgen::JsCast;
use web_sys::{HtmlHeadElement, Node};
use web_sys::HtmlHeadElement;
mod body;
mod html;
@ -157,7 +157,61 @@ pub struct ServerMetaContext {
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 {
/*/// Metadata associated with the `<html>` element
pub html: HtmlContext,
@ -173,7 +227,9 @@ struct ServerMetaContextInner {
impl Debug for ServerMetaContext {
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 {
context: ServerMetaContext,
}
@ -399,14 +456,7 @@ impl RenderHtml<Dom> for MetaTagsView {
const MIN_LENGTH: usize = 0;
fn to_html_with_buf(self, buf: &mut String, position: &mut Position) {
if let Some(title) = self.context.title.as_string() {
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);
buf.push_str("<!--HEAD-->");
}
fn hydrate<const FROM_SERVER: bool>(

View File

@ -7,6 +7,7 @@ version.workspace = true
any_spawner = { workspace = true }
or_poisoned = { workspace = true }
futures = "0.3"
hydration_context = { workspace = true, optional = true }
pin-project-lite = "0.2"
rustc-hash = "1.1.0"
serde = { version = "1", features = ["derive"], optional = true }
@ -23,6 +24,7 @@ any_spawner = { workspace = true, features = ["tokio"] }
nightly = []
serde = ["dep:serde"]
tracing = ["dep:tracing"]
hydration = ["dep:hydration_context"]
[package.metadata.docs.rs]
all-features = true

View File

@ -71,7 +71,7 @@ impl<T> DefinedAt for ArcAsyncDerived<T> {
// This helps create a derived async signal.
// 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.
macro_rules! spawn_derived {
($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
where
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
Self::new_with_initial(AsyncState::Loading, fun)
}
@ -186,7 +186,7 @@ impl<T: 'static> ArcAsyncDerived<T> {
) -> Self
where
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
spawn_derived!(Executor::spawn, initial_value, fun)
}

View File

@ -55,7 +55,7 @@ impl<T: Send + Sync + 'static> AsyncDerived<T> {
) -> Self
where
T: Send + Sync + 'static,
Fut: Future<Output = T> + Send + Sync + 'static,
Fut: Future<Output = T> + Send + 'static,
{
Self {
#[cfg(debug_assertions)]

View File

@ -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)]
pub fn new(
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) {
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
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) {
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) {
self.write().or_poisoned().sources.insert(source);
}

View File

@ -12,6 +12,17 @@ pub struct Memo<T: Send + Sync + 'static> {
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> {
#[track_caller]
#[cfg_attr(

View File

@ -84,6 +84,7 @@ pub mod selector;
mod serde;
pub mod signal;
pub mod traits;
pub mod wrappers;
pub use graph::untrack;

View File

@ -4,10 +4,11 @@ use crate::{
ArcReadSignal, ArcRwSignal, ArcWriteSignal, ReadSignal, RwSignal,
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))?),*) => {
$(
#[cfg(feature = "nightly")]
@ -16,7 +17,7 @@ macro_rules! impl_get_fn_traits {
#[inline(always)]
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> {
#[inline(always)]
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> {
#[inline(always)]
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 {
($($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))?),*) => {
$(
#[cfg(feature = "nightly")]
@ -92,7 +131,7 @@ macro_rules! impl_get_fn_traits_send {
#[inline(always)]
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> {
#[inline(always)]
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> {
#[inline(always)]
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 {
($($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_send![ReadSignal, RwSignal, Memo, ArcMemo];
impl_get_fn_traits_read![ArcReadSignal, ArcRwSignal];
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_send![WriteSignal];

View File

@ -1,3 +1,5 @@
#[cfg(feature = "hydration")]
use hydration_context::SharedContext;
use or_poisoned::OrPoisoned;
use rustc_hash::FxHashMap;
use std::{
@ -18,6 +20,8 @@ pub use context::*;
#[must_use]
pub struct Owner {
pub(crate) inner: Arc<RwLock<OwnerInner>>,
#[cfg(feature = "hydration")]
pub(crate) shared_context: Option<Arc<dyn SharedContext + Send + Sync>>,
}
thread_local! {
@ -25,9 +29,21 @@ thread_local! {
}
impl Owner {
pub fn debug_id(&self) -> usize {
Arc::as_ptr(&self.inner) as usize
}
pub fn new() -> Self {
#[cfg(not(feature = "hydration"))]
let parent = OWNER
.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 {
inner: Arc::new(RwLock::new(OwnerInner {
parent,
@ -35,11 +51,29 @@ impl Owner {
contexts: 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 {
let parent = Some(Arc::downgrade(&self.inner));
let parent = Some(Arc::clone(&self.inner));
Self {
inner: Arc::new(RwLock::new(OwnerInner {
parent,
@ -47,6 +81,8 @@ impl Owner {
contexts: 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> {
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)]
pub(crate) struct OwnerInner {
pub parent: Option<Weak<RwLock<OwnerInner>>>,
pub parent: Option<Arc<RwLock<OwnerInner>>>,
nodes: Vec<NodeId>,
pub contexts: FxHashMap<TypeId, Box<dyn Any + Send + Sync>>,
pub cleanups: Vec<Box<dyn FnOnce() + Send + Sync>>,

View File

@ -14,7 +14,7 @@ impl Owner {
fn use_context<T: Clone + 'static>(&self) -> Option<T> {
let ty = TypeId::of::<T>();
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;
if let Some(context) = contexts.get(&ty) {
context.downcast_ref::<T>().cloned()
@ -28,8 +28,7 @@ impl Owner {
if let Some(value) = downcast {
return Some(value);
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
parent = this_parent.parent.as_ref().map(|p| p.clone());
}
}
None

View File

@ -184,12 +184,10 @@ where
}
}
pub trait With: WithUntracked + Track {
#[track_caller]
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U> {
self.track();
self.try_with_untracked(fun)
}
pub trait With: DefinedAt {
type Value: ?Sized;
fn try_with<U>(&self, fun: impl FnOnce(&Self::Value) -> U) -> Option<U>;
#[track_caller]
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 {}
pub trait GetUntracked: WithUntracked
impl<T> With for T
where
Self::Value: Clone,
T: WithUntracked + Track,
{
fn try_get_untracked(&self) -> Option<Self::Value> {
self.try_with_untracked(Self::Value::clone)
type Value = <T as WithUntracked>::Value;
#[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]
fn get_untracked(&self) -> Self::Value {
@ -219,20 +225,21 @@ where
T: WithUntracked,
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
where
Self::Value: Clone,
{
fn try_get(&self) -> Option<Self::Value> {
self.try_with(Self::Value::clone)
}
pub trait Get: DefinedAt {
type Value: Clone;
fn try_get(&self) -> Option<Self::Value>;
#[track_caller]
fn get(&self) -> Self::Value {
self.try_with(Self::Value::clone)
.unwrap_or_else(unwrap_signal!(self))
self.try_get().unwrap_or_else(unwrap_signal!(self))
}
}
@ -241,6 +248,11 @@ where
T: With,
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 {

View File

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

View File

@ -7,7 +7,7 @@ use std::{
};
use tachys::{renderer::Renderer, view::RenderHtml};
#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
/// A route that this application can serve.
pub struct RouteListing {
path: Vec<PathSegment>,
@ -64,6 +64,10 @@ impl RouteListing {
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
/// is not marked as statically rendered. All route parameters to use when resolving all paths
@ -100,6 +104,12 @@ impl RouteListing {
#[derive(Debug, Default)]
pub struct RouteList(Vec<RouteListing>);
impl From<Vec<RouteListing>> for RouteList {
fn from(value: Vec<RouteListing>) -> Self {
Self(value)
}
}
impl RouteList {
pub fn push(&mut self, data: RouteListing) {
self.0.push(data);

View File

@ -20,11 +20,6 @@ impl fmt::Debug for 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) {
if let Ok(hash) = window().location().hash() {
if !hash.is_empty() {
@ -50,6 +45,11 @@ impl BrowserUrl {
impl Location for BrowserUrl {
type Error = JsValue;
fn new() -> Result<Self, JsValue> {
let url = ArcRwSignal::new(Self::current()?);
Ok(Self { url })
}
fn as_url(&self) -> &ArcRwSignal<Url> {
&self.url
}

View File

@ -71,9 +71,11 @@ impl Default for LocationChange {
}
}
pub trait Location {
pub trait Location: Sized {
type Error: Debug;
fn new() -> Result<Self, Self::Error>;
fn as_url(&self) -> &ArcRwSignal<Url>;
fn current() -> Result<Url, Self::Error>;

View File

@ -11,6 +11,12 @@ impl RequestUrl {
}
}
impl AsRef<str> for RequestUrl {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Default for RequestUrl {
fn default() -> Self {
Self::new("/")
@ -18,7 +24,7 @@ impl Default for 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)
}

View File

@ -214,14 +214,12 @@ where
) -> impl IntoIterator<Item = Vec<PathSegment>> + '_ {
let mut segment_routes = Vec::new();
self.segments.generate_path(&mut segment_routes);
let segment_routes = segment_routes.into_iter();
let children_routes = self.children.as_ref().into_iter().flat_map(|child| child.generate_routes().into_iter());
children_routes.map(move |child_routes| {
segment_routes
.clone()
.chain(child_routes)
.filter(|seg| seg != &PathSegment::Unit)
.collect()
})
let children = self.children.as_ref();
match children {
None => Either::Left(iter::once(segment_routes)),
Some(children) => {
Either::Right(children.generate_routes().into_iter())
}
}
}
}

View File

@ -316,7 +316,7 @@ macro_rules! tuples {
}
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!(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);
@ -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!(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);
*/

View File

@ -7,3 +7,14 @@ pub enum PathSegment {
Param(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,
}
}
}

View File

@ -7,6 +7,12 @@ impl Params {
pub fn new() -> Self {
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

View File

@ -1,11 +1,12 @@
use crate::{
generate_route_list::RouteList,
location::Location,
location::{Location, RequestUrl},
matching::{
MatchInterface, MatchNestedRoutes, PossibleRouteMatch, RouteMatchId,
Routes,
},
ChooseView, MatchParams, Params,
ChooseView, MatchParams, Method, Params, PathSegment, RouteListing,
SsrMode,
};
use core::marker::PhantomData;
use either_of::*;
@ -13,17 +14,23 @@ use once_cell::unsync::Lazy;
use reactive_graph::{
computed::{ArcMemo, Memo},
effect::RenderEffect,
owner::Owner,
owner::{use_context, Owner},
signal::ArcRwSignal,
traits::{Get, Read, Set, Track},
};
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::{
html::attribute::Attribute,
hydration::Cursor,
renderer::Renderer,
renderer::{dom::Dom, Renderer},
ssr::StreamBuilder,
view::{
add_attr::AddAnyAttr,
@ -36,7 +43,7 @@ use tachys::{
#[derive(Debug)]
pub struct Router<Rndr, Loc, Children, FallbackFn> {
base: Option<Cow<'static, str>>,
location: Loc,
location: PhantomData<Loc>,
pub routes: Routes<Children, Rndr>,
fallback: FallbackFn,
}
@ -49,13 +56,12 @@ where
FallbackFn: Fn() -> Fallback,
{
pub fn new(
location: Loc,
routes: Routes<Children, Rndr>,
fallback: FallbackFn,
) -> Router<Rndr, Loc, Children, FallbackFn> {
Self {
base: None,
location,
location: PhantomData,
routes,
fallback,
}
@ -63,13 +69,12 @@ where
pub fn new_with_base(
base: impl Into<Cow<'static, str>>,
location: Loc,
routes: Routes<Children, Rndr>,
fallback: FallbackFn,
) -> Router<Rndr, Loc, Children, FallbackFn> {
Self {
base: Some(base.into()),
location,
location: PhantomData,
routes,
fallback,
}
@ -87,7 +92,7 @@ where
}
}
pub struct RouteData<R>
pub struct RouteData<R = Dom>
where
R: Renderer + 'static,
{
@ -120,8 +125,9 @@ where
type FallibleState = (); // TODO
fn build(self) -> Self::State {
self.location.init(self.base);
let url = self.location.as_url().clone();
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()
@ -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>
where
Matcher: MatchInterface<R>,
@ -238,6 +416,53 @@ where
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>
@ -270,11 +495,11 @@ where
.map(|child| get_inner_view(outlets, &owner, child))
.unwrap_or_default();
let inner = Rc::new(RefCell::new(OutletStateInner {
state: Lazy::new({
let params = params.clone();
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 {
@ -282,10 +507,68 @@ where
outlet,
})
})
.into_any()
.build()
.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(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 {
@ -332,9 +615,7 @@ where
id: RouteMatchId(0),
owner: Owner::current().unwrap(),
params: ArcRwSignal::new(Params::new()),
inner: Rc::new(RefCell::new(OutletStateInner {
state: Lazy::new(Box::new(|| ().into_any().build())),
})),
inner: Default::default(),
}
}
}
@ -373,7 +654,19 @@ where
const MIN_LENGTH: usize = 0; // TODO
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>(
@ -381,24 +674,41 @@ where
cursor: &Cursor<R>,
position: &PositionState,
) -> 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>
where
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>>>,
}
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>
where
R: Renderer + 'static,
{
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 {
view,
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>
where
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>
for Router<Rndr, Loc, Children, FallbackFn>
where

View File

@ -10,7 +10,7 @@ pub enum StaticMode {
}
// TODO
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct StaticDataMap;
impl StaticDataMap {

View File

@ -66,6 +66,8 @@ web-sys = { version = "0.3", optional = true, features = [
"console",
"ReadableStream",
"ReadableStreamDefaultReader",
"AbortController",
"AbortSignal"
] }
# reqwest client
@ -74,6 +76,7 @@ reqwest = { version = "0.12", default-features = false, optional = true, feature
"stream",
] }
url = "2"
pin-project-lite = "0.2.13"
[features]
default = ["json", "cbor"]

View File

@ -38,11 +38,19 @@ pub trait Client<CustErr> {
pub mod browser {
use super::Client;
use crate::{
error::ServerFnError, request::browser::BrowserRequest,
error::ServerFnError,
request::browser::{AbortOnDrop, BrowserRequest, RequestInner},
response::browser::BrowserResponse,
};
use gloo_net::{http::Response, Error};
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.
pub struct BrowserClient;
@ -56,8 +64,17 @@ pub mod browser {
) -> impl Future<Output = Result<Self::Response, ServerFnError<CustErr>>>
+ Send {
SendWrapper::new(async move {
req.0
.take()
let mut req = req.0.take();
let RequestInner {
request,
abort_ctrl,
} = req;
/*BrowserRequestFuture {
request_fut: request.send(),
abort_ctrl,
cust_err: PhantomData,
}*/
request
.send()
.await
.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")]

View File

@ -8,27 +8,39 @@ use send_wrapper::SendWrapper;
use std::ops::{Deref, DerefMut};
use wasm_bindgen::JsValue;
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.
#[derive(Debug)]
pub struct BrowserRequest(pub(crate) SendWrapper<Request>);
pub struct BrowserRequest(pub(crate) SendWrapper<RequestInner>);
impl From<Request> for BrowserRequest {
fn from(value: Request) -> Self {
Self(SendWrapper::new(value))
#[derive(Debug)]
pub(crate) struct RequestInner {
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 {
fn from(value: BrowserRequest) -> Self {
value.0.take()
value.0.take().request
}
}
impl From<BrowserRequest> for web_sys::Request {
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;
fn deref(&self) -> &Self::Target {
self.0.deref()
&self.0.deref().request
}
}
impl DerefMut for BrowserRequest {
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 {
type FormData = BrowserFormData;
@ -65,6 +83,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
query: &str,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(
server_url.len() + path.len() + 1 + query.len(),
@ -73,13 +92,15 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
url.push_str(path);
url.push('?');
url.push_str(query);
Ok(Self(SendWrapper::new(
Request::get(&url)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::get(&url)
.header("Content-Type", content_type)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.build()
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_post(
@ -88,17 +109,20 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
body: String,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
url.push_str(server_url);
url.push_str(path);
Ok(Self(SendWrapper::new(
Request::post(&url)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::post(&url)
.header("Content-Type", content_type)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_post_bytes(
@ -107,19 +131,22 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
body: Bytes,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
url.push_str(server_url);
url.push_str(path);
let body: &[u8] = &body;
let body = Uint8Array::from(body).buffer();
Ok(Self(SendWrapper::new(
Request::post(&url)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::post(&url)
.header("Content-Type", content_type)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_multipart(
@ -127,16 +154,19 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
accepts: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let server_url = get_server_url();
let mut url = String::with_capacity(server_url.len() + path.len());
url.push_str(server_url);
url.push_str(path);
Ok(Self(SendWrapper::new(
Request::post(&url)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::post(&url)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(body.0.take())
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_post_form_data(
@ -145,6 +175,7 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
body: Self::FormData,
) -> Result<Self, ServerFnError<CustErr>> {
let (abort_ctrl, abort_signal) = abort_signal();
let form_data = body.0.take();
let url_params =
UrlSearchParams::new_with_str_sequence_sequence(&form_data)
@ -156,13 +187,15 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
},
))
})?;
Ok(Self(SendWrapper::new(
Request::post(path)
Ok(Self(SendWrapper::new(RequestInner {
request: Request::post(path)
.header("Content-Type", content_type)
.header("Accept", accepts)
.abort_signal(abort_signal.as_ref())
.body(url_params)
.map_err(|e| ServerFnError::Request(e.to_string()))?,
)))
abort_ctrl,
})))
}
fn try_new_streaming(
@ -171,9 +204,13 @@ impl<CustErr> ClientReq<CustErr> for BrowserRequest {
content_type: &str,
body: impl Stream<Item = Bytes> + 'static,
) -> Result<Self, ServerFnError<CustErr>> {
// TODO abort signal
let req = streaming_request(path, accepts, content_type, body)
.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,
body: impl Stream<Item = Bytes> + 'static,
) -> Result<Request, JsValue> {
let (abort_ctrl, abort_signal) = abort_signal();
let stream = ReadableStream::from_stream(body.map(|bytes| {
let data = Uint8Array::from(bytes.as_ref());
let data = JsValue::from(data);

View File

@ -2,6 +2,7 @@ use super::{Attribute, NextAttribute};
use crate::renderer::Renderer;
use std::{
any::{Any, TypeId},
fmt::Debug,
marker::PhantomData,
};
@ -17,6 +18,15 @@ pub struct AnyAttribute<R: Renderer> {
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>
where
R: Renderer,

View File

@ -3,7 +3,7 @@ use crate::{
renderer::DomRenderer,
view::{Position, ToTemplate},
};
use std::marker::PhantomData;
use std::{marker::PhantomData, rc::Rc, sync::Arc};
#[inline(always)]
pub fn class<C, R>(class: C) -> Class<C, R>
@ -114,7 +114,7 @@ impl<'a, R> IntoClass<R> for &'a str
where
R: DomRenderer,
{
type State = (R::Element, &'a str);
type State = (R::Element, Self);
fn to_html(self, class: &mut String) {
class.push_str(self);
@ -145,7 +145,7 @@ impl<R> IntoClass<R> for String
where
R: DomRenderer,
{
type State = (R::Element, String);
type State = (R::Element, Self);
fn to_html(self, class: &mut String) {
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)
where
R: DomRenderer,

View File

@ -4,12 +4,12 @@ use crate::{
renderer::{DomRenderer, Renderer},
view::add_attr::AddAnyAttr,
};
use std::marker::PhantomData;
use std::{marker::PhantomData, rc::Rc, sync::Arc};
#[inline(always)]
pub fn inner_html<T, R>(value: T) -> InnerHtml<T, R>
where
T: AsRef<str>,
T: InnerHtmlValue<R>,
R: DomRenderer,
{
InnerHtml {
@ -21,8 +21,8 @@ where
#[derive(Debug, Clone, Copy)]
pub struct InnerHtml<T, R>
where
T: AsRef<str>,
R: Renderer,
T: InnerHtmlValue<R>,
R: DomRenderer,
{
value: T,
rndr: PhantomData<R>,
@ -30,12 +30,12 @@ where
impl<T, R> Attribute<R> for InnerHtml<T, R>
where
T: AsRef<str> + PartialEq,
T: InnerHtmlValue<R>,
R: DomRenderer,
{
const MIN_LENGTH: usize = 0;
type State = (R::Element, T);
type State = T::State;
fn to_html(
self,
@ -44,33 +44,28 @@ where
_style: &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>(
self,
el: &<R as Renderer>::Element,
) -> Self::State {
(el.clone(), self.value)
self.value.hydrate::<FROM_SERVER>(el)
}
fn build(self, el: &<R as Renderer>::Element) -> Self::State {
R::set_inner_html(el, self.value.as_ref());
(el.clone(), self.value)
self.value.build(el)
}
fn rebuild(self, state: &mut Self::State) {
let (el, prev) = state;
if self.value != *prev {
R::set_inner_html(el, self.value.as_ref());
*prev = self.value;
}
self.value.rebuild(state);
}
}
impl<T, R> NextAttribute<R> for InnerHtml<T, R>
where
T: AsRef<str> + PartialEq,
T: InnerHtmlValue<R>,
R: DomRenderer,
{
type Output<NewAttr: Attribute<R>> = (Self, NewAttr);
@ -85,7 +80,7 @@ where
pub trait InnerHtmlAttribute<T, Rndr>
where
T: AsRef<str> + PartialEq,
T: InnerHtmlValue<Rndr>,
Rndr: DomRenderer,
Self: Sized + AddAnyAttr<Rndr>,
{
@ -103,7 +98,7 @@ where
Self: AddAnyAttr<Rndr>,
E: ElementWithChildren,
At: Attribute<Rndr>,
T: AsRef<str> + PartialEq,
T: InnerHtmlValue<Rndr>,
Rndr: DomRenderer,
{
fn inner_html(
@ -113,3 +108,188 @@ where
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!()
}
}

View File

@ -1,8 +1,8 @@
use crate::{
html::attribute::AttributeValue,
html::{attribute::AttributeValue, class::IntoClass},
hydration::Cursor,
prelude::{Mountable, Render, RenderHtml},
renderer::Renderer,
renderer::{DomRenderer, Renderer},
view::{strings::StrState, Position, PositionState, ToTemplate},
};
use oco::Oco;
@ -142,3 +142,34 @@ where
*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;
}
}

View File

@ -3,6 +3,7 @@ use crate::{
error::AnyError,
html::{
attribute::{Attribute, AttributeValue},
element::InnerHtmlValue,
property::IntoProperty,
},
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)]
mod tests {

View File

@ -1,5 +1,5 @@
use super::{Mountable, Position, PositionState, Render, RenderHtml};
use crate::{hydration::Cursor, renderer::Renderer};
use crate::{hydration::Cursor, renderer::Renderer, ssr::StreamBuilder};
use std::{
any::{Any, TypeId},
fmt::Debug,
@ -12,8 +12,9 @@ where
{
type_id: TypeId,
value: Box<dyn Any>,
// TODO add async HTML rendering for AnyView
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>,
rebuild: fn(TypeId, Box<dyn Any>, &mut AnyViewState<R>),
#[allow(clippy::type_complexity)]
@ -122,6 +123,24 @@ where
.expect("AnyView::to_html could not be downcast");
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 value = value
.downcast::<T>()
@ -198,6 +217,8 @@ where
type_id: TypeId::of::<T>(),
value,
to_html,
to_html_async,
to_html_async_ooo,
build,
rebuild,
hydrate_from_server,
@ -243,6 +264,20 @@ where
(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>(
self,
cursor: &Cursor<R>,