deno_core icon indicating copy to clipboard operation
deno_core copied to clipboard

How should I register an asynchronous method and call this asynchronous method from JS?

Open hotlif opened this issue 1 year ago • 0 comments

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use deno_core::anyhow::Ok;
use deno_core::v8::Global;
use deno_core::{
  serde_v8, v8, PollEventLoopOptions
};
use deno_core::{JsRuntime, RuntimeOptions};
use crate::util::error::Error;

pub struct Engine {
    v8_runtime: JsRuntime
}

impl Engine {
    pub fn new(extensions: Vec<deno_core::Extension>) -> Self {
        let v8_runtime = JsRuntime::new(RuntimeOptions {
            extensions,
            ..Default::default()
        });
        Self {
            v8_runtime: v8_runtime
        }
    }

    pub async fn execute_script(&mut self, code: String, args: serde_json::Value) -> Result<Option<serde_json::Value>, deno_core::anyhow::Error> {
        let module_specifier_url = &deno_core::ModuleSpecifier::parse("script:main.js")?;
        let mod_id = self.v8_runtime.load_side_es_module_from_code(
            module_specifier_url,
            code
        ).await?;

        let future_result = self.v8_runtime.mod_evaluate(mod_id);
        self.v8_runtime.run_event_loop(Default::default()).await?;

        let namespace = self.v8_runtime.get_module_namespace(mod_id)?;
        let scope = &mut self.v8_runtime.handle_scope();
 
        let namespace = namespace.open(scope);
        let default_name = v8::String::new(scope, "default").unwrap().into();
        let default_function = namespace.get(scope, default_name);
        let args = serde_v8::to_v8(scope, args)?;

        if default_function.is_none() {
            return Err(Error {
                message: "There is no default export function".to_string()
            }.into());
        }
        let default_function = default_function.unwrap().cast::<v8::Function>();
        let result = default_function.call(scope, default_function.into(), &[args]);
        let mut return_result = None;
        if result.is_some() && result.unwrap().is_promise() {
            let result = result.unwrap().cast::<v8::Promise>();
            let result = result.result(scope);
            let result = serde_v8::from_v8::<serde_json::Value>(scope, result)?;
 
            return_result = Some(result);
        } else if result.is_some() {
            let result = serde_v8::from_v8::<serde_json::Value>(scope, result.unwrap())?;
            return_result = Some(result);
        } else {
            return_result = None;
        }
        future_result.await?;
        Ok(return_result)
    }
}

#[cfg(test)]
mod tests {

    use super::Engine;

    #[actix_web::test]
    async fn test_asynchronous_task() -> std::io::Result<()> {
        let mut engine = Engine::new(vec![]);
        let result = engine.execute_script(
            r#"
            async function task () {
                Deno.core.print("test ---- 1 \n")
                return {
                    "name": "hello word"
                }
            }
            export default task;
            "#.to_string(),
            serde_json::json!({})
        ).await.unwrap();

        assert!(result.is_some());

        if let Some(result) = result {
            let name = result.get("name").unwrap().as_str().unwrap();
            println!("response -> {}", result.to_string());
            assert_eq!(name, "hello word");
        }
        Ok(())
    }


    #[actix_web::test]
    async fn test_synchronous_task() -> std::io::Result<()> {
        let mut engine = Engine::new(vec![]);
        let result = engine.execute_script(
            r#"
            function task () {
                return {
                    "name": "hello word"
                }
            }
            export default task;
            "#.to_string(),
            serde_json::json!({})
        ).await.unwrap();

        assert!(result.is_some());

        if let Some(result) = result {
            let name = result.get("name").unwrap().as_str().unwrap();
            println!("response -> {}", result.to_string());
            assert_eq!(name, "hello word");
        }
        Ok(())
    }
}

The above is an example I wrote for calling the method, which runs without issues, but it throws an error when I register an asynchronous method.

*** Registering the plugin here will throw an error.***


use std::{cell::RefCell, rc::Rc};

use actix_web::web;
use deno_core::{
    extension, op2, OpState
};
use sea_orm::{DatabaseBackend, FromQueryResult, Statement};

