use std::cell::{Cell, RefCell};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use bevy::log::{debug, error, info, warn};
use crossbeam_channel::Sender;
use deno_core::{Extension, JsRuntime, OpDecl, OpState, RuntimeOptions, op2};
use tokio::sync::Mutex;
use tokio::sync::Notify;
use tokio::sync::mpsc::UnboundedReceiver;
use crate::animations::AnimationCommand;
use crate::message::ReactMessage;
use crate::protocol::{Op, Outbound};
use crate::request::RawRequest;
struct OpSender(Sender<Vec<Op>>);
struct EmitSender(Sender<ReactMessage>);
struct RequestSender(Sender<RawRequest>);
struct AnimSender(Sender<AnimationCommand>);
struct OutboundReceiver(Rc<Mutex<UnboundedReceiver<Outbound>>>);
struct ReloadReceiver(Rc<Mutex<UnboundedReceiver<()>>>);
struct ReloadFlag(Rc<Cell<bool>>);
struct ReloadNotify(Rc<Notify>);
#[op2]
fn op_flush(state: &mut OpState, #[serde] ops: Vec<Op>) {
let sender = state.borrow::<OpSender>();
let _ = sender.0.send(ops);
}
#[op2]
fn op_emit(state: &mut OpState, #[string] name: String, #[serde] value: serde_json::Value) {
let sender = state.borrow::<EmitSender>();
let _ = sender.0.send(ReactMessage { name, value });
}
#[op2(fast)]
fn op_log(#[string] level: String, #[string] msg: String) {
match level.as_str() {
"error" => error!(target: "bevy_react::js", "{msg}"),
"warn" => warn!(target: "bevy_react::js", "{msg}"),
"debug" => debug!(target: "bevy_react::js", "{msg}"),
_ => info!(target: "bevy_react::js", "{msg}"),
}
}
#[op2]
fn op_animate(state: &mut OpState, #[serde] cmd: AnimationCommand) {
let sender = state.borrow::<AnimSender>();
let _ = sender.0.send(cmd);
}
#[op2]
fn op_request(
state: &mut OpState,
#[bigint] id: u64,
#[string] name: String,
#[serde] value: serde_json::Value,
) {
let sender = state.borrow::<RequestSender>();
let _ = sender.0.send(RawRequest { id, name, value });
}
#[op2]
#[serde]
async fn op_next_event(state: Rc<RefCell<OpState>>) -> Option<Outbound> {
let (events, reload, flag, notify) = {
let state = state.borrow();
(
state.borrow::<OutboundReceiver>().0.clone(),
state.borrow::<ReloadReceiver>().0.clone(),
state.borrow::<ReloadFlag>().0.clone(),
state.borrow::<ReloadNotify>().0.clone(),
)
};
let mut events = events.lock().await;
let mut reload = reload.lock().await;
tokio::select! {
ev = events.recv() => ev, r = reload.recv() => match r {
Some(()) => {
flag.set(true);
notify.notify_one();
Some(Outbound::Reload)
}
None => None, }
}
}
#[op2]
async fn op_sleep(ms: f64) {
let ms = ms.max(0.0) as u64;
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
}
const PRELUDE: &str = r#"
let __nextTimer = 1;
const __cancelled = new Set();
globalThis.setTimeout = (cb, ms = 0, ...args) => {
const id = __nextTimer++;
const delay = Math.max(0, +ms || 0);
const run = () => { if (!__cancelled.delete(id)) cb(...args); };
if (delay === 0) Promise.resolve().then(run);
else Deno.core.ops.op_sleep(delay).then(run);
return id;
};
globalThis.clearTimeout = (id) => { if (id != null) __cancelled.add(id); };
globalThis.setInterval = (cb, ms = 0, ...args) => {
const id = __nextTimer++;
const delay = Math.max(0, +ms || 0);
(async () => {
while (!__cancelled.has(id)) {
await Deno.core.ops.op_sleep(delay);
if (__cancelled.has(id)) break;
cb(...args);
}
__cancelled.delete(id);
})();
return id;
};
globalThis.clearInterval = (id) => { if (id != null) __cancelled.add(id); };
globalThis.queueMicrotask = globalThis.queueMicrotask || ((cb) => { Promise.resolve().then(cb); });
const __fmtArg = (a) => {
if (typeof a === "string") return a;
if (a instanceof Error) return a.stack || (a.name + ": " + a.message);
try { return JSON.stringify(a); } catch { return String(a); }
};
const __log = (level) => (...args) =>
Deno.core.ops.op_log(level, args.map(__fmtArg).join(" "));
globalThis.console = {
log: __log("info"),
info: __log("info"),
debug: __log("debug"),
trace: __log("debug"),
warn: __log("warn"),
error: __log("error"),
dir: __log("info"),
table: __log("info"),
// No-op fallbacks so libraries that probe these never throw:
group: () => {}, groupCollapsed: () => {}, groupEnd: () => {}, assert: () => {},
};
"#;
enum Pumped {
Reload,
Shutdown,
}
#[derive(Clone)]
struct Senders {
ops: Sender<Vec<Op>>,
emit: Sender<ReactMessage>,
request: Sender<RawRequest>,
anim: Sender<AnimationCommand>,
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_js_thread(
vendor_path: PathBuf,
app_path: PathBuf,
ops_tx: Sender<Vec<Op>>,
emit_tx: Sender<ReactMessage>,
request_tx: Sender<RawRequest>,
anim_tx: Sender<AnimationCommand>,
outbound_rx: UnboundedReceiver<Outbound>,
reload_rx: UnboundedReceiver<()>,
) {
std::thread::Builder::new()
.name("js-runtime".to_string())
.spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build current-thread tokio runtime");
rt.block_on(async move {
let senders = Senders {
ops: ops_tx,
emit: emit_tx,
request: request_tx,
anim: anim_tx,
};
let outbound_rx = Rc::new(Mutex::new(outbound_rx));
let reload_rx = Rc::new(Mutex::new(reload_rx));
let reload_flag = Rc::new(Cell::new(false));
let reload_notify = Rc::new(Notify::new());
let mut last_good_app = match read_app(&app_path) {
Ok(code) => code,
Err(e) => {
error!(target: "bevy_react::js", "reading app failed: {e:?}");
return;
}
};
let mut runtime = match build_runtime(
&vendor_path,
&last_good_app,
&senders,
outbound_rx.clone(),
reload_rx.clone(),
reload_flag.clone(),
reload_notify.clone(),
) {
Ok(rt) => rt,
Err(e) => {
error!(target: "bevy_react::js", "initial runtime build failed: {e:?}");
return;
}
};
loop {
reload_flag.set(false);
match pump(&mut runtime, &reload_flag, &reload_notify).await {
Pumped::Shutdown => break,
Pumped::Reload => {
let new_code = match read_app(&app_path) {
Ok(code) => code,
Err(e) => {
warn!(target: "bevy_react::js", "reading rebuilt app failed ({e}); keeping the previous working version");
continue;
}
};
match runtime.execute_script("[app-update]", new_code.clone()) {
Ok(_) => last_good_app = new_code,
Err(e) => {
warn!(target: "bevy_react::js", "update rejected ({e}); keeping the previous working version");
if let Err(e) = runtime
.execute_script("[app-restore]", last_good_app.clone())
{
error!(target: "bevy_react::js", "restoring previous app failed: {e:?}");
}
}
}
}
}
}
});
})
.expect("spawn js-runtime thread");
}
fn build_runtime(
vendor_path: &Path,
app_code: &str,
senders: &Senders,
outbound_rx: Rc<Mutex<UnboundedReceiver<Outbound>>>,
reload_rx: Rc<Mutex<UnboundedReceiver<()>>>,
reload_flag: Rc<Cell<bool>>,
reload_notify: Rc<Notify>,
) -> anyhow::Result<JsRuntime> {
const FLUSH: OpDecl = op_flush();
const EMIT: OpDecl = op_emit();
const REQUEST: OpDecl = op_request();
const ANIMATE: OpDecl = op_animate();
const NEXT: OpDecl = op_next_event();
const SLEEP: OpDecl = op_sleep();
const LOG: OpDecl = op_log();
let ext = Extension {
name: "bevy_react_bridge",
ops: std::borrow::Cow::Borrowed(&[FLUSH, EMIT, REQUEST, ANIMATE, NEXT, SLEEP, LOG]),
..Default::default()
};
let mut runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![ext],
..Default::default()
});
{
let op_state = runtime.op_state();
let mut op_state = op_state.borrow_mut();
op_state.put(OpSender(senders.ops.clone()));
op_state.put(EmitSender(senders.emit.clone()));
op_state.put(RequestSender(senders.request.clone()));
op_state.put(AnimSender(senders.anim.clone()));
op_state.put(OutboundReceiver(outbound_rx));
op_state.put(ReloadReceiver(reload_rx));
op_state.put(ReloadFlag(reload_flag));
op_state.put(ReloadNotify(reload_notify));
}
runtime.execute_script("[prelude]", PRELUDE)?;
let vendor_code = std::fs::read_to_string(vendor_path)
.map_err(|e| anyhow::anyhow!("reading vendor {}: {e}", vendor_path.display()))?;
runtime.execute_script("[vendor]", vendor_code)?;
runtime.execute_script("[app]", app_code.to_owned())?;
Ok(runtime)
}
async fn pump(
runtime: &mut JsRuntime,
reload_flag: &Rc<Cell<bool>>,
reload_notify: &Notify,
) -> Pumped {
loop {
let loop_result = tokio::select! {
biased;
res = runtime.run_event_loop(Default::default()) => Some(res),
_ = reload_notify.notified() => None,
};
match loop_result {
None => {
if reload_flag.get() {
return Pumped::Reload;
}
}
Some(Err(e)) => {
error!(target: "bevy_react::js", "event loop error: {e}");
return Pumped::Reload;
}
Some(Ok(())) => {
return if reload_flag.get() {
Pumped::Reload
} else {
Pumped::Shutdown
};
}
}
}
}
fn read_app(app_path: &Path) -> anyhow::Result<String> {
std::fs::read_to_string(app_path)
.map_err(|e| anyhow::anyhow!("reading app {}: {e}", app_path.display()))
}