hornbeam 0.0.5

Hornbeam template engine (high-level crate for use in applications)
Documentation
use crate::interpreted::load_new_template_system;
use arc_swap::ArcSwap;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Extension, Router};
use hornbeam_interpreter::localisation::fluent::FluentLocalisationSystem;
use hornbeam_interpreter::LoadedTemplates;
use notify::{Event, EventKind, RecursiveMode, Watcher};
use std::error::Error;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::mpsc::RecvTimeoutError;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::net::TcpListener;
use tokio::sync::oneshot;

pub(crate) fn start_hot_reloader(
    default_locale: String,
    templates: Arc<ArcSwap<LoadedTemplates<FluentLocalisationSystem>>>,
    template_dir: PathBuf,
    translation_dir: PathBuf,
) {
    let auto_reloader_waiting_queue = start_auto_hot_reloader();

    std::thread::Builder::new()
        .name(String::from("Hornbeam Hot Reload"))
        .spawn(move || {
            let (notif_tx, notif_rx) = std::sync::mpsc::sync_channel(1);

            let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
                match res {
                    Ok(event) => {
                        let ekind: &EventKind = &event.kind;
                        match ekind {
                            EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) => {
                                // TODO be a bit more specific.
                                notif_tx
                                    .send(())
                                    .expect("Hornbeam: failed to handle file change");
                            }
                            EventKind::Access(_) => {}
                            EventKind::Remove(_) => {}
                            EventKind::Other => {}
                        }
                    }
                    Err(e) => println!("Hornbeam watch error: {:?}", e),
                }
            })
            .expect("Failed to start Hornbeam watcher");

            watcher
                .watch(&template_dir, RecursiveMode::Recursive)
                .expect("Failed to watch Hornbeam template dir");
            watcher
                .watch(&translation_dir, RecursiveMode::Recursive)
                .expect("Failed to watch Hornbeam translation dir");
            let default_locale = default_locale;

            loop {
                notif_rx.recv().unwrap();

                // Debounce, because editors often make a series of modifications and we don't
                // want to reload before it's ready.
                let deadline = Instant::now() + Duration::from_millis(100);
                loop {
                    let now = Instant::now();
                    if now >= deadline {
                        break;
                    }
                    if let Err(RecvTimeoutError::Disconnected) =
                        notif_rx.recv_timeout(deadline - now)
                    {
                        panic!("Hornbeam watcher: disconnected");
                    }
                }

                eprint!("reloading Hornbeam templates... ");
                match load_new_template_system(&default_locale, &template_dir, &translation_dir) {
                    Ok(new_template_system) => {
                        templates.swap(Arc::new(new_template_system));
                        eprintln!("fine.");
                        for waiter in auto_reloader_waiting_queue.lock().unwrap().drain(..) {
                            let _ = waiter.send(());
                        }
                    }
                    Err(e) => {
                        eprintln!("failed to hot reload Hornbeam: {e}");
                    }
                }
            }
        })
        .expect("can't start Hornbeam Hot Reload thread");
}

type WaiterList = Arc<Mutex<Vec<oneshot::Sender<()>>>>;

pub(crate) fn start_auto_hot_reloader() -> WaiterList {
    let waiter_list = Arc::new(Mutex::new(Vec::new()));
    let app = Router::new()
        .route("/wait_hot_reload", get(hot_reload_longpoll))
        .layer(Extension(waiter_list.clone()));

    let addr = SocketAddr::from(([127, 0, 0, 1], 7015));

    tokio::spawn(async move {
        let result: Result<(), Box<dyn Error>> = async move {
            let listener = TcpListener::bind(addr).await?;
            eprintln!("Hornbeam Auto Hot Reload: Listening on http://{}", addr);
            axum::serve(listener, app).await?;
            Ok(())
        }
        .await;
        if let Err(e) = result {
            eprintln!("Hornbeam Auto Hot Reload failed: {e}");
        }
    });

    waiter_list
}

async fn hot_reload_longpoll(Extension(waiter_list): Extension<WaiterList>) -> impl IntoResponse {
    let (tx, rx) = oneshot::channel::<()>();
    {
        waiter_list.lock().unwrap().push(tx);
    }

    let _ = rx.await;

    "reload"
}