From e855b6f7315b1f2db6a5c3a5a46fffeeb15251b0 Mon Sep 17 00:00:00 2001 From: Martin Hafskjold Thoresen Date: Sat, 4 Jan 2025 17:02:57 +0100 Subject: POC async timeout for a thing --- src/main.rs | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 0cc864f..e164981 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, net::SocketAddr, sync::Arc}; +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use anyhow::{Context, Result}; use axum::{ @@ -10,10 +10,11 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, routing::{get, post}, - Router, + Form, Router, }; use maud::{html, Markup, PreEscaped, DOCTYPE}; use rand::random; +use serde::Deserialize; use tokio::sync::{ broadcast::{self, Receiver, Sender}, Mutex, @@ -23,7 +24,7 @@ use tower_http::{ services::ServeDir, trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}, }; -use tracing::{error, info, trace, Level}; +use tracing::{error, info, trace, warn, Level}; use tracing_subscriber::FmtSubscriber; /// If a handler panics we run this function and return the [Response]. @@ -40,11 +41,31 @@ fn handle_panic(err: Box) -> Response { (StatusCode::INTERNAL_SERVER_ERROR).into_response() } +#[derive(Deserialize)] +struct NoteDatum { + // Epoch time in ms + time: u64, + // Fraction up/down + y: f64, +} + +#[derive(Deserialize)] +struct Notes { + notes: Vec, +} + +#[derive(Deserialize)] +struct NotesForm { + notes: String, +} + struct Game { id: u64, is_started: bool, + submitted: Option, + /// Sender to broadcast things broadcast_tx: Sender, /// Broadcast receiver to send to WebSocket @@ -52,24 +73,66 @@ struct Game { } impl Game { + /// Return the appropriate HTML for the game state. fn get_html(&self) -> Markup { let gid = self.id; - if !self.is_started { + + if self.submitted.is_some() { return html! { - h1 { "Game" } - p { "Current game id is " (gid)} - div #messages {}; - button hx-post=(format!("/game/{gid}/start")) { "Start" } + h1 { "Submitted!" } }; - } else { + } + if self.is_started { return html! { + script { ({PreEscaped("document.notes = []; ")})} h1 { "Playing Game" } p { "Were playing here" } + + canvas + #canvas + height=(256) width=(256) + style="position:relative; background: cyan;" + {}; + script { (PreEscaped(r#"document.getElementById("canvas").onclick = (e) => { + const notes = JSON.parse(e.target.dataset.notes ?? "[]"); + notes.push({ time: Date.now(), y: e.layerY / e.target.height}); + e.target.dataset.notes = JSON.stringify(notes); + }"#))} + + button hx-post=(format!("/game/{gid}/submit")) + hx-vals=r#"js:{ "notes": document.getElementById("canvas").dataset.notes }"# + { "Submit" } }; } + + html! { + h1 { "Game" } + p { "Current game id is " (gid)} + div #messages {}; + button hx-post=(format!("/game/{gid}/start")) { "Start" } + } } } +async fn submit_game_answer(lock: Arc>, notes: Notes) { + lock.lock().await.submitted = Some(notes); + + tokio::spawn(async move { + let _ = tokio::time::sleep(Duration::from_secs(2)).await; + let mut g = lock.lock().await; + g.is_started = false; + g.submitted = None; + g.broadcast_tx.send( + html! { + section #content { + (g.get_html()) + } + } + .into_string(), + ) + }); +} + struct ServerState { games: HashMap>>, } @@ -80,6 +143,7 @@ impl ServerState { let g = Game { id: gid, is_started: false, + submitted: None, broadcast_rx: rx, broadcast_tx: tx, }; @@ -134,7 +198,7 @@ fn html_response(html: PreEscaped) -> Response { .expect("Failed to set body") } -async fn post_game(State(st): State) -> Response { +async fn create_game(State(st): State) -> Response { let gid = random::(); let _ = { @@ -201,6 +265,35 @@ async fn get_game(Path(gid): Path, State(st): State) -> Response { }) } +async fn submit_game( + Path(gid): Path, + State(st): State, + Form(form): Form, +) -> Response { + let glock = { + if let Some(g) = st.lock().await.games.get(&gid) { + g.clone() + } else { + return (StatusCode::NOT_FOUND).into_response(); + } + }; + + let Ok(notes) = serde_json::from_str(&form.notes) else { + warn!(notes = form.notes); + return (StatusCode::UNPROCESSABLE_ENTITY).into_response(); + }; + + submit_game_answer(glock.clone(), Notes { notes }).await; + + let g = glock.lock().await; + let html = g.get_html(); + g.broadcast_tx + .send(html! { section #content { (html) } }.into_string()) + .expect("failed to send"); + + StatusCode::OK.into_response() +} + async fn game_ws(ws: WebSocketUpgrade, Path(gid): Path, State(st): State) -> Response { let game = { let s = st.lock().await; @@ -262,9 +355,10 @@ async fn main() -> Result<()> { let app = Router::new() .route("/", get(route_index)) - .route("/game", post(post_game)) + .route("/game", post(create_game)) .route("/game/{gid}", get(get_game)) .route("/game/{gid}/start", post(start_game)) + .route("/game/{gid}/submit", post(submit_game)) .route("/game/{gid}/ws", get(game_ws)) .nest_service("/static", ServeDir::new("static")) .layer(CatchPanicLayer::custom(handle_panic)) -- cgit v1.2.3