use std::cell::RefCell;
use js_sys::{Array, Object, Reflect, Uint8Array};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{MessageEvent, Worker};
use super::{dom, templates};
const WATCHDOG_MS: i32 = 4000;
const MAX_OUTPUT_BYTES: u32 = 256 * 1024;
pub(crate) struct CliRun {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub truncated: bool,
}
pub(crate) struct CliFailure {
pub detail: String,
}
thread_local! {
static CLI: RefCell<Option<CliHandle>> = const { RefCell::new(None) };
}
struct CliHandle {
worker: Worker,
_onmessage: Closure<dyn FnMut(MessageEvent)>,
watchdog: Option<i32>,
}
impl Drop for CliHandle {
fn drop(&mut self) {
if let Some(id) = self.watchdog.take() {
if let Ok(win) = dom::window() {
win.clear_interval_with_handle(id);
}
}
self.worker.terminate();
}
}
fn stop_worker() {
CLI.with(|c| *c.borrow_mut() = None);
}
pub(crate) async fn run_wasm_cli(
wasm_bytes: &[u8],
args: &[String],
) -> Result<CliRun, CliFailure> {
stop_worker();
let outcome: std::rc::Rc<RefCell<Option<Result<CliRun, CliFailure>>>> =
std::rc::Rc::new(RefCell::new(None));
let worker = Worker::new("/wasi-worker.js")
.map_err(|e| CliFailure { detail: format!("worker spawn failed: {e:?}") })?;
let onmessage = {
let outcome = outcome.clone();
Closure::<dyn FnMut(MessageEvent)>::new(move |e: MessageEvent| {
let data = e.data();
let ty = Reflect::get(&data, &JsValue::from_str("type"))
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default();
if outcome.borrow().is_some() {
return;
}
match ty.as_str() {
"done" => {
let getf = |k: &str| {
Reflect::get(&data, &JsValue::from_str(k)).ok().and_then(|v| v.as_f64())
};
let gets = |k: &str| {
Reflect::get(&data, &JsValue::from_str(k))
.ok()
.and_then(|v| v.as_string())
.unwrap_or_default()
};
let getb = |k: &str| {
Reflect::get(&data, &JsValue::from_str(k))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(false)
};
*outcome.borrow_mut() = Some(Ok(CliRun {
exit_code: getf("exitCode").unwrap_or(0.0) as i32,
stdout: gets("stdout"),
stderr: gets("stderr"),
truncated: getb("truncated"),
}));
}
"error" => {
let detail = Reflect::get(&data, &JsValue::from_str("detail"))
.ok()
.and_then(|v| v.as_string())
.unwrap_or_else(|| "unknown worker error".into());
*outcome.borrow_mut() = Some(Err(CliFailure { detail }));
}
_ => {}
}
})
};
worker.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
let arr = Uint8Array::from(wasm_bytes);
let buf = arr.buffer();
let argv = Array::new();
for a in args {
argv.push(&JsValue::from_str(a));
}
let msg = Object::new();
let _ = Reflect::set(&msg, &JsValue::from_str("type"), &JsValue::from_str("run"));
let _ = Reflect::set(&msg, &JsValue::from_str("wasm"), &buf);
let _ = Reflect::set(&msg, &JsValue::from_str("args"), &argv);
let _ = Reflect::set(
&msg,
&JsValue::from_str("maxOutput"),
&JsValue::from_f64(MAX_OUTPUT_BYTES as f64),
);
let transfer = Array::new();
transfer.push(&buf);
if let Err(e) = worker.post_message_with_transfer(&msg, &transfer) {
return Err(CliFailure { detail: format!("worker post failed: {e:?}") });
}
CLI.with(|c| {
*c.borrow_mut() = Some(CliHandle { worker, _onmessage: onmessage, watchdog: None });
});
let mut waited = 0i32;
loop {
if let Some(result) = outcome.borrow_mut().take() {
stop_worker();
return result;
}
if waited >= WATCHDOG_MS {
stop_worker(); return Err(CliFailure {
detail: format!(
"the program did not finish within {}ms — terminated (likely an \
unbounded loop; bound your work or print + exit promptly)",
WATCHDOG_MS
),
});
}
crate::runtime::sleep_ms(50).await;
waited += 50;
}
}
pub(crate) fn show_terminal(argv: &str, run: &CliRun) {
dom::remember_focus();
dom::swap_outer(
"terminal-overlay",
&templates::terminal_overlay(argv, run).into_string(),
);
dom::focus_first_in("terminal-overlay");
}
pub(crate) fn toggle_terminal() {
if dom::by_id("terminal-surface").is_some() {
close_terminal();
} else if let Some((argv, run)) = LAST_RUN.with(|c| c.borrow().clone()) {
show_terminal(&argv, &run);
}
}
pub(crate) fn close_terminal() {
dom::swap_outer(
"terminal-overlay",
&templates::terminal_overlay_closed().into_string(),
);
dom::restore_focus();
}
thread_local! {
static LAST_RUN: RefCell<Option<(String, CliRun)>> = const { RefCell::new(None) };
}
impl Clone for CliRun {
fn clone(&self) -> Self {
CliRun {
exit_code: self.exit_code,
stdout: self.stdout.clone(),
stderr: self.stderr.clone(),
truncated: self.truncated,
}
}
}
pub(crate) fn remember_run(argv: &str, run: &CliRun) {
LAST_RUN.with(|c| *c.borrow_mut() = Some((argv.to_string(), run.clone())));
}