use serde::{Deserialize, Serialize};
pub use serde_json::Value;
#[cfg(not(feature = "boa"))]
use std::sync::atomic::{AtomicU64, Ordering};
use std::{
collections::{BTreeMap, VecDeque},
future::Future,
pin::Pin,
sync::{Arc, Mutex, OnceLock},
task::{Context as TaskContext, Poll, Waker},
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error(pub String);
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for Error {}
type Result = std::result::Result<Value, Error>;
struct State {
result: Option<Result>,
waker: Option<Waker>,
}
type Shared = Arc<Mutex<State>>;
fn finish(state: &Shared, value: Result) {
let waker = {
let mut state = state.lock().unwrap();
if state.result.is_some() {
return;
}
state.result = Some(value);
state.waker.take()
};
if let Some(waker) = waker {
waker.wake();
}
}
pub struct Evaluation {
state: Shared,
}
impl Future for Evaluation {
type Output = Result;
fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Result> {
let mut state = self.state.lock().unwrap();
if let Some(result) = state.result.take() {
Poll::Ready(result)
} else {
state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
impl Evaluation {
pub fn try_result(&self) -> Option<Result> {
self.state.lock().unwrap().result.take()
}
}
#[cfg(not(feature = "boa"))]
static NEXT: AtomicU64 = AtomicU64::new(1);
#[cfg(not(feature = "boa"))]
fn next() -> String {
NEXT.fetch_add(1, Ordering::Relaxed).to_string()
}
pub struct Context {
#[cfg(not(feature = "boa"))]
id: String,
#[cfg(feature = "boa")]
worker: std::sync::mpsc::Sender<(String, Shared)>,
}
impl Context {
pub fn new() -> std::result::Result<Self, Error> {
#[cfg(feature = "boa")]
let worker = boa::worker()?;
#[cfg(not(feature = "boa"))]
if !hub().lock().unwrap().available {
return Err(Error(
"JavaScript host is not running; enable javascript in the target".into(),
));
}
Ok(Self {
#[cfg(not(feature = "boa"))]
id: next(),
#[cfg(feature = "boa")]
worker,
})
}
pub fn eval(&self, source: impl Into<String>) -> Evaluation {
let state = Arc::new(Mutex::new(State {
result: None,
waker: None,
}));
let source = source.into();
#[cfg(feature = "boa")]
if self.worker.send((source, state.clone())).is_err() {
finish(&state, Err(Error("JavaScript context stopped".into())));
}
#[cfg(not(feature = "boa"))]
{
let mut hub = hub().lock().unwrap();
if hub.available {
let id = next();
hub.pending
.insert(id.clone(), (self.id.clone(), None, state.clone()));
hub.queue.push_back(Request {
id,
context: self.id.clone(),
source: Some(source),
});
} else {
finish(&state, Err(Error("JavaScript host stopped".into())));
}
}
Evaluation { state }
}
}
impl Drop for Context {
fn drop(&mut self) {
#[cfg(not(feature = "boa"))]
{
let states = {
let mut hub = hub().lock().unwrap();
hub.queue.retain(|r| r.context != self.id);
if hub.available {
hub.queue.push_back(Request {
id: next(),
context: self.id.clone(),
source: None,
});
}
let ids: Vec<_> = hub
.pending
.iter()
.filter(|(_, p)| p.0 == self.id)
.map(|(id, _)| id.clone())
.collect();
ids.into_iter()
.filter_map(|id| hub.pending.remove(&id).map(|p| p.2))
.collect::<Vec<_>>()
};
for state in states {
finish(&state, Err(Error("JavaScript context disposed".into())));
}
}
}
}
#[derive(Serialize)]
struct Request {
id: String,
context: String,
source: Option<String>,
}
#[derive(Default)]
struct Hub {
available: bool,
queue: VecDeque<Request>,
pending: BTreeMap<String, (String, Option<f64>, Shared)>,
}
fn hub() -> &'static Mutex<Hub> {
static HUB: OnceLock<Mutex<Hub>> = OnceLock::new();
HUB.get_or_init(Default::default)
}
pub mod host {
use super::*;
pub fn start() {
hub().lock().unwrap().available = true;
}
pub fn stop() {
let states = {
let mut hub = hub().lock().unwrap();
hub.available = false;
hub.queue.clear();
std::mem::take(&mut hub.pending)
};
for (_, (_, _, state)) in states {
finish(&state, Err(Error("JavaScript host stopped".into())));
}
}
pub fn poll(now_ms: f64) -> String {
let (requests, expired) = {
let mut hub = hub().lock().unwrap();
let ids: Vec<_> = hub
.pending
.iter_mut()
.filter_map(|(id, p)| {
let deadline = p.1.get_or_insert(now_ms + 30_000.0);
(now_ms >= *deadline).then(|| id.clone())
})
.collect();
let expired: Vec<_> = ids
.into_iter()
.filter_map(|id| hub.pending.remove(&id))
.collect();
(hub.queue.drain(..).collect::<Vec<_>>(), expired)
};
for (_, _, state) in expired {
finish(&state, Err(Error("JavaScript evaluation timed out".into())));
}
if requests.is_empty() {
String::new()
} else {
serde_json::to_string(&requests).unwrap()
}
}
pub fn complete(json: &str) {
#[derive(Deserialize)]
struct Reply {
id: String,
#[serde(default)]
value: Value,
error: Option<String>,
}
let Ok(reply) = serde_json::from_str::<Reply>(json) else {
return;
};
let pending = hub().lock().unwrap().pending.remove(&reply.id);
if let Some((_, _, state)) = pending {
finish(
&state,
reply.error.map_or(Ok(reply.value), |e| Err(Error(e))),
);
}
}
}
#[cfg(feature = "boa")]
mod boa {
use super::*;
use boa_engine::{
Context as JsContext, Source, builtins::promise::PromiseState, object::builtins::JsPromise,
};
pub fn worker() -> std::result::Result<std::sync::mpsc::Sender<(String, Shared)>, Error> {
let (tx, rx) = std::sync::mpsc::channel::<(String, Shared)>();
std::thread::Builder::new()
.name("bern-js".into())
.spawn(move || {
let mut context = JsContext::default();
context
.runtime_limits_mut()
.set_loop_iteration_limit(1_000_000);
context.runtime_limits_mut().set_recursion_limit(512);
for (source, state) in rx {
let result = evaluate(&source, &mut context).map_err(Error);
finish(&state, result);
}
})
.map_err(|e| Error(e.to_string()))?;
Ok(tx)
}
fn evaluate(source: &str, context: &mut JsContext) -> std::result::Result<Value, String> {
let value = context
.eval(Source::from_bytes(source))
.map_err(|e| e.to_string())?;
context.run_jobs().map_err(|e| e.to_string())?;
let value = if let Some(object) = value.as_object() {
if let Ok(promise) = JsPromise::from_object(object.clone()) {
match promise.state() {
PromiseState::Fulfilled(value) => value,
PromiseState::Rejected(error) => {
return Err(error
.to_string(context)
.map(|s| s.to_std_string_escaped())
.unwrap_or_else(|e| e.to_string()));
}
PromiseState::Pending => {
return Err(
"Promise did not settle; no external async host is installed".into(),
);
}
}
} else {
value
}
} else {
value
};
if value.is_undefined() {
return Ok(Value::Null);
}
value
.to_json(context)
.map(|v| v.unwrap_or(Value::Null))
.map_err(|e| e.to_string())
}
}
pub const HOST_RUNTIME: &str = include_str!("runtime.js");