use bevy::prelude::*;
use crate::bridge::OutboundSender;
use crate::js_thread::spawn_js_thread;
use crate::protocol::Outbound;
use super::{HostConfig, HostSenders};
pub(crate) fn spawn(app: &mut App, config: HostConfig, senders: HostSenders) -> OutboundSender {
let (outbound_tx, outbound_rx) = tokio::sync::mpsc::unbounded_channel::<Outbound>();
let (reload_tx, reload_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
let bundle = config.bundle;
let vendor = bundle.with_file_name("vendor.js");
for (label, path) in [("app bundle", &bundle), ("vendor bundle", &vendor)] {
if !path.exists() {
let shown = std::path::absolute(path).unwrap_or_else(|_| path.clone());
panic!(
"JS {label} not found at {}.\nBuild your app first (e.g. `npm run build`).",
shown.display()
);
}
}
spawn_js_thread(
vendor,
bundle.clone(),
senders.ops,
senders.emit,
senders.request,
senders.anim,
outbound_rx,
reload_rx,
);
if config.hot_reload {
app.insert_resource(BundleWatch {
last_modified: file_mtime(&bundle),
path: bundle,
timer: Timer::from_seconds(0.3, TimerMode::Repeating),
reload_tx,
})
.add_systems(Update, watch_bundle);
} else {
app.insert_resource(ReloadKeepAlive(reload_tx));
}
outbound_tx
}
#[derive(Resource)]
struct ReloadKeepAlive(#[allow(dead_code)] tokio::sync::mpsc::UnboundedSender<()>);
#[derive(Resource)]
struct BundleWatch {
path: std::path::PathBuf,
last_modified: Option<std::time::SystemTime>,
timer: Timer,
reload_tx: tokio::sync::mpsc::UnboundedSender<()>,
}
fn watch_bundle(time: Res<Time>, mut watch: ResMut<BundleWatch>) {
watch.timer.tick(time.delta());
if !watch.timer.just_finished() {
return;
}
let current = file_mtime(&watch.path);
if current.is_some() && current != watch.last_modified {
watch.last_modified = current;
info!("bundle changed — hot reloading React app");
let _ = watch.reload_tx.send(());
}
}
fn file_mtime(path: &std::path::Path) -> Option<std::time::SystemTime> {
std::fs::metadata(path).and_then(|m| m.modified()).ok()
}