use crate::util::context::Context;


fn json_to_sql_type(values: &Vec<serde_json::Value>) -> Vec<sea_orm::Value> {
    let mut rev: Vec<sea_orm::Value> = vec![];
    for value in values {
        match value {
            serde_json::Value::Array(value) => {}
            serde_json::Value::Bool(value) => {
                let value = sea_orm::Value::Bool(Some(*value));
                rev.push(value);
            }
            serde_json::Value::Number(value) => {
                if value.is_i64() {
                    let value = sea_orm::Value::BigInt(value.as_i64());
                    rev.push(value);
                } else if value.is_f64() {
                    let value = sea_orm::Value::Double(value.as_f64());
                    rev.push(value);
                } else if value.is_u64() {
                    let value = sea_orm::Value::BigUnsigned(value.as_u64());
                    rev.push(value);
                }
            }
            serde_json::Value::Object(value) => {}
            serde_json::Value::String(value) => {
                let value = sea_orm::Value::String(Some(Box::new(value.to_string())));
                rev.push(value);
            }
            serde_json::Value::Null => {}
        }
    }
    return rev;
}


#[op2(async)]
#[serde]
async fn op_context_query_sql(
    #[string] sql: String,
    #[serde] values: Vec<serde_json::Value>,
    state: Rc<RefCell<OpState>>,
) -> Result<Vec<serde_json::Value>, deno_core::anyhow::Error> {

    let mut state = state.borrow_mut();
    let app_data = state.try_take::<web::Data<Context>>().unwrap();

    let database_connection = &app_data.database_connection;

    let param: Vec<sea_orm::sea_query::Value> = json_to_sql_type(&values);

    let result = sea_orm::JsonValue::find_by_statement(Statement::from_sql_and_values(
        DatabaseBackend::MySql,
        sql,
        param,
    ))
    .all(database_connection)
    .await?;
    Ok(result)
}

extension!(
    ext_context,
    ops = [
        op_context_query_sql,
    ],
    esm_entry_point = "script:context",
    esm = [
        dir "src/script/ecmascript",
        "script:context" = "context.js",
    ]
);

pub fn configure_extension(app_data: web::Data<Context>) -> deno_core::Extension {
    let mut extension = ext_context::init_ops_and_esm();
    extension.op_state_fn = Some(Box::new(move |state| {
        state.put(app_data.clone());
    }));
    extension
}

#[cfg(test)]
mod tests {
    use crate::{script::{engine::Engine, ext_context::configure_extension}, util};

    #[actix_web::test]
    async fn test_asynchronous_task() -> std::io::Result<()> {

        let conf = util::conf::get_conf()?;
        let db = util::mysql::get_mysql(&conf).await.unwrap();
        let app_data = actix_web::web::Data::new(util::context::Context {
            database_connection: db.clone(),
            conf: conf.clone()
        });

        let mut engine = Engine::new(vec![configure_extension(app_data)]);
        let result = engine.execute_script(
            r#"
            import context from "script:context";
            async function task () {
                let data = await context.sql.select("select * from user", []);
                Deno.core.print(JSON.stringify(data))
                return data;
            }
            export default task;
            "#.to_string(),
            serde_json::json!({})
        ).await.unwrap();

        assert!(result.is_some());

        if let Some(result) = result {
            println!("response -> {}", result.to_string());
            let length = result.as_array().unwrap().len();
            assert_eq!(length, 1);
        }
        Ok(())
    }
}

context.js

export default {
    sql: {
        select: async (sql, param) => {
            let result = await globalThis.Deno.core.ops.op_context_query_sql(sql, param);
            return result;
        }
    }
}

The error message is as follows.


#
# Fatal error in v8_Promise_Result
# Promise is still pending
#

I suspect that the issue is caused by not entering the deno_core event loop when using let scope = &mut self.v8_runtime.handle_scope() and then calling let result = default_function.call(scope, default_function.into(), &[args]);.

hotlif avatar Nov 14 '24 08:11 hotlif