use std::{
cell::RefCell,
rc::Rc,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
use deno_core::{Extension, JsRuntime, OpState, RuntimeOptions, op2, v8};
use serde_json::Value;
use crate::types::{LogEntry, LogLevel, ResourceLimits};
#[derive(Clone)]
struct LogCollector {
logs: Arc<Mutex<Vec<LogEntry>>>,
max_entries: usize,
}
#[op2(fast)]
#[allow(clippy::inline_always)] #[allow(clippy::needless_pass_by_value)] fn fraiseql_log(state: Rc<RefCell<OpState>>, #[smi] level: u8, #[string] message: String) {
let state = state.borrow();
let collector = state.borrow::<LogCollector>();
let mut logs = collector.logs.lock().expect("log mutex poisoned");
if logs.len() < collector.max_entries {
let log_level = match level {
0 => LogLevel::Debug,
2 => LogLevel::Warn,
3 => LogLevel::Error,
_ => LogLevel::Info,
};
logs.push(LogEntry {
level: log_level,
message,
timestamp: chrono::Utc::now(),
});
}
}
fn make_fraiseql_extension(collector: LogCollector) -> Extension {
Extension {
name: "fraiseql",
ops: std::borrow::Cow::Owned(vec![fraiseql_log()]),
op_state_fn: Some(Box::new(move |state: &mut OpState| {
state.put(collector);
})),
..Default::default()
}
}
fn wrap_source(source: &str, event_json: &str) -> String {
let inner = source
.replace("export default async function", "const __fn = async function")
.replace("export default async", "const __fn = async")
.replace("export default function", "const __fn = function")
.replace("export default", "const __fn =");
format!(
r"
{inner}
(async () => {{
try {{
const __event = {event_json};
const __result = await __fn(__event);
globalThis.__fraiseql_result = JSON.stringify(__result);
globalThis.__fraiseql_error = null;
}} catch (e) {{
globalThis.__fraiseql_result = null;
globalThis.__fraiseql_error = String(e);
}}
}})();
"
)
}
pub struct ExecutionResult {
pub value: Value,
pub logs: Vec<LogEntry>,
}
pub fn run_in_dedicated_thread(
source: &str,
event_value: &Value,
limits: &ResourceLimits,
) -> Result<ExecutionResult, String> {
let logs_arc: Arc<Mutex<Vec<LogEntry>>> = Arc::new(Mutex::new(Vec::new()));
let collector = LogCollector {
logs: Arc::clone(&logs_arc),
max_entries: limits.max_log_entries,
};
let event_json = serde_json::to_string(event_value).map_err(|e| e.to_string())?;
let wrapped = wrap_source(source, &event_json);
let max_duration = limits.max_duration;
let max_memory_bytes = usize::try_from(limits.max_memory_bytes).unwrap_or(usize::MAX);
let mem_exceeded_run = Arc::new(AtomicBool::new(false));
let timed_out_run = Arc::new(AtomicBool::new(false));
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("Failed to create tokio runtime: {e}"))?;
let result = rt.block_on(async move {
let create_params = v8::CreateParams::default().heap_limits(0, max_memory_bytes);
let mut js_runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![make_fraiseql_extension(collector)],
create_params: Some(create_params),
..Default::default()
});
let isolate_handle = js_runtime.v8_isolate().thread_safe_handle();
let watchdog_done = Arc::new(AtomicBool::new(false));
let watchdog_done_thread = Arc::clone(&watchdog_done);
let timed_out_watchdog = Arc::clone(&timed_out_run);
let watchdog = std::thread::spawn(move || {
let deadline = std::time::Instant::now() + max_duration;
let poll = std::time::Duration::from_millis(10);
while std::time::Instant::now() < deadline {
if watchdog_done_thread.load(Ordering::Acquire) {
return;
}
std::thread::sleep(poll);
}
if !watchdog_done_thread.load(Ordering::Acquire) {
timed_out_watchdog.store(true, Ordering::Release);
isolate_handle.terminate_execution();
}
});
let mem_flag = Arc::clone(&mem_exceeded_run);
let heap_handle = js_runtime.v8_isolate().thread_safe_handle();
js_runtime.add_near_heap_limit_callback(move |current_limit, _initial| {
mem_flag.store(true, Ordering::Release);
heap_handle.terminate_execution();
current_limit.saturating_add(current_limit / 2).max(current_limit + 1)
});
let classify = |raw: &str, mem: &Arc<AtomicBool>, time: &Arc<AtomicBool>| -> String {
if mem.load(Ordering::Acquire) {
"Memory limit exceeded: heap allocation exceeded the configured limit".to_string()
} else if time.load(Ordering::Acquire) {
"Execution timeout: script exceeded the configured time limit".to_string()
} else if raw.contains("SyntaxError") || raw.contains("Parse") {
format!("SyntaxError: {raw}")
} else {
format!("Execution error: {raw}")
}
};
let exec_outcome = js_runtime.execute_script("<fraiseql-function>", wrapped);
watchdog_done.store(true, Ordering::Release);
if let Err(e) = exec_outcome {
return Err(classify(&e.to_string(), &mem_exceeded_run, &timed_out_run));
}
let loop_outcome = tokio::time::timeout(
max_duration,
js_runtime.run_event_loop(deno_core::PollEventLoopOptions::default()),
)
.await;
match loop_outcome {
Err(_) => {
return Err("Execution timeout: event loop exceeded time limit".to_string());
},
Ok(Err(e)) => {
return Err(classify(&e.to_string(), &mem_exceeded_run, &timed_out_run));
},
Ok(Ok(())) => {},
}
watchdog_done.store(true, Ordering::Release);
let _ = watchdog.join();
let result_global = js_runtime
.execute_script("<get-result>", "globalThis.__fraiseql_result")
.map_err(|e| format!("Failed to read result: {e}"))?;
let error_global = js_runtime
.execute_script("<get-error>", "globalThis.__fraiseql_error")
.map_err(|e| format!("Failed to read error: {e}"))?;
let (result_json, error_str) = {
let scope = &mut js_runtime.handle_scope();
let result_local = deno_core::v8::Local::new(scope, result_global);
let error_local = deno_core::v8::Local::new(scope, error_global);
if result_local.is_undefined() && error_local.is_undefined() {
return Err("Execution incomplete: function did not produce a result \
(possible unresolved promise)"
.to_string());
}
let error_str = if error_local.is_null_or_undefined() {
None
} else {
Some(error_local.to_rust_string_lossy(scope))
};
let result_json = if result_local.is_null_or_undefined() {
None
} else {
Some(result_local.to_rust_string_lossy(scope))
};
(result_json, error_str)
};
if let Some(err) = error_str {
return Err(format!("Runtime error: {err}"));
}
let value: Value = match result_json {
Some(json_str) => serde_json::from_str(&json_str).unwrap_or(Value::String(json_str)),
None => Value::Null,
};
Ok(value)
});
let logs = logs_arc.lock().expect("log mutex poisoned").clone();
match result {
Ok(value) => Ok(ExecutionResult { value, logs }),
Err(e) => Err(e),
}
}