use crate::error::MdxError;
use anyhow::Result as AnyhowResult;
use deno_core::JsRuntime;
use std::cell::RefCell;
use std::rc::Rc;
use super::constants::script_tags;
const CONTEXT_SETUP_OVERHEAD: usize = 150;
pub(super) struct RuntimeCleanupGuard {
runtime: Rc<RefCell<JsRuntime>>,
}
impl RuntimeCleanupGuard {
pub(super) fn new(runtime: Rc<RefCell<JsRuntime>>) -> Self {
Self { runtime }
}
}
impl Drop for RuntimeCleanupGuard {
fn drop(&mut self) {
if let Ok(mut runtime) = self.runtime.try_borrow_mut() {
if let Err(err) = cleanup_runtime(&mut runtime) {
eprintln!("Renderer runtime cleanup failed: {err:?}");
}
}
}
}
pub(super) fn with_runtime<R>(
runtime: Rc<RefCell<JsRuntime>>,
f: impl FnOnce(&mut JsRuntime) -> AnyhowResult<R>,
) -> AnyhowResult<R> {
let mut rt = runtime.try_borrow_mut().map_err(|e| {
anyhow::anyhow!(
"Failed to borrow runtime mutably: {e}. This may indicate concurrent access, \
cleanup in progress, or recursive runtime operations within the same thread."
)
})?;
let _cleanup = RuntimeCleanupGuard::new(Rc::clone(&runtime));
f(&mut rt)
}
pub(super) fn cleanup_runtime(runtime: &mut JsRuntime) -> Result<(), MdxError> {
const CLEANUP_SCRIPT: &str = r#"
try {
if (Array.isArray(globalThis.__registered_component_names)) {
for (const name of globalThis.__registered_component_names) {
if (typeof name === 'string') {
delete globalThis[name];
}
}
globalThis.__registered_component_names.length = 0;
}
delete globalThis.Component;
if (typeof module !== 'undefined' && module && module.exports) {
module.exports = {};
}
if (typeof globalThis.exports !== 'undefined' && globalThis.exports) {
globalThis.exports = {};
}
delete globalThis.context;
} catch (cleanupError) {
console.warn('Renderer cleanup failed', cleanupError);
}
"#;
runtime
.execute_script(script_tags::CLEANUP_RUNTIME, CLEANUP_SCRIPT)
.map_err(|e| MdxError::TsxTransform(format!("Failed to cleanup runtime: {e:?}")))?;
Ok(())
}
pub(super) fn setup_context(runtime: &mut JsRuntime, props_json: &str) -> Result<(), MdxError> {
use std::fmt::Write;
let mut setup_context = String::with_capacity(props_json.len() + CONTEXT_SETUP_OVERHEAD + 200);
write!(
setup_context,
r#"
try {{
const contextData = {props_json};
globalThis.context = (function(options) {{
return function(key) {{
return key.split(".").reduce((o, i) => {{
if (o) return o[i];
return undefined;
}}, options);
}};
}})(contextData);
}} catch (e) {{
console.warn('Failed to parse metadata, using empty object for context');
globalThis.context = (function(options) {{
return function(key) {{
return key.split(".").reduce((o, i) => {{
if (o) return o[i];
return undefined;
}}, options);
}};
}})({{}});
}}
"#,
props_json = props_json
)
.map_err(|e| MdxError::TsxTransform(format!("Failed to build context setup script: {e}")))?;
runtime
.execute_script(script_tags::SETUP_CONTEXT, setup_context)
.map_err(|e| MdxError::TsxTransform(format!("Failed to setup context: {e:?}")))?;
Ok(())
}
pub(super) fn extract_string_from_v8(
result: deno_core::v8::Global<deno_core::v8::Value>,
runtime: &mut JsRuntime,
error_msg: &str,
) -> Result<String, MdxError> {
let scope = &mut runtime.handle_scope();
let local = deno_core::v8::Local::new(scope, result);
if local.is_string() {
local
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope))
.ok_or_else(|| MdxError::TsxTransform(error_msg.to_string()))
} else {
Err(MdxError::TsxTransform(format!(
"{error_msg}: result is not a string"
)))
}
}