use std::cell::RefCell;
use std::collections::VecDeque;
use bevy::prelude::*;
use crossbeam_channel::{Receiver, Sender};
use js_sys::{Function, Object, Promise, Reflect};
use serde::Serialize;
use wasm_bindgen::prelude::*;
use crate::animations::AnimationCommand;
use crate::bridge::OutboundSender;
use crate::message::ReactMessage;
use crate::protocol::{op::Op, op::OpBatch, outbound::Outbound};
use crate::request::RawRequest;
use super::{FlushInfo, HostConfig, HostSenders};
struct WebHost {
ops: Sender<Vec<Op>>,
flush: Sender<FlushInfo>,
emit: Sender<ReactMessage>,
request: Sender<RawRequest>,
anim: Sender<AnimationCommand>,
ext: crate::ext::ExtRegistrySlot,
held_flushes: VecDeque<(String, bool)>,
pending_events: VecDeque<JsValue>,
waiters: VecDeque<Function>,
}
thread_local! {
static HOST: RefCell<Option<WebHost>> = const { RefCell::new(None) };
}
pub(crate) fn spawn(app: &mut App, config: HostConfig, senders: HostSenders) -> OutboundSender {
console_error_panic_hook::set_once();
let (outbound_tx, outbound_rx) = crossbeam_channel::unbounded::<Outbound>();
HOST.with(|h| {
*h.borrow_mut() = Some(WebHost {
ops: senders.ops,
flush: senders.flush,
emit: senders.emit,
request: senders.request,
anim: senders.anim,
ext: config.ext,
held_flushes: VecDeque::new(),
pending_events: VecDeque::new(),
waiters: VecDeque::new(),
});
});
let host_obj = install_host_object();
let global: JsValue = js_sys::global().into();
Reflect::set(&global, &JsValue::from_str("__bevyHost"), &host_obj)
.expect("install globalThis.__bevyHost");
app.insert_resource(OutboundDrain(outbound_rx))
.add_systems(First, release_held_flushes)
.add_systems(Last, drain_outbound);
outbound_tx
}
fn install_host_object() -> Object {
let host = Object::new();
let flush = Closure::<dyn Fn(JsValue, JsValue)>::new(|json: JsValue, devtools: JsValue| {
let Some(json) = json.as_string() else {
wasm_bindgen::throw_str("op_flush: expected the op batch as a JSON string");
};
let devtools = devtools.as_bool().unwrap_or(false);
if !registry_ready() || with_host(|h| !h.held_flushes.is_empty()) {
with_host(|h| h.held_flushes.push_back((json, devtools)));
release_held_flushes();
return;
}
if let Err(e) = decode_and_send(&json, devtools) {
wasm_bindgen::throw_str(&e);
}
});
set_method(&host, "op_flush", flush.as_ref());
flush.forget();
let take_warnings = Closure::<dyn Fn() -> JsValue>::new(|| {
serde_wasm_bindgen::to_value(&crate::diag::take_decode_warnings()).unwrap_or(JsValue::NULL)
});
set_method(&host, "op_take_decode_warnings", take_warnings.as_ref());
take_warnings.forget();
let emit = Closure::<dyn Fn(JsValue, JsValue)>::new(|name: JsValue, value: JsValue| {
let name = name.as_string().unwrap_or_default();
let value = serde_wasm_bindgen::from_value(value).unwrap_or(serde_json::Value::Null);
with_host(|h| {
let _ = h.emit.send(ReactMessage { name, value });
});
});
set_method(&host, "op_emit", emit.as_ref());
emit.forget();
let request =
Closure::<dyn Fn(JsValue, JsValue, JsValue)>::new(|id: JsValue, name: JsValue, value| {
let id = bigint_to_u64(&id);
let name = name.as_string().unwrap_or_default();
let value = serde_wasm_bindgen::from_value(value).unwrap_or(serde_json::Value::Null);
with_host(|h| {
let _ = h.request.send(RawRequest { id, name, value });
});
});
set_method(&host, "op_request", request.as_ref());
request.forget();
let animate = Closure::<dyn Fn(JsValue)>::new(|cmd: JsValue| {
match serde_wasm_bindgen::from_value::<AnimationCommand>(cmd) {
Ok(cmd) => with_host(|h| {
let _ = h.anim.send(cmd);
}),
Err(e) => error(&format!("op_animate decode: {e}")),
}
});
set_method(&host, "op_animate", animate.as_ref());
animate.forget();
let next = Closure::<dyn Fn() -> Promise>::new(next_event_promise);
set_method(&host, "op_next_event", next.as_ref());
next.forget();
host
}
fn registry_ready() -> bool {
if crate::ext::with_thread_registry(|r| r.is_some()) {
return true;
}
match with_host(|h| h.ext.get()) {
Some(registry) => {
crate::ext::set_thread_registry(registry);
true
}
None => false,
}
}
fn decode_and_send(json: &str, devtools: bool) -> Result<(), String> {
let batch =
serde_json::from_str::<OpBatch>(json).map_err(|e| format!("op_flush decode: {e}"))?;
with_host(|h| {
let _ = h.flush.send(FlushInfo {
sent: None,
devtools,
});
let _ = h.ops.send(batch.0);
});
Ok(())
}
fn release_held_flushes() {
if !registry_ready() {
return;
}
while let Some((json, devtools)) = with_host(|h| h.held_flushes.pop_front()) {
if let Err(e) = decode_and_send(&json, devtools) {
error(&e);
}
}
}
fn next_event_promise() -> Promise {
HOST.with(|h| {
let mut guard = h.borrow_mut();
let host = guard.as_mut().expect("host installed before op_next_event");
if let Some(ev) = host.pending_events.pop_front() {
Promise::resolve(&ev)
} else {
let mut resolve_slot: Option<Function> = None;
let promise = Promise::new(&mut |resolve, _reject| resolve_slot = Some(resolve));
host.waiters
.push_back(resolve_slot.expect("Promise executor runs synchronously"));
promise
}
})
}
#[derive(Resource)]
struct OutboundDrain(Receiver<Outbound>);
fn drain_outbound(drain: Res<OutboundDrain>) {
while let Ok(ev) = drain.0.try_recv() {
let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
let js = match ev.serialize(&serializer) {
Ok(v) => v,
Err(e) => {
error(&format!("outbound encode: {e}"));
continue;
}
};
let resolve = with_host(|h| match h.waiters.pop_front() {
Some(resolve) => Some(resolve),
None => {
h.pending_events.push_back(js.clone());
None
}
});
if let Some(resolve) = resolve {
let _ = resolve.call1(&JsValue::NULL, &js);
}
}
}
fn with_host<R: Default>(f: impl FnOnce(&mut WebHost) -> R) -> R {
HOST.with(|h| match h.borrow_mut().as_mut() {
Some(host) => f(host),
None => R::default(),
})
}
fn bigint_to_u64(v: &JsValue) -> u64 {
js_sys::BigInt::new(v)
.ok()
.and_then(|b| b.to_string(10).ok())
.map(String::from)
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
fn set_method(host: &Object, name: &str, f: &JsValue) {
Reflect::set(host.as_ref(), &JsValue::from_str(name), f).expect("set host method");
}
fn error(msg: &str) {
web_sys::console::error_1(&JsValue::from_str(msg));
}