Skip to main content

workers_rsx/
dispatch.rs

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
9/// Trait for a reducer that knows how to initialize its state
10/// and reduce actions.
11pub 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
25/// Generic htmx dispatch: decode state → extract action → reduce → diff → return HTML.
26///
27/// Returns `Some(html)` with the OOB diff fragments, or `None` if no action was found.
28pub 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
50/// Handle an HTTP request for an htmx app.
51///
52/// - POST with `HX-Request` header: parses the JSON body, dispatches the action, returns diff HTML.
53/// - Everything else: returns a full-page render from `S::initial()`.
54///
55/// ```ignore
56/// #[event(fetch)]
57/// pub async fn main(req: Request, _env: Env, _ctx: Context) -> Result<Response> {
58///     handle::<PageState, TodosPage>(req).await
59/// }
60/// ```
61pub 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        // Inject state script before </body>
79        if let Some(pos) = html.rfind("</body>") {
80            html.insert_str(pos, &state_init_script(&state));
81        }
82        Response::from_html(html)
83    }
84}