diff --git a/Cargo.lock b/Cargo.lock index 0ef6221..7cf994f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,11 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + [[package]] name = "bumpalo" version = "3.2.1" @@ -28,6 +34,12 @@ version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef" +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" + [[package]] name = "itoa" version = "0.4.5" @@ -64,6 +76,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + [[package]] name = "memory_units" version = "0.4.0" @@ -76,8 +94,10 @@ version = "0.1.0" dependencies = [ "cfg-if", "console_error_panic_hook", + "hex", "js-sys", "lazy_static", + "pulldown-cmark", "serde", "serde_json", "wasm-bindgen", @@ -105,6 +125,17 @@ dependencies = [ "unicode-xid 0.2.0", ] +[[package]] +name = "pulldown-cmark" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c2d7fd131800e0d63df52aff46201acaab70b431a4a1ec6f0343fe8e64f35a4" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + [[package]] name = "quote" version = "0.6.13" @@ -177,6 +208,15 @@ dependencies = [ "unicode-xid 0.2.0", ] +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-xid" version = "0.1.0" @@ -189,6 +229,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +[[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" + [[package]] name = "wasm-bindgen" version = "0.2.60" diff --git a/Cargo.toml b/Cargo.toml index aa7330d..6b2faed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,20 @@ default = ["console_error_panic_hook", "wee_alloc"] [dependencies] cfg-if = "0.1.2" lazy_static = "1.4" +hex = "0.4" js-sys = "0.3" +pulldown-cmark = { version = "0.7", default-features = false } serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3", features = [ + "Crypto", "Headers", "Request", "Response", "ResponseInit", + "SubtleCrypto", "Url", "UrlSearchParams" ] } diff --git a/src/blog.rs b/src/blog.rs index 45b3916..58834c5 100644 --- a/src/blog.rs +++ b/src/blog.rs @@ -6,6 +6,7 @@ // unnecessary from KV. use crate::store; use crate::utils::*; +use pulldown_cmark::*; use serde::{Serialize, Deserialize}; use std::vec::Vec; @@ -112,4 +113,89 @@ impl Post { Self::create_url_mapping(&self.url, &self.uuid).await?; store::put_obj(&Self::uuid_to_post_key(&self.uuid), self).await } +} + +const CACHE_VERSION: &'static str = "0001"; + +// Cached version of rendered blog content HTMLs +// compiled from Markdown +#[derive(Serialize, Deserialize)] +pub struct PostContentCache { + // UUID of the original post + uuid: String, + // If version != CACHE_VERSION, the cache is invalidated + version: String, + // Digest of the original content + orig_digest: String, + // Compiled content in HTML + pub content: String +} + +impl PostContentCache { + fn uuid_to_cache_key(uuid: &str) -> String { + format!("content_cache_{}", uuid) + } + + async fn find_by_uuid(uuid: &str) -> MyResult { + store::get_obj(&Self::uuid_to_cache_key(uuid)).await + } + + pub async fn find_by_post(post: &Post) -> Option { + let cache = match Self::find_by_uuid(&post.uuid).await { + Ok(cache) => cache, + Err(_) => return None + }; + + if cache.version != CACHE_VERSION { + return None; + } + + if cache.orig_digest != crate::utils::sha1(&post.content).await { + return None; + } + + Some(cache) + } + + // Only renders the content and spits out a cache object + // can be used to display the page or to write to cache + // Despite the signature, this function BLOCKS + // async only comes from digesting via SubtleCrypto + pub async fn render(post: &Post) -> PostContentCache { + // TODO: enable some options; pre-process posts to enable + // inline image caching; also generate a summary (?) + // from first few paragraphs + let parser = Parser::new(&post.content); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + PostContentCache { + uuid: post.uuid.clone(), + version: CACHE_VERSION.to_owned(), + orig_digest: crate::utils::sha1(&post.content).await, + content: html_output + } + } + + // Tries to find the rendered content cache of post + // if a valid cache cannot be found, this method + // will render the content, write that into cache + // and return this newly-rendered one + // This will block if it tries to render; if that's a + // concern, use find_by_post + pub async fn find_or_render(post: &Post) -> PostContentCache { + match Self::find_by_post(post).await { + Some(cache) => cache, + None => { + let ret = Self::render(post).await; + // Ignore save error since if save failed, it can be regenerated anyway + let _ = ret.save().await; + ret + } + } + } + + // Save the current cache object to KV + pub async fn save(&self) -> MyResult<()> { + store::put_obj(&Self::uuid_to_cache_key(&self.uuid), self).await + } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 4fd012f..507b93a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,7 @@ async fn default_route(_req: Request, url: Url) -> MyResult { } else { // TODO: Actually render the page... return Response::new_with_opt_str_and_init( - Some(&post.content), + Some(&blog::PostContentCache::find_or_render(&post).await.content), ResponseInit::new() .status(200) .headers(headers!{ diff --git a/src/sn.rs b/src/sn.rs index 50f9717..df88045 100644 --- a/src/sn.rs +++ b/src/sn.rs @@ -225,6 +225,9 @@ async fn create_or_update_post(req: Request, url: Url) -> MyResult { } else { list.remove_post(&post.uuid).await?; } + // Also pre-render the post + blog::PostContentCache::find_or_render(&post).await; + // Finally, save the post post.write_to_kv().await?; Response::new_with_opt_str_and_init( diff --git a/src/utils.rs b/src/utils.rs index 867edd9..12dac5d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,9 @@ use cfg_if::cfg_if; use serde::Deserialize; -use web_sys::Headers; +use js_sys::*; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::*; +use web_sys::*; cfg_if! { // When the `console_error_panic_hook` feature is enabled, we can call the @@ -61,6 +64,20 @@ pub fn title_to_url(uuid: &str, title: &str) -> String { format!("{}/{}", &uuid[0..4], title_part) } +#[wasm_bindgen] +extern "C" { + static crypto: Crypto; +} + +// SHA-1 digest (hexed) via SubtleCrypto +pub async fn sha1(s: &str) -> String { + let mut bytes: Vec = s.bytes().collect(); + let promise = crypto.subtle().digest_with_str_and_u8_array("SHA-1", &mut bytes).unwrap(); + let buffer: ArrayBuffer = JsFuture::from(promise).await.unwrap().into(); + let digest_arr = Uint8Array::new(&buffer).to_vec(); + hex::encode(digest_arr) +} + pub trait HeadersExt { fn add_cors(self) -> Self; }