diff --git a/packages/ssr/src/fs_cache.rs b/packages/ssr/src/fs_cache.rs new file mode 100644 index 000000000..efaa52b80 --- /dev/null +++ b/packages/ssr/src/fs_cache.rs @@ -0,0 +1,128 @@ +#![allow(non_snake_case)] + + + +use std::{ + ops::{Deref, DerefMut}, + path::{PathBuf}, + time::{Duration}, +}; + + +/// Information about the freshness of a rendered response +#[derive(Debug, Clone, Copy)] +pub struct RenderFreshness { + /// The age of the rendered response + age: u64, + /// The maximum age of the rendered response + max_age: Option, +} + +impl RenderFreshness { + /// Create new freshness information + pub fn new(age: u64, max_age: u64) -> Self { + Self { + age, + max_age: Some(max_age), + } + } + + /// Create new freshness information with only the age + pub fn new_age(age: u64) -> Self { + Self { age, max_age: None } + } + + /// Create new freshness information at the current time + pub fn now(max_age: Option) -> Self { + Self { + age: 0, + max_age: max_age.map(|d| d.as_secs()), + } + } + + /// Get the age of the rendered response in seconds + pub fn age(&self) -> u64 { + self.age + } + + /// Get the maximum age of the rendered response in seconds + pub fn max_age(&self) -> Option { + self.max_age + } + + /// Write the freshness to the response headers. + pub fn write(&self, headers: &mut http::HeaderMap) { + let age = self.age(); + headers.insert(http::header::AGE, age.into()); + if let Some(max_age) = self.max_age() { + headers.insert( + http::header::CACHE_CONTROL, + http::HeaderValue::from_str(&format!("max-age={}", max_age)).unwrap(), + ); + } + } +} + +struct WriteBuffer { + buffer: Vec, +} + +impl std::fmt::Write for WriteBuffer { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.buffer.extend_from_slice(s.as_bytes()); + Ok(()) + } +} + +impl Deref for WriteBuffer { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.buffer + } +} + +impl DerefMut for WriteBuffer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.buffer + } +} + +pub(crate) struct ValidCachedPath { + pub(crate) full_path: PathBuf, + pub(crate) timestamp: std::time::SystemTime, +} + +impl ValidCachedPath { + pub fn try_from_path(value: PathBuf) -> Option { + if value.extension() != Some(std::ffi::OsStr::new("html")) { + return None; + } + let timestamp = decode_timestamp(value.file_stem()?.to_str()?)?; + let full_path = value; + Some(Self { + full_path, + timestamp, + }) + } + + pub fn freshness(&self, max_age: Option) -> Option { + let age = self.timestamp.elapsed().ok()?.as_secs(); + let max_age = max_age.map(|max_age| max_age.as_secs()); + Some(RenderFreshness::new(age, max_age?)) + } +} + +fn decode_timestamp(timestamp: &str) -> Option { + let timestamp = u64::from_str_radix(timestamp, 16).ok()?; + Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(timestamp)) +} + +pub fn timestamp() -> String { + let datetime = std::time::SystemTime::now(); + let timestamp = datetime + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + format!("{:x}", timestamp) +} diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs index 441800838..00fd3e9e2 100644 --- a/packages/ssr/src/incremental.rs +++ b/packages/ssr/src/incremental.rs @@ -2,150 +2,30 @@ #![allow(non_snake_case)] +use crate::fs_cache::ValidCachedPath; use dioxus_core::{Element, Scope, VirtualDom}; use rustc_hash::FxHasher; use std::{ hash::BuildHasherDefault, io::Write, - num::NonZeroUsize, ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - sync::Arc, + path::{PathBuf}, time::{Duration, SystemTime}, }; use tokio::io::{AsyncWrite, AsyncWriteExt, BufReader}; -/// Something that can render a HTML page from a body. -pub trait WrapBody { - /// Render the HTML before the body - fn render_before_body(&self, to: &mut R) -> Result<(), IncrementalRendererError>; - /// Render the HTML after the body - fn render_after_body(&self, to: &mut R) -> Result<(), IncrementalRendererError>; -} - -/// The default page renderer -pub struct DefaultRenderer { - /// The HTML before the body. - pub before_body: String, - /// The HTML after the body. - pub after_body: String, -} - -impl Default for DefaultRenderer { - fn default() -> Self { - let before = r#" - - - - - Dioxus Application - - "#; - let after = r#" - "#; - Self { - before_body: before.to_string(), - after_body: after.to_string(), - } - } -} - -impl WrapBody for DefaultRenderer { - fn render_before_body(&self, to: &mut R) -> Result<(), IncrementalRendererError> { - to.write_all(self.before_body.as_bytes())?; - Ok(()) - } - - fn render_after_body(&self, to: &mut R) -> Result<(), IncrementalRendererError> { - to.write_all(self.after_body.as_bytes())?; - Ok(()) - } -} - -type PathMapFn = Arc PathBuf + Send + Sync>; - -/// A configuration for the incremental renderer. -#[derive(Clone)] -pub struct IncrementalRendererConfig { - static_dir: PathBuf, - memory_cache_limit: usize, - invalidate_after: Option, - map_path: Option, -} - -impl Default for IncrementalRendererConfig { - fn default() -> Self { - Self::new() - } -} - -impl IncrementalRendererConfig { - /// Create a new incremental renderer configuration. - pub fn new() -> Self { - Self { - static_dir: PathBuf::from("./static"), - memory_cache_limit: 10000, - invalidate_after: None, - map_path: None, - } - } - - /// Set a mapping from the route to the file path. This will override the default mapping configured with `static_dir`. - /// The function should return the path to the folder to store the index.html file in. - pub fn map_path PathBuf + Send + Sync + 'static>(mut self, map_path: F) -> Self { - self.map_path = Some(Arc::new(map_path)); - self - } - - /// Set the static directory. - pub fn static_dir>(mut self, static_dir: P) -> Self { - self.static_dir = static_dir.as_ref().to_path_buf(); - self - } - - /// Set the memory cache limit. - pub const fn memory_cache_limit(mut self, memory_cache_limit: usize) -> Self { - self.memory_cache_limit = memory_cache_limit; - self - } - - /// Set the invalidation time. - pub fn invalidate_after(mut self, invalidate_after: Duration) -> Self { - self.invalidate_after = Some(invalidate_after); - self - } - - /// Build the incremental renderer. - pub fn build(self) -> IncrementalRenderer { - let static_dir = self.static_dir.clone(); - IncrementalRenderer { - static_dir: self.static_dir.clone(), - memory_cache: NonZeroUsize::new(self.memory_cache_limit) - .map(|limit| lru::LruCache::with_hasher(limit, Default::default())), - invalidate_after: self.invalidate_after, - ssr_renderer: crate::Renderer::new(), - map_path: self.map_path.unwrap_or_else(move || { - Arc::new(move |route: &str| { - let mut path = static_dir.clone(); - for segment in route.split('/') { - path.push(segment); - } - path - }) - }), - } - } -} +pub use crate::fs_cache::*; +pub use crate::incremental_cfg::*; /// An incremental renderer. pub struct IncrementalRenderer { - static_dir: PathBuf, + pub(crate) static_dir: PathBuf, #[allow(clippy::type_complexity)] - memory_cache: + pub(crate) memory_cache: Option), BuildHasherDefault>>, - invalidate_after: Option, - ssr_renderer: crate::Renderer, - map_path: PathMapFn, + pub(crate) invalidate_after: Option, + pub(crate) ssr_renderer: crate::Renderer, + pub(crate) map_path: PathMapFn, } impl IncrementalRenderer { @@ -366,60 +246,6 @@ impl IncrementalRenderer { } } -/// Information about the freshness of a rendered response -#[derive(Debug, Clone, Copy)] -pub struct RenderFreshness { - /// The age of the rendered response - age: u64, - /// The maximum age of the rendered response - max_age: Option, -} - -impl RenderFreshness { - /// Create new freshness information - pub fn new(age: u64, max_age: u64) -> Self { - Self { - age, - max_age: Some(max_age), - } - } - - /// Create new freshness information with only the age - pub fn new_age(age: u64) -> Self { - Self { age, max_age: None } - } - - /// Create new freshness information at the current time - pub fn now(max_age: Option) -> Self { - Self { - age: 0, - max_age: max_age.map(|d| d.as_secs()), - } - } - - /// Get the age of the rendered response in seconds - pub fn age(&self) -> u64 { - self.age - } - - /// Get the maximum age of the rendered response in seconds - pub fn max_age(&self) -> Option { - self.max_age - } - - /// Write the freshness to the response headers. - pub fn write(&self, headers: &mut http::HeaderMap) { - let age = self.age(); - headers.insert(http::header::AGE, age.into()); - if let Some(max_age) = self.max_age() { - headers.insert( - http::header::CACHE_CONTROL, - http::HeaderValue::from_str(&format!("max-age={}", max_age)).unwrap(), - ); - } - } -} - struct WriteBuffer { buffer: Vec, } @@ -445,45 +271,6 @@ impl DerefMut for WriteBuffer { } } -struct ValidCachedPath { - full_path: PathBuf, - timestamp: std::time::SystemTime, -} - -impl ValidCachedPath { - fn try_from_path(value: PathBuf) -> Option { - if value.extension() != Some(std::ffi::OsStr::new("html")) { - return None; - } - let timestamp = decode_timestamp(value.file_stem()?.to_str()?)?; - let full_path = value; - Some(Self { - full_path, - timestamp, - }) - } - - fn freshness(&self, max_age: Option) -> Option { - let age = self.timestamp.elapsed().ok()?.as_secs(); - let max_age = max_age.map(|max_age| max_age.as_secs()); - Some(RenderFreshness::new(age, max_age?)) - } -} - -fn decode_timestamp(timestamp: &str) -> Option { - let timestamp = u64::from_str_radix(timestamp, 16).ok()?; - Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(timestamp)) -} - -fn timestamp() -> String { - let datetime = std::time::SystemTime::now(); - let timestamp = datetime - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - format!("{:x}", timestamp) -} - /// An error that can occur while rendering a route or retrieving a cached route. #[derive(Debug, thiserror::Error)] pub enum IncrementalRendererError { diff --git a/packages/ssr/src/incremental_cfg.rs b/packages/ssr/src/incremental_cfg.rs new file mode 100644 index 000000000..1a4ea3418 --- /dev/null +++ b/packages/ssr/src/incremental_cfg.rs @@ -0,0 +1,134 @@ +#![allow(non_snake_case)] + +use crate::incremental::IncrementalRenderer; +use crate::incremental::IncrementalRendererError; + +use std::{ + io::Write, + num::NonZeroUsize, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; + +/// Something that can render a HTML page from a body. +pub trait WrapBody { + /// Render the HTML before the body + fn render_before_body(&self, to: &mut R) -> Result<(), IncrementalRendererError>; + /// Render the HTML after the body + fn render_after_body(&self, to: &mut R) -> Result<(), IncrementalRendererError>; +} + +/// The default page renderer +pub struct DefaultRenderer { + /// The HTML before the body. + pub before_body: String, + /// The HTML after the body. + pub after_body: String, +} + +impl Default for DefaultRenderer { + fn default() -> Self { + let before = r#" + + + + + Dioxus Application + + "#; + let after = r#" + "#; + Self { + before_body: before.to_string(), + after_body: after.to_string(), + } + } +} + +impl WrapBody for DefaultRenderer { + fn render_before_body(&self, to: &mut R) -> Result<(), IncrementalRendererError> { + to.write_all(self.before_body.as_bytes())?; + Ok(()) + } + + fn render_after_body(&self, to: &mut R) -> Result<(), IncrementalRendererError> { + to.write_all(self.after_body.as_bytes())?; + Ok(()) + } +} + +pub(crate) type PathMapFn = Arc PathBuf + Send + Sync>; + +/// A configuration for the incremental renderer. +#[derive(Clone)] +pub struct IncrementalRendererConfig { + static_dir: PathBuf, + memory_cache_limit: usize, + invalidate_after: Option, + map_path: Option, +} + +impl Default for IncrementalRendererConfig { + fn default() -> Self { + Self::new() + } +} + +impl IncrementalRendererConfig { + /// Create a new incremental renderer configuration. + pub fn new() -> Self { + Self { + static_dir: PathBuf::from("./static"), + memory_cache_limit: 10000, + invalidate_after: None, + map_path: None, + } + } + + /// Set a mapping from the route to the file path. This will override the default mapping configured with `static_dir`. + /// The function should return the path to the folder to store the index.html file in. + pub fn map_path PathBuf + Send + Sync + 'static>(mut self, map_path: F) -> Self { + self.map_path = Some(Arc::new(map_path)); + self + } + + /// Set the static directory. + pub fn static_dir>(mut self, static_dir: P) -> Self { + self.static_dir = static_dir.as_ref().to_path_buf(); + self + } + + /// Set the memory cache limit. + pub const fn memory_cache_limit(mut self, memory_cache_limit: usize) -> Self { + self.memory_cache_limit = memory_cache_limit; + self + } + + /// Set the invalidation time. + pub fn invalidate_after(mut self, invalidate_after: Duration) -> Self { + self.invalidate_after = Some(invalidate_after); + self + } + + /// Build the incremental renderer. + pub fn build(self) -> IncrementalRenderer { + let static_dir = self.static_dir.clone(); + IncrementalRenderer { + static_dir: self.static_dir.clone(), + memory_cache: NonZeroUsize::new(self.memory_cache_limit) + .map(|limit| lru::LruCache::with_hasher(limit, Default::default())), + invalidate_after: self.invalidate_after, + ssr_renderer: crate::Renderer::new(), + map_path: self.map_path.unwrap_or_else(move || { + Arc::new(move |route: &str| { + let mut path = static_dir.clone(); + for segment in route.split('/') { + path.push(segment); + } + path + }) + }), + } + } +} diff --git a/packages/ssr/src/lib.rs b/packages/ssr/src/lib.rs index b78bcfeb3..0f91e02d9 100644 --- a/packages/ssr/src/lib.rs +++ b/packages/ssr/src/lib.rs @@ -2,9 +2,12 @@ mod cache; pub mod config; +mod fs_cache; pub mod incremental; +mod incremental_cfg; pub mod renderer; pub mod template; + use dioxus_core::{Element, LazyNodes, Scope, VirtualDom}; use std::cell::Cell;