1use serde::{Deserialize, Serialize};
4pub use serde_json::Value;
5#[cfg(not(feature = "boa"))]
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::{
8 collections::{BTreeMap, VecDeque},
9 future::Future,
10 pin::Pin,
11 sync::{Arc, Mutex, OnceLock},
12 task::{Context as TaskContext, Poll, Waker},
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Error(pub String);
17impl std::fmt::Display for Error {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 f.write_str(&self.0)
20 }
21}
22impl std::error::Error for Error {}
23type Result = std::result::Result<Value, Error>;
24struct State {
25 result: Option<Result>,
26 waker: Option<Waker>,
27}
28type Shared = Arc<Mutex<State>>;
29fn finish(state: &Shared, value: Result) {
30 let waker = {
31 let mut state = state.lock().unwrap();
32 if state.result.is_some() {
33 return;
34 }
35 state.result = Some(value);
36 state.waker.take()
37 };
38 if let Some(waker) = waker {
39 waker.wake();
40 }
41}
42pub struct Evaluation {
44 state: Shared,
45}
46impl Future for Evaluation {
47 type Output = Result;
48 fn poll(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Result> {
49 let mut state = self.state.lock().unwrap();
50 if let Some(result) = state.result.take() {
51 Poll::Ready(result)
52 } else {
53 state.waker = Some(cx.waker().clone());
54 Poll::Pending
55 }
56 }
57}
58impl Evaluation {
59 pub fn try_result(&self) -> Option<Result> {
60 self.state.lock().unwrap().result.take()
61 }
62}
63#[cfg(not(feature = "boa"))]
64static NEXT: AtomicU64 = AtomicU64::new(1);
65#[cfg(not(feature = "boa"))]
66fn next() -> String {
67 NEXT.fetch_add(1, Ordering::Relaxed).to_string()
68}
69
70pub struct Context {
71 #[cfg(not(feature = "boa"))]
72 id: String,
73 #[cfg(feature = "boa")]
74 worker: std::sync::mpsc::Sender<(String, Shared)>,
75}
76impl Context {
77 pub fn new() -> std::result::Result<Self, Error> {
78 #[cfg(feature = "boa")]
79 let worker = boa::worker()?;
80 #[cfg(not(feature = "boa"))]
81 if !hub().lock().unwrap().available {
82 return Err(Error(
83 "JavaScript host is not running; enable javascript in the target".into(),
84 ));
85 }
86 Ok(Self {
87 #[cfg(not(feature = "boa"))]
88 id: next(),
89 #[cfg(feature = "boa")]
90 worker,
91 })
92 }
93 pub fn eval(&self, source: impl Into<String>) -> Evaluation {
94 let state = Arc::new(Mutex::new(State {
95 result: None,
96 waker: None,
97 }));
98 let source = source.into();
99 #[cfg(feature = "boa")]
100 if self.worker.send((source, state.clone())).is_err() {
101 finish(&state, Err(Error("JavaScript context stopped".into())));
102 }
103 #[cfg(not(feature = "boa"))]
104 {
105 let mut hub = hub().lock().unwrap();
106 if hub.available {
107 let id = next();
108 hub.pending
109 .insert(id.clone(), (self.id.clone(), None, state.clone()));
110 hub.queue.push_back(Request {
111 id,
112 context: self.id.clone(),
113 source: Some(source),
114 });
115 } else {
116 finish(&state, Err(Error("JavaScript host stopped".into())));
117 }
118 }
119 Evaluation { state }
120 }
121}
122impl Drop for Context {
123 fn drop(&mut self) {
124 #[cfg(not(feature = "boa"))]
125 {
126 let states = {
127 let mut hub = hub().lock().unwrap();
128 hub.queue.retain(|r| r.context != self.id);
129 if hub.available {
130 hub.queue.push_back(Request {
131 id: next(),
132 context: self.id.clone(),
133 source: None,
134 });
135 }
136 let ids: Vec<_> = hub
137 .pending
138 .iter()
139 .filter(|(_, p)| p.0 == self.id)
140 .map(|(id, _)| id.clone())
141 .collect();
142 ids.into_iter()
143 .filter_map(|id| hub.pending.remove(&id).map(|p| p.2))
144 .collect::<Vec<_>>()
145 };
146 for state in states {
147 finish(&state, Err(Error("JavaScript context disposed".into())));
148 }
149 }
150 }
151}
152
153#[derive(Serialize)]
154struct Request {
155 id: String,
156 context: String,
157 source: Option<String>,
158}
159#[derive(Default)]
160struct Hub {
161 available: bool,
162 queue: VecDeque<Request>,
163 pending: BTreeMap<String, (String, Option<f64>, Shared)>,
164}
165fn hub() -> &'static Mutex<Hub> {
166 static HUB: OnceLock<Mutex<Hub>> = OnceLock::new();
167 HUB.get_or_init(Default::default)
168}
169pub mod host {
171 use super::*;
172 pub fn start() {
173 hub().lock().unwrap().available = true;
174 }
175 pub fn stop() {
176 let states = {
177 let mut hub = hub().lock().unwrap();
178 hub.available = false;
179 hub.queue.clear();
180 std::mem::take(&mut hub.pending)
181 };
182 for (_, (_, _, state)) in states {
183 finish(&state, Err(Error("JavaScript host stopped".into())));
184 }
185 }
186 pub fn poll(now_ms: f64) -> String {
188 let (requests, expired) = {
189 let mut hub = hub().lock().unwrap();
190 let ids: Vec<_> = hub
191 .pending
192 .iter_mut()
193 .filter_map(|(id, p)| {
194 let deadline = p.1.get_or_insert(now_ms + 30_000.0);
195 (now_ms >= *deadline).then(|| id.clone())
196 })
197 .collect();
198 let expired: Vec<_> = ids
199 .into_iter()
200 .filter_map(|id| hub.pending.remove(&id))
201 .collect();
202 (hub.queue.drain(..).collect::<Vec<_>>(), expired)
203 };
204 for (_, _, state) in expired {
205 finish(&state, Err(Error("JavaScript evaluation timed out".into())));
206 }
207 if requests.is_empty() {
208 String::new()
209 } else {
210 serde_json::to_string(&requests).unwrap()
211 }
212 }
213 pub fn complete(json: &str) {
214 #[derive(Deserialize)]
215 struct Reply {
216 id: String,
217 #[serde(default)]
218 value: Value,
219 error: Option<String>,
220 }
221 let Ok(reply) = serde_json::from_str::<Reply>(json) else {
222 return;
223 };
224 let pending = hub().lock().unwrap().pending.remove(&reply.id);
225 if let Some((_, _, state)) = pending {
226 finish(
227 &state,
228 reply.error.map_or(Ok(reply.value), |e| Err(Error(e))),
229 );
230 }
231 }
232}
233#[cfg(feature = "boa")]
234mod boa {
235 use super::*;
236 use boa_engine::{
237 Context as JsContext, Source, builtins::promise::PromiseState, object::builtins::JsPromise,
238 };
239 pub fn worker() -> std::result::Result<std::sync::mpsc::Sender<(String, Shared)>, Error> {
240 let (tx, rx) = std::sync::mpsc::channel::<(String, Shared)>();
241 std::thread::Builder::new()
242 .name("bern-js".into())
243 .spawn(move || {
244 let mut context = JsContext::default();
245 context
246 .runtime_limits_mut()
247 .set_loop_iteration_limit(1_000_000);
248 context.runtime_limits_mut().set_recursion_limit(512);
249 for (source, state) in rx {
250 let result = evaluate(&source, &mut context).map_err(Error);
251 finish(&state, result);
252 }
253 })
254 .map_err(|e| Error(e.to_string()))?;
255 Ok(tx)
256 }
257 fn evaluate(source: &str, context: &mut JsContext) -> std::result::Result<Value, String> {
258 let value = context
259 .eval(Source::from_bytes(source))
260 .map_err(|e| e.to_string())?;
261 context.run_jobs().map_err(|e| e.to_string())?;
262 let value = if let Some(object) = value.as_object() {
263 if let Ok(promise) = JsPromise::from_object(object.clone()) {
264 match promise.state() {
265 PromiseState::Fulfilled(value) => value,
266 PromiseState::Rejected(error) => {
267 return Err(error
268 .to_string(context)
269 .map(|s| s.to_std_string_escaped())
270 .unwrap_or_else(|e| e.to_string()));
271 }
272 PromiseState::Pending => {
273 return Err(
274 "Promise did not settle; no external async host is installed".into(),
275 );
276 }
277 }
278 } else {
279 value
280 }
281 } else {
282 value
283 };
284 if value.is_undefined() {
285 return Ok(Value::Null);
286 }
287 value
288 .to_json(context)
289 .map(|v| v.unwrap_or(Value::Null))
290 .map_err(|e| e.to_string())
291 }
292}
293
294pub const HOST_RUNTIME: &str = include_str!("runtime.js");