Skip to main content

elm_rust_binding/
quickjs.rs

1use 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    /// Calls the elm function with the given input and return the output.
82    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}