From c1ec81a6deba2d86f601b183025cf068b7d297bb Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Sat, 20 Jan 2024 03:57:32 +0800 Subject: [PATCH] chore: sync upstream --- Cargo.toml | 37 +- README.md | 2 +- examples/todo_app_sqlite_viz/Cargo.toml | 2 + src/lib.rs | 1337 +++++++++++++---------- 4 files changed, 799 insertions(+), 579 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed3c31e..d4b5497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,15 +12,24 @@ http-body-util = "0.1.0" hyper = "1.1.0" serde = { version = "1", features = ["derive"] } serde_json = "1" -tokio = { version = "1", default-features = false } parking_lot = "0.12.1" +cfg-if = "1" + +tokio = { version = "1", default-features = false } +tokio-util = { version = "0.7", features = ["rt"] } leptos_viz = { path = "." } -leptos = { git = "https://github.com/leptos-rs/leptos.git", rev = "1eaf886" } -leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", rev = "1eaf886" } -leptos_router = { git = "https://github.com/leptos-rs/leptos.git", rev = "1eaf886" } -leptos_reactive = { git = "https://github.com/leptos-rs/leptos.git", rev = "1eaf886" } -leptos_integration_utils = { git = "https://github.com/leptos-rs/leptos.git", rev = "1eaf886" } +leptos = { git = "https://github.com/leptos-rs/leptos.git", rev = "04747fc" } +leptos_macro = { git = "https://github.com/leptos-rs/leptos.git", rev = "04747fc" } +leptos_meta = { git = "https://github.com/leptos-rs/leptos.git", rev = "04747fc" } +leptos_router = { git = "https://github.com/leptos-rs/leptos.git", rev = "04747fc" } +leptos_reactive = { git = "https://github.com/leptos-rs/leptos.git", rev = "04747fc" } +leptos_integration_utils = { git = "https://github.com/leptos-rs/leptos.git", rev = "04747fc" } +server_fn = { git = "https://github.com/leptos-rs/leptos.git", rev = "04747fc" } + +# registration system +dashmap = "5" +once_cell = "1" [package] name = "leptos_viz" @@ -37,17 +46,27 @@ futures.workspace = true http.workspace = true http-body-util.workspace = true hyper.workspace = true +serde_json.workspace = true +tokio.workspace = true +parking_lot.workspace = true +cfg-if.workspace = true + leptos = { workspace = true, features = ["ssr"] } leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } leptos_integration_utils.workspace = true -serde_json.workspace = true -tokio.workspace = true -parking_lot.workspace = true +server_fn = { workspace = true, features = ["ssr"] } + +dashmap.workspace = true +once_cell.workspace = true + +tokio-util.workspace = true [dev-dependencies] tokio = { workspace = true, features = ["full"] } [features] nonce = ["leptos/nonce"] +wasm = [] +default = ["tokio/full"] experimental-islands = ["leptos_integration_utils/experimental-islands"] diff --git a/README.md b/README.md index 3f16a90..85f4018 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ leptos_viz ----------- -Viz integrations for the [Leptos] web framework. +Viz integration for the [Leptos] web framework. An example can be found [here]. diff --git a/examples/todo_app_sqlite_viz/Cargo.toml b/examples/todo_app_sqlite_viz/Cargo.toml index 2b27b40..1b266a4 100644 --- a/examples/todo_app_sqlite_viz/Cargo.toml +++ b/examples/todo_app_sqlite_viz/Cargo.toml @@ -26,6 +26,7 @@ http.workspace = true futures.workspace = true leptos.workspace = true leptos_viz = { workspace = true, optional = true } +leptos_macro = { workspace = true, features = ["nightly"] } leptos_meta = { workspace = true, features = ["nightly"] } leptos_router = { workspace = true, features = ["nightly"] } leptos_reactive = { workspace = true, features = ["nightly"] } @@ -38,6 +39,7 @@ ssr = [ "dep:tokio", "dep:sqlx", "leptos/ssr", + "leptos_macro/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_viz", diff --git a/src/lib.rs b/src/lib.rs index 732b619..b551beb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,39 +6,99 @@ //! [`examples`](https://github.com/leptos-rs/leptos/tree/main/examples) //! directory in the Leptos repository. +use dashmap::DashMap; use futures::{ channel::mpsc::{Receiver, Sender}, Future, SinkExt, Stream, StreamExt, }; -use http::{header, method::Method, uri::Uri, version::Version, StatusCode}; -use http_body_util::{BodyExt, Full}; -use leptos::{ - leptos_server::{server_fn_by_path, Payload}, - server_fn::Encoding, - ssr::*, - *, +use http::{ + header::{self, HeaderName, HeaderValue, ACCEPT, LOCATION, REFERER}, + request::Parts, + HeaderMap, Method, StatusCode, }; +use http_body_util::Full; +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 once_cell::sync::{Lazy, OnceCell}; use parking_lot::RwLock; -use std::{pin::Pin, sync::Arc}; -use tokio::task::spawn_blocking; + +use server_fn::{ + codec::Encoding, initialize_server_fn_map, middleware::BoxedService, redirect::REDIRECT_HEADER, + ServerFn, ServerFnTraitObj, +}; +use std::{fmt::Debug, io, pin::Pin, sync::Arc, thread::available_parallelism}; +use tokio_util::task::LocalPoolHandle; +use tracing::Instrument; use viz::{ - headers::{HeaderMap, HeaderName, HeaderValue}, - Body, Bytes, Error, Handler, IntoResponse, Request, RequestExt, Response, ResponseExt, Result, - Router, + types::RouteInfo, Body, Bytes, Error, Handler, IntoResponse, Request, RequestExt, Response, + Result, Router, }; -/// A struct to hold the parts of the incoming Request. Since `http::Request` isn't cloneable, we're forced -/// to construct this for Leptos to use in viz -#[derive(Debug, Clone)] -pub struct RequestParts { - pub version: Version, - pub method: Method, - pub uri: Uri, - pub headers: HeaderMap, - pub body: Bytes, +type LazyServerFnMap = Lazy>>; + +static REGISTERED_SERVER_FUNCTIONS: LazyServerFnMap = + initialize_server_fn_map!(Request, Response); + +/// Explicitly register a server function. This is only necessary if you are +/// running the server in a WASM environment (or a rare environment that the +/// `inventory`). +pub fn register_explicit() +where + T: ServerFn + 'static, +{ + REGISTERED_SERVER_FUNCTIONS.insert( + T::PATH, + ServerFnTraitObj::new( + T::PATH, + T::InputEncoding::METHOD, + |req| Box::pin(T::run_on_server(req)), + T::middlewares, + ), + ); +} + +/// The set of all registered server function paths. +pub fn server_fn_paths() -> impl Iterator { + REGISTERED_SERVER_FUNCTIONS + .iter() + .map(|item| (item.path(), item.method())) +} + +/// A Viz handler that responds to a server function request. +pub async fn handle_server_fn(req: Request) -> Response { + let path = req.uri().path(); + + if let Some(mut service) = get_server_fn_service(path) { + service.0.run(req).await + } else { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(Full::from(format!( + "Could not find a server function at the route {path}. \ + \n\nIt's likely that either\n 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.", + )))) + .unwrap() + } +} + +/// Returns the server function at the given path as a service that can be modified. +pub fn get_server_fn_service(path: &str) -> Option> { + REGISTERED_SERVER_FUNCTIONS.get(path).map(|server_fn| { + let middleware = server_fn.middleware(); + let mut service = BoxedService::new(server_fn.clone()); + for middleware in middleware { + service = middleware.layer(service); + } + service + }) } /// This struct lets you define headers and override the status of the Response from an Element or a Server Function @@ -94,28 +154,47 @@ impl ResponseOptions { /// it sets a StatusCode of 302 and a LOCATION header with the provided value. /// If looking to redirect from the client, `leptos_router::use_navigate()` should be used instead pub fn redirect(path: &str) { - if let Some(response_options) = use_context::() { - response_options.set_status(StatusCode::FOUND); - response_options.insert_header( + if let (Some(req), Some(res)) = (use_context::(), use_context::()) { + // insert the Location header in any case + res.insert_header( header::LOCATION, header::HeaderValue::from_str(path).expect("Failed to create HeaderValue"), ); + + let accepts_html = req + .headers + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + if accepts_html { + // if the request accepts text/html, it's a plain form request and needs + // to have the 302 code set + res.set_status(StatusCode::FOUND); + } else { + // otherwise, we sent it from the server fn client and actually don't want + // to set a real redirect, as this will break the ability to return data + // instead, set the REDIRECT_HEADER to indicate that the client should redirect + res.insert_header( + HeaderName::from_static(REDIRECT_HEADER), + HeaderValue::from_str("").unwrap(), + ); + } + } else { + tracing::warn!( + "Couldn't retrieve either Parts or ResponseOptions while trying \ + to redirect()." + ); } } /// Decomposes an HTTP request into its parts, allowing you to read its headers -/// and other data without consuming the body. -pub async fn generate_request_parts(req: Request) -> RequestParts { - // provide request headers as context in server scope +/// and other data without consuming the body. Creates a new Request from the +/// original parts for further processing +pub fn generate_request_and_parts(req: Request) -> (Request, Parts) { let (parts, body) = req.into_parts(); - let body = BodyExt::collect(body).await.unwrap_or_default().to_bytes(); - RequestParts { - method: parts.method, - uri: parts.uri, - headers: parts.headers, - version: parts.version, - body, - } + let parts2 = parts.clone(); + (Request::from_parts(parts, body), parts2) } /// A Viz handlers to listens for a request with Leptos server function arguments in the body, @@ -126,20 +205,20 @@ pub async fn generate_request_parts(req: Request) -> RequestParts { /// ``` /// use leptos::*; /// use std::net::SocketAddr; -/// use tokio::net::TcpListener; /// use viz::{serve, Router}; /// /// # if false { // don't actually try to run a server in a doctest... +/// #[cfg(feature = "default")] /// #[tokio::main] /// async fn main() { /// let addr = SocketAddr::from(([127, 0, 0, 1], 8082)); -/// let listener = TcpListener::bind(addr).await.unwrap(); /// /// // build our application with a route -/// let app = -/// Router::new().post("/api/:fn_name*", leptos_viz::handle_server_fns); +/// let app = Router::new() +/// .post("/api/*fn_name", leptos_viz::handle_server_fns); /// -/// // run our app +/// // run our app with hyper +/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); /// serve(listener, app).await.unwrap(); /// } /// # } @@ -149,13 +228,34 @@ pub async fn generate_request_parts(req: Request) -> RequestParts { /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] +/// - [`Parts`] +/// - [`ResponseOptions`] +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub async fn handle_server_fns(req: Request) -> Result { - handle_server_fns_inner(req, || {}).await + handle_server_fns_inner::(req).await } -/// A Viz handlers to listens for a request with Leptos server function arguments in the body, +/// 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")] { + 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."); + spawn_local($block); + } + } + }; +} + +/// An Viz handlers to listens for a request with Leptos server function arguments in the body, /// run the server function if found, and return the resulting [Response]. /// /// This can then be set up at an appropriate route in your application: @@ -166,7 +266,7 @@ pub async fn handle_server_fns(req: Request) -> Result { /// of one that should work much like this one. /// /// **NOTE**: If your server functions expect a context, make sure to provide it both in -/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`](LeptosRoutes#leptos_routes_with_context) (or whatever +/// [`handle_server_fns_with_context`] **and** in [`leptos_routes_with_context`] (or whatever /// rendering method you are using). During SSR, server functions are called by the rendering /// method, while subsequent calls from the client are handled by the server function handler. /// The same context needs to be provided to both handlers. @@ -175,160 +275,110 @@ pub async fn handle_server_fns(req: Request) -> Result { /// This function always provides context values including the following types: /// - [RequestParts] /// - [ResponseOptions] -pub async fn handle_server_fns_with_context( - req: Request, - additional_context: impl Fn() + Clone + Send + 'static, -) -> Result { - handle_server_fns_inner(req, additional_context).await +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub async fn handle_server_fns_with_context(req: Request) -> Result +where + F: Fn() + Clone + Send + Sync + 'static, +{ + handle_server_fns_inner::(req).await } -async fn handle_server_fns_inner( - req: Request, - additional_context: impl Fn() + Clone + Send + 'static, -) -> Result { - let fn_name = req.params::()?; - let headers = req.headers().clone(); - let query = req.query_string().unwrap_or("").to_owned().into(); +async fn handle_server_fns_inner(req: Request) -> Result +where + F: Fn() + Clone + Send + Sync + 'static, +{ + let additional_context = req.state::().unwrap(); let (tx, rx) = futures::channel::oneshot::channel(); - spawn_blocking({ - move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - async move { - let res = if let Some(server_fn) = server_fn_by_path(fn_name.as_str()) { - let runtime = create_runtime(); - - additional_context(); - - let req_parts = generate_request_parts(req).await; - // Add this so we can get details about the Request - provide_context(req_parts.clone()); - // Add this so that we can set headers and status of the response - provide_context(ResponseOptions::default()); - - let data = match &server_fn.encoding() { - Encoding::Url | Encoding::Cbor => &req_parts.body, - Encoding::GetJSON | Encoding::GetCBOR => &query, - }; - - let res = match server_fn.call((), data).await { - Ok(serialized) => { - // If ResponseOptions are set, add the headers and status to the request - let res_options = use_context::(); - - // if this is Accept: application/json then send a serialized JSON response - let accept_header = - headers.get("Accept").and_then(|value| value.to_str().ok()); - let mut res = Response::builder(); - - // Add headers from ResponseParts if they exist. These should be added as long - // as the server function returns an OK response - let res_options_outer = res_options.unwrap().0; - let res_options_inner = res_options_outer.read(); - let (status, mut res_headers) = ( - res_options_inner.status, - res_options_inner.headers.clone(), - ); - - if let Some(header_ref) = res.headers_mut() { - header_ref.extend(res_headers.drain()); - }; - - if accept_header == Some("application/json") - || accept_header - == Some( - "application/\ - x-www-form-urlencoded", - ) - || accept_header == Some("application/cbor") - { - res = res.status(StatusCode::OK); - } - // otherwise, it's probably a
submit or something: redirect back to the referrer - else { - let referer = headers - .get("Referer") - .and_then(|value| value.to_str().ok()) - .unwrap_or("/"); - - res = res - .status(StatusCode::SEE_OTHER) - .header("Location", referer); - } - // Override StatusCode if it was set in a Resource or Element - res = match status { - Some(status) => res.status(status), - None => res, - }; - match serialized { - Payload::Binary(data) => res - .header(header::CONTENT_TYPE, "application/cbor") - .body(Body::from(Full::from(data))), - Payload::Url(data) => res - .header( - header::CONTENT_TYPE, - "application/\ - x-www-form-urlencoded", - ) - .body(Body::from(Full::from(data))), - Payload::Json(data) => res - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(Full::from(data))), - } - } - Err(e) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from(Full::from( - serde_json::to_string(&e).unwrap_or_else(|_| e.to_string()), - ))), - }; - runtime.dispose(); - res - } else { - Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from(Full::from(format!( - "Could not find a server function at the \ - route {fn_name}. \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.", - )))) - } - .expect("could not build Response"); - _ = tx.send(res); + spawn_task!(async move { + let path = req.uri().path().to_string(); + let (req, parts) = generate_request_and_parts(req); + + let res = if let Some(mut service) = get_server_fn_service(&path) { + let runtime = create_runtime(); + + additional_context(); + provide_context(parts); + provide_context(ResponseOptions::default()); + + // store Accepts and Referer in case we need them for redirect (below) + let accepts_html = req + .headers() + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + let referrer = req.headers().get(REFERER).cloned(); + + // actually run the server fn + let mut res = service.0.run(req).await; + + // update response as needed + let res_options = expect_context::().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(); + if !has_location { + *res.status_mut() = StatusCode::FOUND; + res.headers_mut().insert(LOCATION, referrer); } - }) + } + } + + // 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()); + + // clean up the scope + runtime.dispose(); + Ok(res) + } else { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(Full::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.", + )))) } + .expect("could not build Response"); + + _ = tx.send(res); }); rx.await.map_err(Error::boxed) } -/// Returns a Viz [Handler] that listens for a `GET` request and tries + +pub type PinnedHtmlStream = Pin> + Send>>; + +/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries /// to route it using [leptos_router], serving an HTML stream of your application. /// /// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before /// rendering it, and includes any meta tags injected using [leptos_meta]. /// -/// The HTML stream is rendered using [render_to_stream], and includes everything described in -/// the documentation for that function. +/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and +/// includes everything described in the documentation for that function. /// /// This can then be set up at an appropriate route in your application: /// ``` /// use leptos::*; /// use leptos_config::get_configuration; /// use std::{env, net::SocketAddr}; -/// use tokio::net::TcpListener; /// use viz::{serve, Router}; /// /// #[component] @@ -337,23 +387,21 @@ async fn handle_server_fns_inner( /// } /// /// # if false { // don't actually try to run a server in a doctest... +/// #[cfg(feature = "default")] /// #[tokio::main] /// async fn main() { /// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); /// let leptos_options = conf.leptos_options; /// let addr = leptos_options.site_addr.clone(); -/// let listener = TcpListener::bind(addr).await.unwrap(); /// /// // build our application with a route -/// let app = Router::new().any( -/// "*", -/// leptos_viz::render_app_to_stream( -/// leptos_options, -/// || view! { }, -/// ), -/// ); +/// let app = Router::new().any("*", leptos_viz::render_app_to_stream( +/// leptos_options, +/// || view! { }, +/// )); /// -/// // run our app +/// // run our app with hyper +/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); /// serve(listener, app).await.unwrap(); /// } /// # } @@ -361,10 +409,11 @@ async fn handle_server_fns_inner( /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] -/// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] +/// - [`Parts`] +/// - [`ResponseOptions`] +/// - [`MetaContext`](leptos_meta::MetaContext) +/// - [`RouterIntegrationContext`](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream( options: LeptosOptions, app_fn: impl Fn() -> IV + Clone + Send + 'static, @@ -378,15 +427,35 @@ where render_app_to_stream_with_context(options, || {}, app_fn) } -/// Returns a Viz [Handler] that listens for a `GET` request and tries +/// Returns a Viz [Handler](viz::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` 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( + options: LeptosOptions, + paths: Vec, + app_fn: impl Fn() -> IV + Clone + Send + 'static, +) -> impl Fn(Request) -> Pin> + Send + 'static>> + + Clone + + Send + + 'static +where + IV: IntoView, +{ + render_route_with_context(options, paths, || {}, app_fn) +} + +/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries +/// to route it using [leptos_router], serving an in-order HTML stream of your application. /// This stream will pause at each `` node and wait for it to resolve before /// sending down its HTML. The app will become interactive once it has fully loaded. /// /// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before /// rendering it, and includes any meta tags injected using [leptos_meta]. /// -/// The HTML stream is rendered using [render_to_stream], and includes everything described in +/// The HTML stream is rendered using [render_to_stream_in_order], and includes everything described in /// the documentation for that function. /// /// This can then be set up at an appropriate route in your application: @@ -394,7 +463,6 @@ where /// use leptos::*; /// use leptos_config::get_configuration; /// use std::{env, net::SocketAddr}; -/// use tokio::net::TcpListener; /// use viz::{serve, Router}; /// /// #[component] @@ -403,23 +471,22 @@ where /// } /// /// # if false { // don't actually try to run a server in a doctest... +/// #[cfg(feature = "default")] /// #[tokio::main] /// async fn main() { /// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); /// let leptos_options = conf.leptos_options; /// let addr = leptos_options.site_addr.clone(); -/// let listener = TcpListener::bind(addr).await.unwrap(); /// /// // build our application with a route -/// let app = Router::new().any( -/// "*", -/// leptos_viz::render_app_to_stream_in_order( +/// let app = +/// Router::new().any("*", leptos_viz::render_app_to_stream_in_order( /// leptos_options, /// || view! { }, -/// ), -/// ); +/// )); /// -/// // run our app +/// // run our app with hyper +/// let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); /// serve(listener, app).await.unwrap(); /// } /// # } @@ -427,10 +494,11 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream_in_order( options: LeptosOptions, app_fn: impl Fn() -> IV + Clone + Send + 'static, @@ -444,36 +512,19 @@ where render_app_to_stream_in_order_with_context(options, || {}, app_fn) } -/// Returns a Viz [Handler] that listens for a `GET` request and tries +/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries /// to route it using [leptos_router], serving an HTML stream of your application. /// -/// This version allows us to pass Viz State/Extractor or other infro from Viz or network -/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides -/// the data to leptos in a closure. An example is below -/// ```ignore -/// async fn custom_handler(req: Request) -> Result { -/// let id = req.params::()?; -/// let options = &*req.state::>().ok_or(Error::Responder(Response::text("missing state type LeptosOptions")))?; -/// let handler = leptos_viz::render_app_to_stream_with_context(options.clone(), -/// move || { -/// provide_context(id.clone()); -/// }, -/// || view! { } -/// ); -/// handler(req).await -/// } -/// ``` -/// Otherwise, this function is identical to [render_app_to_stream]. -/// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream_with_context( options: LeptosOptions, - additional_context: impl Fn() + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send, app_fn: impl Fn() -> IV + Clone + Send + 'static, ) -> impl Fn(Request) -> Pin> + Send + 'static>> + Clone @@ -485,10 +536,78 @@ where render_app_to_stream_with_context_and_replace_blocks(options, additional_context, app_fn, false) } -/// Returns a Viz [Handler] that listens for a `GET` request and tries +/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries +/// 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` 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( + options: LeptosOptions, + paths: Vec, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + Send + 'static, +) -> impl Fn(Request) -> Pin> + Send + 'static>> + + Clone + + Send + + 'static +where + IV: IntoView, +{ + let ooo = render_app_to_stream_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + ); + let pb = render_app_to_stream_with_context_and_replace_blocks( + options.clone(), + additional_context.clone(), + app_fn.clone(), + true, + ); + let io = render_app_to_stream_in_order_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + ); + let asyn = render_app_async_stream_with_context( + options.clone(), + additional_context.clone(), + app_fn.clone(), + ); + + move |req| { + // 1. Process route to match the values in routeListing + let path = req + .extensions() + .get::() + .expect("Failed to get Viz router rule") + .pattern + .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(|| { + panic!( + "Failed to find the route {path} requested by the user. \ + This suggests that the routing rules in the Router that \ + call this handler needs to be edited!" + ) + }); + // 3. Match listing mode against known, and choose function + match listing.mode() { + SsrMode::OutOfOrder => ooo(req), + SsrMode::PartiallyBlocked => pb(req), + SsrMode::InOrder => io(req), + SsrMode::Async => asyn(req), + } + } +} + +/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries /// to route it using [leptos_router], serving an HTML stream of your application. /// -/// This version allows us to pass Viz State/Extractor or other infro from Viz or network +/// This version allows us to pass Viz State/Extension/Extractor or other info from Viz or network /// layers above Leptos itself. To use it, you'll need to write your own handler function that provides /// the data to leptos in a closure. /// @@ -501,13 +620,14 @@ where /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream_with_context_and_replace_blocks( options: LeptosOptions, - additional_context: impl Fn() + Clone + Send + 'static, + additional_context: impl Fn() + 'static + Clone + Send, app_fn: impl Fn() -> IV + Clone + Send + 'static, replace_blocks: bool, ) -> impl Fn(Request) -> Pin> + Send + 'static>> @@ -517,7 +637,7 @@ pub fn render_app_to_stream_with_context_and_replace_blocks( where IV: IntoView, { - move |req: Request| { + move |req: Request| { Box::pin({ let options = options.clone(); let app_fn = app_fn.clone(); @@ -525,67 +645,48 @@ where let default_res_options = ResponseOptions::default(); let res_options2 = default_res_options.clone(); let res_options3 = default_res_options.clone(); - - async move { - // 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 - let path = req.uri().path_and_query().unwrap().as_str(); - - let full_path = format!("http://leptos.dev{path}"); - - let (tx, rx) = futures::channel::mpsc::channel(8); - - spawn_blocking({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); + let (tx, rx) = futures::channel::mpsc::channel(8); + + let current_span = tracing::Span::current(); + spawn_task!(async move { + let app = { + // 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 + let path = req.uri().path_and_query().unwrap().as_str(); + + let full_path = format!("http://leptos.dev{path}"); + let (_, req_parts) = generate_request_and_parts(req); move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let app = { - let full_path = full_path.clone(); - let req_parts = generate_request_parts(req).await; - 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 - ); - - forward_stream(&options, res_options2, bundle, tx).await; - - runtime.dispose(); - }) - .await; - } - }); + 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 + ); - generate_response(res_options3, rx).await - } + forward_stream(&options, res_options2, bundle, tx).await; + + runtime.dispose(); + }.instrument(current_span)); + + generate_response(res_options3, rx) }) } } +#[tracing::instrument(level = "trace", fields(error), skip_all)] async fn generate_response(res_options: ResponseOptions, rx: Receiver) -> Result { - let mut stream = Box::pin(rx.map(|html| Ok::<_, std::io::Error>(Bytes::from(html)))); + let mut stream = Box::pin(rx.map(|html| Ok(Bytes::from(html)))); // Get the first and second chunks in the stream, which renders the app shell, and thus allows Resources to run let first_chunk = stream.next().await; + let second_chunk = stream.next().await; // Extract the resources now that they've been rendered @@ -594,17 +695,30 @@ async fn generate_response(res_options: ResponseOptions, rx: Receiver) - let complete_stream = futures::stream::iter([first_chunk.unwrap(), second_chunk.unwrap()]).chain(stream); - let mut res = Response::stream(complete_stream); + let mut res = Response::new(Body::from_stream( + Box::pin(complete_stream) as PinnedHtmlStream + )); if let Some(status) = res_options.status { *res.status_mut() = status } - let mut res_headers = res_options.headers.clone(); - res.headers_mut().extend(res_headers.drain()); + let headers = res.headers_mut(); + + let mut res_headers = res_options.headers.clone(); + headers.extend(res_headers.drain()); + + if !headers.contains_key(header::CONTENT_TYPE) { + // Set the Content Type headers on all responses. This makes Firefox show the page source + // without complaining + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str("text/html; charset=utf-8").unwrap(), + ); + } Ok(res) } - +#[tracing::instrument(level = "trace", fields(error), skip_all)] async fn forward_stream( options: &LeptosOptions, res_options2: ResponseOptions, @@ -637,35 +751,18 @@ async fn forward_stream( tx.close_channel(); } -/// Returns a Viz [Handler] that listens for a `GET` request and tries +/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries /// to route it using [leptos_router], serving an in-order HTML stream of your application. /// This stream will pause at each `` node and wait for it to resolve before /// sending down its HTML. The app will become interactive once it has fully loaded. /// -/// This version allows us to pass Viz State/Extractor or other infro from Viz or network -/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides -/// the data to leptos in a closure. An example is below -/// ```ignore -/// async fn custom_handler(req: Request) -> Result { -/// let id = req.params::()?; -/// let options = &*req.state::>().ok_or(StateError::new::>())?; -/// let handler = leptos_viz::render_app_to_stream_in_order_with_context(options.clone(), -/// move || { -/// provide_context(id.clone()); -/// }, -/// || view! { } -/// ); -/// handler(req).await -/// } -/// ``` -/// Otherwise, this function is identical to [render_app_to_stream]. -/// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_to_stream_in_order_with_context( options: LeptosOptions, additional_context: impl Fn() + 'static + Clone + Send, @@ -695,43 +792,29 @@ where let full_path = format!("http://leptos.dev{path}"); let (tx, rx) = futures::channel::mpsc::channel(8); + let current_span = tracing::Span::current(); + spawn_task!(async move { + let app = { + let full_path = full_path.clone(); + let (parts, _) = req.into_parts(); + move || { + provide_contexts(full_path, parts, default_res_options); + app_fn().into_view() + } + }; - spawn_blocking({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let app = { - let full_path = full_path.clone(); - let req_parts = generate_request_parts(req).await; - move || { - provide_contexts(full_path, req_parts, default_res_options); - app_fn().into_view() - } - }; - - let (bundle, runtime) = - leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context( - app, - || generate_head_metadata_separated().1.into(), - add_context, - ); - - forward_stream(&options, res_options2, bundle, tx).await; - runtime.dispose(); - }) - .await; - } - }); - } - }); + let (bundle, runtime) = + leptos::ssr::render_to_stream_in_order_with_prefix_undisposed_with_context( + app, + || generate_head_metadata_separated().1.into(), + add_context, + ); + + forward_stream(&options, res_options2, bundle, tx).await; + + runtime.dispose(); + } + .instrument(current_span)); generate_response(res_options3, rx).await } @@ -739,20 +822,21 @@ where } } -fn provide_contexts(path: String, req_parts: RequestParts, default_res_options: ResponseOptions) { +#[tracing::instrument(level = "trace", fields(error), skip_all)] +fn provide_contexts(path: String, parts: Parts, default_res_options: ResponseOptions) { let integration = ServerIntegration { path }; provide_context(RouterIntegrationContext::new(integration)); provide_context(MetaContext::new()); - provide_context(req_parts); + provide_context(parts); provide_context(default_res_options); provide_server_redirect(redirect); #[cfg(feature = "nonce")] leptos::nonce::provide_nonce(); } -/// Returns a Viz [Handler] that listens for a `GET` request and tries +/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries /// to route it using [leptos_router], asynchronously rendering an HTML page after all -/// `async` [Resource]s have loaded. +/// `async` [Resource](leptos::Resource)s have loaded. /// /// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before /// rendering it, and includes any meta tags injected using [leptos_meta]. @@ -765,7 +849,6 @@ fn provide_contexts(path: String, req_parts: RequestParts, default_res_options: /// use leptos::*; /// use leptos_config::get_configuration; /// use std::{env, net::SocketAddr}; -/// use tokio::net::TcpListener; /// use viz::{serve, Router}; /// /// #[component] @@ -774,20 +857,22 @@ fn provide_contexts(path: String, req_parts: RequestParts, default_res_options: /// } /// /// # if false { // don't actually try to run a server in a doctest... +/// #[cfg(feature = "default")] /// #[tokio::main] /// async fn main() { /// let conf = get_configuration(Some("Cargo.toml")).await.unwrap(); /// let leptos_options = conf.leptos_options; /// let addr = leptos_options.site_addr.clone(); -/// let listener = TcpListener::bind(addr).await.unwrap(); /// /// // build our application with a route -/// let app = Router::new().any( -/// "*", -/// leptos_viz::render_app_async(leptos_options, || view! { }), -/// ); +/// let app = Router::new().any("*", leptos_viz::render_app_async( +/// leptos_options, +/// || view! { }, +/// )); /// -/// // run our app +/// // run our app with hyper +/// let listener = +/// tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); /// serve(listener, app).await.unwrap(); /// } /// # } @@ -795,10 +880,11 @@ fn provide_contexts(path: String, req_parts: RequestParts, default_res_options: /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_async( options: LeptosOptions, app_fn: impl Fn() -> IV + Clone + Send + 'static, @@ -812,34 +898,120 @@ where render_app_async_with_context(options, || {}, app_fn) } -/// Returns a Viz [Handler] that listens for a `GET` request and tries +/// Returns an Viz [Handler](viz::Handler) that listens for a `GET` request and tries /// to route it using [leptos_router], asynchronously rendering an HTML page after all -/// `async` [Resource]s have loaded. +/// `async` [Resource](leptos::Resource)s have loaded. /// -/// This version allows us to pass Viz State/Extractor or other infro from Viz or network -/// layers above Leptos itself. To use it, you'll need to write your own handler function that provides -/// the data to leptos in a closure. An example is below -/// ```ignore -/// async fn custom_handler(req: Request) -> Result { -/// let id = req.params::()?; -/// let options = &*req.state::>().ok_or(StateError::new::>())?; -/// let handler = leptos_viz::render_app_async_with_context(options.clone(), -/// move || { -/// provide_context(id.clone()); -/// }, -/// || view! { } -/// ); -/// handler(req).await.into_response() -/// } -/// ``` -/// Otherwise, this function is identical to [render_app_to_stream]. +/// ## Provided Context Types +/// This function always provides context values including the following types: +/// - [Parts] +/// - [ResponseOptions] +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] +pub fn render_app_async_stream_with_context( + options: LeptosOptions, + additional_context: impl Fn() + 'static + Clone + Send, + app_fn: impl Fn() -> IV + Clone + Send + 'static, +) -> impl Fn(Request) -> Pin> + Send + 'static>> + + Clone + + Send + + 'static +where + IV: IntoView, +{ + move |req: Request| { + 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(); + + async move { + // 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 + let path = req.uri().path_and_query().unwrap().as_str(); + + let full_path = format!("http://leptos.dev{path}"); + + let (tx, rx) = futures::channel::oneshot::channel(); + spawn_task!(async move { + let app = { + let full_path = full_path.clone(); + let (_, req_parts) = generate_request_and_parts(req); + move || { + provide_contexts(full_path, req_parts, default_res_options); + app_fn().into_view() + } + }; + + let (stream, runtime) = + render_to_stream_in_order_with_prefix_undisposed_with_context( + app, + || "".into(), + add_context, + ); + + // Extract the value of ResponseOptions from here + let res_options = use_context::().unwrap(); + + let html = build_async_response(stream, &options, runtime).await; + + let new_res_parts = res_options.0.read().clone(); + + let mut writable = res_options2.0.write(); + *writable = new_res_parts; + + _ = tx.send(html); + }); + + let html = rx.await.expect("to complete HTML rendering"); + + let res_options = res_options3.0.read(); + + let complete_stream = futures::stream::iter([Ok(Bytes::from(html))]); + + let mut res = Response::new(Body::from_stream( + Box::pin(complete_stream) as PinnedHtmlStream + )); + if let Some(status) = res_options.status { + *res.status_mut() = status + } + let headers = res.headers_mut(); + let mut res_headers = res_options.headers.clone(); + + headers.extend(res_headers.drain()); + + // This one doesn't use generate_response(), so we need to do this seperately + if !headers.contains_key(header::CONTENT_TYPE) { + // Set the Content Type headers on all responses. This makes Firefox show the page source + // without complaining + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_str("text/html; charset=utf-8").unwrap(), + ); + } + + Ok(res) + } + }) + } +} + +/// Returns an Viz [Handler](viz::Handler) that listens for a `GET` request and tries +/// to route it using [leptos_router], asynchronously rendering an HTML page after all +/// `async` [Resource](leptos::Resource)s have loaded. /// /// ## Provided Context Types /// This function always provides context values including the following types: -/// - [RequestParts] +/// - [Parts] /// - [ResponseOptions] -/// - [MetaContext] -/// - [RouterIntegrationContext] +/// - [MetaContext](leptos_meta::MetaContext) +/// - [RouterIntegrationContext](leptos_router::RouterIntegrationContext) +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn render_app_async_with_context( options: LeptosOptions, additional_context: impl Fn() + 'static + Clone + Send, @@ -870,56 +1042,39 @@ where let (tx, rx) = futures::channel::oneshot::channel(); - spawn_blocking({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let app_fn = app_fn.clone(); - let add_context = add_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let app = { - let full_path = full_path.clone(); - let req_parts = generate_request_parts(req).await; - move || { - provide_contexts(full_path, req_parts, default_res_options); - app_fn().into_view() - } - }; - - let (stream, runtime) = - render_to_stream_with_prefix_undisposed_with_context( - app, - || "".into(), - add_context, - ); - - // Extract the value of ResponseOptions from here - let res_options = - use_context::().unwrap(); - - let html = build_async_response(stream, &options, runtime).await; - - let new_res_parts = res_options.0.read().clone(); - - let mut writable = res_options2.0.write(); - *writable = new_res_parts; - - _ = tx.send(html); - }) - .await; - } - }); - } + spawn_task!(async move { + let app = { + let full_path = full_path.clone(); + let (_, req_parts) = generate_request_and_parts(req); + move || { + provide_contexts(full_path, req_parts, default_res_options); + app_fn().into_view() + } + }; + + let (stream, runtime) = + render_to_stream_in_order_with_prefix_undisposed_with_context( + app, + || "".into(), + add_context, + ); + + // Extract the value of ResponseOptions from here + let res_options = use_context::().unwrap(); + + let html = build_async_response(stream, &options, runtime).await; + + let new_res_parts = res_options.0.read().clone(); + + let mut writable = res_options2.0.write(); + *writable = new_res_parts; + + _ = tx.send(html); }); let html = rx.await.expect("to complete HTML rendering"); - let mut res = Response::html(html); + let mut res = Response::new(Body::from(Full::from(html))); let res_options = res_options3.0.read(); @@ -938,6 +1093,7 @@ where /// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically /// create routes in Viz'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 Viz compatible paths. +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn generate_route_list(app_fn: impl Fn() -> IV + 'static + Clone) -> Vec where IV: IntoView + 'static, @@ -948,6 +1104,7 @@ where /// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically /// create routes in Viz'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 Viz compatible paths. +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn generate_route_list_with_ssg( app_fn: impl Fn() -> IV + 'static + Clone, ) -> (Vec, StaticDataMap) @@ -959,7 +1116,9 @@ where /// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically /// create routes in Viz'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 Viz compatible paths. +/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths. Adding excluded_routes +/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Viz path format +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn generate_route_list_with_exclusions( app_fn: impl Fn() -> IV + 'static + Clone, excluded_routes: Option>, @@ -970,9 +1129,29 @@ where generate_route_list_with_exclusions_and_ssg(app_fn, excluded_routes).0 } +/// TODO docs +pub async fn build_static_routes( + options: &LeptosOptions, + app_fn: impl Fn() -> IV + 'static + Send + Clone, + routes: &[RouteListing], + static_data_map: StaticDataMap, +) where + IV: IntoView + 'static, +{ + let options = options.clone(); + let routes = routes.to_owned(); + spawn_task!(async move { + leptos_router::build_static_routes(&options, app_fn, &routes, &static_data_map) + .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 /// create routes in Viz'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 Viz compatible paths. +/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths. Adding excluded_routes +/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Viz path format +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn generate_route_list_with_exclusions_and_ssg( app_fn: impl Fn() -> IV + 'static + Clone, excluded_routes: Option>, @@ -985,8 +1164,10 @@ where /// Generates a list of all routes defined in Leptos's Router in your app. We can then use this to automatically /// create routes in Viz'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 Viz compatible paths. +/// as an argument so it can walk you app tree. This version is tailored to generate Viz compatible paths. Adding excluded_routes +/// to this function will stop `.leptos_routes()` from generating a route for it, allowing a custom handler. These need to be in Viz path format /// Additional context will be provided to the app Element. +#[tracing::instrument(level = "trace", fields(error), skip_all)] pub fn generate_route_list_with_exclusions_and_ssg_and_context( app_fn: impl Fn() -> IV + 'static + Clone, excluded_routes: Option>, @@ -1026,6 +1207,7 @@ where None, )] } else { + // Routes to exclude from auto generation if let Some(excluded_routes) = excluded_routes { routes.retain(|p| !excluded_routes.iter().any(|e| e == p.path())) } @@ -1035,13 +1217,42 @@ where ) } +/// This trait allows one to pass a list of routes and a render function to Viz's router, letting us avoid +/// having to use wildcards or manually define all routes in multiple places. +pub trait LeptosRoutes { + fn leptos_routes( + self, + options: LeptosOptions, + paths: Vec, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, + ) -> Self + where + IV: IntoView + 'static; + + fn leptos_routes_with_context( + self, + options: LeptosOptions, + paths: Vec, + additional_context: impl Fn() + Clone + Send + Sync + 'static, + app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, + ) -> Self + where + IV: IntoView + 'static; + + fn leptos_routes_with_handler(self, paths: Vec, handler: H) -> Self + where + H: Handler> + Clone, + O: IntoResponse + Send + 'static; +} + +#[cfg(feature = "default")] fn handle_static_response( path: String, options: LeptosOptions, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - additional_context: impl Fn() + Clone + Send + Sync + 'static, + app_fn: impl Fn() -> IV + Clone + Send + 'static, + additional_context: impl Fn() + Clone + Send + 'static, res: StaticResponse, -) -> Pin> + 'static>> +) -> Pin> + 'static>> where IV: IntoView + 'static, { @@ -1052,7 +1263,7 @@ where status, content_type, } => { - let mut res = Response::html(body); + let mut res = Response::new(body); if let Some(v) = content_type { res.headers_mut().insert( HeaderName::from_static("content-type"), @@ -1064,7 +1275,7 @@ where StaticStatusCode::NotFound => StatusCode::NOT_FOUND, StaticStatusCode::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, }; - Ok(res) + res } StaticResponse::RenderDynamic => { let res = @@ -1106,6 +1317,7 @@ where }) } +#[cfg(feature = "default")] fn static_route( router: Router, path: &str, @@ -1122,51 +1334,31 @@ where StaticMode::Incremental => { let handler = move |req: Request| { Box::pin({ - let path = req.path().to_string(); + let path = req.uri().path().to_string(); let options = options.clone(); let app_fn = app_fn.clone(); let additional_context = additional_context.clone(); async move { let (tx, rx) = futures::channel::oneshot::channel(); - spawn_blocking(move || { - let path = path.clone(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let path = path.clone(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let res = incremental_static_route( - tokio::fs::read_to_string(static_file_path( - &options, &path, - )) - .await, - ); - let res = handle_static_response( - path.clone(), - options, - app_fn, - additional_context, - res, - ) - .await; - - let _ = tx.send(res); - }) - .await; - } - }) + spawn_task!(async move { + let res = incremental_static_route( + tokio::fs::read_to_string(static_file_path(&options, &path)).await, + ); + let res = handle_static_response( + path.clone(), + options, + app_fn, + additional_context, + res, + ) + .await; + + let _ = tx.send(res); }); - - rx.await.expect("to complete HTML rendering") + rx.await + .map(|res| res.map(|body| Body::from(Full::from(body)))) + .map_err(Error::boxed) } }) }; @@ -1181,51 +1373,31 @@ where StaticMode::Upfront => { let handler = move |req: Request| { Box::pin({ - let path = req.path().to_string(); + let path = req.uri().path().to_string(); let options = options.clone(); let app_fn = app_fn.clone(); let additional_context = additional_context.clone(); async move { let (tx, rx) = futures::channel::oneshot::channel(); - spawn_blocking(move || { - let path = path.clone(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on({ - let path = path.clone(); - let options = options.clone(); - let app_fn = app_fn.clone(); - let additional_context = additional_context.clone(); - async move { - tokio::task::LocalSet::new() - .run_until(async { - let res = upfront_static_route( - tokio::fs::read_to_string(static_file_path( - &options, &path, - )) - .await, - ); - let res = handle_static_response( - path.clone(), - options, - app_fn, - additional_context, - res, - ) - .await; - - let _ = tx.send(res); - }) - .await; - } - }) + spawn_task!(async move { + let res = upfront_static_route( + tokio::fs::read_to_string(static_file_path(&options, &path)).await, + ); + let res = handle_static_response( + path.clone(), + options, + app_fn, + additional_context, + res, + ) + .await; + + let _ = tx.send(res); }); - - rx.await.expect("to complete HTML rendering") + rx.await + .map(|res| res.map(|body| Body::from(Full::from(body)))) + .map_err(Error::boxed) } }) }; @@ -1240,37 +1412,10 @@ where } } -/// This trait allows one to pass a list of routes and a render function to Viz's router, letting us avoid -/// having to use wildcards or manually define all routes in multiple places. -pub trait LeptosRoutes { - fn leptos_routes( - self, - options: LeptosOptions, - paths: Vec, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - ) -> Self - where - IV: IntoView + 'static; - - fn leptos_routes_with_context( - self, - options: LeptosOptions, - paths: Vec, - additional_context: impl Fn() + Clone + Send + Sync + 'static, - app_fn: impl Fn() -> IV + Clone + Send + Sync + 'static, - ) -> Self - where - IV: IntoView + 'static; - - fn leptos_routes_with_handler(self, paths: Vec, handler: H) -> Self - where - H: Handler> + Clone, - O: IntoResponse + Send + Sync + 'static; -} - /// The default implementation of `LeptosRoutes` which takes in a list of paths, and dispatches GET requests /// to those paths to Leptos's renderer. impl LeptosRoutes for Router { + #[tracing::instrument(level = "trace", fields(error), skip_all)] fn leptos_routes( self, options: LeptosOptions, @@ -1283,6 +1428,7 @@ impl LeptosRoutes for Router { self.leptos_routes_with_context(options, paths, || {}, app_fn) } + #[tracing::instrument(level = "trace", fields(error), skip_all)] fn leptos_routes_with_context( self, options: LeptosOptions, @@ -1293,23 +1439,36 @@ impl LeptosRoutes for Router { where IV: IntoView + 'static, { - paths.iter().fold(self, |router, listing| { + let mut router = self; + + // register router paths + for listing in paths.iter() { let path = listing.path(); - let mode = listing.mode(); - - listing.methods().fold(router, |router, method| { - if let Some(static_mode) = listing.static_mode() { - static_route( - router, - path, - options.clone(), - app_fn.clone(), - additional_context.clone(), - method, - static_mode, - ) + + for method in listing.methods() { + router = if let Some(static_mode) = listing.static_mode() { + #[cfg(feature = "default")] + { + static_route( + router, + path, + options.clone(), + app_fn.clone(), + additional_context.clone(), + method, + static_mode, + ) + } + #[cfg(not(feature = "default"))] + { + _ = static_mode; + panic!( + "Static site generation is not currently \ + supported on WASM32 server targets." + ) + } } else { - match mode { + match listing.mode() { SsrMode::OutOfOrder => { let s = render_app_to_stream_with_context( options.clone(), @@ -1368,26 +1527,66 @@ impl LeptosRoutes for Router { } } } + }; + } + } + + // register server functions + for (path, method) in server_fn_paths() { + let additional_context = additional_context.clone(); + let handler = move |mut req: Request| { + let _ = req.set_state(additional_context.clone()); + async move { handle_server_fns_with_context::(req).await } + }; + router = match method { + Method::GET => router.get(path, handler), + Method::POST => router.post(path, handler), + Method::PUT => router.put(path, handler), + Method::DELETE => router.delete(path, handler), + Method::PATCH => router.patch(path, handler), + _ => { + panic!( + "Unsupported server function HTTP method: \ + {method:?}" + ); } - }) - }) + }; + } + + router } + #[tracing::instrument(level = "trace", fields(error), skip_all)] fn leptos_routes_with_handler(self, paths: Vec, handler: H) -> Self where H: Handler> + Clone, - O: IntoResponse + Send + Sync + 'static, + O: IntoResponse + Send + 'static, { - paths.iter().fold(self, |router, listing| { - listing - .methods() - .fold(router, |router, method| match method { - leptos_router::Method::Get => router.get(listing.path(), handler.clone()), - leptos_router::Method::Post => router.post(listing.path(), handler.clone()), - leptos_router::Method::Put => router.put(listing.path(), handler.clone()), - leptos_router::Method::Delete => router.delete(listing.path(), handler.clone()), - leptos_router::Method::Patch => router.patch(listing.path(), handler.clone()), - }) - }) + let mut router = self; + for listing in paths.iter() { + for method in listing.methods() { + let path = listing.path(); + router = match method { + leptos_router::Method::Get => router.get(path, handler.clone()), + leptos_router::Method::Post => router.post(path, handler.clone()), + leptos_router::Method::Put => router.put(path, handler.clone()), + leptos_router::Method::Delete => router.delete(path, handler.clone()), + leptos_router::Method::Patch => router.patch(path, handler.clone()), + }; + } + } + router } } + +#[tracing::instrument(level = "trace", fields(error), skip_all)] +fn get_leptos_pool() -> LocalPoolHandle { + static LOCAL_POOL: OnceCell = OnceCell::new(); + LOCAL_POOL + .get_or_init(|| { + tokio_util::task::LocalPoolHandle::new( + available_parallelism().map(Into::into).unwrap_or(1), + ) + }) + .clone() +}