rohas_runtime/
node_runtime.rs

1use crate::error::Result;
2use crate::handler::{HandlerContext, HandlerResult};
3use once_cell::sync::Lazy;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex};
7use tracing::{debug, info};
8use v8::{Context, ContextScope, HandleScope, Script};
9
10static V8_PLATFORM: Lazy<()> = Lazy::new(|| {
11    let platform = v8::new_default_platform(0, false).make_shared();
12    v8::V8::initialize_platform(platform);
13    v8::V8::initialize();
14    info!("V8 platform initialized");
15});
16
17pub struct NodeRuntime {
18    /// Loaded modules cache
19    modules: Arc<Mutex<HashMap<String, String>>>,
20    /// Project root for resolving compiled output
21    project_root: Option<PathBuf>,
22}
23
24impl NodeRuntime {
25    pub fn set_project_root(&mut self, root: PathBuf) {
26        self.project_root = Some(root);
27    }
28
29    fn resolve_handler_path(&self, handler_path: &Path) -> PathBuf {
30        if let Some(project_root) = &self.project_root {
31            let relative_path = if handler_path.is_absolute() {
32                handler_path.strip_prefix(project_root).ok()
33            } else {
34                Some(handler_path)
35            };
36
37            if let Some(rel_path) = relative_path {
38                if let Some(ext) = rel_path.extension() {
39                    if ext == "ts" || ext == "tsx" {
40                        let stripped = rel_path.strip_prefix("src").unwrap_or(rel_path);
41                        let mut compiled_path = project_root.join(".rohas").join(stripped);
42                        compiled_path.set_extension("js");
43
44                        if compiled_path.exists() {
45                            debug!("Resolved to compiled path: {:?}", compiled_path);
46                            return compiled_path;
47                        }
48                    }
49                }
50            }
51        }
52
53        handler_path.to_path_buf()
54    }
55
56    pub fn new() -> Result<Self> {
57        Lazy::force(&V8_PLATFORM);
58
59        info!("V8 JavaScript runtime initialized");
60
61        Ok(Self {
62            modules: Arc::new(Mutex::new(HashMap::new())),
63            project_root: None,
64        })
65    }
66
67    pub async fn execute_handler(
68        &self,
69        handler_path: &Path,
70        context: HandlerContext,
71    ) -> Result<HandlerResult> {
72        let start = std::time::Instant::now();
73
74        debug!("Executing JavaScript handler with V8: {:?}", handler_path);
75
76        let resolved_path = self.resolve_handler_path(handler_path);
77
78        debug!("Resolved handler path: {:?}", resolved_path);
79
80        let absolute_path = if resolved_path.is_absolute() {
81            resolved_path
82        } else {
83            std::env::current_dir()?.join(&resolved_path)
84        };
85
86        let handler_code = tokio::fs::read_to_string(&absolute_path).await?;
87
88        let module_key = absolute_path.to_string_lossy().to_string();
89        {
90            let mut modules = self.modules.lock().unwrap();
91            modules.insert(module_key.clone(), handler_code.clone());
92        }
93
94        let result = tokio::task::spawn_blocking(move || {
95            Self::execute_js_code_sync(&handler_code, &context)
96        })
97        .await
98        .map_err(|e| {
99            crate::error::RuntimeError::ExecutionFailed(format!("Blocking task failed: {}", e))
100        })??;
101
102        let execution_time_ms = start.elapsed().as_millis() as u64;
103        Ok(HandlerResult {
104            execution_time_ms,
105            ..result
106        })
107    }
108
109    fn execute_js_code_sync(handler_code: &str, context: &HandlerContext) -> Result<HandlerResult> {
110        let context_json = serde_json::to_string(context)?;
111        let handler_name = &context.handler_name;
112
113        let wrapper = Self::generate_wrapper(handler_code, &context_json, handler_name);
114
115        let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
116        let scope = std::pin::pin!(HandleScope::new(isolate));
117        let scope = &mut scope.init();
118        let v8_context = Context::new(scope, Default::default());
119        let scope = &mut ContextScope::new(scope, v8_context);
120
121        let code = v8::String::new(scope, &wrapper).ok_or_else(|| {
122            crate::error::RuntimeError::ExecutionFailed("Failed to create V8 string".into())
123        })?;
124
125        let script = Script::compile(scope, code, None).ok_or_else(|| {
126            crate::error::RuntimeError::ExecutionFailed("Failed to compile script".into())
127        })?;
128
129        let result = script.run(scope).ok_or_else(|| {
130            crate::error::RuntimeError::ExecutionFailed("Script execution failed".into())
131        })?;
132
133        let result = if result.is_promise() {
134            let promise = v8::Local::<v8::Promise>::try_from(result).map_err(|_| {
135                crate::error::RuntimeError::ExecutionFailed("Failed to cast to Promise".into())
136            })?;
137
138            while promise.state() == v8::PromiseState::Pending {
139                scope.perform_microtask_checkpoint();
140            }
141
142            if promise.state() == v8::PromiseState::Fulfilled {
143                promise.result(scope)
144            } else {
145                let exception = promise.result(scope);
146                let error_msg = exception
147                    .to_string(scope)
148                    .unwrap()
149                    .to_rust_string_lossy(scope);
150                return Ok(HandlerResult {
151                    success: false,
152                    data: None,
153                    error: Some(error_msg),
154                    execution_time_ms: 0,
155                    triggers: Vec::new(),
156                    auto_trigger_payloads: std::collections::HashMap::new(),
157                });
158            }
159        } else {
160            result
161        };
162
163        let json_result = v8::json::stringify(scope, result).ok_or_else(|| {
164            crate::error::RuntimeError::ExecutionFailed("Failed to stringify result".into())
165        })?;
166
167        let result_str = json_result.to_rust_string_lossy(scope);
168
169        let mut result_value: serde_json::Value = serde_json::from_str(&result_str)?;
170        
171        if let Some(logs) = result_value.get("_rohas_logs").and_then(|v| v.as_array()) {
172            for log in logs {
173                if let (Some(level), Some(handler), Some(message)) = (
174                    log.get("level").and_then(|v| v.as_str()),
175                    log.get("handler").and_then(|v| v.as_str()),
176                    log.get("message").and_then(|v| v.as_str()),
177                ) {
178                    // Convert fields to HashMap
179                    let mut field_map = std::collections::HashMap::new();
180                    if let Some(fields) = log.get("fields") {
181                        if let Some(fields_obj) = fields.as_object() {
182                            for (key, value) in fields_obj {
183                                field_map.insert(key.clone(), format!("{:?}", value));
184                            }
185                        }
186                    }
187                    
188                    // Emit tracing event
189                    let span = tracing::span!(
190                        tracing::Level::INFO,
191                        "handler_log",
192                        handler = %handler
193                    );
194                    let _enter = span.enter();
195                    
196                    match level {
197                        "error" => tracing::error!(message = %message, ?field_map),
198                        "warn" => tracing::warn!(message = %message, ?field_map),
199                        "info" => tracing::info!(message = %message, ?field_map),
200                        "debug" => tracing::debug!(message = %message, ?field_map),
201                        "trace" => tracing::trace!(message = %message, ?field_map),
202                        _ => tracing::info!(message = %message, ?field_map),
203                    }
204                }
205            }
206        }
207        
208        let triggers: Vec<crate::handler::TriggeredEvent> = result_value
209            .get("_rohas_triggers")
210            .and_then(|v| serde_json::from_value(v.clone()).ok())
211            .unwrap_or_default();
212        
213        let auto_trigger_payloads = result_value
214            .get("_rohas_auto_trigger_payloads")
215            .and_then(|v| serde_json::from_value::<std::collections::HashMap<String, serde_json::Value>>(v.clone()).ok())
216            .unwrap_or_default();
217        
218        if let Some(obj) = result_value.as_object_mut() {
219            obj.remove("_rohas_logs");
220            obj.remove("_rohas_triggers");
221            obj.remove("_rohas_auto_trigger_payloads");
222        }
223        
224        let mut handler_result: HandlerResult = serde_json::from_value(result_value)?;
225        
226        handler_result.triggers = triggers;
227        handler_result.auto_trigger_payloads = auto_trigger_payloads;
228
229        Ok(handler_result)
230    }
231
232    fn generate_wrapper(handler_code: &str, context_json: &str, handler_name: &str) -> String {
233        let context_escaped = context_json
234            .replace('\\', "\\\\")
235            .replace('\'', "\\'")
236            .replace('\n', "\\n")
237            .replace('\r', "\\r");
238        let handler_name_escaped = handler_name
239            .replace('\\', "\\\\")
240            .replace('\'', "\\'");
241        let handle_name_escaped = format!("handle_{}", handler_name)
242            .replace('\\', "\\\\")
243            .replace('\'', "\\'");
244
245        format!(
246            r#"
247(async () => {{
248    try {{
249        // CommonJS shim for V8
250        const module = {{ exports: {{}} }};
251        const exports = module.exports;
252        const require = function(id) {{
253            throw new Error('require() is not supported in V8 runtime: ' + id);
254        }};
255
256        // Logging function that stores logs for later processing
257        const _rohas_logs = [];
258        const _rohas_log_fn = function(level, handler, message, fields) {{
259            _rohas_logs.push({{
260                level: level,
261                handler: handler,
262                message: message,
263                fields: fields || {{}},
264                timestamp: new Date().toISOString()
265            }});
266        }};
267
268        // State class for handlers
269        class Logger {{
270            constructor(handlerName, logFn) {{
271                this.handlerName = handlerName;
272                this.logFn = logFn;
273            }}
274            info(message, fields) {{
275                if (this.logFn) {{
276                    this.logFn("info", this.handlerName, message, fields || {{}});
277                }}
278            }}
279            error(message, fields) {{
280                if (this.logFn) {{
281                    this.logFn("error", this.handlerName, message, fields || {{}});
282                }}
283            }}
284            warning(message, fields) {{
285                if (this.logFn) {{
286                    this.logFn("warn", this.handlerName, message, fields || {{}});
287                }}
288            }}
289            warn(message, fields) {{
290                this.warning(message, fields);
291            }}
292            debug(message, fields) {{
293                if (this.logFn) {{
294                    this.logFn("debug", this.handlerName, message, fields || {{}});
295                }}
296            }}
297            trace(message, fields) {{
298                if (this.logFn) {{
299                    this.logFn("trace", this.handlerName, message, fields || {{}});
300                }}
301            }}
302        }}
303
304        class State {{
305            constructor(handlerName, logFn) {{
306                this.triggers = [];
307                this.autoTriggerPayloads = new Map();
308                this.logger = new Logger(handlerName || "unknown", logFn);
309            }}
310            triggerEvent(eventName, payload) {{
311                this.triggers.push({{ eventName, payload }});
312            }}
313            setPayload(eventName, payload) {{
314                this.autoTriggerPayloads.set(eventName, payload);
315            }}
316            getTriggers() {{
317                return [...this.triggers];
318            }}
319            getAutoTriggerPayload(eventName) {{
320                return this.autoTriggerPayloads.get(eventName);
321            }}
322            getAllAutoTriggerPayloads() {{
323                return new Map(this.autoTriggerPayloads);
324            }}
325        }}
326
327        // Load handler code (CommonJS or plain)
328        (function(exports, module, require) {{
329            {}
330        }})(exports, module, require);
331
332        // Parse context
333        const context = JSON.parse('{}');
334
335        // Create State object with logging
336        const state = new State('{}', _rohas_log_fn);
337
338        // Find handler function
339        let handlerFn;
340
341        // Try CommonJS exports - check if module.exports is directly a function
342        if (typeof module.exports === 'function') {{
343            handlerFn = module.exports;
344        }} else if (module.exports && typeof module.exports === 'object') {{
345            // Try CommonJS exports (exports.handler or exports.handleXxx)
346            const exportKeys = Object.keys(module.exports);
347            
348            // For event handlers, try "handle_<handler_name>" first, then any exported function
349            const handleName = '{}';
350            if (module.exports[handleName] && typeof module.exports[handleName] === 'function') {{
351                handlerFn = module.exports[handleName];
352            }} else {{
353                // Look for any exported function (handleXxx, handler, default)
354                for (const key of exportKeys) {{
355                    if (typeof module.exports[key] === 'function') {{
356                        handlerFn = module.exports[key];
357                        break;
358                    }}
359                }}
360            }}
361        }}
362
363        // Fallback to global handler function (try handle_<handler_name> first, then handler)
364        if (!handlerFn) {{
365            const handleName = '{}';
366            if (typeof window !== 'undefined' && typeof window[handleName] === 'function') {{
367                handlerFn = window[handleName];
368            }} else if (typeof global !== 'undefined' && typeof global[handleName] === 'function') {{
369                handlerFn = global[handleName];
370            }} else if (typeof {} !== 'undefined') {{
371                handlerFn = {};
372            }} else if (typeof handler !== 'undefined') {{
373                handlerFn = handler;
374            }}
375        }}
376
377        if (!handlerFn) {{
378            throw new Error('Handler not found: No exported function or global handler. Tried: {}, handler, and module.exports');
379        }}
380
381        // Execute handler - pass state if handler accepts 2 parameters
382        let result;
383        const paramCount = handlerFn.length;
384        if (paramCount >= 2) {{
385            result = await handlerFn(context, state);
386        }} else {{
387            result = await handlerFn(context);
388        }}
389
390        // Return success result with logs
391        return {{
392            success: true,
393            data: result,
394            error: null,
395            execution_time_ms: 0,
396            _rohas_logs: _rohas_logs,
397            _rohas_triggers: state.getTriggers(),
398            _rohas_auto_trigger_payloads: Object.fromEntries(state.getAllAutoTriggerPayloads())
399        }};
400    }} catch (error) {{
401        // Return error result
402        return {{
403            success: false,
404            data: null,
405            error: error.message + '\n' + (error.stack || ''),
406            execution_time_ms: 0
407        }};
408    }}
409}})()
410"#,
411            handler_code, context_escaped, handler_name_escaped, handle_name_escaped, handle_name_escaped, handle_name_escaped, handle_name_escaped, handle_name_escaped
412        )
413    }
414
415    pub async fn load_module(&self, module_path: &Path) -> Result<()> {
416        info!("Loading JavaScript module: {:?}", module_path);
417
418        let absolute_path = if module_path.is_absolute() {
419            module_path.to_path_buf()
420        } else {
421            std::env::current_dir()?.join(module_path)
422        };
423
424        let code = tokio::fs::read_to_string(&absolute_path).await?;
425        let module_key = absolute_path.to_string_lossy().to_string();
426
427        let mut modules = self.modules.lock().unwrap();
428        modules.insert(module_key.clone(), code);
429
430        info!("Module loaded: {}", module_key);
431        Ok(())
432    }
433
434    pub async fn reload_module(&self, module_name: &str) -> Result<()> {
435        let mut modules = self.modules.lock().unwrap();
436        modules.remove(module_name);
437        info!("Reloaded module: {}", module_name);
438        Ok(())
439    }
440
441    pub async fn clear_cache(&self) -> Result<()> {
442        let mut modules = self.modules.lock().unwrap();
443        modules.clear();
444        info!("Cleared all cached modules");
445        Ok(())
446    }
447
448    pub async fn get_loaded_modules(&self) -> Vec<String> {
449        let modules = self.modules.lock().unwrap();
450        modules.keys().cloned().collect()
451    }
452}
453
454impl Default for NodeRuntime {
455    fn default() -> Self {
456        Self::new().expect("Failed to initialize V8 runtime")
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[tokio::test]
465    async fn test_node_runtime_creation() {
466        let runtime = NodeRuntime::new();
467        assert!(runtime.is_ok());
468    }
469
470    #[tokio::test]
471    async fn test_simple_handler_execution() {
472        let _runtime = NodeRuntime::new().unwrap();
473
474        let handler_code = r#"
475            module.exports = async function handler(context) {
476                return { message: "Hello from V8", payload: context.payload };
477            };
478        "#;
479
480        let context = HandlerContext::new("test", serde_json::json!({"data": "test"}));
481
482        let result = NodeRuntime::execute_js_code_sync(handler_code, &context);
483        assert!(result.is_ok());
484
485        let result = result.unwrap();
486        assert!(result.success);
487        assert!(result.data.is_some());
488    }
489}