use super::super::host::EnvVars;
use crate::engine::ctx::Ctx as EngineCtx;
use crate::engine::duration;
use crate::runtime::report::Event;
use rquickjs::function::Opt;
use rquickjs::{Ctx, Error as JsError, Function, JsLifetime, Value};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub(in crate::script::js) struct HostState {
pub(in crate::script::js) eng: Arc<EngineCtx>,
pub(in crate::script::js) reg: Arc<super::super::host::Registry>,
pub(in crate::script::js) env: EnvVars,
pub(in crate::script::js) base: PathBuf,
}
unsafe impl<'js> JsLifetime<'js> for HostState {
type Changed<'to> = HostState;
}
#[ringo_flow_macros::ts_global(name = "log")]
pub(in crate::script::js) fn log_global(cx: Ctx<'_>, msg: String) {
let h = cx
.userdata::<HostState>()
.expect("host state stored at install");
h.eng.emit(&Event::Log { message: &msg });
}
#[ringo_flow_macros::ts_global(name = "env")]
pub(in crate::script::js) fn env_global(cx: Ctx<'_>, key: String) -> rquickjs::Result<String> {
let h = cx
.userdata::<HostState>()
.expect("host state stored at install");
if let Some(v) = h.env.lock().unwrap().get(&key) {
return Ok(v.clone());
}
std::env::var(&key).map_err(|_| {
super::super::convert::throw(&cx, &format!("environment variable `{key}` is not set"))
})
}
#[ringo_flow_macros::ts_global(name = "loadEnv")]
pub(in crate::script::js) fn load_env_global(cx: Ctx<'_>, path: String) -> rquickjs::Result<()> {
let h = cx
.userdata::<HostState>()
.expect("host state stored at install");
let p = h.base.join(&path);
crate::script::dotenv::merge_dotenv(&p, &mut h.env.lock().unwrap())
.map_err(|e| super::super::convert::throw(&cx, &e.to_string()))
}
#[ringo_flow_macros::ts_global(name = "defaultTimeout")]
pub(in crate::script::js) fn default_timeout_global(
cx: Ctx<'_>,
duration: String,
) -> rquickjs::Result<()> {
let d =
duration::parse_duration(&duration).map_err(|e| super::super::convert::throw(&cx, &e))?;
cx.userdata::<HostState>()
.expect("host state stored at install")
.eng
.set_default_timeout(d);
Ok(())
}
#[ringo_flow_macros::ts_global(name = "uuid")]
pub(in crate::script::js) fn uuid_global() -> String {
uuid::Uuid::new_v4().to_string()
}
#[ringo_flow_macros::ts_global(name = "wait")]
pub(in crate::script::js) async fn wait_async<'js>(
cx: Ctx<'js>,
seconds: i64,
) -> rquickjs::Result<()> {
let eng = cx
.userdata::<HostState>()
.expect("host state stored at install")
.eng
.clone();
let secs = seconds.max(0) as u64;
eng.emit(&Event::Wait {
seconds: secs as f64,
});
let watchers = {
let sessions = eng.sessions.lock().unwrap_or_else(|e| e.into_inner());
sessions
.iter()
.map(|(name, s)| (name.clone(), s.state()))
.collect::<Vec<_>>()
};
match eng
.rt
.spawn(crate::runtime::wait_holding(
Duration::from_secs(secs),
watchers,
))
.await
{
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => Err(super::super::convert::throw(&cx, &e.to_string())),
Err(e) => Err(super::super::convert::throw(
&cx,
&format!("wait task failed: {e}"),
)),
}
}
#[ringo_flow_macros::ts_global(name = "until")]
pub(in crate::script::js) async fn until_async<'js>(
cx: Ctx<'js>,
#[jsdoc(type = "() => unknown")] cond: Function<'js>,
within: Opt<String>,
) -> rquickjs::Result<Value<'js>> {
const TICK: Duration = Duration::from_millis(25);
let (eng, reg) = {
let st = cx
.userdata::<HostState>()
.expect("host state stored at install");
(st.eng.clone(), st.reg.clone())
};
let timeout = match within.0 {
Some(s) => {
duration::parse_duration(&s).map_err(|e| super::super::convert::throw(&cx, &e))?
}
None => eng.default_timeout(),
};
eng.set_assert_silent(true);
let deadline = Instant::now() + timeout;
let outcome: Result<Value, (String, bool)> = loop {
super::mock::pump_bridged(&cx, ®);
match cond.call::<_, Value>(()) {
Ok(v) => break Ok(v),
Err(JsError::Exception) => {
let (text, fatal) = caught_error(&cx);
if fatal {
break Err((text, true));
}
if Instant::now() >= deadline {
break Err((text, false));
}
}
Err(e) => break Err((e.to_string(), false)),
}
let _ = eng.rt.spawn(async { tokio::time::sleep(TICK).await }).await;
};
eng.set_assert_silent(false);
eng.emit_last_assert();
outcome.map_err(|(e, fatal)| {
let msg = if fatal {
format!(
"condition threw a JS error — a bug in the condition, not an unmet assertion:\n{e}"
)
} else {
format!("not satisfied within {timeout:?}: {e}")
};
super::super::convert::throw(&cx, &msg)
})
}
pub(in crate::script::js) fn exception_text(ctx: &Ctx<'_>) -> String {
let caught = ctx.catch();
if let Some(ex) = caught.as_exception() {
let msg = ex
.message()
.or_else(|| ex.to_string().into())
.unwrap_or_else(|| "exception".into());
match ex.stack().filter(|s| !s.trim().is_empty()) {
Some(s) => format!("{msg}\n{}", s.trim_end()),
None => msg,
}
} else if let Some(s) = caught.as_string() {
s.to_string().unwrap_or_else(|_| "exception".into())
} else {
"exception".into()
}
}
fn caught_error(ctx: &Ctx<'_>) -> (String, bool) {
let caught = ctx.catch();
if let Some(obj) = caught.as_object() {
let name = obj.get::<_, String>("name").ok();
let message = obj
.get::<_, String>("message")
.ok()
.filter(|m| !m.is_empty());
let fatal = matches!(
name.as_deref(),
Some("TypeError" | "ReferenceError" | "SyntaxError")
);
let stack = obj
.get::<_, String>("stack")
.ok()
.filter(|s| !s.trim().is_empty());
let mut text = match (name, message) {
(Some(n), Some(m)) => format!("{n}: {m}"),
(Some(n), None) => n,
(None, Some(m)) => m,
(None, None) => "exception".into(),
};
if let Some(s) = stack {
text.push('\n');
text.push_str(s.trim_end());
}
return (text, fatal);
}
let text = caught
.as_string()
.and_then(|s| s.to_string().ok())
.unwrap_or_else(|| "exception".into());
(text, false)
}
pub fn format_exception(ctx: &Ctx<'_>, err: JsError, label: &str) -> String {
match err {
JsError::Exception => format!("in {label}: {}", exception_text(ctx)),
other => format!("in {label}: {other}"),
}
}