//! Promise-based access to the running app's existing control channel. use std::cell::RefCell; use std::rc::Rc; use std::sync::mpsc::{Receiver, Sender, TryRecvError}; use std::time::Duration; use serde_json::{Value, json}; use solidcraft_ui_egui::ControlRequest; use wasm_bindgen::{JsValue, closure::Closure}; const MAX_PENDING: usize = 256; const MAX_JSON_BYTES: usize = 16 * 1024 * 1024; struct Pending { reply: Receiver, resolve: js_sys::Function, reject: js_sys::Function, } #[derive(Clone)] pub struct Api { sender: Sender, pending: Rc>>, ctx: egui::Context, } fn params(value: &JsValue) -> Result { if value.is_undefined() || value.is_null() { return Ok(json!({})); } let text = js_sys::JSON::stringify(value).ok().and_then(|v| v.as_string()).ok_or("params must be JSON serializable")?; if text.len() > MAX_JSON_BYTES { return Err("params exceed 16 MiB".into()); } serde_json::from_str(&text).map_err(|e| format!("invalid params: {e}")) } fn error(message: &str) -> JsValue { js_sys::Error::new(message).into() } impl Api { pub fn install(sender: Sender, ctx: egui::Context) -> Result { let api = Self { sender, pending: Rc::default(), ctx }; let object = js_sys::Object::new(); let request = api.clone(); let execute = api.clone(); let commands = api.clone(); let inspect = api.clone(); for (name, function) in [ ( "request", Closure:: js_sys::Promise>::new(move |method: String, value: JsValue| { request.request(&method, params(&value)) }) .into_js_value(), ), ( "execute", Closure:: js_sys::Promise>::new(move |command: String, value: JsValue| { execute.request("engine.execute", params(&value).map(|p| json!({"command": command, "params": p}))) }) .into_js_value(), ), ("commands", Closure:: js_sys::Promise>::new(move || commands.request("engine.commands", Ok(json!({})))).into_js_value()), ( "inspect", Closure:: js_sys::Promise>::new(move |value: JsValue| inspect.request("document.inspect", params(&value))) .into_js_value(), ), ] { js_sys::Reflect::set(&object, &name.into(), &function)?; } let window = web_sys::window().ok_or_else(|| error("no window"))?; js_sys::Reflect::set(&window, &"solidcraft".into(), &object)?; Ok(api) } fn request(&self, method: &str, params: Result) -> js_sys::Promise { let params = match params { Ok(params) => params, Err(message) => return js_sys::Promise::reject(&error(&message)), }; if self.pending.borrow().len() >= MAX_PENDING { return js_sys::Promise::reject(&error("too many pending SolidCraft requests (maximum 256)")); } js_sys::Promise::new(&mut |resolve, reject| { let (request, reply) = ControlRequest::new(method, params.clone()); if self.sender.send(request).is_err() { let _ = reject.call1(&JsValue::UNDEFINED, &error("SolidCraft is not running")); return; } self.pending.borrow_mut().push(Pending { reply, resolve, reject }); self.ctx.request_repaint(); }) } pub fn poll(&self) { let mut completed = Vec::new(); self.pending.borrow_mut().retain_mut(|pending| { let result = match pending.reply.try_recv() { Ok(reply) => { if reply.get("ok").and_then(Value::as_bool) == Some(true) { js_sys::JSON::parse(&reply.get("result").unwrap_or(&Value::Null).to_string()) } else { Err(error(reply.get("error").and_then(Value::as_str).unwrap_or("SolidCraft request failed"))) } } Err(TryRecvError::Empty) => return true, Err(TryRecvError::Disconnected) => Err(error("SolidCraft request dropped")), }; completed.push((pending.resolve.clone(), pending.reject.clone(), result)); false }); // Release the queue before calling JavaScript, which can submit another request. for (resolve, reject, result) in completed { match result { Ok(value) => { let _ = resolve.call1(&JsValue::UNDEFINED, &value); } Err(error) => { let _ = reject.call1(&JsValue::UNDEFINED, &error); } } } if !self.pending.borrow().is_empty() { self.ctx.request_repaint_after(Duration::from_millis(16)); } } }