Headers, cache tweaks
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||
+ for<'a> Fn(
|
||||
&'a Page,
|
||||
&'a RequestContext,
|
||||
) -> Pin<Box<dyn Future<Output = Markup> + '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<String>, default_image: Option<String>) -> 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<HashMap<String, (String, DateTime<Utc>)>>,
|
||||
html_cache: Mutex<LruCache<(String, RequestContext), (String, DateTime<Utc>)>>,
|
||||
|
||||
/// 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<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||
+ for<'a> Fn(
|
||||
&'a Page,
|
||||
&'a RequestContext,
|
||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||
>,
|
||||
}
|
||||
|
||||
@@ -209,12 +224,18 @@ impl PageServer {
|
||||
render_page: Box<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ for<'a> Fn(&'a Page) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||
+ for<'a> Fn(
|
||||
&'a Page,
|
||||
&'a RequestContext,
|
||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>>,
|
||||
>,
|
||||
) -> Arc<Self> {
|
||||
#[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<String> {
|
||||
async fn render_page(
|
||||
&self,
|
||||
reason: &'static str,
|
||||
route: &str,
|
||||
req_ctx: RequestContext,
|
||||
) -> Option<(String, Option<DateTime<Utc>>)> {
|
||||
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<Self>, 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::<Vec<_>>();
|
||||
|
||||
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<ServiceConnectInfo>,
|
||||
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<Self>) -> 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
struct CachedRequestInner<T> {
|
||||
last_fetch: DateTime<Utc>,
|
||||
last_value: Option<Arc<T>>,
|
||||
}
|
||||
|
||||
pub struct CachedRequest<T> {
|
||||
inner: Mutex<CachedRequestInner<T>>,
|
||||
ttl: TimeDelta,
|
||||
get: Box<dyn Fn() -> Pin<Box<dyn Future<Output = T> + Send + Sync>> + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<T> CachedRequest<T> {
|
||||
pub fn new(
|
||||
ttl: TimeDelta,
|
||||
get: Box<dyn Fn() -> Pin<Box<dyn Future<Output = T> + Send + Sync>> + Send + Sync>,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
get,
|
||||
ttl,
|
||||
inner: Mutex::new(CachedRequestInner {
|
||||
last_fetch: Utc::now(),
|
||||
last_value: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get(self: Arc<Self>) -> Arc<T> {
|
||||
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<Self>, 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<Vec<HandoutEntry>, reqwest::Error> {
|
||||
let start = Instant::now();
|
||||
let res = reqwest::get(
|
||||
@@ -49,28 +114,59 @@ async fn get_index() -> Result<Vec<HandoutEntry>, 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! {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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<PageServer> {
|
||||
server
|
||||
}
|
||||
|
||||
fn page_wrapper<'a>(page: &'a Page) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>> {
|
||||
fn page_wrapper<'a>(
|
||||
page: &'a Page,
|
||||
req_ctx: &'a RequestContext,
|
||||
) -> Pin<Box<dyn Future<Output = Markup> + 'a + Send + Sync>> {
|
||||
Box::pin(async move {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
@@ -54,7 +55,7 @@ fn page_wrapper<'a>(page: &'a Page) -> Pin<Box<dyn Future<Output = Markup> + 'a
|
||||
|
||||
body {
|
||||
div class="wrapper" {
|
||||
main { ( page.generate_html().await ) }
|
||||
main { ( page.generate_html(req_ctx).await ) }
|
||||
|
||||
footer {
|
||||
hr class = "footline" {}
|
||||
|
||||
Reference in New Issue
Block a user