1use serde::de::DeserializeOwned;
2use serde::{Deserialize, Serialize};
3use worker::{Method, Request, Response};
4
5use crate::fragments::diff_html;
6use crate::render::Render;
7use crate::state::{decode_state, state_init_script};
8
9pub trait Reducer: Serialize + DeserializeOwned + Clone {
12 type Action: Serialize + DeserializeOwned;
13
14 fn initial() -> Self;
15 fn reduce(&self, action: Self::Action) -> Self;
16}
17
18#[derive(Deserialize)]
19struct DispatchRequest<A> {
20 _state: Option<String>,
21 #[serde(flatten)]
22 action: Option<A>,
23}
24
25pub fn dispatch<S, V>(body: &str) -> Option<String>
29where
30 S: Reducer,
31 V: Render + From<S>,
32{
33 let parsed: DispatchRequest<S::Action> = serde_json::from_str(body).ok()?;
34
35 let old_state = parsed
36 ._state
37 .as_deref()
38 .and_then(decode_state::<S>)
39 .unwrap_or_else(S::initial);
40
41 let action = parsed.action?;
42
43 let new_state = old_state.reduce(action);
44 let old_html = Render::render(V::from(old_state));
45 let new_html = Render::render(V::from(new_state.clone()));
46
47 Some(diff_html(&old_html, &new_html, &new_state))
48}
49
50pub async fn handle<S, V>(mut req: Request) -> worker::Result<Response>
62where
63 S: Reducer,
64 V: Render + From<S>,
65{
66 let is_htmx_post = req.method() == Method::Post
67 && req.headers().get("HX-Request").ok().flatten().is_some();
68
69 if is_htmx_post {
70 let body = req.text().await?;
71 match dispatch::<S, V>(&body) {
72 Some(html) => Response::from_html(html),
73 None => Response::ok(""),
74 }
75 } else {
76 let state = S::initial();
77 let mut html = Render::render(V::from(state.clone()));
78 if let Some(pos) = html.rfind("</body>") {
80 html.insert_str(pos, &state_init_script(&state));
81 }
82 Response::from_html(html)
83 }
84}