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 modules: Arc<Mutex<HashMap<String, String>>>,
20 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 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 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}