Skip to main content

arcane_engine/scripting/
runtime.rs

1#[cfg(feature = "renderer")]
2use std::cell::RefCell;
3use std::path::Path;
4use std::rc::Rc;
5
6use anyhow::Context;
7use deno_core::JsRuntime;
8use deno_core::ModuleSpecifier;
9use deno_core::OpState;
10use deno_core::RuntimeOptions;
11
12use super::{ImportMap, TsModuleLoader};
13
14/// Wraps a `deno_core::JsRuntime` configured with our TypeScript module loader.
15pub struct ArcaneRuntime {
16    runtime: JsRuntime,
17}
18
19/// Newtype to store eval results in OpState.
20struct AgentEvalResult(String);
21
22deno_core::extension!(
23    arcane_ext,
24    ops = [op_crypto_random_uuid, op_agent_store_eval_result],
25);
26
27/// Polyfill for `crypto.randomUUID()` which deno_core's V8 doesn't provide.
28#[deno_core::op2]
29#[string]
30fn op_crypto_random_uuid() -> String {
31    generate_uuid()
32}
33
34/// Store a string value from JS into OpState for eval_to_string to read back.
35#[deno_core::op2(fast)]
36fn op_agent_store_eval_result(state: &mut OpState, #[string] value: &str) {
37    // Replace any previous result
38    if state.has::<AgentEvalResult>() {
39        state.take::<AgentEvalResult>();
40    }
41    state.put(AgentEvalResult(value.to_string()));
42}
43
44/// Generate a v4 UUID string.
45pub(super) fn generate_uuid() -> String {
46    let mut bytes = [0u8; 16];
47    getrandom(&mut bytes);
48    bytes[6] = (bytes[6] & 0x0f) | 0x40;
49    bytes[8] = (bytes[8] & 0x3f) | 0x80;
50
51    format!(
52        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
53        bytes[0], bytes[1], bytes[2], bytes[3],
54        bytes[4], bytes[5],
55        bytes[6], bytes[7],
56        bytes[8], bytes[9],
57        bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
58    )
59}
60
61/// Fill buffer with random bytes using platform APIs.
62fn getrandom(buf: &mut [u8]) {
63    use std::collections::hash_map::RandomState;
64    use std::hash::{BuildHasher, Hasher};
65    let mut i = 0;
66    while i < buf.len() {
67        let s = RandomState::new();
68        let mut h = s.build_hasher();
69        h.write_u64(i as u64);
70        let bytes = h.finish().to_le_bytes();
71        let remaining = buf.len() - i;
72        let copy_len = remaining.min(8);
73        buf[i..i + copy_len].copy_from_slice(&bytes[..copy_len]);
74        i += copy_len;
75    }
76}
77
78const CRYPTO_POLYFILL: &str = r#"
79if (typeof globalThis.crypto === "undefined") {
80    globalThis.crypto = {};
81}
82if (typeof globalThis.crypto.randomUUID !== "function") {
83    globalThis.crypto.randomUUID = () => Deno.core.ops.op_crypto_random_uuid();
84}
85"#;
86
87impl ArcaneRuntime {
88    /// Create a new runtime with the TypeScript module loader and polyfills.
89    pub fn new() -> Self {
90        Self::new_with_import_map(ImportMap::new())
91    }
92
93    /// Create a new runtime with a custom import map for module resolution.
94    pub fn new_with_import_map(import_map: ImportMap) -> Self {
95        let runtime = JsRuntime::new(RuntimeOptions {
96            module_loader: Some(Rc::new(TsModuleLoader::with_import_map(import_map))),
97            extensions: vec![arcane_ext::init()],
98            ..Default::default()
99        });
100
101        let mut rt = Self { runtime };
102        rt.runtime
103            .execute_script("<crypto_polyfill>", CRYPTO_POLYFILL)
104            .expect("Failed to install crypto polyfill");
105        rt
106    }
107
108    /// Execute an inline script (not a module).
109    pub fn execute_script(
110        &mut self,
111        name: &'static str,
112        source: &'static str,
113    ) -> anyhow::Result<()> {
114        self.runtime
115            .execute_script(name, source)
116            .context("Script execution failed")?;
117        Ok(())
118    }
119
120    /// Execute an inline script and return the v8 global handle.
121    pub fn execute_script_global(
122        &mut self,
123        name: &'static str,
124        source: &'static str,
125    ) -> anyhow::Result<deno_core::v8::Global<deno_core::v8::Value>> {
126        self.runtime
127            .execute_script(name, source)
128            .map_err(|e| anyhow::anyhow!("{e}"))
129    }
130
131    /// Load and evaluate a TypeScript/JavaScript file as an ES module.
132    pub async fn execute_file(&mut self, path: &Path) -> anyhow::Result<()> {
133        let specifier = ModuleSpecifier::from_file_path(path).map_err(|_| {
134            anyhow::anyhow!("Cannot convert path to module specifier: {}", path.display())
135        })?;
136
137        let mod_id = self
138            .runtime
139            .load_main_es_module(&specifier)
140            .await
141            .context("Failed to load module")?;
142
143        let result = self.runtime.mod_evaluate(mod_id);
144        self.runtime
145            .run_event_loop(Default::default())
146            .await
147            .context("Event loop error")?;
148        result.await.context("Module evaluation failed")?;
149        Ok(())
150    }
151
152    /// Create a runtime with the render bridge extension for `arcane dev`.
153    /// Includes both crypto polyfill and render ops.
154    #[cfg(feature = "renderer")]
155    #[cfg(feature = "renderer")]
156    pub fn new_with_render_bridge(
157        bridge: Rc<RefCell<super::render_ops::RenderBridgeState>>,
158    ) -> Self {
159        Self::new_with_render_bridge_and_import_map(bridge, ImportMap::new())
160    }
161
162    #[cfg(feature = "renderer")]
163    pub fn new_with_render_bridge_and_import_map(
164        bridge: Rc<RefCell<super::render_ops::RenderBridgeState>>,
165        import_map: ImportMap,
166    ) -> Self {
167        let runtime = JsRuntime::new(RuntimeOptions {
168            module_loader: Some(Rc::new(TsModuleLoader::with_import_map(import_map))),
169            extensions: vec![
170                arcane_ext::init(),
171                super::render_ops::render_ext::init(),
172            ],
173            ..Default::default()
174        });
175
176        let mut rt = Self { runtime };
177
178        // Store bridge state in op_state
179        {
180            let op_state = rt.runtime.op_state();
181            op_state.borrow_mut().put(bridge);
182        }
183
184        rt.runtime
185            .execute_script("<crypto_polyfill>", CRYPTO_POLYFILL)
186            .expect("Failed to install crypto polyfill");
187        rt
188    }
189
190    /// Execute a non-static script string. Used for per-frame callbacks.
191    pub fn execute_script_string(
192        &mut self,
193        name: &'static str,
194        source: impl Into<String>,
195    ) -> anyhow::Result<()> {
196        let source: String = source.into();
197        self.runtime
198            .execute_script(name, deno_core::FastString::from(source))
199            .context("Script execution failed")?;
200        Ok(())
201    }
202
203    /// Evaluate a script and return the result as a string.
204    /// Works in headless mode — used by agent protocol commands.
205    pub fn eval_to_string(&mut self, source: &str) -> anyhow::Result<String> {
206        // Wrap the expression: convert to string, then store via op
207        let script = format!(
208            "Deno.core.ops.op_agent_store_eval_result(String({}))",
209            source
210        );
211        self.runtime
212            .execute_script(
213                "<agent_eval>",
214                deno_core::FastString::from(script),
215            )
216            .context("Agent eval failed")?;
217
218        // Read result from OpState
219        let op_state = self.runtime.op_state();
220        let result = op_state
221            .borrow_mut()
222            .take::<AgentEvalResult>()
223            .0;
224        Ok(result)
225    }
226
227    /// Access the inner JsRuntime for advanced operations.
228    pub fn inner(&mut self) -> &mut JsRuntime {
229        &mut self.runtime
230    }
231}