Skip to main content

arcane_engine/scripting/
runtime.rs

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