dioxus_isrg/
lib.rs

1//! Incremental file based incremental rendering
2
3#![allow(non_snake_case)]
4
5mod config;
6mod freshness;
7#[cfg(not(target_arch = "wasm32"))]
8mod fs_cache;
9mod memory_cache;
10
11use std::time::Duration;
12
13use chrono::Utc;
14pub use config::*;
15pub use freshness::*;
16
17use self::memory_cache::InMemoryCache;
18
19/// A render that was cached from a previous render.
20pub struct CachedRender<'a> {
21    /// The route that was rendered
22    pub route: String,
23    /// The freshness information for the rendered response
24    pub freshness: RenderFreshness,
25    /// The rendered response
26    pub response: &'a [u8],
27}
28
29/// An incremental renderer.
30pub struct IncrementalRenderer {
31    pub(crate) memory_cache: InMemoryCache,
32    #[cfg(not(target_arch = "wasm32"))]
33    pub(crate) file_system_cache: fs_cache::FileSystemCache,
34    invalidate_after: Option<Duration>,
35}
36
37impl IncrementalRenderer {
38    /// Create a new incremental renderer builder.
39    pub fn builder() -> IncrementalRendererConfig {
40        IncrementalRendererConfig::new()
41    }
42
43    /// Remove a route from the cache.
44    pub fn invalidate(&mut self, route: &str) {
45        self.memory_cache.invalidate(route);
46        #[cfg(not(target_arch = "wasm32"))]
47        self.file_system_cache.invalidate(route);
48    }
49
50    /// Remove all routes from the cache.
51    pub fn invalidate_all(&mut self) {
52        self.memory_cache.clear();
53        #[cfg(not(target_arch = "wasm32"))]
54        self.file_system_cache.clear();
55    }
56
57    /// Cache a rendered response.
58    ///
59    /// ```rust
60    /// # use dioxus_isrg::IncrementalRenderer;
61    /// # let mut renderer = IncrementalRenderer::builder().build();
62    /// let route = "/index".to_string();
63    /// let response = b"<html><body>Hello world</body></html>";
64    /// renderer.cache(route, response).unwrap();
65    /// ```
66    pub fn cache(
67        &mut self,
68        route: String,
69        html: impl Into<Vec<u8>>,
70    ) -> Result<RenderFreshness, IncrementalRendererError> {
71        let timestamp = Utc::now();
72        let html = html.into();
73        #[cfg(not(target_arch = "wasm32"))]
74        self.file_system_cache
75            .put(route.clone(), timestamp, html.clone())?;
76        self.memory_cache.put(route, timestamp, html);
77        Ok(RenderFreshness::created_at(
78            timestamp,
79            self.invalidate_after,
80        ))
81    }
82
83    /// Try to get a cached response for a route.
84    ///
85    /// ```rust
86    /// # use dioxus_isrg::IncrementalRenderer;
87    /// # let mut renderer = IncrementalRenderer::builder().build();
88    /// # let route = "/index".to_string();
89    /// # let response = b"<html><body>Hello world</body></html>";
90    /// # renderer.cache(route, response).unwrap();
91    /// let route = "/index";
92    /// let response = renderer.get(route).unwrap();
93    /// assert_eq!(response.unwrap().response, b"<html><body>Hello world</body></html>");
94    /// ```
95    ///
96    /// If the route is not cached, `None` is returned.
97    ///
98    /// ```rust
99    /// # use dioxus_isrg::IncrementalRenderer;
100    /// # let mut renderer = IncrementalRenderer::builder().build();
101    /// let route = "/index";
102    /// let response = renderer.get(route).unwrap();
103    /// assert!(response.is_none());
104    /// ```
105    pub fn get<'a>(
106        &'a mut self,
107        route: &str,
108    ) -> Result<Option<CachedRender<'a>>, IncrementalRendererError> {
109        let Self {
110            memory_cache,
111            #[cfg(not(target_arch = "wasm32"))]
112            file_system_cache,
113            ..
114        } = self;
115
116        #[allow(unused)]
117        enum FsGetError {
118            NotPresent,
119            Error(IncrementalRendererError),
120        }
121
122        // The borrow checker prevents us from simply using a match/if and returning early. Instead we need to use the more complex closure API
123        // non lexical lifetimes will make this possible (it works with polonius)
124        let or_insert = || {
125            // check the file cache
126            #[cfg(not(target_arch = "wasm32"))]
127            return match file_system_cache.get(route) {
128                Ok(Some((freshness, bytes))) => Ok((freshness.timestamp(), bytes)),
129                Ok(None) => Err(FsGetError::NotPresent),
130                Err(e) => Err(FsGetError::Error(e)),
131            };
132
133            #[allow(unreachable_code)]
134            Err(FsGetError::NotPresent)
135        };
136
137        match memory_cache.try_get_or_insert(route, or_insert) {
138            Ok(Some((freshness, bytes))) => Ok(Some(CachedRender {
139                route: route.to_string(),
140                freshness,
141                response: bytes,
142            })),
143            Err(FsGetError::NotPresent) | Ok(None) => Ok(None),
144            Err(FsGetError::Error(e)) => Err(e),
145        }
146    }
147}
148
149/// An error that can occur while rendering a route or retrieving a cached route.
150#[derive(Debug, thiserror::Error)]
151#[non_exhaustive]
152pub enum IncrementalRendererError {
153    /// An formatting error occurred while rendering a route.
154    #[error("RenderError: {0}")]
155    RenderError(#[from] std::fmt::Error),
156    /// An IO error occurred while rendering a route.
157    #[error("IoError: {0}")]
158    IoError(#[from] std::io::Error),
159    /// An IO error occurred while rendering a route.
160    #[error("Other: {0}")]
161    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
162}