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    pub fn new_with_render_bridge(
156        bridge: Rc<RefCell<super::render_ops::RenderBridgeState>>,
157    ) -> Self {
158        Self::new_with_render_bridge_and_import_map(bridge, ImportMap::new())
159    }
160
161    pub fn new_with_render_bridge_and_import_map(
162        bridge: Rc<RefCell<super::render_ops::RenderBridgeState>>,
163        import_map: ImportMap,
164    ) -> Self {
165        let runtime = JsRuntime::new(RuntimeOptions {
166            module_loader: Some(Rc::new(TsModuleLoader::with_import_map(import_map))),
167            extensions: vec![
168                arcane_ext::init(),
169                super::render_ops::render_ext::init(),
170            ],
171            ..Default::default()
172        });
173
174        let mut rt = Self { runtime };
175
176        // Store bridge state in op_state
177        {
178            let op_state = rt.runtime.op_state();
179            op_state.borrow_mut().put(bridge);
180        }
181
182        rt.runtime
183            .execute_script("<crypto_polyfill>", CRYPTO_POLYFILL)
184            .expect("Failed to install crypto polyfill");
185        rt
186    }
187
188    /// Execute a non-static script string. Used for per-frame callbacks.
189    pub fn execute_script_string(
190        &mut self,
191        name: &'static str,
192        source: impl Into<String>,
193    ) -> anyhow::Result<()> {
194        let source: String = source.into();
195        self.runtime
196            .execute_script(name, deno_core::FastString::from(source))
197            .context("Script execution failed")?;
198        Ok(())
199    }
200
201    /// Evaluate a script and return the result as a string.
202    /// Works in headless mode — used by agent protocol commands.
203    pub fn eval_to_string(&mut self, source: &str) -> anyhow::Result<String> {
204        // Wrap the expression: convert to string, then store via op
205        let script = format!(
206            "Deno.core.ops.op_agent_store_eval_result(String({}))",
207            source
208        );
209        self.runtime
210            .execute_script(
211                "<agent_eval>",
212                deno_core::FastString::from(script),
213            )
214            .context("Agent eval failed")?;
215
216        // Read result from OpState
217        let op_state = self.runtime.op_state();
218        let result = op_state
219            .borrow_mut()
220            .take::<AgentEvalResult>()
221            .0;
222        Ok(result)
223    }
224
225    /// Access the inner JsRuntime for advanced operations.
226    pub fn inner(&mut self) -> &mut JsRuntime {
227        &mut self.runtime
228    }
229}