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, Outbound};
use crate::request::RawRequest;
use super::{HostConfig, HostSenders};
struct WebHost {
ops: Sender<Vec<Op>>,
emit: Sender<ReactMessage>,
request: Sender<RawRequest>,
anim: Sender<AnimationCommand>,
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,
emit: senders.emit,
request: senders.request,
anim: senders.anim,
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(Last, drain_outbound);
outbound_tx
}
fn install_host_object() -> Object {
let host = Object::new();
let flush =
Closure::<dyn Fn(JsValue)>::new(|ops: JsValue| {
match serde_wasm_bindgen::from_value::<Vec<Op>>(ops) {
Ok(batch) => with_host(|h| {
let _ = h.ops.send(batch);
}),
Err(e) => error(&format!("op_flush decode: {e}")),
}
});
set_method(&host, "op_flush", flush.as_ref());
flush.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 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));
}