diff --git a/Cargo.lock b/Cargo.lock index 4a71c1f..3676dec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -263,6 +278,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -282,6 +318,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -368,9 +406,12 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" dependencies = [ + "brotli", "compression-core", "flate2", "memchr", + "zstd", + "zstd-safe", ] [[package]] @@ -618,6 +659,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -785,6 +832,11 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -1096,6 +1148,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.82" @@ -1200,6 +1262,15 @@ dependencies = [ "prost-types", ] +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +dependencies = [ + "hashbrown 0.16.0", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -1566,6 +1637,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plist" version = "1.8.0" @@ -2137,6 +2214,7 @@ dependencies = [ "emojis", "lazy_static", "libservice", + "lru", "macro-assets", "macro-sass", "markdown-it", @@ -2147,6 +2225,7 @@ dependencies = [ "serde_yaml", "strum", "tokio", + "tower-http", "tracing", ] @@ -2532,13 +2611,17 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ + "async-compression", "bitflags", "bytes", + "futures-core", "futures-util", "http", "http-body", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -3342,3 +3425,31 @@ dependencies = [ "log", "simd-adler32", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index f0f13db..961e7db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ service-webpage = { path = "crates/service/service-webpage" } # MARK: Server # axum = { version = "0.8.6", features = ["macros", "multipart"] } -tower-http = { version = "0.6.6", features = ["trace"] } +tower-http = { version = "0.6.6", features = ["trace", "compression-full"] } utoipa = "5.4.0" utoipa-swagger-ui = { version = "9.0.2", features = [ "axum", diff --git a/crates/macro/macro-assets/src/lib.rs b/crates/macro/macro-assets/src/lib.rs index 4445f3a..dd7c21c 100644 --- a/crates/macro/macro-assets/src/lib.rs +++ b/crates/macro/macro-assets/src/lib.rs @@ -142,8 +142,18 @@ pub fn assets(input: TokenStream) -> TokenStream { quote! { #[doc = #router_doc] pub fn #router_name() -> (&'static str, ::axum::Router<()>) { + use ::tower_http::compression::{CompressionLayer, DefaultPredicate}; + + let compression: CompressionLayer = CompressionLayer::new() + .br(true) + .deflate(true) + .gzip(true) + .zstd(true) + .compress_when(DefaultPredicate::new()); + let router = ::axum::Router::new() - #(#route_definitions)*; + #(#route_definitions)* + .layer(compression); (#prefix, router) } } @@ -240,27 +250,39 @@ impl Parse for AssetDefinition { match field_name.to_string().as_str() { "source" => { if source.is_some() { - return Err(syn::Error::new(field_name.span(), "duplicate 'source' field")); + return Err(syn::Error::new( + field_name.span(), + "duplicate 'source' field", + )); } source = Some(content.parse()?); } "target" => { if target.is_some() { - return Err(syn::Error::new(field_name.span(), "duplicate 'target' field")); + return Err(syn::Error::new( + field_name.span(), + "duplicate 'target' field", + )); } let target_lit: LitStr = content.parse()?; target = Some(target_lit.value()); } "headers" => { if headers.is_some() { - return Err(syn::Error::new(field_name.span(), "duplicate 'headers' field")); + return Err(syn::Error::new( + field_name.span(), + "duplicate 'headers' field", + )); } headers = Some(content.parse()?); } _ => { return Err(syn::Error::new( field_name.span(), - format!("unknown field '{}', expected 'source', 'target', or 'headers'", field_name) + format!( + "unknown field '{}', expected 'source', 'target', or 'headers'", + field_name + ), )); } } @@ -272,8 +294,10 @@ impl Parse for AssetDefinition { } // Validate required fields - let source = source.ok_or_else(|| syn::Error::new(name.span(), "missing required field 'source'"))?; - let target = target.ok_or_else(|| syn::Error::new(name.span(), "missing required field 'target'"))?; + let source = source + .ok_or_else(|| syn::Error::new(name.span(), "missing required field 'source'"))?; + let target = target + .ok_or_else(|| syn::Error::new(name.span(), "missing required field 'target'"))?; Ok(AssetDefinition { name, diff --git a/crates/service/service-webpage/Cargo.toml b/crates/service/service-webpage/Cargo.toml index 7a0bd17..41536c8 100644 --- a/crates/service/service-webpage/Cargo.toml +++ b/crates/service/service-webpage/Cargo.toml @@ -25,4 +25,6 @@ lazy_static = { workspace = true } serde_yaml = { workspace = true } serde = { workspace = true } reqwest = { workspace = true } +lru = { workspace = true } +tower-http = { workspace = true } tokio = { workspace = true } diff --git a/crates/service/service-webpage/src/page/mod.rs b/crates/service/service-webpage/src/page/mod.rs index 3b0aedd..2491e12 100644 --- a/crates/service/service-webpage/src/page/mod.rs +++ b/crates/service/service-webpage/src/page/mod.rs @@ -11,16 +11,13 @@ use axum::{ }; use chrono::{DateTime, TimeDelta, Utc}; use libservice::ServiceConnectInfo; +use lru::LruCache; use markdown_it::Node; use maud::{Markup, PreEscaped, Render, html}; -use parking_lot::{Mutex, RwLock}; +use parking_lot::Mutex; use serde::Deserialize; -use std::{ - collections::HashMap, - pin::Pin, - sync::Arc, - time::{Duration, Instant}, -}; +use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant}; +use tower_http::compression::{CompressionLayer, DefaultPredicate}; use tracing::{trace, warn}; use crate::components::{ @@ -95,6 +92,11 @@ impl PageMetadata { // MARK: page // +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RequestContext { + pub client_info: ClientInfo, +} + // Some HTML pub struct Page { pub meta: PageMetadata, @@ -114,7 +116,10 @@ pub struct Page { pub generate_html: Box< dyn Send + Sync - + for<'a> Fn(&'a Page) -> Pin + 'a + Send + Sync>>, + + for<'a> Fn( + &'a Page, + &'a RequestContext, + ) -> Pin + 'a + Send + Sync>>, >, } @@ -125,14 +130,14 @@ impl Default for Page { html_ttl: Some(TimeDelta::seconds(60 * 24 * 30)), //css_ttl: Duration::from_secs(60 * 24 * 30), //generate_css: None, - generate_html: Box::new(|_| Box::pin(async { html!() })), + generate_html: Box::new(|_, _| Box::pin(async { html!() })), } } } impl Page { - pub async fn generate_html(&self) -> Markup { - (self.generate_html)(self).await + pub async fn generate_html(&self, req_info: &RequestContext) -> Markup { + (self.generate_html)(self, req_info).await } pub fn from_markdown(md: impl Into, default_image: Option) -> Self { @@ -154,7 +159,7 @@ impl Page { Page { meta, - generate_html: Box::new(move |page| { + generate_html: Box::new(move |page, _| { let html = html.clone(); Box::pin(async move { html! { @@ -176,6 +181,13 @@ impl Page { // MARK: server // +// Rerender considerations: +// - rerendering often in the background is wasteful. Maybe we should fall asleep? +// - rerendering on request is slow +// - rerendering in the background after a request could be a good idea. Maybe implement? +// +// - cached pages only make sense for static assets. +// - user pages can't be pre-rendered! pub struct PageServer { /// If true, expired pages will be rerendered before being sent to the user. /// If false, requests never trigger rerenders. We rely on the rerender task. @@ -190,7 +202,7 @@ pub struct PageServer { /// Map of `{ route: (page data, expire time) }` /// /// We use an LruCache for bounded memory usage. - html_cache: RwLock)>>, + html_cache: Mutex)>>, /// Called whenever we need to render a page. /// - this method should call `page.generate_html()`, @@ -200,7 +212,10 @@ pub struct PageServer { render_page: Box< dyn Send + Sync - + for<'a> Fn(&'a Page) -> Pin + 'a + Send + Sync>>, + + for<'a> Fn( + &'a Page, + &'a RequestContext, + ) -> Pin + 'a + Send + Sync>>, >, } @@ -209,12 +224,18 @@ impl PageServer { render_page: Box< dyn Send + Sync - + for<'a> Fn(&'a Page) -> Pin + 'a + Send + Sync>>, + + for<'a> Fn( + &'a Page, + &'a RequestContext, + ) -> Pin + 'a + Send + Sync>>, >, ) -> Arc { + #[expect(clippy::unwrap_used)] + let cache_size = NonZero::new(128).unwrap(); + Arc::new(Self { pages: Arc::new(Mutex::new(HashMap::new())), - html_cache: RwLock::new(HashMap::new()), + html_cache: Mutex::new(LruCache::new(cache_size)), render_page, never_rerender_on_request: true, }) @@ -236,7 +257,12 @@ impl PageServer { /// Does nothing if there is no page at `route`. /// /// Returns the rendered page's content. - async fn render_page(&self, reason: &'static str, route: &str) -> Option { + async fn render_page( + &self, + reason: &'static str, + route: &str, + req_ctx: RequestContext, + ) -> Option<(String, Option>)> { let now = Utc::now(); let start = Instant::now(); trace!(message = "Rendering page", route, reason); @@ -249,50 +275,19 @@ impl PageServer { } }; - let html = (self.render_page)(&page).await.0; + let html = (self.render_page)(&page, &req_ctx).await.0; + let mut expires = None; if let Some(ttl) = page.html_ttl { + expires = Some(now + ttl); self.html_cache - .write() - .insert(route.to_owned(), (html.clone(), now + ttl)); + .lock() + .put((route.to_owned(), req_ctx), (html.clone(), now + ttl)); } let elapsed = start.elapsed().as_millis(); trace!(message = "Rendered page", route, reason, time_ms = elapsed); - return Some(html); - } - - // Rerender considerations: - // - rerendering often in the background is wasteful. Maybe we should fall asleep? - // - rerendering on request is slow - // - rerendering in the background after a request could be a good idea. Maybe implement? - // - // - cached pages only make sense for static assets. - // - user pages can't be pre-rendered! - pub async fn start_rerender_task(self: Arc, interval: Duration) { - loop { - tokio::time::sleep(interval).await; - - let now = Utc::now(); - let pages = self - .pages - .lock() - .iter() - .filter(|(_, v)| v.html_ttl.is_some()) - .map(|(k, _)| k.clone()) - .collect::>(); - - for route in pages { - let needs_render = match self.html_cache.read().get(&route) { - Some(x) => x.1 < now, // Expired - None => true, // Never rendered - }; - - if needs_render { - self.render_page("rerender_task", &route).await; - } - } - } + return Some((html, expires)); } async fn handler( @@ -301,30 +296,74 @@ impl PageServer { ConnectInfo(addr): ConnectInfo, headers: HeaderMap, ) -> Response { - trace!(message = "Serving route", route, addr = ?addr.addr, user_agent = ?headers["user-agent"]); + let client_info = ClientInfo::from_headers(&headers); + let ua = headers + .get("user-agent") + .and_then(|x| x.to_str().ok()) + .unwrap_or(""); + trace!( + message = "Serving route", + route, + addr = ?addr.addr, + user_agent = ua, + device_type = ?client_info.device_type + ); + + let req_ctx = RequestContext { client_info }; + + let cache_key = (route.clone(), req_ctx.clone()); let now = Utc::now(); - let headers = [( - header::CONTENT_TYPE, - HeaderValue::from_static("text/html; charset=utf-8"), - )]; + let mut html_expires = None; - if let Some((html, expires)) = state.html_cache.read().get(&route) + // Get from cache, if availablee + if let Some((html, expires)) = state.html_cache.lock().get(&cache_key) && (*expires > now || state.never_rerender_on_request) { - // TODO: no clone? - return (headers, html.clone()).into_response(); + html_expires = Some((html.clone(), Some(*expires))); }; - let html = match state.render_page("request", &route).await { - Some(x) => x.clone(), - None => return (StatusCode::NOT_FOUND, "page doesn't exist").into_response(), + if html_expires.is_none() { + html_expires = match state.render_page("request", &route, req_ctx).await { + Some(x) => Some(x.clone()), + None => return (StatusCode::NOT_FOUND, "page doesn't exist").into_response(), + }; + } + + #[expect(clippy::unwrap_used)] + let (html, expires) = html_expires.unwrap(); + + let mut headers = HeaderMap::with_capacity(3); + headers.append( + header::CONTENT_TYPE, + HeaderValue::from_static("text/html; charset=utf-8"), + ); + + let max_age = match expires { + Some(expires) => (expires - now).num_seconds().max(1), + None => 1, }; + #[expect(clippy::unwrap_used)] + headers.append( + header::CACHE_CONTROL, + // immutable; public/private + HeaderValue::from_str(&format!("immutable, public, max-age={}", max_age)).unwrap(), + ); + + headers.append("Accept-CH", HeaderValue::from_static("Sec-CH-UA-Mobile")); + return (headers, html).into_response(); } pub fn into_router(self: Arc) -> Router<()> { + let compression: CompressionLayer = CompressionLayer::new() + .br(true) + .deflate(true) + .gzip(true) + .zstd(true) + .compress_when(DefaultPredicate::new()); + Router::new() .route( "/", @@ -333,6 +372,57 @@ impl PageServer { }), ) .route("/{*path}", get(Self::handler)) + .layer(compression) .with_state(self) } } + +// +// MARK: UserAgent +// + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DeviceType { + Mobile, + Desktop, +} + +impl Default for DeviceType { + fn default() -> Self { + Self::Desktop + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ClientInfo { + /// This is an estimate, but it's probably good enough. + pub device_type: DeviceType, +} + +impl ClientInfo { + pub fn from_headers(headers: &HeaderMap) -> Self { + let ua = headers + .get("user-agent") + .and_then(|x| x.to_str().ok()) + .unwrap_or(""); + + let ch_mobile = headers + .get("Sec-CH-UA-Mobile") + .and_then(|x| x.to_str().ok()) + .unwrap_or(""); + + let mut device_type = None; + + if device_type.is_none() && ch_mobile.contains("1") { + device_type = Some(DeviceType::Mobile); + } + + if device_type.is_none() && ua.contains("Mobile") { + device_type = Some(DeviceType::Mobile); + } + + Self { + device_type: device_type.unwrap_or_default(), + } + } +} diff --git a/crates/service/service-webpage/src/pages/handouts.rs b/crates/service/service-webpage/src/pages/handouts.rs index 52f382d..672ea76 100644 --- a/crates/service/service-webpage/src/pages/handouts.rs +++ b/crates/service/service-webpage/src/pages/handouts.rs @@ -1,8 +1,13 @@ -use std::time::Instant; +use std::{ + pin::Pin, + sync::Arc, + time::{Duration, Instant}, +}; use assetserver::Asset; -use chrono::TimeDelta; +use chrono::{DateTime, TimeDelta, Utc}; use maud::{Markup, PreEscaped, html}; +use parking_lot::Mutex; use serde::Deserialize; use tracing::{debug, warn}; @@ -11,7 +16,7 @@ use crate::{ md::Markdown, misc::{Backlinks, FarLink}, }, - page::{Page, PageMetadata}, + page::{DeviceType, Page, PageMetadata, RequestContext}, routes::assets::Image_Icon, }; @@ -23,6 +28,66 @@ struct HandoutEntry { solutions: Option, } +struct CachedRequestInner { + last_fetch: DateTime, + last_value: Option>, +} + +pub struct CachedRequest { + inner: Mutex>, + ttl: TimeDelta, + get: Box Pin + Send + Sync>> + Send + Sync>, +} + +impl CachedRequest { + pub fn new( + ttl: TimeDelta, + get: Box Pin + Send + Sync>> + Send + Sync>, + ) -> Arc { + Arc::new(Self { + get, + ttl, + inner: Mutex::new(CachedRequestInner { + last_fetch: Utc::now(), + last_value: None, + }), + }) + } + + pub async fn get(self: Arc) -> Arc { + let now = Utc::now(); + let expires = self.inner.lock().last_fetch + self.ttl; + + if now < expires + && let Some(last_value) = self.inner.lock().last_value.clone() + { + return last_value; + } + + let res = Arc::new((self.get)().await); + + let mut inner = self.inner.lock(); + inner.last_fetch = now; + inner.last_value = Some(res.clone()); + return res; + } + + pub async fn autoget(self: Arc, interval: Duration) { + loop { + { + let now = Utc::now(); + let res = Arc::new((self.get)().await); + + let mut inner = self.inner.lock(); + inner.last_fetch = now; + inner.last_value = Some(res.clone()); + } + + tokio::time::sleep(interval).await; + } + } +} + async fn get_index() -> Result, reqwest::Error> { let start = Instant::now(); let res = reqwest::get( @@ -49,28 +114,59 @@ async fn get_index() -> Result, reqwest::Error> { return Ok(res); } -fn build_list_for_group(handouts: &[HandoutEntry], group: &str) -> Markup { - html! { - ul class="handout-ul" { +fn build_list_for_group( + handouts: &[HandoutEntry], + group: &str, + req_ctx: &RequestContext, +) -> Markup { + let mobile = req_ctx.client_info.device_type == DeviceType::Mobile; - @for h in handouts { - @if h.group ==group { - li { - span class="handdout-li-title" { - strong { (h.title) } - } - " " - span class="handout-li-links" { - "[ " + if mobile { + html! { + ul class="handout-ul" { + + @for h in handouts { + @if h.group ==group { + li { + span class="handout-li-title" { + a href=(h.handout) class="underline-link" { + strong style="text-decoration: underline;text-underline-offset:1.5pt;color:var(--fgColor);" { (h.title) } + } + } @if let Some(solutions) = &h.solutions { - a href=(h.handout) {"handout"} - " | " - a href=(solutions) {"solutions"} - } @else { - a href=(h.handout) {"handout"} + " [" + a href=(solutions) { "sols" } + "]" + } + } + } + } + } + } + } else { + html! { + ul class="handout-ul" { + + @for h in handouts { + @if h.group ==group { + li { + span class="handout-li-title" { + strong { (h.title) } + } + " " + span class="handout-li-links" { + "[ " + + @if let Some(solutions) = &h.solutions { + a href=(h.handout) {"handout"} + " | " + a href=(solutions) {"solutions"} + } @else { + a href=(h.handout) {"handout"} + } + " ]" } - " ]" } } } @@ -86,6 +182,13 @@ fn build_list_for_group(handouts: &[HandoutEntry], group: &str) -> Markup { pub fn handouts() -> Page { let md = Markdown::parse(include_str!("handouts.md")); + let index = CachedRequest::new( + TimeDelta::minutes(30), + Box::new(|| Box::pin(async move { get_index().await })), + ); + + tokio::spawn(index.clone().autoget(Duration::from_secs(60 * 20))); + #[expect(clippy::unwrap_used)] let mut meta = PageMetadata::from_markdown_frontmatter(&md) .unwrap() @@ -101,44 +204,36 @@ pub fn handouts() -> Page { meta, html_ttl: Some(TimeDelta::seconds(300)), - generate_html: Box::new(move |page| { + generate_html: Box::new(move |page, req_ctx| { let html = html.clone(); // TODO: find a way to not clone here + let index = index.clone(); Box::pin(async move { - let handouts = get_index().await; + let handouts = index.get().await; - let warmups = match &handouts { - Ok(handouts) => build_list_for_group(handouts, "Warm-Ups"), + let fallback = html! { + span style="color:var(--yellow)" { + "Could not load handouts, something broke." + } + " " + ( + FarLink( + "https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest", + "Try this direct link." + ) + ) + }; + + let warmups = match &*handouts { + Ok(handouts) => build_list_for_group(handouts, "Warm-Ups", req_ctx), Err(error) => { warn!("Could not load handout index: {error:?}"); - html! { - span style="color:var(--yellow)" { - "Could not load handouts, something broke." - } - " " - ( - FarLink( - "https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest", - "Try this direct link." - ) - ) - } + fallback.clone() } }; - let advanced = match &handouts { - Ok(handouts) => build_list_for_group(handouts, "Advanced"), - Err(_) => html! { - span style="color:var(--yellow)" { - "Could not load handouts, something broke." - } - " " - ( - FarLink( - "https://git.betalupi.com/Mark/-/packages/generic/ormc-handouts/latest", - "Try this direct link." - ) - ) - }, + let advanced = match &*handouts { + Ok(handouts) => build_list_for_group(handouts, "Advanced", req_ctx), + Err(_) => fallback, }; html! { diff --git a/crates/service/service-webpage/src/pages/index.rs b/crates/service/service-webpage/src/pages/index.rs index 438b6fd..900d272 100644 --- a/crates/service/service-webpage/src/pages/index.rs +++ b/crates/service/service-webpage/src/pages/index.rs @@ -22,7 +22,7 @@ pub fn index() -> Page { slug: None, }, - generate_html: Box::new(move |_page| { + generate_html: Box::new(move |_page, _| { Box::pin(async { html! { h2 id="about" { "About" } diff --git a/crates/service/service-webpage/src/routes/mod.rs b/crates/service/service-webpage/src/routes/mod.rs index 0acdee2..475da44 100644 --- a/crates/service/service-webpage/src/routes/mod.rs +++ b/crates/service/service-webpage/src/routes/mod.rs @@ -1,4 +1,4 @@ -use std::{pin::Pin, sync::Arc, time::Duration}; +use std::{pin::Pin, sync::Arc}; use assetserver::Asset; use axum::Router; @@ -7,7 +7,7 @@ use tracing::info; use crate::{ components::misc::FarLink, - page::{Page, PageServer}, + page::{Page, PageServer, RequestContext}, pages, routes::assets::Styles_Main, }; @@ -18,9 +18,7 @@ pub(super) fn router() -> Router<()> { let (asset_prefix, asset_router) = assets::asset_router(); info!("Serving assets at {asset_prefix}"); - let server = build_server(); - tokio::task::spawn(server.clone().start_rerender_task(Duration::from_secs(3))); - let router = server.into_router(); + let router = build_server().into_router(); Router::new().merge(router).nest(asset_prefix, asset_router) } @@ -35,7 +33,10 @@ fn build_server() -> Arc { server } -fn page_wrapper<'a>(page: &'a Page) -> Pin + 'a + Send + Sync>> { +fn page_wrapper<'a>( + page: &'a Page, + req_ctx: &'a RequestContext, +) -> Pin + 'a + Send + Sync>> { Box::pin(async move { html! { (DOCTYPE) @@ -54,7 +55,7 @@ fn page_wrapper<'a>(page: &'a Page) -> Pin + 'a body { div class="wrapper" { - main { ( page.generate_html().await ) } + main { ( page.generate_html(req_ctx).await ) } footer { hr class = "footline" {}