//! Development-mode support: hot-swapping the render pool when the server //! bundle is rebuilt, and pointing the page at Vite's dev server for the client. use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use std::time::Duration; use anyhow::{Context as _, Result}; use notify_debouncer_mini::notify::{RecommendedWatcher, RecursiveMode}; use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer}; use crate::pool::RenderPool; /// A render pool that can be replaced wholesale at runtime. /// /// Reloading is done by building a brand-new pool and swapping the `Arc`. /// In-flight renders keep using the old pool; once they finish and drop their /// handle, the old workers see their channel close and exit. pub type SharedPool = Arc>>; pub struct BundleWatcher { _debouncer: Debouncer, } /// Watches `bundle_path` and rebuilds `pool` whenever it changes. /// /// If the new bundle fails to load (e.g. a syntax error mid-edit), the error is /// logged and the previous pool stays in service. pub fn watch_server_bundle( bundle_path: PathBuf, pool: SharedPool, workers: usize, ) -> Result { let dir = bundle_path .parent() .context("bundle path has no parent directory")? .to_path_buf(); let file_name = bundle_path .file_name() .context("bundle path has no file name")? .to_os_string(); let mut debouncer = new_debouncer( Duration::from_millis(200), move |events: DebounceEventResult| { let events = match events { Ok(events) => events, Err(err) => { eprintln!("bundle watcher error: {err}"); return; } }; // Vite also writes a `.map` next to the bundle; only react to the // bundle itself. if !events .iter() .any(|event| event.path.file_name() == Some(file_name.as_os_str())) { return; } match reload(&bundle_path, workers) { Ok(new_pool) => { *pool .write() .unwrap_or_else(|poisoned| poisoned.into_inner()) = Arc::new(new_pool); println!("server bundle changed; reloaded {workers} render workers"); } Err(err) => eprintln!("failed to reload server bundle, keeping old one:\n{err:#}"), } }, )?; debouncer .watcher() .watch(&dir, RecursiveMode::NonRecursive) .with_context(|| format!("failed to watch {}", dir.display()))?; Ok(BundleWatcher { _debouncer: debouncer, }) } fn reload(bundle_path: &Path, workers: usize) -> Result { let bundle = std::fs::read_to_string(bundle_path) .with_context(|| format!("failed to read {}", bundle_path.display()))?; RenderPool::new(Arc::from(bundle), workers) } /// Script tags that load the client through Vite's dev server with HMR. /// /// The React Refresh preamble is what `@vitejs/plugin-react` would normally /// inject into `index.html`; since Rust owns the HTML we emit it ourselves. pub fn vite_client_scripts(origin: &str) -> String { format!( r#" "# ) }