From 1f0e03ca1900804df9f3416b0d21c7d8644f3abb Mon Sep 17 00:00:00 2001 From: Koji AGAWA Date: Sat, 17 Feb 2024 05:12:33 +0900 Subject: [PATCH 1/2] feat(fullstack): support wasm target --- Cargo.lock | 1 + packages/cli/src/server/web/mod.rs | 4 +- packages/fullstack/Cargo.toml | 9 +- packages/fullstack/src/adapters/mod.rs | 71 +++++++--- packages/fullstack/src/launch.rs | 2 +- packages/fullstack/src/render.rs | 180 +++++++++++++------------ packages/fullstack/src/serve_config.rs | 18 ++- packages/ssr/Cargo.toml | 8 +- packages/ssr/src/incremental.rs | 25 ++-- 9 files changed, 190 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 478626fa8..61c930a61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2917,6 +2917,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "web-time", ] [[package]] diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs index 582b81129..6fe7cca4d 100644 --- a/packages/cli/src/server/web/mod.rs +++ b/packages/cli/src/server/web/mod.rs @@ -369,8 +369,8 @@ async fn start_server( #[cfg(feature = "plugin")] PluginManager::on_serve_start(_config)?; - // Parse address - let addr = format!("0.0.0.0:{}", port).parse().unwrap(); + // Bind the server to `[::]` and it will LISTEN for both IPv4 and IPv6. (required IPv6 dual stack) + let addr = format!("[::]:{}", port).parse().unwrap(); // Open the browser if start_browser { diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 63fde221c..bdd102fa3 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -45,14 +45,13 @@ dioxus-mobile = { workspace = true, optional = true } tracing = { workspace = true } tracing-futures = { workspace = true, optional = true } once_cell = "1.17.1" -tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true } tokio-util = { version = "0.7.8", features = ["rt"], optional = true } anymap = { version = "0.12.1", optional = true } serde = "1.0.159" serde_json = { version = "1.0.95", optional = true } tokio-stream = { version = "0.1.12", features = ["sync"], optional = true } -futures-util = { workspace = true, default-features = false, optional = true } +futures-util = { workspace = true, default-features = false } ciborium = "0.2.1" base64 = "0.21.0" @@ -66,12 +65,16 @@ web-sys = { version = "0.3.61", optional = true, features = ["Window", "Document dioxus-cli-config = { workspace = true, optional = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { workspace = true, features = ["rt", "sync"], optional = true } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dioxus-hot-reload = { workspace = true } +tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true } [features] default = ["hot-reload"] -hot-reload = ["serde_json", "futures-util"] +hot-reload = ["serde_json"] web = ["dioxus-web", "web-sys"] desktop = ["dioxus-desktop"] mobile = ["dioxus-mobile"] diff --git a/packages/fullstack/src/adapters/mod.rs b/packages/fullstack/src/adapters/mod.rs index 61f24bf3c..478d9047d 100644 --- a/packages/fullstack/src/adapters/mod.rs +++ b/packages/fullstack/src/adapters/mod.rs @@ -19,7 +19,9 @@ pub mod warp_adapter; use http::StatusCode; use server_fn::{Encoding, Payload}; +use std::future::Future; use std::sync::{Arc, RwLock}; +use tokio::task::JoinError; use crate::{ layer::{BoxedService, Service}, @@ -80,7 +82,7 @@ impl Service for ServerFnHandler { server_context, function, } = self.clone(); - Box::pin(async move { + let f = async move { let query = req.uri().query().unwrap_or_default().as_bytes().to_vec(); let (parts, body) = req.into_parts(); let body = hyper::body::to_bytes(body).await?.to_vec(); @@ -89,26 +91,22 @@ impl Service for ServerFnHandler { let parts = Arc::new(RwLock::new(parts)); // Because the future returned by `server_fn_handler` is `Send`, and the future returned by this function must be send, we need to spawn a new runtime - let pool = get_local_pool(); - let result = pool - .spawn_pinned({ - let function = function.clone(); - let mut server_context = server_context.clone(); - server_context.parts = parts; - move || async move { - let data = match function.encoding() { - Encoding::Url | Encoding::Cbor => &body, - Encoding::GetJSON | Encoding::GetCBOR => &query, - }; - let server_function_future = function.call((), data); - let server_function_future = ProvideServerContext::new( - server_function_future, - server_context.clone(), - ); - server_function_future.await - } - }) - .await?; + let result = spawn_platform({ + let function = function.clone(); + let mut server_context = server_context.clone(); + server_context.parts = parts; + move || async move { + let data = match function.encoding() { + Encoding::Url | Encoding::Cbor => &body, + Encoding::GetJSON | Encoding::GetCBOR => &query, + }; + let server_function_future = function.call((), data); + let server_function_future = + ProvideServerContext::new(server_function_future, server_context.clone()); + server_function_future.await + } + }) + .await?; let mut res = http::Response::builder(); // Set the headers from the server context @@ -147,7 +145,36 @@ impl Service for ServerFnHandler { res.body(data.into())? } }) - }) + }; + #[cfg(not(target_arch = "wasm32"))] + { + Box::pin(f) + } + #[cfg(target_arch = "wasm32")] + { + use futures_util::future::FutureExt; + + let result = tokio::task::spawn_local(f); + let result = result.then(|f| async move { f.unwrap() }); + Box::pin(result) + } + } +} + +async fn spawn_platform(create_task: F) -> Result<::Output, JoinError> +where + F: FnOnce() -> Fut, + F: Send + 'static, + Fut: Future + 'static, + Fut::Output: Send + 'static, +{ + #[cfg(not(target_arch = "wasm32"))] + { + get_local_pool().spawn_pinned(create_task).await + } + #[cfg(target_arch = "wasm32")] + { + Ok(create_task().await) } } diff --git a/packages/fullstack/src/launch.rs b/packages/fullstack/src/launch.rs index 70deaa981..7181c5092 100644 --- a/packages/fullstack/src/launch.rs +++ b/packages/fullstack/src/launch.rs @@ -20,7 +20,7 @@ pub fn launch( vdom }; - #[cfg(feature = "server")] + #[cfg(all(feature = "server", not(target_arch = "wasm32")))] tokio::runtime::Runtime::new() .unwrap() .block_on(async move { diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index 84ca1a6e5..3813c21ce 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -7,13 +7,33 @@ use dioxus_ssr::{ Renderer, }; use serde::Serialize; +use std::future::Future; use std::sync::Arc; use std::sync::RwLock; -use tokio::task::spawn_blocking; +use tokio::task::{spawn_blocking, JoinHandle}; use crate::prelude::*; use dioxus_lib::prelude::*; +fn spawn_platform(f: impl FnOnce() -> Fut + Send + 'static) -> JoinHandle +where + Fut: Future + 'static, + Fut::Output: Send + 'static, +{ + #[cfg(not(target_arch = "wasm32"))] + { + spawn_blocking(move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(f()) + }) + } + #[cfg(target_arch = "wasm32")] + { + tokio::task::spawn_local(f()) + } +} + enum SsrRendererPool { Renderer(RwLock>), Incremental(RwLock>), @@ -39,51 +59,41 @@ impl SsrRendererPool { let (tx, rx) = tokio::sync::oneshot::channel(); - spawn_blocking(move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on(async move { - let mut vdom = virtual_dom_factory(); - let mut to = WriteBuffer { buffer: Vec::new() }; - // before polling the future, we need to set the context - let prev_context = - SERVER_CONTEXT.with(|ctx| ctx.replace(server_context)); - // poll the future, which may call server_context() - tracing::info!("Rebuilding vdom"); - let _ = vdom.rebuild(&mut NoOpMutations); - vdom.wait_for_suspense().await; - tracing::info!("Suspense resolved"); - // after polling the future, we need to restore the context - SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); + spawn_platform(move || async move { + let mut vdom = virtual_dom_factory(); + let mut to = WriteBuffer { buffer: Vec::new() }; + // before polling the future, we need to set the context + let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(server_context)); + // poll the future, which may call server_context() + tracing::info!("Rebuilding vdom"); + let _ = vdom.rebuild(&mut NoOpMutations); + vdom.wait_for_suspense().await; + tracing::info!("Suspense resolved"); + // after polling the future, we need to restore the context + SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); - if let Err(err) = wrapper.render_before_body(&mut *to) { - let _ = tx.send(Err(err)); - return; - } - if let Err(err) = renderer.render_to(&mut to, &vdom) { - let _ = tx.send(Err( - dioxus_ssr::incremental::IncrementalRendererError::RenderError( - err, - ), - )); - return; - } - if let Err(err) = wrapper.render_after_body(&mut *to) { - let _ = tx.send(Err(err)); - return; - } - match String::from_utf8(to.buffer) { - Ok(html) => { - let _ = - tx.send(Ok((renderer, RenderFreshness::now(None), html))); - } - Err(err) => { - dioxus_ssr::incremental::IncrementalRendererError::Other( - Box::new(err), - ); - } - } - }); + if let Err(err) = wrapper.render_before_body(&mut *to) { + let _ = tx.send(Err(err)); + return; + } + if let Err(err) = renderer.render_to(&mut to, &vdom) { + let _ = tx.send(Err( + dioxus_ssr::incremental::IncrementalRendererError::RenderError(err), + )); + return; + } + if let Err(err) = wrapper.render_after_body(&mut *to) { + let _ = tx.send(Err(err)); + return; + } + match String::from_utf8(to.buffer) { + Ok(html) => { + let _ = tx.send(Ok((renderer, RenderFreshness::now(None), html))); + } + Err(err) => { + dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)); + } + } }); let (renderer, freshness, html) = rx.await.unwrap()?; pool.write().unwrap().push(renderer); @@ -98,53 +108,49 @@ impl SsrRendererPool { let (tx, rx) = tokio::sync::oneshot::channel(); let server_context = server_context.clone(); - spawn_blocking(move || { - tokio::runtime::Runtime::new() - .expect("couldn't spawn runtime") - .block_on(async move { - let mut to = WriteBuffer { buffer: Vec::new() }; - match renderer - .render( - route, - virtual_dom_factory, - &mut *to, - |vdom| { - Box::pin(async move { - // before polling the future, we need to set the context - let prev_context = SERVER_CONTEXT - .with(|ctx| ctx.replace(Box::new(server_context))); - // poll the future, which may call server_context() - tracing::info!("Rebuilding vdom"); - let _ = vdom.rebuild(&mut NoOpMutations); - vdom.wait_for_suspense().await; - tracing::info!("Suspense resolved"); - // after polling the future, we need to restore the context - SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); - }) - }, - &wrapper, - ) - .await - { - Ok(freshness) => { - match String::from_utf8(to.buffer).map_err(|err| { - dioxus_ssr::incremental::IncrementalRendererError::Other( - Box::new(err), - ) - }) { - Ok(html) => { - let _ = tx.send(Ok((freshness, html))); - } - Err(err) => { - let _ = tx.send(Err(err)); - } - } + spawn_platform(move || async move { + let mut to = WriteBuffer { buffer: Vec::new() }; + match renderer + .render( + route, + virtual_dom_factory, + &mut *to, + |vdom| { + Box::pin(async move { + // before polling the future, we need to set the context + let prev_context = SERVER_CONTEXT + .with(|ctx| ctx.replace(Box::new(server_context))); + // poll the future, which may call server_context() + tracing::info!("Rebuilding vdom"); + let _ = vdom.rebuild(&mut NoOpMutations); + vdom.wait_for_suspense().await; + tracing::info!("Suspense resolved"); + // after polling the future, we need to restore the context + SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); + }) + }, + &wrapper, + ) + .await + { + Ok(freshness) => { + match String::from_utf8(to.buffer).map_err(|err| { + dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new( + err, + )) + }) { + Ok(html) => { + let _ = tx.send(Ok((freshness, html))); } Err(err) => { let _ = tx.send(Err(err)); } } - }) + } + Err(err) => { + let _ = tx.send(Err(err)); + } + } }); let (freshness, html) = rx.await.unwrap()?; diff --git a/packages/fullstack/src/serve_config.rs b/packages/fullstack/src/serve_config.rs index 62f13156d..0a5e8e4b9 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/fullstack/src/serve_config.rs @@ -13,6 +13,7 @@ use dioxus_lib::prelude::*; #[derive(Clone)] pub struct ServeConfigBuilder { pub(crate) root_id: Option<&'static str>, + pub(crate) index_html: Option, pub(crate) index_path: Option, pub(crate) assets_path: Option, pub(crate) incremental: @@ -44,6 +45,7 @@ impl ServeConfigBuilder { pub fn new() -> Self { Self { root_id: None, + index_html: None, index_path: None, assets_path: None, incremental: None, @@ -56,6 +58,12 @@ impl ServeConfigBuilder { self } + /// Set the contents of the index.html file to be served. (precedence over index_path) + pub fn index_html(mut self, index_html: String) -> Self { + self.index_html = Some(index_html); + self + } + /// Set the path of the index.html file to be served. (defaults to {assets_path}/index.html) pub fn index_path(mut self, index_path: PathBuf) -> Self { self.index_path = Some(index_path); @@ -90,8 +98,11 @@ impl ServeConfigBuilder { let root_id = self.root_id.unwrap_or("main"); - let index = load_index_html(index_path, root_id); + let index_html = self + .index_html + .unwrap_or_else(|| load_index_path(index_path)); + let index = load_index_html(index_html, root_id); ServeConfig { index, assets_path, @@ -100,13 +111,16 @@ impl ServeConfigBuilder { } } -fn load_index_html(path: PathBuf, root_id: &'static str) -> IndexHtml { +fn load_index_path(path: PathBuf) -> String { let mut file = File::open(path).expect("Failed to find index.html. Make sure the index_path is set correctly and the WASM application has been built."); let mut contents = String::new(); file.read_to_string(&mut contents) .expect("Failed to read index.html"); + contents +} +fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml { let (pre_main, post_main) = contents.split_once(&format!("id=\"{root_id}\"")).unwrap_or_else(|| panic!("Failed to find id=\"{root_id}\" in index.html. The id is used to inject the application into the page.")); let post_main = post_main.split_once('>').unwrap_or_else(|| { diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 89cc10edc..62da951d5 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -18,9 +18,15 @@ rustc-hash = "1.1.0" lru = "0.10.0" tracing = { workspace = true } http = "0.2.9" -tokio = { version = "1.28", features = ["fs", "io-util"], optional = true } async-trait = "0.1.58" serde_json = { version = "1.0" } +web-time = "1.0.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { version = "1.28", features = ["io-util"], optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1.28", features = ["fs", "io-util"], optional = true } [dev-dependencies] dioxus = { workspace = true } diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs index d1d4e9665..2e81bb3a2 100644 --- a/packages/ssr/src/incremental.rs +++ b/packages/ssr/src/incremental.rs @@ -23,8 +23,9 @@ pub use crate::incremental_cfg::*; pub struct IncrementalRenderer { pub(crate) static_dir: PathBuf, #[allow(clippy::type_complexity)] - pub(crate) memory_cache: - Option), BuildHasherDefault>>, + pub(crate) memory_cache: Option< + lru::LruCache), BuildHasherDefault>, + >, pub(crate) invalidate_after: Option, pub(crate) ssr_renderer: crate::Renderer, pub(crate) map_path: PathMapFn, @@ -98,22 +99,25 @@ impl IncrementalRenderer { route: String, html: Vec, ) -> Result { - let file_path = self.route_as_path(&route); - if let Some(parent) = file_path.parent() { - if !parent.exists() { - std::fs::create_dir_all(parent)?; + #[cfg(not(target_arch = "wasm32"))] + { + let file_path = self.route_as_path(&route); + if let Some(parent) = file_path.parent() { + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } } + let file = std::fs::File::create(file_path)?; + let mut file = std::io::BufWriter::new(file); + file.write_all(&html)?; } - let file = std::fs::File::create(file_path)?; - let mut file = std::io::BufWriter::new(file); - file.write_all(&html)?; self.add_to_memory_cache(route, html); Ok(RenderFreshness::now(self.invalidate_after)) } fn add_to_memory_cache(&mut self, route: String, html: Vec) { if let Some(cache) = self.memory_cache.as_mut() { - cache.put(route, (SystemTime::now(), html)); + cache.put(route, (web_time::SystemTime::now(), html)); } } @@ -151,6 +155,7 @@ impl IncrementalRenderer { } } // check the file cache + #[cfg(not(target_arch = "wasm32"))] if let Some(file_path) = self.find_file(&route) { if let Some(freshness) = file_path.freshness(self.invalidate_after) { if let Ok(file) = tokio::fs::File::open(file_path.full_path).await { From 67248476855d46013304c62a9092d58cf055666c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 19 Feb 2024 08:03:29 -0600 Subject: [PATCH 2/2] swap out web-time for chrono --- Cargo.lock | 2 +- packages/ssr/Cargo.toml | 4 ++-- packages/ssr/src/incremental.rs | 31 ++++++++++++++++--------------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61c930a61..52dfefe69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2902,6 +2902,7 @@ dependencies = [ "argh", "askama_escape", "async-trait", + "chrono", "dioxus", "dioxus-core", "dioxus-html", @@ -2917,7 +2918,6 @@ dependencies = [ "thiserror", "tokio", "tracing", - "web-time", ] [[package]] diff --git a/packages/ssr/Cargo.toml b/packages/ssr/Cargo.toml index 62da951d5..107d42284 100644 --- a/packages/ssr/Cargo.toml +++ b/packages/ssr/Cargo.toml @@ -20,7 +20,7 @@ tracing = { workspace = true } http = "0.2.9" async-trait = "0.1.58" serde_json = { version = "1.0" } -web-time = "1.0.0" +chrono = { verison = "0.4.34", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] tokio = { version = "1.28", features = ["io-util"], optional = true } @@ -41,4 +41,4 @@ dioxus-signals = { workspace = true } [features] default = [] -incremental = ["dep:tokio"] +incremental = ["dep:tokio", "chrono"] diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs index 2e81bb3a2..1ed16db5f 100644 --- a/packages/ssr/src/incremental.rs +++ b/packages/ssr/src/incremental.rs @@ -3,6 +3,8 @@ #![allow(non_snake_case)] use crate::fs_cache::ValidCachedPath; +use chrono::offset::Utc; +use chrono::DateTime; use dioxus_core::VirtualDom; use rustc_hash::FxHasher; use std::{ @@ -23,9 +25,8 @@ pub use crate::incremental_cfg::*; pub struct IncrementalRenderer { pub(crate) static_dir: PathBuf, #[allow(clippy::type_complexity)] - pub(crate) memory_cache: Option< - lru::LruCache), BuildHasherDefault>, - >, + pub(crate) memory_cache: + Option, Vec), BuildHasherDefault>>, pub(crate) invalidate_after: Option, pub(crate) ssr_renderer: crate::Renderer, pub(crate) map_path: PathMapFn, @@ -117,7 +118,7 @@ impl IncrementalRenderer { fn add_to_memory_cache(&mut self, route: String, html: Vec) { if let Some(cache) = self.memory_cache.as_mut() { - cache.put(route, (web_time::SystemTime::now(), html)); + cache.put(route, (Utc::now(), html)); } } @@ -138,20 +139,20 @@ impl IncrementalRenderer { .as_mut() .and_then(|cache| cache.get(&route)) { - if let Ok(elapsed) = timestamp.elapsed() { - let age = elapsed.as_secs(); - if let Some(invalidate_after) = self.invalidate_after { - if elapsed < invalidate_after { - tracing::trace!("memory cache hit {:?}", route); - output.write_all(cache_hit).await?; - let max_age = invalidate_after.as_secs(); - return Ok(Some(RenderFreshness::new(age, max_age))); - } - } else { + let now = Utc::now(); + let elapsed = timestamp.signed_duration_since(now); + let age = elapsed.num_seconds(); + if let Some(invalidate_after) = self.invalidate_after { + if elapsed.to_std().unwrap() < invalidate_after { tracing::trace!("memory cache hit {:?}", route); output.write_all(cache_hit).await?; - return Ok(Some(RenderFreshness::new_age(age))); + let max_age = invalidate_after.as_secs(); + return Ok(Some(RenderFreshness::new(age as u64, max_age))); } + } else { + tracing::trace!("memory cache hit {:?}", route); + output.write_all(cache_hit).await?; + return Ok(Some(RenderFreshness::new_age(age as u64))); } } // check the file cache