incremental.rs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. //! Incremental file based incremental rendering
  2. #![allow(non_snake_case)]
  3. use crate::fs_cache::ValidCachedPath;
  4. use chrono::offset::Utc;
  5. use chrono::DateTime;
  6. use dioxus_core::VirtualDom;
  7. use rustc_hash::FxHasher;
  8. use std::{
  9. future::Future,
  10. hash::BuildHasherDefault,
  11. ops::{Deref, DerefMut},
  12. path::PathBuf,
  13. pin::Pin,
  14. time::{Duration, SystemTime},
  15. };
  16. use tokio::io::{AsyncWrite, AsyncWriteExt};
  17. pub use crate::fs_cache::*;
  18. pub use crate::incremental_cfg::*;
  19. /// An incremental renderer.
  20. pub struct IncrementalRenderer {
  21. pub(crate) static_dir: PathBuf,
  22. #[allow(clippy::type_complexity)]
  23. pub(crate) memory_cache:
  24. Option<lru::LruCache<String, (DateTime<Utc>, Vec<u8>), BuildHasherDefault<FxHasher>>>,
  25. pub(crate) invalidate_after: Option<Duration>,
  26. pub(crate) ssr_renderer: crate::Renderer,
  27. pub(crate) map_path: PathMapFn,
  28. }
  29. impl IncrementalRenderer {
  30. /// Get the inner renderer.
  31. pub fn renderer(&self) -> &crate::Renderer {
  32. &self.ssr_renderer
  33. }
  34. /// Get the inner renderer mutably.
  35. pub fn renderer_mut(&mut self) -> &mut crate::Renderer {
  36. &mut self.ssr_renderer
  37. }
  38. /// Create a new incremental renderer builder.
  39. pub fn builder() -> IncrementalRendererConfig {
  40. IncrementalRendererConfig::new()
  41. }
  42. /// Remove a route from the cache.
  43. pub fn invalidate(&mut self, route: &str) {
  44. if let Some(cache) = &mut self.memory_cache {
  45. cache.pop(route);
  46. }
  47. if let Some(path) = self.find_file(route) {
  48. let _ = std::fs::remove_file(path.full_path);
  49. }
  50. }
  51. /// Remove all routes from the cache.
  52. pub fn invalidate_all(&mut self) {
  53. if let Some(cache) = &mut self.memory_cache {
  54. cache.clear();
  55. }
  56. // clear the static directory
  57. let _ = std::fs::remove_dir_all(&self.static_dir);
  58. }
  59. #[cfg(not(target_arch = "wasm32"))]
  60. fn track_timestamps(&self) -> bool {
  61. self.invalidate_after.is_some()
  62. }
  63. async fn render_and_cache<'a, R: WrapBody + Send + Sync>(
  64. &'a mut self,
  65. route: String,
  66. mut virtual_dom: VirtualDom,
  67. output: &'a mut (impl AsyncWrite + Unpin + Send),
  68. rebuild_with: impl FnOnce(&mut VirtualDom) -> Pin<Box<dyn Future<Output = ()> + '_>>,
  69. renderer: &'a R,
  70. ) -> Result<RenderFreshness, IncrementalRendererError> {
  71. let mut html_buffer = WriteBuffer { buffer: Vec::new() };
  72. {
  73. rebuild_with(&mut virtual_dom).await;
  74. renderer.render_before_body(&mut *html_buffer)?;
  75. self.ssr_renderer
  76. .render_to(&mut html_buffer, &virtual_dom)?;
  77. }
  78. renderer.render_after_body(&mut *html_buffer)?;
  79. let html_buffer = html_buffer.buffer;
  80. output.write_all(&html_buffer).await?;
  81. self.add_to_cache(route, html_buffer)
  82. }
  83. fn add_to_cache(
  84. &mut self,
  85. route: String,
  86. html: Vec<u8>,
  87. ) -> Result<RenderFreshness, IncrementalRendererError> {
  88. #[cfg(not(target_arch = "wasm32"))]
  89. {
  90. use std::io::Write;
  91. let file_path = self.route_as_path(&route);
  92. if let Some(parent) = file_path.parent() {
  93. if !parent.exists() {
  94. std::fs::create_dir_all(parent)?;
  95. }
  96. }
  97. let file = std::fs::File::create(file_path)?;
  98. let mut file = std::io::BufWriter::new(file);
  99. file.write_all(&html)?;
  100. }
  101. self.add_to_memory_cache(route, html);
  102. Ok(RenderFreshness::now(self.invalidate_after))
  103. }
  104. fn add_to_memory_cache(&mut self, route: String, html: Vec<u8>) {
  105. if let Some(cache) = self.memory_cache.as_mut() {
  106. cache.put(route, (Utc::now(), html));
  107. }
  108. }
  109. #[cfg(not(target_arch = "wasm32"))]
  110. fn promote_memory_cache<K: AsRef<str>>(&mut self, route: K) {
  111. if let Some(cache) = self.memory_cache.as_mut() {
  112. cache.promote(route.as_ref())
  113. }
  114. }
  115. async fn search_cache(
  116. &mut self,
  117. route: String,
  118. output: &mut (impl AsyncWrite + Unpin + std::marker::Send),
  119. ) -> Result<Option<RenderFreshness>, IncrementalRendererError> {
  120. // check the memory cache
  121. if let Some((timestamp, cache_hit)) = self
  122. .memory_cache
  123. .as_mut()
  124. .and_then(|cache| cache.get(&route))
  125. {
  126. let now = Utc::now();
  127. let elapsed = timestamp.signed_duration_since(now);
  128. let age = elapsed.num_seconds();
  129. if let Some(invalidate_after) = self.invalidate_after {
  130. if elapsed.to_std().unwrap() < invalidate_after {
  131. tracing::trace!("memory cache hit {:?}", route);
  132. output.write_all(cache_hit).await?;
  133. let max_age = invalidate_after.as_secs();
  134. return Ok(Some(RenderFreshness::new(age as u64, max_age)));
  135. }
  136. } else {
  137. tracing::trace!("memory cache hit {:?}", route);
  138. output.write_all(cache_hit).await?;
  139. return Ok(Some(RenderFreshness::new_age(age as u64)));
  140. }
  141. }
  142. // check the file cache
  143. #[cfg(not(target_arch = "wasm32"))]
  144. if let Some(file_path) = self.find_file(&route) {
  145. if let Some(freshness) = file_path.freshness(self.invalidate_after) {
  146. if let Ok(file) = tokio::fs::File::open(file_path.full_path).await {
  147. let mut file = tokio::io::BufReader::new(file);
  148. tokio::io::copy_buf(&mut file, output).await?;
  149. tracing::trace!("file cache hit {:?}", route);
  150. self.promote_memory_cache(&route);
  151. return Ok(Some(freshness));
  152. }
  153. }
  154. }
  155. Ok(None)
  156. }
  157. /// Render a route or get it from cache.
  158. pub async fn render<R: WrapBody + Send + Sync>(
  159. &mut self,
  160. route: String,
  161. virtual_dom_factory: impl FnOnce() -> VirtualDom,
  162. output: &mut (impl AsyncWrite + Unpin + std::marker::Send),
  163. rebuild_with: impl FnOnce(&mut VirtualDom) -> Pin<Box<dyn Future<Output = ()> + '_>>,
  164. renderer: &R,
  165. ) -> Result<RenderFreshness, IncrementalRendererError> {
  166. // check if this route is cached
  167. if let Some(freshness) = self.search_cache(route.to_string(), output).await? {
  168. Ok(freshness)
  169. } else {
  170. // if not, create it
  171. let freshness = self
  172. .render_and_cache(route, virtual_dom_factory(), output, rebuild_with, renderer)
  173. .await?;
  174. tracing::trace!("cache miss");
  175. Ok(freshness)
  176. }
  177. }
  178. fn find_file(&self, route: &str) -> Option<ValidCachedPath> {
  179. let mut file_path = (self.map_path)(route);
  180. if let Some(deadline) = self.invalidate_after {
  181. // find the first file that matches the route and is a html file
  182. file_path.push("index");
  183. if let Ok(dir) = std::fs::read_dir(file_path) {
  184. let mut file = None;
  185. for entry in dir.flatten() {
  186. if let Some(cached_path) = ValidCachedPath::try_from_path(entry.path()) {
  187. if let Ok(elapsed) = cached_path.timestamp.elapsed() {
  188. if elapsed < deadline {
  189. file = Some(cached_path);
  190. continue;
  191. }
  192. }
  193. // if the timestamp is invalid or passed, delete the file
  194. if let Err(err) = std::fs::remove_file(entry.path()) {
  195. tracing::error!("Failed to remove file: {}", err);
  196. }
  197. }
  198. }
  199. file
  200. } else {
  201. None
  202. }
  203. } else {
  204. file_path.push("index.html");
  205. file_path.exists().then_some({
  206. ValidCachedPath {
  207. full_path: file_path,
  208. timestamp: SystemTime::now(),
  209. }
  210. })
  211. }
  212. }
  213. #[cfg(not(target_arch = "wasm32"))]
  214. fn route_as_path(&self, route: &str) -> PathBuf {
  215. let mut file_path = (self.map_path)(route);
  216. if self.track_timestamps() {
  217. file_path.push("index");
  218. file_path.push(timestamp());
  219. } else {
  220. file_path.push("index");
  221. }
  222. file_path.set_extension("html");
  223. file_path
  224. }
  225. }
  226. struct WriteBuffer {
  227. buffer: Vec<u8>,
  228. }
  229. impl std::fmt::Write for WriteBuffer {
  230. fn write_str(&mut self, s: &str) -> std::fmt::Result {
  231. self.buffer.extend_from_slice(s.as_bytes());
  232. Ok(())
  233. }
  234. }
  235. impl Deref for WriteBuffer {
  236. type Target = Vec<u8>;
  237. fn deref(&self) -> &Self::Target {
  238. &self.buffer
  239. }
  240. }
  241. impl DerefMut for WriteBuffer {
  242. fn deref_mut(&mut self) -> &mut Self::Target {
  243. &mut self.buffer
  244. }
  245. }
  246. /// An error that can occur while rendering a route or retrieving a cached route.
  247. #[derive(Debug, thiserror::Error)]
  248. pub enum IncrementalRendererError {
  249. /// An formatting error occurred while rendering a route.
  250. #[error("RenderError: {0}")]
  251. RenderError(#[from] std::fmt::Error),
  252. /// An IO error occurred while rendering a route.
  253. #[error("IoError: {0}")]
  254. IoError(#[from] std::io::Error),
  255. /// An IO error occurred while rendering a route.
  256. #[error("Other: {0}")]
  257. Other(#[from] Box<dyn std::error::Error + Send + Sync>),
  258. }