1use rquickjs::function::Rest;
2use rquickjs::promise::{Promise, PromiseState};
3use rquickjs::{Context, Ctx, Function, Object, Runtime, Value};
4use std::collections::{HashMap, HashSet};
5use std::future::Future;
6use std::pin::Pin;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11use crate::error::{Result, XriptError};
12use crate::manifest::{Binding, Manifest, NamespaceBinding};
13
14pub type HostFn =
15 Arc<dyn Fn(&[serde_json::Value]) -> std::result::Result<serde_json::Value, String> + Send + Sync>;
16
17pub type AsyncHostFn = Arc<
18 dyn Fn(
19 &[serde_json::Value],
20 ) -> Pin<
21 Box<dyn Future<Output = std::result::Result<serde_json::Value, String>> + Send>,
22 > + Send
23 + Sync,
24>;
25
26pub struct HostBindings {
27 bindings: HashMap<String, HostBinding>,
28}
29
30enum HostBinding {
31 Function(HostFn),
32 AsyncFunction(AsyncHostFn),
33 Namespace(HashMap<String, HostFn>),
34 AsyncNamespace(HashMap<String, AsyncHostFn>),
35}
36
37impl HostBindings {
38 pub fn new() -> Self {
39 Self {
40 bindings: HashMap::new(),
41 }
42 }
43
44 pub fn add_function<F>(&mut self, name: impl Into<String>, f: F)
45 where
46 F: Fn(&[serde_json::Value]) -> std::result::Result<serde_json::Value, String>
47 + Send
48 + Sync
49 + 'static,
50 {
51 self.bindings
52 .insert(name.into(), HostBinding::Function(Arc::new(f)));
53 }
54
55 pub fn add_namespace(&mut self, name: impl Into<String>, members: HashMap<String, HostFn>) {
56 self.bindings
57 .insert(name.into(), HostBinding::Namespace(members));
58 }
59
60 pub fn add_async_function<F, Fut>(&mut self, name: impl Into<String>, f: F)
61 where
62 F: Fn(&[serde_json::Value]) -> Fut + Send + Sync + 'static,
63 Fut: Future<Output = std::result::Result<serde_json::Value, String>> + Send + 'static,
64 {
65 self.bindings.insert(
66 name.into(),
67 HostBinding::AsyncFunction(Arc::new(move |args| Box::pin(f(args)))),
68 );
69 }
70
71 pub fn add_async_namespace(
72 &mut self,
73 name: impl Into<String>,
74 members: HashMap<String, AsyncHostFn>,
75 ) {
76 self.bindings
77 .insert(name.into(), HostBinding::AsyncNamespace(members));
78 }
79}
80
81impl Default for HostBindings {
82 fn default() -> Self {
83 Self::new()
84 }
85}
86
87pub struct ConsoleHandler {
88 pub log: Box<dyn Fn(&str) + Send + Sync>,
89 pub warn: Box<dyn Fn(&str) + Send + Sync>,
90 pub error: Box<dyn Fn(&str) + Send + Sync>,
91}
92
93impl Default for ConsoleHandler {
94 fn default() -> Self {
95 Self {
96 log: Box::new(|_| {}),
97 warn: Box::new(|_| {}),
98 error: Box::new(|_| {}),
99 }
100 }
101}
102
103pub struct RuntimeOptions {
104 pub host_bindings: HostBindings,
105 pub capabilities: Vec<String>,
106 pub console: ConsoleHandler,
107}
108
109#[derive(Debug)]
110pub struct ExecutionResult {
111 pub value: serde_json::Value,
112 pub duration_ms: f64,
113}
114
115pub struct XriptRuntime {
116 rt: Runtime,
117 ctx: Context,
118 manifest: Manifest,
119}
120
121impl std::fmt::Debug for XriptRuntime {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.debug_struct("XriptRuntime")
124 .field("manifest_name", &self.manifest.name)
125 .finish_non_exhaustive()
126 }
127}
128
129impl XriptRuntime {
130 pub fn new(manifest: Manifest, options: RuntimeOptions) -> Result<Self> {
131 crate::manifest::validate_structure(&manifest)?;
132
133 let rt = Runtime::new().map_err(|e| XriptError::Engine(e.to_string()))?;
134
135 if let Some(ref limits) = manifest.limits {
136 if let Some(memory_mb) = limits.memory_mb {
137 rt.set_memory_limit(memory_mb as usize * 1024 * 1024);
138 }
139 if let Some(stack) = limits.max_stack_depth {
140 rt.set_max_stack_size(stack * 1024);
141 }
142 }
143
144 let ctx = Context::full(&rt).map_err(|e| XriptError::Engine(e.to_string()))?;
145
146 let granted: HashSet<String> = options.capabilities.into_iter().collect();
147
148 ctx.with(|ctx| -> Result<()> {
149 remove_dangerous_globals(&ctx)?;
150 register_console(&ctx, options.console)?;
151 register_bindings(&ctx, &manifest, options.host_bindings, &granted)?;
152 register_hooks(&ctx, &manifest, &granted)?;
153 register_fragment_hooks(&ctx)?;
154 Ok(())
155 })?;
156
157 Ok(Self { rt, ctx, manifest })
158 }
159
160 pub fn execute(&self, code: &str) -> Result<ExecutionResult> {
161 let timeout_ms = self
162 .manifest
163 .limits
164 .as_ref()
165 .and_then(|l| l.timeout_ms)
166 .unwrap_or(5000);
167
168 let interrupted = Arc::new(AtomicBool::new(false));
169 let interrupted_clone = interrupted.clone();
170 let start = Instant::now();
171 let deadline = start + Duration::from_millis(timeout_ms);
172
173 self.rt
174 .set_interrupt_handler(Some(Box::new(move || {
175 if Instant::now() >= deadline {
176 interrupted_clone.store(true, Ordering::Relaxed);
177 true
178 } else {
179 false
180 }
181 }) as Box<dyn FnMut() -> bool + Send>));
182
183 let result = self.ctx.with(|ctx| {
184 let res: std::result::Result<Value, _> = ctx.eval(code);
185 match res {
186 Ok(val) => {
187 while ctx.execute_pending_job() {}
188 let resolved = resolve_if_promise(&ctx, val);
189 let json = js_value_to_json(&ctx, &resolved);
190 Ok(json)
191 }
192 Err(_) => {
193 if interrupted.load(Ordering::Relaxed) {
194 Err(XriptError::ExecutionLimit {
195 limit: "timeout_ms".into(),
196 })
197 } else {
198 let msg: std::result::Result<String, _> =
199 ctx.eval("(() => { try { throw undefined; } catch(e) { return String(e); } })()");
200 let error_msg = msg.unwrap_or_else(|_| "unknown script error".into());
201 Err(XriptError::Script(error_msg))
202 }
203 }
204 }
205 });
206
207 self.rt
208 .set_interrupt_handler(None::<Box<dyn FnMut() -> bool + Send>>);
209
210 let duration_ms = start.elapsed().as_secs_f64() * 1000.0;
211
212 result.map(|value| ExecutionResult { value, duration_ms })
213 }
214
215 pub fn manifest(&self) -> &Manifest {
216 &self.manifest
217 }
218
219 pub fn load_mod(
220 &self,
221 mod_manifest_json: &str,
222 fragment_sources: HashMap<String, String>,
223 granted_capabilities: &HashSet<String>,
224 entry_source: Option<&str>,
225 ) -> Result<crate::fragment::ModInstance> {
226 let mod_instance = crate::fragment::load_mod(
227 mod_manifest_json,
228 &self.manifest,
229 granted_capabilities,
230 &fragment_sources,
231 )?;
232
233 if let Some(source) = entry_source {
234 let mod_name = mod_instance.name.clone();
235 self.ctx.with(|ctx| {
236 let res: std::result::Result<Value, _> = ctx.eval(source);
237 if let Err(_) = res {
238 let msg: std::result::Result<String, _> =
239 ctx.eval("(() => { try { throw undefined; } catch(e) { return String(e); } })()");
240 let error_msg = msg.unwrap_or_else(|_| "unknown entry script error".into());
241 return Err(XriptError::ModEntry {
242 mod_name,
243 message: error_msg,
244 });
245 }
246 Ok(())
247 })?;
248 }
249
250 Ok(mod_instance)
251 }
252
253 pub fn fire_fragment_hook(
254 &self,
255 fragment_id: &str,
256 lifecycle: &str,
257 bindings: Option<&serde_json::Value>,
258 ) -> Result<Vec<serde_json::Value>> {
259 let bindings_json = match bindings {
260 Some(b) => serde_json::to_string(b).unwrap_or("{}".into()),
261 None => "{}".into(),
262 };
263
264 let code = format!(
265 r#"(function() {{
266 var handlers = globalThis.__xript_fragment_handlers || {{}};
267 var key = "fragment:{lifecycle}:{fid}";
268 var list = handlers[key] || [];
269 var results = [];
270 var bindingsObj = JSON.parse('{bindings_json}');
271 for (var i = 0; i < list.length; i++) {{
272 var ops = [];
273 var proxy = {{
274 toggle: function(sel, cond) {{ ops.push({{ op: "toggle", selector: sel, value: !!cond }}); }},
275 addClass: function(sel, cls) {{ ops.push({{ op: "addClass", selector: sel, value: cls }}); }},
276 removeClass: function(sel, cls) {{ ops.push({{ op: "removeClass", selector: sel, value: cls }}); }},
277 setText: function(sel, txt) {{ ops.push({{ op: "setText", selector: sel, value: txt }}); }},
278 setAttr: function(sel, attr, val) {{ ops.push({{ op: "setAttr", selector: sel, attr: attr, value: val }}); }},
279 replaceChildren: function(sel, html) {{ ops.push({{ op: "replaceChildren", selector: sel, value: html }}); }}
280 }};
281 list[i](bindingsObj, proxy);
282 results.push(ops);
283 }}
284 return JSON.stringify(results);
285 }})()"#,
286 lifecycle = lifecycle,
287 fid = fragment_id,
288 bindings_json = bindings_json.replace('\'', "\\'"),
289 );
290
291 let result = self.execute(&code)?;
292 match serde_json::from_str::<Vec<serde_json::Value>>(
293 result.value.as_str().unwrap_or("[]"),
294 ) {
295 Ok(ops) => Ok(ops),
296 Err(_) => Ok(vec![]),
297 }
298 }
299}
300
301fn remove_dangerous_globals(ctx: &Ctx<'_>) -> Result<()> {
302 let script = r#"
303 delete globalThis.eval;
304 if (typeof globalThis.Function !== 'undefined') {
305 Object.defineProperty(globalThis, 'Function', {
306 get: function() { throw new Error("Function constructor is not permitted. Dynamic code generation is disabled in xript."); },
307 configurable: false
308 });
309 }
310 "#;
311 ctx.eval::<(), _>(script)
312 .map_err(|e| XriptError::Engine(e.to_string()))?;
313 Ok(())
314}
315
316fn register_console(ctx: &Ctx<'_>, console: ConsoleHandler) -> Result<()> {
317 let console_obj = Object::new(ctx.clone()).map_err(|e| XriptError::Engine(e.to_string()))?;
318
319 let log = Arc::new(console.log);
320 let log_clone = log.clone();
321 let log_fn = Function::new(ctx.clone(), move |args: Rest<String>| {
322 let msg = args.0.join(" ");
323 log_clone(&msg);
324 })
325 .map_err(|e| XriptError::Engine(e.to_string()))?;
326
327 let warn = Arc::new(console.warn);
328 let warn_clone = warn.clone();
329 let warn_fn = Function::new(ctx.clone(), move |args: Rest<String>| {
330 let msg = args.0.join(" ");
331 warn_clone(&msg);
332 })
333 .map_err(|e| XriptError::Engine(e.to_string()))?;
334
335 let error = Arc::new(console.error);
336 let error_clone = error.clone();
337 let error_fn = Function::new(ctx.clone(), move |args: Rest<String>| {
338 let msg = args.0.join(" ");
339 error_clone(&msg);
340 })
341 .map_err(|e| XriptError::Engine(e.to_string()))?;
342
343 console_obj
344 .set("log", log_fn)
345 .map_err(|e| XriptError::Engine(e.to_string()))?;
346 console_obj
347 .set("warn", warn_fn)
348 .map_err(|e| XriptError::Engine(e.to_string()))?;
349 console_obj
350 .set("error", error_fn)
351 .map_err(|e| XriptError::Engine(e.to_string()))?;
352
353 ctx.globals()
354 .set("console", console_obj)
355 .map_err(|e| XriptError::Engine(e.to_string()))?;
356
357 Ok(())
358}
359
360fn make_throwing_function<'js>(ctx: &Ctx<'js>, message: &str) -> Result<Function<'js>> {
361 let escaped = message.replace('\\', "\\\\").replace('"', "\\\"");
362 let script = format!("(function() {{ throw new Error(\"{}\"); }})", escaped);
363 ctx.eval::<Function, _>(script.as_str())
364 .map_err(|e| XriptError::Engine(e.to_string()))
365}
366
367fn register_bindings(
368 ctx: &Ctx<'_>,
369 manifest: &Manifest,
370 host_bindings: HostBindings,
371 granted: &HashSet<String>,
372) -> Result<()> {
373 let Some(ref bindings) = manifest.bindings else {
374 return Ok(());
375 };
376
377 for (name, binding) in bindings {
378 match binding {
379 Binding::Function(func_def) => {
380 if let Some(ref cap) = func_def.capability {
381 if !granted.contains(cap) {
382 let msg = format!(
383 "{}() requires the \"{}\" capability, which hasn't been granted to this script",
384 name, cap
385 );
386 let deny_fn = make_throwing_function(ctx, &msg)?;
387 ctx.globals()
388 .set(name.as_str(), deny_fn)
389 .map_err(|e| XriptError::Engine(e.to_string()))?;
390 continue;
391 }
392 }
393
394 match host_bindings.bindings.get(name) {
395 Some(HostBinding::Function(f)) => {
396 let js_fn = create_host_function(ctx, name, f.clone())?;
397 ctx.globals()
398 .set(name.as_str(), js_fn)
399 .map_err(|e| XriptError::Engine(e.to_string()))?;
400 }
401 Some(HostBinding::AsyncFunction(f)) => {
402 let js_fn = create_async_host_function(ctx, name, f.clone())?;
403 ctx.globals()
404 .set(name.as_str(), js_fn)
405 .map_err(|e| XriptError::Engine(e.to_string()))?;
406 }
407 _ => {
408 let msg = format!("host binding '{}' is not provided", name);
409 let missing_fn = make_throwing_function(ctx, &msg)?;
410 ctx.globals()
411 .set(name.as_str(), missing_fn)
412 .map_err(|e| XriptError::Engine(e.to_string()))?;
413 }
414 }
415 }
416 Binding::Namespace(ns_def) => {
417 register_namespace_binding(ctx, name, ns_def, &host_bindings, granted)?;
418 }
419 }
420 }
421
422 Ok(())
423}
424
425fn register_namespace_binding(
426 ctx: &Ctx<'_>,
427 name: &str,
428 ns_def: &NamespaceBinding,
429 host_bindings: &HostBindings,
430 granted: &HashSet<String>,
431) -> Result<()> {
432 let ns_obj = Object::new(ctx.clone()).map_err(|e| XriptError::Engine(e.to_string()))?;
433
434 let host_ns = match host_bindings.bindings.get(name) {
435 Some(HostBinding::Namespace(members)) => Some(members),
436 _ => None,
437 };
438
439 let host_async_ns = match host_bindings.bindings.get(name) {
440 Some(HostBinding::AsyncNamespace(members)) => Some(members),
441 _ => None,
442 };
443
444 for (member_name, member_binding) in &ns_def.members {
445 if let Binding::Function(func_def) = member_binding {
446 let full_name = format!("{}.{}", name, member_name);
447
448 if let Some(ref cap) = func_def.capability {
449 if !granted.contains(cap) {
450 let msg = format!(
451 "{}() requires the \"{}\" capability, which hasn't been granted to this script",
452 full_name, cap
453 );
454 let deny_fn = make_throwing_function(ctx, &msg)?;
455 ns_obj
456 .set(member_name.as_str(), deny_fn)
457 .map_err(|e| XriptError::Engine(e.to_string()))?;
458 continue;
459 }
460 }
461
462 if let Some(host_members) = host_ns {
463 if let Some(f) = host_members.get(member_name) {
464 let js_fn = create_host_function(ctx, &full_name, f.clone())?;
465 ns_obj
466 .set(member_name.as_str(), js_fn)
467 .map_err(|e| XriptError::Engine(e.to_string()))?;
468 continue;
469 }
470 }
471
472 if let Some(host_members) = host_async_ns {
473 if let Some(f) = host_members.get(member_name) {
474 let js_fn = create_async_host_function(ctx, &full_name, f.clone())?;
475 ns_obj
476 .set(member_name.as_str(), js_fn)
477 .map_err(|e| XriptError::Engine(e.to_string()))?;
478 continue;
479 }
480 }
481
482 let msg = format!("host binding '{}' is not provided", full_name);
483 let missing_fn = make_throwing_function(ctx, &msg)?;
484 ns_obj
485 .set(member_name.as_str(), missing_fn)
486 .map_err(|e| XriptError::Engine(e.to_string()))?;
487 }
488 }
489
490 ctx.globals()
491 .set(name, ns_obj)
492 .map_err(|e| XriptError::Engine(e.to_string()))?;
493
494 let freeze_script = format!(
495 "Object.freeze(globalThis['{}'])",
496 name.replace('\'', "\\'")
497 );
498 ctx.eval::<(), _>(freeze_script.as_str())
499 .map_err(|e| XriptError::Engine(e.to_string()))?;
500
501 Ok(())
502}
503
504fn register_hooks(ctx: &Ctx<'_>, manifest: &Manifest, granted: &HashSet<String>) -> Result<()> {
505 let Some(ref hooks) = manifest.hooks else {
506 return Ok(());
507 };
508
509 if hooks.is_empty() {
510 return Ok(());
511 }
512
513 let mut hook_setup = String::from("globalThis.__xript_hooks = {};\n");
514 hook_setup.push_str("globalThis.hooks = {};\n");
515
516 for (hook_name, hook_def) in hooks {
517 hook_setup.push_str(&format!(
518 "globalThis.__xript_hooks['{}'] = [];\n",
519 hook_name
520 ));
521
522 if let Some(ref phases) = hook_def.phases {
523 if !phases.is_empty() {
524 hook_setup.push_str(&format!("globalThis.hooks['{}'] = {{}};\n", hook_name));
525 for phase in phases {
526 let registration = if let Some(ref cap) = hook_def.capability {
527 if !granted.contains(cap) {
528 format!(
529 "globalThis.hooks['{hook}']['{phase}'] = function() {{ throw new Error(\"{hook}.{phase}() requires the \\\"{cap}\\\" capability\"); }};",
530 hook = hook_name, phase = phase, cap = cap
531 )
532 } else {
533 format!(
534 "globalThis.hooks['{hook}']['{phase}'] = function(handler) {{ globalThis.__xript_hooks['{hook}'].push({{ phase: '{phase}', handler: handler }}); }};",
535 hook = hook_name, phase = phase
536 )
537 }
538 } else {
539 format!(
540 "globalThis.hooks['{hook}']['{phase}'] = function(handler) {{ globalThis.__xript_hooks['{hook}'].push({{ phase: '{phase}', handler: handler }}); }};",
541 hook = hook_name, phase = phase
542 )
543 };
544 hook_setup.push_str(®istration);
545 hook_setup.push('\n');
546 }
547 }
548 } else {
549 let registration = if let Some(ref cap) = hook_def.capability {
550 if !granted.contains(cap) {
551 format!(
552 "globalThis.hooks['{hook}'] = function() {{ throw new Error(\"{hook}() requires the \\\"{cap}\\\" capability\"); }};",
553 hook = hook_name, cap = cap
554 )
555 } else {
556 format!(
557 "globalThis.hooks['{hook}'] = function(handler) {{ globalThis.__xript_hooks['{hook}'].push({{ handler: handler }}); }};",
558 hook = hook_name
559 )
560 }
561 } else {
562 format!(
563 "globalThis.hooks['{hook}'] = function(handler) {{ globalThis.__xript_hooks['{hook}'].push({{ handler: handler }}); }};",
564 hook = hook_name
565 )
566 };
567 hook_setup.push_str(®istration);
568 hook_setup.push('\n');
569 }
570 }
571
572 hook_setup.push_str("Object.freeze(globalThis.hooks);\n");
573
574 ctx.eval::<(), _>(hook_setup.as_str())
575 .map_err(|e| XriptError::Engine(e.to_string()))?;
576
577 Ok(())
578}
579
580fn register_fragment_hooks(ctx: &Ctx<'_>) -> Result<()> {
581 let script = r#"
582 globalThis.__xript_fragment_handlers = {};
583
584 var existingHooks = {};
585 if (typeof globalThis.hooks === 'object' && globalThis.hooks !== null) {
586 var hookKeys = Object.getOwnPropertyNames(globalThis.hooks);
587 for (var i = 0; i < hookKeys.length; i++) {
588 existingHooks[hookKeys[i]] = globalThis.hooks[hookKeys[i]];
589 }
590 }
591
592 var fragmentNs = {};
593 var lifecycles = ['mount', 'unmount', 'update', 'suspend', 'resume'];
594 for (var j = 0; j < lifecycles.length; j++) {
595 (function(lifecycle) {
596 fragmentNs[lifecycle] = function(fragmentId, handler) {
597 var key = "fragment:" + lifecycle + ":" + fragmentId;
598 if (!globalThis.__xript_fragment_handlers[key]) {
599 globalThis.__xript_fragment_handlers[key] = [];
600 }
601 globalThis.__xript_fragment_handlers[key].push(handler);
602 };
603 })(lifecycles[j]);
604 }
605 Object.freeze(fragmentNs);
606 existingHooks.fragment = fragmentNs;
607
608 globalThis.hooks = existingHooks;
609 Object.freeze(globalThis.hooks);
610 "#;
611
612 ctx.eval::<(), _>(script)
613 .map_err(|e| XriptError::Engine(e.to_string()))?;
614
615 Ok(())
616}
617
618fn create_host_function<'js>(
619 ctx: &Ctx<'js>,
620 name: &str,
621 f: HostFn,
622) -> Result<Function<'js>> {
623 let bridge_fn = Function::new(ctx.clone(), move |args_json: String| -> String {
624 let args: Vec<serde_json::Value> = match serde_json::from_str(&args_json) {
625 Ok(a) => a,
626 Err(e) => {
627 let err = serde_json::json!({"__xript_err": format!("invalid args: {}", e)});
628 return serde_json::to_string(&err).unwrap();
629 }
630 };
631 match f(&args) {
632 Ok(result) => {
633 let wrapped = serde_json::json!({"__xript_ok": result});
634 serde_json::to_string(&wrapped).unwrap_or("{\"__xript_ok\":null}".into())
635 }
636 Err(msg) => {
637 let err = serde_json::json!({"__xript_err": msg});
638 serde_json::to_string(&err).unwrap()
639 }
640 }
641 })
642 .map_err(|e| {
643 XriptError::Engine(format!("failed to create host function '{}': {}", name, e))
644 })?;
645
646 ctx.globals()
647 .set("__xript_tmp_bridge", bridge_fn)
648 .map_err(|e| XriptError::Engine(e.to_string()))?;
649
650 let wrapper: Function = ctx.eval(
651 "(function(bridge) { return function() { var args = Array.prototype.slice.call(arguments); var raw = bridge(JSON.stringify(args)); var envelope = JSON.parse(raw); if (envelope.__xript_err !== undefined) { throw new Error(envelope.__xript_err); } return envelope.__xript_ok; }; })(__xript_tmp_bridge)",
652 )
653 .map_err(|e| XriptError::Engine(e.to_string()))?;
654
655 ctx.eval::<(), _>("delete globalThis.__xript_tmp_bridge")
656 .map_err(|e| XriptError::Engine(e.to_string()))?;
657
658 Ok(wrapper)
659}
660
661fn create_async_host_function<'js>(
662 ctx: &Ctx<'js>,
663 name: &str,
664 f: AsyncHostFn,
665) -> Result<Function<'js>> {
666 let bridge_fn = Function::new(ctx.clone(), move |args_json: String| -> String {
667 let args: Vec<serde_json::Value> = match serde_json::from_str(&args_json) {
668 Ok(a) => a,
669 Err(e) => {
670 let err = serde_json::json!({"__xript_err": format!("invalid args: {}", e)});
671 return serde_json::to_string(&err).unwrap();
672 }
673 };
674 match pollster::block_on(f(&args)) {
675 Ok(result) => {
676 let wrapped = serde_json::json!({"__xript_ok": result});
677 serde_json::to_string(&wrapped).unwrap_or("{\"__xript_ok\":null}".into())
678 }
679 Err(msg) => {
680 let err = serde_json::json!({"__xript_err": msg});
681 serde_json::to_string(&err).unwrap()
682 }
683 }
684 })
685 .map_err(|e| {
686 XriptError::Engine(format!(
687 "failed to create async host function '{}': {}",
688 name, e
689 ))
690 })?;
691
692 ctx.globals()
693 .set("__xript_tmp_bridge", bridge_fn)
694 .map_err(|e| XriptError::Engine(e.to_string()))?;
695
696 let wrapper: Function = ctx.eval(
697 "(function(bridge) { return function() { var args = Array.prototype.slice.call(arguments); var raw = bridge(JSON.stringify(args)); var envelope = JSON.parse(raw); if (envelope.__xript_err !== undefined) { return Promise.reject(new Error(envelope.__xript_err)); } return Promise.resolve(envelope.__xript_ok); }; })(__xript_tmp_bridge)",
698 )
699 .map_err(|e| XriptError::Engine(e.to_string()))?;
700
701 ctx.eval::<(), _>("delete globalThis.__xript_tmp_bridge")
702 .map_err(|e| XriptError::Engine(e.to_string()))?;
703
704 Ok(wrapper)
705}
706
707fn resolve_if_promise<'js>(ctx: &Ctx<'js>, val: Value<'js>) -> Value<'js> {
708 if let Ok(promise) = Promise::from_value(val.clone()) {
709 loop {
710 match promise.state() {
711 PromiseState::Pending => {
712 if !ctx.execute_pending_job() {
713 break val;
714 }
715 }
716 PromiseState::Resolved => {
717 break promise.result::<Value>().unwrap_or(Ok(val)).unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
718 }
719 PromiseState::Rejected => {
720 break promise.result::<Value>().unwrap_or(Ok(val)).unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
721 }
722 }
723 }
724 } else {
725 val
726 }
727}
728
729fn js_value_to_json(ctx: &Ctx<'_>, val: &Value<'_>) -> serde_json::Value {
730 if val.is_undefined() || val.is_null() {
731 return serde_json::Value::Null;
732 }
733
734 if let Some(b) = val.as_bool() {
735 return serde_json::Value::Bool(b);
736 }
737
738 if let Some(n) = val.as_int() {
739 return serde_json::json!(n);
740 }
741
742 if let Some(n) = val.as_float() {
743 if n.is_finite() {
744 return serde_json::json!(n);
745 }
746 return serde_json::Value::Null;
747 }
748
749 if let Some(s) = val.as_string() {
750 if let Ok(s) = s.to_string() {
751 return serde_json::Value::String(s);
752 }
753 }
754
755 let stringify_result: std::result::Result<String, _> = ctx.eval(
756 "((v) => JSON.stringify(v))",
757 );
758 if let Ok(stringify_fn_str) = stringify_result {
759 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&stringify_fn_str) {
760 return v;
761 }
762 }
763
764 serde_json::Value::Null
765}