use std::{ collections::HashMap, sync::{ atomic::{AtomicUsize, Ordering}, mpsc::{self, Sender}, Arc, Mutex, }, thread, }; use indoc::formatdoc; use touche::{body::HttpBody, Body, Method, Request, Response, Server, StatusCode}; #[derive(Debug)] enum Event { User(usize), Message(usize, String), } type Users = Arc>>>; fn main() -> std::io::Result<()> { static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); let users: Users = Arc::new(Mutex::new(HashMap::new())); Server::bind("0.0.0.0:4444").serve(move |req: Request| { let users = users.clone(); match req.uri().path() { "/messages" if req.method() == Method::POST => { let user_id = req .headers() .get("x-user-id") .and_then(|h| h.to_str().ok()) .and_then(|user_id| user_id.parse::().ok()); match user_id { Some(user_id) => { let text = req.into_body().into_bytes().unwrap_or_default(); let text = std::str::from_utf8(&text).unwrap_or_default(); users.lock().unwrap().retain(|id, tx| { if user_id != *id { tx.send(Event::Message(user_id, text.to_string())).is_ok() } else { true } }); Response::builder() .status(StatusCode::CREATED) .body(Body::empty()) } None => Response::builder() .status(StatusCode::UNAUTHORIZED) .body("Missing x-user-id header".into()), } } "/messages" => { let user_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); let (tx, rx) = mpsc::channel(); tx.send(Event::User(user_id)).unwrap(); users.lock().unwrap().insert(user_id, tx); let (sender, body) = Body::channel(); thread::spawn(move || { for event in rx { let message = match event { Event::User(id) => formatdoc! {r#" event: user data: {{"id": "{id}"}} "#}, Event::Message(user_id, text) => formatdoc! {r#" event: message data: {{"userId": {user_id}, "message": "{text}"}} "#}, }; if let Err(_err) = sender.send(message) { break; } } }); Response::builder() .status(StatusCode::OK) .header("content-type", "text/event-stream") .header("connection", "close") .body(body) } "/" => Response::builder() .status(StatusCode::OK) .header("content-type", "text/html") .body(include_str!("sse-chat.html").into()), _ => Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::empty()), } }) }