use axum::{
Router,
extract::{
State, WebSocketUpgrade,
ws::{Message, WebSocket},
},
response::IntoResponse,
routing::get,
};
use console::style;
use futures_util::{SinkExt, StreamExt};
use std::{
net::SocketAddr,
sync::{Arc, Mutex},
};
use tokio::net::TcpListener;
use tokio::sync::broadcast;
const PORT_START: u16 = 59039;
const PORT_RANGE: u16 = 100;
const DEV_JS: &str = include_str!("dev.js");
#[derive(Clone, Copy)]
pub enum Event {
Rebuilding,
BuildFailed,
AppExited,
Reload,
}
impl Event {
fn message(self) -> &'static str {
match self {
Self::Rebuilding => "rebuilding",
Self::BuildFailed => "build-failed",
Self::AppExited => "app-exited",
Self::Reload => "reload",
}
}
}
#[derive(Clone)]
pub struct EventBus {
tx: Arc<broadcast::Sender<Event>>,
status: Arc<Mutex<Option<Event>>>,
}
impl EventBus {
pub fn new() -> Self {
Self {
tx: Arc::new(broadcast::channel(16).0),
status: Arc::default(),
}
}
pub fn publish(&self, event: Event) {
*self.status.lock().unwrap() = if matches!(event, Event::Reload) {
None
} else {
Some(event)
};
let _ = self.tx.send(event);
}
fn subscribe(&self) -> broadcast::Receiver<Event> {
self.tx.subscribe()
}
fn status(&self) -> Option<Event> {
*self.status.lock().unwrap()
}
}
pub async fn bind() -> TcpListener {
for port in PORT_START..=PORT_START.saturating_add(PORT_RANGE) {
if let Ok(listener) = TcpListener::bind(("127.0.0.1", port)).await {
return listener;
}
}
panic!(
"failed to bind dev server port ({PORT_START}-{})",
PORT_START + PORT_RANGE
);
}
pub async fn run(listener: TcpListener, events: EventBus) {
let app = Router::new()
.route("/dev.js", get(serve_dev_js))
.route("/ws", get(ws_handler))
.with_state(events);
let _ = axum::serve(listener, app).await;
}
async fn serve_dev_js() -> impl IntoResponse {
(
[("content-type", "application/javascript; charset=utf-8")],
DEV_JS,
)
}
async fn ws_handler(ws: WebSocketUpgrade, State(events): State<EventBus>) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_socket(socket, events))
}
async fn handle_socket(ws: WebSocket, events: EventBus) {
let (mut sink, mut stream) = ws.split();
let mut rx = events.subscribe();
if let Some(event) = events.status()
&& sink
.send(Message::Text(event.message().into()))
.await
.is_err()
{
return;
}
loop {
tokio::select! {
msg = stream.next() => {
let Some(Ok(msg)) = msg else { break };
if let Message::Text(text) = msg
&& let Some(message) = ReadyMessage::parse(&text) {
match message {
ReadyMessage::Ready { addr: Some(addr) } => {
eprintln!(
" {} {}",
style("ready on").green().bold(),
style(format!("http://{addr}")).cyan()
);
eprintln!();
}
ReadyMessage::Ready { addr: None } => {
eprintln!(" {}", style("ready").green().bold());
eprintln!();
}
}
events.publish(Event::Reload);
}
}
Ok(event) = rx.recv() => {
if sink.send(Message::Text(event.message().into())).await.is_err() {
break;
}
}
}
}
}
enum ReadyMessage {
Ready { addr: Option<SocketAddr> },
}
impl ReadyMessage {
fn parse(text: &str) -> Option<Self> {
if text == "ready" {
Some(Self::Ready { addr: None })
} else {
text.strip_prefix("ready ")
.and_then(|addr| addr.parse().ok())
.map(|addr| Self::Ready { addr: Some(addr) })
}
}
}