use std::{path::Path, sync::Arc, time::Duration};
use axum::{
Router,
response::sse::{Event, Sse},
routing::get,
};
use tokio::sync::broadcast;
use tokio_stream::{StreamExt, wrappers::BroadcastStream};
use tower_http::services::ServeDir;
#[derive(Debug, Clone)]
pub enum ReloadMessage {
Reload,
CssReload,
}
#[derive(Clone)]
pub struct ServerState {
pub reload_tx: broadcast::Sender<ReloadMessage>,
}
impl ServerState {
pub fn new() -> Self {
let (reload_tx, _) = broadcast::channel(16);
Self { reload_tx }
}
pub fn notify_reload(&self) {
let _ = self.reload_tx.send(ReloadMessage::Reload);
}
#[allow(dead_code)]
pub fn notify_css_reload(&self) {
let _ = self.reload_tx.send(ReloadMessage::CssReload);
}
}
impl Default for ServerState {
fn default() -> Self {
Self::new()
}
}
pub fn create_router(output_dir: &Path, state: Arc<ServerState>) -> Router {
Router::new()
.route("/__livereload", get(livereload_handler))
.fallback_service(ServeDir::new(output_dir))
.with_state(state)
}
async fn livereload_handler(
axum::extract::State(state): axum::extract::State<Arc<ServerState>>,
) -> Sse<impl tokio_stream::Stream<Item = Result<Event, std::convert::Infallible>>> {
let rx = state.reload_tx.subscribe();
let stream = BroadcastStream::new(rx).filter_map(|msg| {
match msg {
Ok(ReloadMessage::Reload) => Some(Ok(Event::default().data("reload"))),
Ok(ReloadMessage::CssReload) => Some(Ok(Event::default().data("css-reload"))),
Err(_) => None, }
});
Sse::new(stream).keep_alive(
axum::response::sse::KeepAlive::new()
.interval(Duration::from_secs(30))
.text("ping"),
)
}
pub const LIVERELOAD_SCRIPT: &str = r#"
<script>
(function() {
// Prevent duplicate connections
if (window.__livereloadActive) return;
window.__livereloadActive = true;
let source = null;
let reconnectTimer = null;
function connect() {
if (source && source.readyState !== EventSource.CLOSED) {
return;
}
source = new EventSource('/__livereload');
source.onmessage = function(event) {
if (event.data === 'reload') {
cleanup();
window.location.reload();
} else if (event.data === 'css-reload') {
document.querySelectorAll('link[rel="stylesheet"]').forEach(function(link) {
const href = link.href.split('?')[0];
link.href = href + '?v=' + Date.now();
});
}
};
source.onerror = function() {
console.log('[livereload] Connection lost, will retry...');
source.close();
clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(connect, 2000);
};
}
function cleanup() {
clearTimeout(reconnectTimer);
if (source) {
source.close();
source = null;
}
}
// Handle page visibility to save resources
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
cleanup();
} else {
connect();
}
});
// Clean up on page unload
window.addEventListener('beforeunload', cleanup);
window.addEventListener('pagehide', cleanup);
connect();
})();
</script>
"#;