use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use worker::{Method, Request, Response};
use crate::fragments::diff_html;
use crate::render::Render;
use crate::state::{decode_state, state_init_script};
pub trait Reducer: Serialize + DeserializeOwned + Clone {
type Action: Serialize + DeserializeOwned;
fn initial() -> Self;
fn reduce(&self, action: Self::Action) -> Self;
}
#[derive(Deserialize)]
struct DispatchRequest<A> {
_state: Option<String>,
#[serde(flatten)]
action: Option<A>,
}
pub fn dispatch<S, V>(body: &str) -> Option<String>
where
S: Reducer,
V: Render + From<S>,
{
let parsed: DispatchRequest<S::Action> = serde_json::from_str(body).ok()?;
let old_state = parsed
._state
.as_deref()
.and_then(decode_state::<S>)
.unwrap_or_else(S::initial);
let action = parsed.action?;
let new_state = old_state.reduce(action);
let old_html = Render::render(V::from(old_state));
let new_html = Render::render(V::from(new_state.clone()));
Some(diff_html(&old_html, &new_html, &new_state))
}
pub async fn handle<S, V>(mut req: Request) -> worker::Result<Response>
where
S: Reducer,
V: Render + From<S>,
{
let is_htmx_post = req.method() == Method::Post
&& req.headers().get("HX-Request").ok().flatten().is_some();
if is_htmx_post {
let body = req.text().await?;
match dispatch::<S, V>(&body) {
Some(html) => Response::from_html(html),
None => Response::ok(""),
}
} else {
let state = S::initial();
let mut html = Render::render(V::from(state.clone()));
if let Some(pos) = html.rfind("</body>") {
html.insert_str(pos, &state_init_script(&state));
}
Response::from_html(html)
}
}