elm_rust_binding/
quickjs.rs1use std::{
2 collections::HashMap,
3 marker::PhantomData,
4 sync::{Arc, LazyLock, RwLock},
5};
6
7use crate::{error::Result, ElmBinding, TO_ESM_JS};
8use quickjs_runtime::{
9 builder::QuickJsRuntimeBuilder,
10 facades::QuickJsRuntimeFacade,
11 jsutils::{modules::ScriptModuleLoader, Script},
12 quickjsrealmadapter::QuickJsRealmAdapter,
13 values::{JsValueConvertable, JsValueFacade},
14};
15use serde::{de::DeserializeOwned, Serialize};
16
17use crate::ElmRoot;
18
19static LOADER: LazyLock<ScriptModuleLoaderImpl> = LazyLock::new(ScriptModuleLoaderImpl::new);
20
21static RUNTIME: LazyLock<QuickJsRuntimeFacade> = LazyLock::new(|| {
22 QuickJsRuntimeBuilder::new()
23 .script_module_loader(LOADER.clone())
24 .build()
25});
26
27pub struct ElmFunctionHandle<I, O> {
28 function_name: String,
29 _type: PhantomData<(I, O)>,
30}
31
32pub async fn prepare<I, O>(
33 root: &ElmRoot,
34 elm_binding: ElmBinding,
35) -> Result<ElmFunctionHandle<I, O>> {
36 LOADER.register("to-esm.js", TO_ESM_JS);
37 RUNTIME
38 .eval(
39 None,
40 Script::new(
41 "to-esm.global.js",
42 "
43async function toEsm(str) {
44 const toEsmModule = await import('to-esm.js');
45 return toEsmModule.default(str);
46}
47 ",
48 ),
49 )
50 .await?;
51 let args = vec![elm_binding.compiled_binding.to_js_value_facade()];
52 let result = invoke_function("toEsm", args).await?;
53 let esm_compiled_binding = result.get_str();
54 root.write_esm_binding(&elm_binding.binding_module_name, esm_compiled_binding)?;
55
56 let define_global_function = Script::new(
57 &elm_binding.binding_module_name,
58 &RUN_JS_TEMPLATE.replace(
59 "{{ binding_module_name }}",
60 &elm_binding.binding_module_name,
61 ),
62 );
63 LOADER.register(
64 format!("{}.js", elm_binding.binding_module_name),
65 esm_compiled_binding,
66 );
67 RUNTIME.eval(None, define_global_function).await?;
68 let function_name = format!("call_{}", elm_binding.binding_module_name);
69
70 Ok(ElmFunctionHandle {
71 function_name,
72 _type: PhantomData,
73 })
74}
75
76impl<I, O> ElmFunctionHandle<I, O>
77where
78 I: Serialize,
79 O: DeserializeOwned,
80{
81 pub async fn call(&self, input: I) -> Result<O> {
83 let flags = serde_json::to_value(input)?;
84 let args = vec![flags.to_js_value_facade()];
85 let return_value_facade = invoke_function(&self.function_name, args).await?;
86 let return_value = return_value_facade.to_serde_value().await?;
87 let output = serde_json::from_value(return_value)?;
88 Ok(output)
89 }
90}
91
92const RUN_JS_TEMPLATE: &str = include_str!("./templates/run.qjs.template");
93
94async fn invoke_function(name: &str, args: Vec<JsValueFacade>) -> Result<JsValueFacade> {
95 let return_value = RUNTIME.invoke_function(None, &[], name, args).await?;
96 let resolved_return_value = handle_promise(return_value).await?;
97 Ok(resolved_return_value)
98}
99
100async fn handle_promise(value: JsValueFacade) -> Result<JsValueFacade> {
101 if let JsValueFacade::JsPromise { cached_promise } = value {
102 return Ok(cached_promise.get_promise_result().await??);
103 }
104 Ok(value)
105}
106
107#[derive(Clone)]
108struct ScriptModuleLoaderImpl {
109 inner: Arc<RwLock<HashMap<String, String>>>,
110}
111
112impl ScriptModuleLoaderImpl {
113 fn new() -> Self {
114 Self {
115 inner: Default::default(),
116 }
117 }
118
119 fn register<S1, S2>(&self, name: S1, code: S2)
120 where
121 S1: Into<String>,
122 S2: Into<String>,
123 {
124 let mut writer = self.inner.write().unwrap();
125 writer.insert(name.into(), code.into());
126 }
127}
128
129impl ScriptModuleLoader for ScriptModuleLoaderImpl {
130 fn normalize_path(
131 &self,
132 _realm: &QuickJsRealmAdapter,
133 _ref_path: &str,
134 path: &str,
135 ) -> Option<String> {
136 Some(path.to_owned())
137 }
138
139 fn load_module(&self, _realm: &QuickJsRealmAdapter, absolute_path: &str) -> String {
140 let reader = self.inner.read().unwrap();
141 reader
142 .get(absolute_path)
143 .unwrap_or_else(|| {
144 panic!("Call `ScriptModuleLoaderImpl::register` before loading {absolute_path}")
145 })
146 .clone()
147 }
148}