use std::{path::PathBuf, time::Duration};
use anyhow::{Context, Result};
use notify::{Config as NotifyConfig, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use tokio::sync::mpsc::UnboundedReceiver;
#[cfg(test)]
#[path = "watch_tests.rs"]
mod tests;
const DEBOUNCE: Duration = Duration::from_millis(300);
fn reload_endpoint(reload_url: &str) -> String {
format!("{}/api/v1/admin/reload-schema", reload_url.trim_end_matches('/'))
}
pub async fn run(
input: &str,
output: &str,
reload_url: Option<&str>,
admin_token: Option<&str>,
database: Option<&str>,
) -> Result<()> {
compile_once(input, output, database).await;
if let Some(url) = reload_url {
reload_server(url, output, admin_token).await;
}
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<()>();
let mut watcher = RecommendedWatcher::new(
move |res: Result<notify::Event, notify::Error>| {
if let Ok(event) = res {
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
let _ = tx.send(());
}
}
},
NotifyConfig::default().with_poll_interval(Duration::from_millis(500)),
)
.context("Failed to create file watcher")?;
watcher
.watch(&PathBuf::from(input), RecursiveMode::NonRecursive)
.with_context(|| format!("Failed to watch input file `{input}`"))?;
println!("[watch] watching {input} — press Ctrl+C to stop");
loop {
tokio::select! {
received = rx.recv() => {
if received.is_none() {
break; }
debounce(&mut rx).await;
compile_once(input, output, database).await;
if let Some(url) = reload_url {
reload_server(url, output, admin_token).await;
}
}
_ = tokio::signal::ctrl_c() => {
println!("\n[watch] stopped");
break;
}
}
}
Ok(())
}
async fn debounce(rx: &mut UnboundedReceiver<()>) {
loop {
tokio::select! {
() = tokio::time::sleep(DEBOUNCE) => break,
received = rx.recv() => {
if received.is_none() {
break;
}
}
}
}
}
async fn compile_once(input: &str, output: &str, database: Option<&str>) {
match super::compile::run(
input,
None,
None,
Vec::new(),
Vec::new(),
Vec::new(),
output,
false, database,
None, false, false, false, None, )
.await
{
Ok(()) => println!("[watch] compiled {output}"),
Err(e) => eprintln!("[watch] compile failed: {e:#}"),
}
}
async fn reload_server(reload_url: &str, schema_path: &str, admin_token: Option<&str>) {
let url = reload_endpoint(reload_url);
let client = match reqwest::Client::builder().timeout(Duration::from_secs(10)).build() {
Ok(client) => client,
Err(e) => {
eprintln!("[watch] reload client error: {e}");
return;
},
};
let mut req = client.post(&url).json(&serde_json::json!({
"schema_path": schema_path,
"validate_only": false,
}));
if let Some(tok) = admin_token {
req = req.header("Authorization", format!("Bearer {tok}"));
}
match req.send().await {
Ok(resp) if resp.status().is_success() => {
println!("[watch] reloaded {reload_url} (zero-downtime)");
},
Ok(resp) => {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
eprintln!("[watch] reload failed: HTTP {status} {body}");
},
Err(e) => eprintln!("[watch] reload request failed: {e}"),
}
}