use anyhow::{anyhow, Context};
use std::fmt::Debug;
use std::thread;
use std::time::Duration;
use crate::handler::function::vm::BASE_VM;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EvaluateResponse {
pub output: Value,
pub log: Vec<Value>,
}
pub struct Script {
isolate: v8::OwnedIsolate,
timeout: Option<Duration>,
}
impl Script {
pub fn new() -> Self {
Self {
isolate: v8::Isolate::new(
v8::CreateParams::default().snapshot_blob(BASE_VM.as_slice()),
),
timeout: None,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
assert!(timeout > Duration::ZERO);
self.timeout = Some(timeout);
self
}
pub async fn call<P>(&mut self, source: &str, args: &P) -> anyhow::Result<EvaluateResponse>
where
P: Serialize,
{
let handle = self.isolate.thread_safe_handle();
let args_str =
serde_json::to_string(args).context("Failed to serialize function arguments")?;
let js_code_source = format!("const now = Date.now(); main({});", args_str);
let handle_scope = &mut v8::HandleScope::new(&mut self.isolate);
let context = v8::Context::new(handle_scope);
let scope = &mut v8::ContextScope::new(handle_scope, context);
let tc_scope = &mut v8::TryCatch::new(scope);
let src = v8::String::new(tc_scope, source).context("Failed to compile source code")?;
let js_src = v8::String::new(tc_scope, js_code_source.as_str())
.context("Failed to compile source code")?;
if let Some(timeout) = self.timeout {
thread::spawn(move || {
thread::sleep(timeout);
handle.terminate_execution();
});
}
let Some(src_script) = v8::Script::compile(tc_scope, src, None) else {
let exception = tc_scope.exception().context("Failed to load script")?;
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
};
if let None = src_script.run(tc_scope) {
let exception = tc_scope.exception().unwrap();
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
}
let Some(js_script) = v8::Script::compile(tc_scope, js_src, None) else {
let exception = tc_scope.exception().context("Failed to load script")?;
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
};
let Some(result) = js_script.run(tc_scope) else {
if tc_scope.has_terminated() {
return Err(anyhow!("Timeout exceeded"));
}
let exception = tc_scope
.exception()
.context("Failed to run loaded script")?;
return Err(anyhow!(exception.to_rust_string_lossy(tc_scope)));
};
serde_v8::from_v8(tc_scope, result).context("Failed to parse function result")
}
}