#![cfg(any(doc, target_family = "wasm"))]
use async_channel::Sender;
use bevy_app::{App, Plugin, Update};
use bevy_ecs::{
schedule::{
IntoScheduleConfigs,
common_conditions::{resource_exists, resource_exists_and_changed},
},
system::Res,
};
use bevy_remote::{BrpMessage, BrpResult, BrpSender, RemoteMethodSystemId, RemoteMethods};
use serde::Serialize;
use serde_json::Value;
use std::{
cell::RefCell,
collections::BTreeMap,
task::{Poll, Waker},
};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{future_to_promise, spawn_local};
pub struct RemoteWasmPlugin;
impl Plugin for RemoteWasmPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
update_wasm_bridge
.run_if(resource_exists::<BrpSender>)
.run_if(resource_exists_and_changed::<RemoteMethods>),
);
}
}
#[wasm_bindgen(typescript_custom_section)]
const TS_TYPES: &str = include_str!(concat!(env!("OUT_DIR"), "/ts_types.d.ts"));
#[derive(Default)]
struct BridgeState {
bridge: Option<js_sys::Object>,
waker: Option<Waker>,
}
thread_local! {
static BRIDGE_STATE: RefCell<BridgeState> = RefCell::new(BridgeState::default());
}
#[doc(hidden)]
#[wasm_bindgen(js_name = getBridge, skip_typescript)]
pub async fn get_bridge() -> js_sys::Object {
if let Some(bridge) = BRIDGE_STATE.with(|state| state.borrow().bridge.clone()) {
bridge
} else {
std::future::poll_fn(|cx| {
BRIDGE_STATE.with_borrow_mut(|state| {
if let Some(bridge) = state.bridge.clone() {
return Poll::Ready(bridge);
}
state.waker = Some(cx.waker().clone());
Poll::Pending
})
})
.await
}
}
type WasmBridgeMethod = dyn Fn(Option<JsValue>, Option<js_sys::Function>) -> js_sys::Promise + 'static;
type WasmBridgeWatchCloser = dyn Fn() + 'static;
struct BrpApp(BTreeMap<String, Closure<WasmBridgeMethod>>);
struct BrpBridge {
main: BrpApp,
}
fn publish_bridge(bridge: BrpBridge) {
if let Some(waker) = BRIDGE_STATE.with_borrow_mut(|state| {
state.bridge = Some(bridge.into());
state.waker.take()
}) {
waker.wake();
}
}
fn update_wasm_bridge(brp_sender: Res<BrpSender>, remote_methods: Res<RemoteMethods>) {
let main = BrpApp(
remote_methods
.methods()
.into_iter()
.filter_map(|name| remote_methods.get(&name).map(|id| (name, id)))
.map(|(method_name, method_id)| {
let function = build_function(&brp_sender, method_name.to_owned(), *method_id);
(method_name, function)
})
.collect(),
);
publish_bridge(BrpBridge { main });
}
fn build_function(
sender: &Sender<BrpMessage>,
method_name: String,
remote_method_system_id: RemoteMethodSystemId,
) -> Closure<WasmBridgeMethod> {
let sender = sender.clone();
Closure::<WasmBridgeMethod>::new(
move |js_params: Option<JsValue>, callback: Option<js_sys::Function>| -> js_sys::Promise {
let sender = sender.clone();
let method = method_name.clone();
future_to_promise(async move {
match remote_method_system_id {
RemoteMethodSystemId::Instant(_) => {
build_instant_function(sender, method, js_params, callback).await
}
RemoteMethodSystemId::Watching(_) => {
build_watching_function(sender, method, js_params, callback).await
}
}
})
},
)
}
async fn build_instant_function(
sender: Sender<BrpMessage>,
method: String,
js_params: Option<JsValue>,
callback: Option<js_sys::Function>,
) -> Result<JsValue, JsValue> {
let (result_tx, result_rx) = async_channel::bounded(1);
let params = params_from_js(js_params);
sender
.send(BrpMessage {
method,
params,
sender: result_tx,
})
.await
.map_err(|e| js_sys::Error::new(&format!("Failed to send request: {e}")))?;
let result = result_rx
.recv()
.await
.map_err(|_| js_sys::Error::new("Channel closed unexpectedly"))?;
let js_result = result_to_js(result)?;
if let Some(callback) = callback {
callback.call1(&JsValue::NULL, &js_result)?;
Ok(JsValue::UNDEFINED)
} else {
Ok(js_result)
}
}
async fn build_watching_function(
sender: Sender<BrpMessage>,
method: String,
js_params: Option<JsValue>,
callback: Option<js_sys::Function>,
) -> Result<JsValue, JsValue> {
let (result_tx, result_rx) = async_channel::bounded::<BrpResult>(8);
let closer_rx = result_rx.clone();
let params = params_from_js(js_params);
sender
.send(BrpMessage {
method,
params,
sender: result_tx,
})
.await
.map_err(|e| js_sys::Error::new(&format!("Failed to send request: {e}")))?;
if let Some(callback) = callback {
spawn_local(async move {
while let Ok(result) = result_rx.recv().await {
let arg = result_to_js(result).unwrap_or_else(std::convert::identity);
let _ = callback.call1(&JsValue::NULL, &arg);
}
});
}
let closer = Closure::<WasmBridgeWatchCloser>::new(move || {
closer_rx.close();
});
Ok(closer.into_js_value())
}
impl From<BrpApp> for js_sys::Object {
fn from(app: BrpApp) -> Self {
let object = js_sys::Object::new();
for (method_name, function) in app.0 {
js_sys::Reflect::set(&object, &JsValue::from_str(&method_name), &function.into_js_value()).unwrap_or(false);
}
object
}
}
impl From<BrpBridge> for js_sys::Object {
fn from(bridge: BrpBridge) -> Self {
let object = js_sys::Object::new();
let main = js_sys::Object::from(bridge.main);
js_sys::Reflect::set(&object, &JsValue::from_str("main"), &main).unwrap_or(false);
object
}
}
fn params_from_js(params: Option<JsValue>) -> Option<Value> {
serde_wasm_bindgen::from_value(params?).ok()
}
fn result_to_js(result: BrpResult) -> Result<JsValue, JsValue> {
result
.map(|value| {
let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
value.serialize(&serializer).unwrap_or(JsValue::UNDEFINED)
})
.map_err(|err| {
let msg = format!("[{}] {}", err.code, err.message);
js_sys::Error::new(&msg).into()
})
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::prelude::*;
use bevy_remote::RemotePlugin;
use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test(async)]
async fn get_bridge_with_default_methods() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins((RemotePlugin::default(), RemoteWasmPlugin));
app.update();
let bridge = get_bridge().await;
assert_eq!(js_sys::Object::keys(&bridge).length(), 1);
let main = js_sys::Reflect::get(&bridge, &JsValue::from_str("main")).unwrap();
let main = main.dyn_into::<js_sys::Object>().unwrap();
assert!(js_sys::Object::keys(&main).length() > 10);
let method = js_sys::Reflect::get(&main, &JsValue::from_str("rpc.discover")).unwrap();
let method = method.dyn_into::<js_sys::Function>().unwrap();
let promise = method
.call0(&JsValue::NULL)
.unwrap()
.dyn_into::<js_sys::Promise>()
.unwrap();
let result = wasm_bindgen_futures::JsFuture::from(promise).await.unwrap();
let info = js_sys::Reflect::get(&result, &JsValue::from_str("info")).unwrap();
let version = js_sys::Reflect::get(&info, &JsValue::from_str("version")).unwrap();
assert!(
!version.is_undefined(),
"Expected version to be present, got {:?}",
version
);
}
#[wasm_bindgen_test(async)]
async fn get_bridge_without_methods() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins((RemotePlugin::default(), RemoteWasmPlugin));
app.insert_resource(RemoteMethods::new()); app.update();
let bridge = get_bridge().await;
assert_eq!(js_sys::Object::keys(&bridge).length(), 1);
let main = js_sys::Reflect::get(&bridge, &JsValue::from_str("main")).unwrap();
let main = main.dyn_into::<js_sys::Object>().unwrap();
assert_eq!(js_sys::Object::keys(&main).length(), 0);
}
#[wasm_bindgen_test(async)]
async fn get_bridge_with_updated_methods() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins((RemotePlugin::default(), RemoteWasmPlugin));
app.update();
let default_bridge = get_bridge().await;
let default_main = js_sys::Reflect::get(&default_bridge, &JsValue::from_str("main")).unwrap();
let default_main = default_main.dyn_into::<js_sys::Object>().unwrap();
let mut updated_methods = RemoteMethods::new();
let my_handler = |In(_params): In<Option<Value>>| -> BrpResult { Ok(Value::Null) };
let my_handler_id = app.register_system(my_handler);
updated_methods.insert("my_method", RemoteMethodSystemId::Instant(my_handler_id));
app.insert_resource(updated_methods);
app.update();
let updated_bridge = get_bridge().await;
let updated_main = js_sys::Reflect::get(&updated_bridge, &JsValue::from_str("main")).unwrap();
let updated_main = updated_main.dyn_into::<js_sys::Object>().unwrap();
assert_eq!(js_sys::Object::keys(&default_bridge).length(), 1);
assert!(js_sys::Object::keys(&default_main).length() > 10);
assert_eq!(js_sys::Object::keys(&updated_bridge).length(), 1);
assert_eq!(js_sys::Object::keys(&updated_main).length(), 1);
assert!(js_sys::Reflect::has(&updated_main, &JsValue::from_str("my_method")).unwrap());
}
}