use std::sync::{Arc, Mutex};
use miette::Diagnostic;
use tiny_http::{Header, Request};
use super::assets::{http_response, HttpResponse};
use super::limiter::{conversions, lock};
use crate::commands::body::{preview, preview_config, BodyPreview};
const JSON: &str = "application/json";
const TEXT: &str = "text/plain; charset=utf-8";
const GLB: &str = "model/gltf-binary";
type Generated = Result<Arc<BodyPreview>, String>;
#[derive(Default)]
struct Current {
json: Option<Arc<str>>,
revision: String,
board: Option<String>,
}
#[derive(Clone, Default)]
pub struct BodyCache {
current: Arc<Mutex<Current>>,
slot: Arc<Mutex<Option<(String, Generated)>>>,
}
impl BodyCache {
pub fn update(&self, json: Option<String>, revision: String, board: Option<String>) {
*lock(&self.current) = Current {
json: json.map(Arc::from),
revision,
board,
};
}
pub fn serve(&self, request: Request, glb: bool) {
let board = lock(&self.current).board.clone();
let _ = request.respond(self.response(glb, |json| generate(json, board.as_deref())));
}
pub(super) fn resolve_with(
&self,
generate: impl FnOnce(&str) -> Generated,
) -> Option<(String, Generated)> {
let mut slot = lock(&self.slot);
let (json, revision) = {
let current = lock(&self.current);
(current.json.clone()?, current.revision.clone())
};
if let Some((cached, generated)) = slot.as_ref().filter(|(r, _)| *r == revision) {
return Some((cached.clone(), generated.clone()));
}
let generated = generate(&json);
*slot = Some((revision.clone(), generated.clone()));
Some((revision, generated))
}
pub(super) fn response(
&self,
glb: bool,
generate: impl FnOnce(&str) -> Generated,
) -> HttpResponse {
let Some((revision, generated)) = self.resolve_with(generate) else {
let body = serde_json::json!({ "error": "no circuit loaded" }).to_string();
return no_store(http_response(503, JSON, body.into_bytes()));
};
no_store(match (generated, glb) {
(Ok(p), true) => http_response(200, GLB, p.glb.clone()),
(Ok(p), false) => http_response(200, JSON, meta_json(&revision, &p).into_bytes()),
(Err(e), true) => http_response(422, TEXT, e.into_bytes()),
(Err(e), false) => {
let body = serde_json::json!({ "revision": revision, "error": e }).to_string();
http_response(422, JSON, body.into_bytes())
}
})
}
}
fn generate(json: &str, board: Option<&str>) -> Generated {
let _slot = conversions().acquire();
preview(json, preview_config(board))
.map(Arc::new)
.map_err(|e| match e.help() {
Some(help) => format!("{e}: {help}"),
None => e.to_string(),
})
}
pub(super) fn meta_json(revision: &str, p: &BodyPreview) -> String {
serde_json::json!({
"revision": revision,
"board_z": p.board_z,
"board_thickness": p.board_thickness,
"height": p.height,
"wall": p.wall,
"size": p.size,
})
.to_string()
}
pub(super) fn no_store(response: HttpResponse) -> HttpResponse {
let header = Header::from_bytes(&b"Cache-Control"[..], &b"no-store"[..]).expect("valid header");
response.with_header(header)
}