From 5b403b2d5181256d0aaf0f23f880fc8d5aade0c8 Mon Sep 17 00:00:00 2001 From: arctic_hen7 Date: Sun, 15 Aug 2021 11:11:54 +1000 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20made=20rendering=20function?= =?UTF-8?q?s=20asynchronous?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/showcase/app/Cargo.toml | 1 + examples/showcase/app/src/bin/build.rs | 6 +- examples/showcase/app/src/pages/index.rs | 2 +- examples/showcase/app/src/pages/ip.rs | 2 +- examples/showcase/app/src/pages/post.rs | 4 +- examples/showcase/app/src/pages/time.rs | 4 +- examples/showcase/app/src/pages/time_root.rs | 4 +- examples/showcase/server/src/main.rs | 2 +- src/build.rs | 76 +++++++++++--------- src/serve.rs | 28 ++++---- src/template.rs | 76 ++++++++++++++++---- 11 files changed, 134 insertions(+), 71 deletions(-) diff --git a/examples/showcase/app/Cargo.toml b/examples/showcase/app/Cargo.toml index 4458ded5e6..a9c5387888 100644 --- a/examples/showcase/app/Cargo.toml +++ b/examples/showcase/app/Cargo.toml @@ -15,6 +15,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" # Possibly don't need? console_error_panic_hook = "0.1.6" urlencoding = "2.1" +futures = "0.3" # This section is needed for WASM Pack (which we use instead of Trunk for flexibility) [lib] diff --git a/examples/showcase/app/src/bin/build.rs b/examples/showcase/app/src/bin/build.rs index 779d733570..6b0bfa5db4 100644 --- a/examples/showcase/app/src/bin/build.rs +++ b/examples/showcase/app/src/bin/build.rs @@ -4,11 +4,12 @@ use perseus::{ }; use perseus_showcase_app::pages; use sycamore::prelude::SsrNode; +use futures::executor::block_on; fn main() { let config_manager = FsConfigManager::new(); - build_templates(vec![ + let fut = build_templates(vec![ pages::index::get_page::(), pages::about::get_page::(), pages::post::get_page::(), @@ -16,7 +17,8 @@ fn main() { pages::ip::get_page::(), pages::time::get_page::(), pages::time_root::get_page::() - ], &config_manager).expect("Static generation failed!"); + ], &config_manager); + block_on(fut).expect("Static generation failed!"); println!("Static generation successfully completed!"); } diff --git a/examples/showcase/app/src/pages/index.rs b/examples/showcase/app/src/pages/index.rs index c6d440ff0b..6279306ec1 100644 --- a/examples/showcase/app/src/pages/index.rs +++ b/examples/showcase/app/src/pages/index.rs @@ -21,7 +21,7 @@ pub fn get_page() -> Template { .template(template_fn()) } -pub fn get_static_props(_path: String) -> Result { +pub async fn get_static_props(_path: String) -> Result { Ok(serde_json::to_string( &IndexPageProps { greeting: "Hello World!".to_string() diff --git a/examples/showcase/app/src/pages/ip.rs b/examples/showcase/app/src/pages/ip.rs index f6c270e260..d44785a705 100644 --- a/examples/showcase/app/src/pages/ip.rs +++ b/examples/showcase/app/src/pages/ip.rs @@ -26,7 +26,7 @@ pub fn get_page() -> Template { .template(template_fn()) } -pub fn get_request_state(_path: String) -> Result { +pub async fn get_request_state(_path: String) -> Result { Ok(serde_json::to_string( &IpPageProps { ip: "x.x.x.x".to_string() diff --git a/examples/showcase/app/src/pages/post.rs b/examples/showcase/app/src/pages/post.rs index cc9e9d0efc..a301674c8e 100644 --- a/examples/showcase/app/src/pages/post.rs +++ b/examples/showcase/app/src/pages/post.rs @@ -30,7 +30,7 @@ pub fn get_page() -> Template { .template(template_fn()) } -pub fn get_static_props(path: String) -> Result { +pub async fn get_static_props(path: String) -> Result { let path_vec: Vec<&str> = path.split('/').collect(); let title_slug = path_vec[path_vec.len() - 1]; // This is just an example @@ -45,7 +45,7 @@ pub fn get_static_props(path: String) -> Result { ).unwrap()) } // TODO -pub fn get_static_paths() -> Result, String> { +pub async fn get_static_paths() -> Result, String> { Ok(vec![ "test".to_string() ]) diff --git a/examples/showcase/app/src/pages/time.rs b/examples/showcase/app/src/pages/time.rs index e20ba7b78a..4962702d2b 100644 --- a/examples/showcase/app/src/pages/time.rs +++ b/examples/showcase/app/src/pages/time.rs @@ -24,7 +24,7 @@ pub fn get_page() -> Template { .build_paths_fn(Box::new(get_build_paths)) } -pub fn get_build_state(_path: String) -> Result { +pub async fn get_build_state(_path: String) -> Result { Ok(serde_json::to_string( &TimePageProps { time: format!("{:?}", std::time::SystemTime::now()) @@ -32,7 +32,7 @@ pub fn get_build_state(_path: String) -> Result { ).unwrap()) } -pub fn get_build_paths() -> Result, String> { +pub async fn get_build_paths() -> Result, String> { Ok(vec![ "test".to_string() ]) diff --git a/examples/showcase/app/src/pages/time_root.rs b/examples/showcase/app/src/pages/time_root.rs index d2ddcc84fb..186965f18a 100644 --- a/examples/showcase/app/src/pages/time_root.rs +++ b/examples/showcase/app/src/pages/time_root.rs @@ -20,13 +20,13 @@ pub fn get_page() -> Template { // This page will revalidate every five seconds (to illustrate revalidation) // Try changing this to a week, even though the below custom logic says to always revalidate, we'll only do it weekly .revalidate_after("5s".to_string()) - .should_revalidate_fn(Box::new(|| { + .should_revalidate_fn(Box::new(|| async { Ok(true) })) .build_state_fn(Box::new(get_build_state)) } -pub fn get_build_state(_path: String) -> Result { +pub async fn get_build_state(_path: String) -> Result { Ok(serde_json::to_string( &TimePageProps { time: format!("{:?}", std::time::SystemTime::now()) diff --git a/examples/showcase/server/src/main.rs b/examples/showcase/server/src/main.rs index b00e8e4ae5..84934cc4a0 100644 --- a/examples/showcase/server/src/main.rs +++ b/examples/showcase/server/src/main.rs @@ -56,7 +56,7 @@ async fn page_data( ) -> ActixResult { let path = req.match_info().query("filename"); // TODO match different types of errors here - let page_data = get_page(path, &render_cfg, &templates, config_manager.get_ref()).map_err(error::ErrorNotFound)?; + let page_data = get_page(path, &render_cfg, &templates, config_manager.get_ref()).await.map_err(error::ErrorNotFound)?; Ok( serde_json::to_string(&page_data).unwrap() diff --git a/src/build.rs b/src/build.rs index 71a3b0315e..4847cc9961 100644 --- a/src/build.rs +++ b/src/build.rs @@ -8,12 +8,13 @@ use crate::{ use crate::errors::*; use std::collections::HashMap; use sycamore::prelude::SsrNode; +use futures::future::try_join_all; /// Builds a template, writing static data as appropriate. This should be used as part of a larger build process. This returns both a list /// of the extracted render options for this template (needed at request time), a list of pages that it explicitly generated, and a boolean /// as to whether or not it only generated a single page to occupy the template's root path (`true` unless using using build-time path /// generation). -pub fn build_template( +pub async fn build_template( template: Template, config_manager: &impl ConfigManager ) -> Result< @@ -28,7 +29,7 @@ pub fn build_template( // Handle static path generation // Because we iterate over the paths, we need a base path if we're not generating custom ones (that'll be overriden if needed) let paths = match template.uses_build_paths() { - true => template.get_build_paths()?, + true => template.get_build_paths().await?, false => { single_page = true; vec![String::new()] @@ -49,7 +50,7 @@ pub fn build_template( // We'll only write a static state if one is explicitly generated if template.uses_build_state() { // We pass in the latter part of the path, without the base specifier (because that would be the same for everything in the template) - let initial_state = template.get_build_state(path.to_string())?; + let initial_state = template.get_build_state(path.to_string()).await?; // Write that intial state to a static JSON file config_manager .write(&format!("./dist/static/{}.json", full_path), &initial_state)?; @@ -92,42 +93,53 @@ pub fn build_template( Ok((paths, single_page)) } -// TODO function to build pages -/// Runs the build process of building many different templates. -pub fn build_templates(templates: Vec>, config_manager: &impl ConfigManager) -> Result<()> { - // The render configuration stores a list of pages to the root paths of their templates +async fn build_template_and_get_cfg(template: Template, config_manager: &impl ConfigManager) -> Result> { let mut render_cfg = HashMap::new(); - // Create each of the templates - for template in templates { - let template_root_path = template.get_path(); - let is_incremental = template.uses_incremental(); - - let (pages, single_page) = build_template(template, config_manager)?; - // If the tempalte represents a single page itself, we don't need any concatenation - if single_page { + let template_root_path = template.get_path(); + let is_incremental = template.uses_incremental(); + + let (pages, single_page) = build_template(template, config_manager).await?; + // If the template represents a single page itself, we don't need any concatenation + if single_page { + render_cfg.insert( + template_root_path.clone(), + template_root_path.clone() + ); + } else { + // Add each page that the template explicitly generated (ignoring ISR for now) + for page in pages { + render_cfg.insert( + format!("{}/{}", &template_root_path, &page), + template_root_path.clone() + ); + } + // Now if the page uses ISR, add an explicit `/*` in there after the template root path + // Incremental rendering requires build-time path generation + if is_incremental { render_cfg.insert( - template_root_path.clone(), + format!("{}/*", &template_root_path), template_root_path.clone() ); - } else { - // Add each page that the template explicitly generated (ignoring ISR for now) - for page in pages { - render_cfg.insert( - format!("{}/{}", &template_root_path, &page), - template_root_path.clone() - ); - } - // Now if the page uses ISR, add an explicit `/*` in there after the template root path - // Incremental rendering requires build-time path generation - if is_incremental { - render_cfg.insert( - format!("{}/*", &template_root_path), - template_root_path.clone() - ); - } } } + Ok(render_cfg) +} + +/// Runs the build process of building many different templates. +pub async fn build_templates(templates: Vec>, config_manager: &impl ConfigManager) -> Result<()> { + // The render configuration stores a list of pages to the root paths of their templates + let mut render_cfg: HashMap = HashMap::new(); + // Create each of the templates + let mut futs = Vec::new(); + for template in templates { + futs.push(build_template_and_get_cfg(template, config_manager)); + } + let template_cfgs = try_join_all(futs).await?; + for template_cfg in template_cfgs { + render_cfg.extend(template_cfg.into_iter()) + } + config_manager .write("./dist/render_conf.json", &serde_json::to_string(&render_cfg)?)?; diff --git a/src/serve.rs b/src/serve.rs index ca7278c265..7b62292e17 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -41,9 +41,9 @@ fn render_build_state(path_encoded: &str, config_manager: &impl ConfigManager) - Ok((html, state)) } /// Renders a template that generated its state at request-time. Note that revalidation and ISR have no impact on SSR-rendered pages. -fn render_request_state(template: &Template, path: &str) -> Result<(String, Option)> { +async fn render_request_state(template: &Template, path: &str) -> Result<(String, Option)> { // Generate the initial state (this may generate an error, but there's no file that can't exist) - let state = Some(template.get_request_state(path.to_string())?); + let state = Some(template.get_request_state(path.to_string()).await?); // Use that to render the static HTML let html = sycamore::render_to_string( || @@ -63,7 +63,7 @@ fn get_incremental_cached(path_encoded: &str, config_manager: &impl ConfigManage } } /// Checks if a template should revalidate by time. -fn should_revalidate(template: &Template, path_encoded: &str, config_manager: &impl ConfigManager) -> Result { +async fn should_revalidate(template: &Template, path_encoded: &str, config_manager: &impl ConfigManager) -> Result { let mut should_revalidate = false; // If it revalidates after a certain period of time, we needd to check that BEFORE the custom logic if template.revalidates_with_time() { @@ -83,12 +83,12 @@ fn should_revalidate(template: &Template, path_encoded: &str, config_ma // Now run the user's custom revalidation logic if template.revalidates_with_logic() { - should_revalidate = template.should_revalidate()?; + should_revalidate = template.should_revalidate().await?; } Ok(should_revalidate) } /// Revalidates a template -fn revalidate( +async fn revalidate( template: &Template, path: &str, path_encoded: &str, config_manager: &impl ConfigManager @@ -97,7 +97,7 @@ fn revalidate( let state = Some( template.get_build_state( format!("{}/{}", template.get_path(), path) - )? + ).await? ); let html = sycamore::render_to_string( || @@ -123,7 +123,8 @@ fn revalidate( /// Gets the HTML/JSON data for the given page path. This will call SSG/SSR/etc., whatever is needed for that page. Note that HTML generated /// at request-time will **always** replace anything generated at build-time, incrementally, revalidated, etc. // TODO let this function take a request struct of some form -pub fn get_page( +// TODO possible further optimizations on this for futures? +pub async fn get_page( path: &str, render_cfg: &HashMap, templates: &TemplateMap, @@ -183,8 +184,8 @@ pub fn get_page( // It's cached Some(html_val) => { // Check if we need to revalidate - if should_revalidate(template, &path_encoded, config_manager)? { - let (html_val, state) = revalidate(template, path, &path_encoded, config_manager)?; + if should_revalidate(template, &path_encoded, config_manager).await? { + let (html_val, state) = revalidate(template, path, &path_encoded, config_manager).await?; // Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has if html.is_empty() { html = html_val @@ -209,7 +210,7 @@ pub fn get_page( let state = Some( template.get_build_state( format!("{}/{}", template.get_path(), path) - )? + ).await? ); let html_val = sycamore::render_to_string( || @@ -241,8 +242,8 @@ pub fn get_page( } } else { // Handle if we need to revalidate - if should_revalidate(template, &path_encoded, config_manager)? { - let (html_val, state) = revalidate(template, path, &path_encoded, config_manager)?; + if should_revalidate(template, &path_encoded, config_manager).await? { + let (html_val, state) = revalidate(template, path, &path_encoded, config_manager).await?; // Build-time generated HTML is the lowest priority, so we'll only set it if nothing else already has if html.is_empty() { html = html_val @@ -260,12 +261,11 @@ pub fn get_page( } // Handle request state if template.uses_request_state() { - let (html_val, state) = render_request_state(template, path)?; + let (html_val, state) = render_request_state(template, path).await?; // Request-time HTML always overrides anything generated at build-time or incrementally (this has more information) html = html_val; states.request_state = state; } - // TODO support revalidation // Amalgamate the states // If the user has defined custom logic for this, we'll defer to that diff --git a/src/template.rs b/src/template.rs index 7efac6fb02..6cea7ae78f 100644 --- a/src/template.rs +++ b/src/template.rs @@ -5,10 +5,11 @@ use crate::errors::*; use sycamore::prelude::{Template as SycamoreTemplate, GenericNode}; use std::collections::HashMap; +use futures::Future; +use std::pin::Pin; /// Represents all the different states that can be generated for a single template, allowing amalgamation logic to be run with the knowledge /// of what did what (rather than blindly working on a vector). -// TODO update this to reflect reality #[derive(Default)] pub struct States { pub build_state: Option, @@ -42,12 +43,60 @@ impl States { /// A generic error type that mandates a string error. This sidesteps horrible generics while maintaining DX. pub type StringResult = std::result::Result; +/// A generic return type for asynchronous functions that we need to store in a struct. +type AsyncFnReturn = Pin>>; + +/// Creates traits that prevent users from having to pin their functions' return types. We can't make a generic one until desugared function +/// types are stabilized (https://github.com/rust-lang/rust/issues/29625https://github.com/rust-lang/rust/issues/29625). +macro_rules! make_async_trait { + ($name:ident, $return_ty:ty$(, $arg_name:ident: $arg:ty)*) => { + pub trait $name{ + fn call( + &self, + // Each given argument is repeated + $( + $arg_name: $arg, + )* + ) -> AsyncFnReturn<$return_ty>; + } + impl $name for T + where + T: Fn( + $( + $arg, + )* + ) -> F, + F: Future + 'static, + { + fn call( + &self, + $( + $arg_name: $arg, + )* + ) -> AsyncFnReturn<$return_ty> { + Box::pin(self( + $( + $arg_name, + )* + )) + } + } + }; +} + +// A series of asynchronous closure traits that prevent the user from having to pin their functions +make_async_trait!(GetBuildPathsFnType, StringResult>); +make_async_trait!(GetBuildStateFnType, StringResult, path: String); +// TODO add request data to be passed in here +make_async_trait!(GetRequestStateFnType, StringResult, path: String); +make_async_trait!(ShouldRevalidateFnType, StringResult); + // A series of closure types that should not be typed out more than once pub type TemplateFn = Box) -> SycamoreTemplate>; -pub type GetBuildPathsFn = Box StringResult>>; -pub type GetBuildStateFn = Box StringResult>; -pub type GetRequestStateFn = Box StringResult>; -pub type ShouldRevalidateFn = Box StringResult>; +pub type GetBuildPathsFn = Box; +pub type GetBuildStateFn = Box; +pub type GetRequestStateFn = Box; +pub type ShouldRevalidateFn = Box; pub type AmalgamateStatesFn = Box StringResult>>; /// This allows the specification of all the template templates in an app and how to render them. If no rendering logic is provided at all, @@ -76,7 +125,6 @@ pub struct Template get_build_state: Option, /// A function that will run on every request to generate a state for that request. This allows server-side-rendering. This is equivalent /// to `get_server_side_props` in NextJS. This can be used with `get_build_state`, though custom amalgamation logic must be provided. - // TODO add request data to be passed in here get_request_state: Option, /// A function to be run on every request to check if a template prerendered at build-time should be prerendered again. This is equivalent /// to revalidation after a time in NextJS, with the improvement of custom logic. If used with `revalidate_after`, this function will @@ -115,9 +163,9 @@ impl Template { (self.template)(props) } /// Gets the list of templates that should be prerendered for at build-time. - pub fn get_build_paths(&self) -> Result> { + pub async fn get_build_paths(&self) -> Result> { if let Some(get_build_paths) = &self.get_build_paths { - let res = get_build_paths(); + let res = get_build_paths.call().await; match res { Ok(res) => Ok(res), Err(err) => bail!(ErrorKind::RenderFnFailed("get_build_paths".to_string(), self.get_path(), err.to_string())) @@ -128,9 +176,9 @@ impl Template { } /// Gets the initial state for a template. This needs to be passed the full path of the template, which may be one of those generated by /// `.get_build_paths()`. - pub fn get_build_state(&self, path: String) -> Result { + pub async fn get_build_state(&self, path: String) -> Result { if let Some(get_build_state) = &self.get_build_state { - let res = get_build_state(path); + let res = get_build_state.call(path).await; match res { Ok(res) => Ok(res), Err(err) => bail!(ErrorKind::RenderFnFailed("get_build_state".to_string(), self.get_path(), err.to_string())) @@ -141,9 +189,9 @@ impl Template { } /// Gets the request-time state for a template. This is equivalent to SSR, and will not be performed at build-time. Unlike /// `.get_build_paths()` though, this will be passed information about the request that triggered the render. - pub fn get_request_state(&self, path: String) -> Result { + pub async fn get_request_state(&self, path: String) -> Result { if let Some(get_request_state) = &self.get_request_state { - let res = get_request_state(path); + let res = get_request_state.call(path).await; match res { Ok(res) => Ok(res), Err(err) => bail!(ErrorKind::RenderFnFailed("get_request_state".to_string(), self.get_path(), err.to_string())) @@ -166,9 +214,9 @@ impl Template { } /// Checks, by the user's custom logic, if this template should revalidate. This function isn't presently parsed anything, but has /// network access etc., and can really do whatever it likes. - pub fn should_revalidate(&self) -> Result { + pub async fn should_revalidate(&self) -> Result { if let Some(should_revalidate) = &self.should_revalidate { - let res = should_revalidate(); + let res = should_revalidate.call().await; match res { Ok(res) => Ok(res), Err(err) => bail!(ErrorKind::RenderFnFailed("should_revalidate".to_string(), self.get_path(), err.to_string()))