feat: function v2 (#212)

* feat: function v2

* expose evaluate

* remove async trait, add caching

* fix naming

* add reverse compatibility

* add http module

* update impl

* resolve issues with reqwest

* fix

* fix

* fix ssl

* remove unused deps

* fix: tests

* fix
This commit is contained in:
stefan-gorules
2024-07-16 11:22:51 +02:00
committed by GitHub
parent f1fc8ccfcd
commit cc3d938b2f
58 changed files with 4093 additions and 379 deletions
+63
View File
@@ -0,0 +1,63 @@
use std::fmt::{Display, Formatter};
use rquickjs::{CaughtError, Ctx, Error, Exception};
pub type FunctionResult<Ok = ()> = Result<Ok, FunctionError>;
#[derive(Debug)]
pub enum FunctionError {
Caught(String),
Runtime(Error),
}
impl<'js> From<CaughtError<'js>> for FunctionError {
fn from(value: CaughtError<'js>) -> Self {
Self::Caught(value.to_string())
}
}
impl From<Error> for FunctionError {
fn from(value: Error) -> Self {
Self::Runtime(value)
}
}
impl Display for FunctionError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
FunctionError::Caught(c) => f.write_str(c.as_str()),
FunctionError::Runtime(rt) => rt.fmt(f),
}
}
}
pub trait ResultExt<T> {
#[allow(dead_code)]
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result<T>;
fn or_throw(self, ctx: &Ctx) -> rquickjs::Result<T>;
}
impl<T, E: Display> ResultExt<T> for Result<T, E> {
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result<T> {
self.map_err(|_| {
let mut message = String::with_capacity(100);
message.push_str(msg);
message.push_str(".");
Exception::throw_message(ctx, &message)
})
}
fn or_throw(self, ctx: &Ctx) -> rquickjs::Result<T> {
self.map_err(|err| Exception::throw_message(ctx, &err.to_string()))
}
}
impl<T> ResultExt<T> for Option<T> {
fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> rquickjs::Result<T> {
self.ok_or(Exception::throw_message(ctx, msg))
}
fn or_throw(self, ctx: &Ctx) -> rquickjs::Result<T> {
self.ok_or(Exception::throw_message(ctx, "Value is not present"))
}
}
@@ -0,0 +1,125 @@
use std::sync::Arc;
use rquickjs::promise::MaybePromise;
use rquickjs::{async_with, AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::handler::function::error::{FunctionError, FunctionResult, ResultExt};
use crate::handler::function::listener::{RuntimeEvent, RuntimeListener};
use crate::handler::function::module::console::{Console, Log};
use crate::handler::function::module::ModuleLoader;
use crate::handler::function::serde::JsValue;
pub struct FunctionConfig {
pub(crate) listeners: Option<Vec<Box<dyn RuntimeListener>>>,
}
pub struct Function {
rt: Arc<AsyncRuntime>,
ctx: AsyncContext,
listeners: Vec<Box<dyn RuntimeListener>>,
module_loader: ModuleLoader,
}
impl Function {
pub async fn create<'js>(config: FunctionConfig) -> FunctionResult<Self> {
let module_loader = ModuleLoader::new();
let rt = Arc::new(AsyncRuntime::new()?);
rt.set_loader(module_loader.clone(), module_loader.clone())
.await;
let ctx = AsyncContext::full(&rt).await?;
let this = Self {
rt,
ctx,
module_loader,
listeners: config.listeners.unwrap_or_default(),
};
this.dispatch_event(RuntimeEvent::Startup).await?;
Ok(this)
}
async fn dispatch_event_inner(&self, ctx: &Ctx<'_>, event: RuntimeEvent) -> FunctionResult {
for listener in &self.listeners {
if let Err(err) = listener.on_event(ctx.clone(), event.clone()).await {
return Err(err.into());
};
}
return Ok(());
}
async fn dispatch_event(&self, event: RuntimeEvent) -> FunctionResult {
async_with!(&self.ctx => |ctx| {
self.dispatch_event_inner(&ctx, event).await
})
.await
}
pub fn context(&self) -> &AsyncContext {
&self.ctx
}
pub fn runtime(&self) -> &AsyncRuntime {
&self.rt
}
pub fn suggest_module_name<'a>(&self, id: &str, name: &str) -> String {
let declarative_name = format!("node:{name}");
if self.module_loader.has_module(&declarative_name) {
format!("node:{id}")
} else {
declarative_name
}
}
pub async fn register_module(&self, name: &str, source: &str) -> FunctionResult {
let maybe_error: Option<FunctionError> = async_with!(&self.ctx => |ctx| {
if let Err(err) = Module::declare(ctx.clone(), name.as_bytes().to_vec(), source.as_bytes().to_vec()).catch(&ctx) {
return Some(err.into())
}
return None;
}).await;
if let Some(err) = maybe_error {
return Err(err);
}
self.module_loader.add_module(name.to_string());
Ok(())
}
pub(crate) async fn call_handler(
&self,
name: &str,
data: JsValue,
) -> FunctionResult<HandlerResponse> {
let k: FunctionResult<HandlerResponse> = async_with!(&self.ctx => |ctx| {
self.dispatch_event_inner(&ctx, RuntimeEvent::SoftReset).await?;
let m: rquickjs::Object = Module::import(&ctx, name).catch(&ctx)?.into_future().await.catch(&ctx)?;
let handler: rquickjs::Function = m.get("handler").catch(&ctx)?;
let handler_promise: MaybePromise = handler.call((data, 5)).catch(&ctx)?;
let handler_result = handler_promise.into_future::<JsValue>().await.catch(&ctx)?;
let console = Console::from_context(&ctx).or_throw(&ctx)?;
let logs = console.logs.into_inner();
Ok(HandlerResponse { data: handler_result.0, logs })
})
.await;
Ok(k?)
}
}
#[derive(Serialize, Deserialize)]
pub struct HandlerResponse {
pub logs: Vec<Log>,
pub data: Value,
}
@@ -0,0 +1,20 @@
use std::future::Future;
use std::pin::Pin;
use rquickjs::Ctx;
use crate::handler::function::error::FunctionResult;
#[derive(Clone, PartialEq)]
pub(crate) enum RuntimeEvent {
Startup,
SoftReset,
}
pub(crate) trait RuntimeListener {
fn on_event<'js>(
&self,
ctx: Ctx<'js>,
event: RuntimeEvent,
) -> Pin<Box<dyn Future<Output = FunctionResult> + 'js>>;
}
+76 -22
View File
@@ -1,48 +1,102 @@
use std::time::{Duration, Instant};
use std::rc::Rc;
use std::time::Duration;
use ::serde::{Deserialize, Serialize};
use anyhow::anyhow;
use rquickjs::Runtime;
use rquickjs::{async_with, CatchResultExt, Object};
use serde_json::json;
use crate::handler::function::script::Script;
use crate::handler::function::error::FunctionResult;
use crate::handler::function::function::{Function, HandlerResponse};
use crate::handler::function::serde::JsValue;
use crate::handler::node::{NodeRequest, NodeResponse, NodeResult};
use crate::model::DecisionNodeKind;
use crate::model::{DecisionNodeKind, FunctionNodeContent};
mod js_value;
pub(crate) mod runtime;
mod script;
pub(crate) mod error;
pub(crate) mod function;
pub(crate) mod listener;
pub(crate) mod module;
pub(crate) mod serde;
pub struct FunctionHandler {
trace: bool,
runtime: Runtime,
#[derive(Serialize, Deserialize)]
pub struct FunctionResponse {
performance: String,
data: Option<HandlerResponse>,
}
static MAX_DURATION: Duration = Duration::from_millis(500);
pub struct FunctionHandler {
function: Rc<Function>,
trace: bool,
iteration: u8,
max_depth: u8,
}
static MAX_DURATION: Duration = Duration::from_millis(5_000);
impl FunctionHandler {
pub fn new(trace: bool, runtime: Runtime) -> Self {
Self { trace, runtime }
pub fn new(function: Rc<Function>, trace: bool, iteration: u8, max_depth: u8) -> Self {
Self {
function,
trace,
iteration,
max_depth,
}
}
pub async fn handle(&self, request: &NodeRequest<'_>) -> NodeResult {
let content = match &request.node.kind {
DecisionNodeKind::FunctionNode { content } => Ok(content),
DecisionNodeKind::FunctionNode { content } => match content {
FunctionNodeContent::Version2(content) => Ok(content),
_ => Err(anyhow!("Unexpected node type")),
},
_ => Err(anyhow!("Unexpected node type")),
}?;
let start = std::time::Instant::now();
let start = Instant::now();
let module_name = self
.function
.suggest_module_name(request.node.id.as_str(), request.node.name.as_str());
let interrupt_handler = Box::new(move || start.elapsed() > MAX_DURATION);
self.runtime.set_interrupt_handler(Some(interrupt_handler));
self.function
.runtime()
.set_interrupt_handler(Some(interrupt_handler))
.await;
let mut script = Script::new(self.runtime.clone());
let result_response = script.call(content, &request.input).await;
self.attach_globals()
.await
.map_err(|e| anyhow!(e.to_string()))?;
self.runtime.set_interrupt_handler(None);
self.function
.register_module(&module_name, content.source.as_str())
.await
.map_err(|e| anyhow!(e.to_string()))?;
let response = self
.function
.call_handler(&module_name, JsValue(request.input.clone()))
.await
.map_err(|e| anyhow!(e.to_string()))?;
self.function.runtime().set_interrupt_handler(None).await;
let response = result_response?;
Ok(NodeResponse {
output: response.output,
trace_data: self.trace.then(|| json!({ "log": response.log })),
output: response.data,
trace_data: self.trace.then(|| json!({ "log": response.logs })),
})
}
async fn attach_globals(&self) -> FunctionResult {
async_with!(self.function.context() => |ctx| {
let config = Object::new(ctx.clone()).catch(&ctx)?;
config.prop("iteration", self.iteration).catch(&ctx)?;
config.prop("maxDepth", self.max_depth).catch(&ctx)?;
config.prop("trace", self.trace).catch(&ctx)?;
ctx.globals().set("config", config).catch(&ctx)?;
Ok(())
})
.await
}
}
@@ -0,0 +1,104 @@
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::time::{Duration, Instant};
use crate::handler::function::error::{FunctionResult, ResultExt};
use crate::handler::function::listener::{RuntimeEvent, RuntimeListener};
use rquickjs::prelude::Rest;
use rquickjs::{Ctx, Object, Value};
use serde::{Deserialize, Serialize};
pub(crate) struct ConsoleListener;
impl RuntimeListener for ConsoleListener {
fn on_event<'js>(
&self,
ctx: Ctx<'js>,
event: RuntimeEvent,
) -> Pin<Box<dyn Future<Output = FunctionResult> + 'js>> {
Box::pin(async move {
match event {
RuntimeEvent::Startup => Console::init(&ctx)?,
RuntimeEvent::SoftReset => Console::init(&ctx)?,
}
Ok(())
})
}
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Log {
lines: Vec<String>,
ms_since_run: usize,
}
#[derive(rquickjs::class::Trace, Clone)]
#[rquickjs::class]
pub struct Console {
#[qjs(skip_trace)]
pub logs: RefCell<Vec<Log>>,
#[qjs(skip_trace)]
created_at: Instant,
}
#[rquickjs::methods(rename_all = "camelCase")]
impl Console {
fn new() -> Self {
Self {
logs: Default::default(),
created_at: Instant::now(),
}
}
#[qjs(skip)]
pub fn init(ctx: &Ctx) -> rquickjs::Result<()> {
ctx.globals().set("console", Self::new())?;
Ok(())
}
#[qjs(skip)]
pub fn from_context(ctx: &Ctx) -> rquickjs::Result<Self> {
let obj: Self = ctx.globals().get("console")?;
Ok(obj)
}
pub fn log<'js>(&self, ctx: Ctx<'js>, args: Rest<Value<'js>>) -> rquickjs::Result<()> {
let config: Object = ctx.globals().get("config").or_throw(&ctx)?;
let trace: bool = config.get("trace").or_throw(&ctx)?;
if !trace {
return Ok(());
}
let step1 = args
.0
.into_iter()
.map(|arg| ctx.json_stringify(arg))
.collect::<Result<Vec<Option<rquickjs::String<'js>>>, _>>()?;
let step2 = step1
.into_iter()
.map(|s| s.map(|i| i.to_string()).transpose())
.collect::<Result<Vec<Option<String>>, _>>()?;
let step3 = step2
.into_iter()
.map(|s| s.unwrap_or_default())
.collect::<Vec<String>>();
let mut logs = self.logs.borrow_mut();
logs.push(Log {
lines: step3,
ms_since_run: self.created_at.elapsed().as_millis() as usize,
});
Ok(())
}
pub async fn sleep(&self, duration_ms: u64) -> rquickjs::Result<()> {
tokio::time::sleep(Duration::from_millis(duration_ms)).await;
Ok(())
}
}
@@ -0,0 +1,241 @@
use reqwest::header::{HeaderMap, HeaderName};
use reqwest::Method;
use rquickjs::module::{Declarations, Exports, ModuleDef};
use rquickjs::prelude::{Async, Func, Opt};
use rquickjs::{CatchResultExt, Ctx, FromJs, IntoAtom, IntoJs, Object, Value};
use std::str::FromStr;
use std::sync::OnceLock;
use crate::handler::function::error::ResultExt;
use crate::handler::function::module::export_default;
use crate::handler::function::serde::JsValue;
pub(crate) struct HttpResponse<'js> {
data: Value<'js>,
headers: Object<'js>,
status: u16,
}
impl<'js> IntoJs<'js> for HttpResponse<'js> {
fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let object = Object::new(ctx.clone())?;
object.set("data", self.data)?;
object.set("headers", self.headers)?;
object.set("status", self.status)?;
Ok(object.into_value())
}
}
async fn execute_http<'js>(
ctx: Ctx<'js>,
method: Method,
url: String,
data: Option<JsValue>,
config: Option<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
let client = HTTP_CLIENT.get_or_init(|| reqwest::Client::new()).clone();
let mut builder = client.request(method, url);
if let Some(data) = data {
builder = builder.json(&data.0);
}
if let Some(config) = config {
builder = builder
.headers(config.headers)
.query(config.params.as_slice());
if let Some(data) = config.data {
builder = builder.json(&data.0);
}
}
let response = builder.send().await.or_throw(&ctx)?;
let status = response.status().as_u16();
let header_object = Object::new(ctx.clone()).catch(&ctx).or_throw(&ctx)?;
for (key, value) in response.headers() {
header_object.set(
key.as_str().into_atom(&ctx)?,
value.to_str().or_throw(&ctx).into_js(&ctx),
)?;
}
let data: serde_json::Value = response.json().await.or_throw(&ctx)?;
Ok(HttpResponse {
data: JsValue(data).into_js(&ctx)?,
headers: header_object,
status,
})
}
#[derive(Default)]
pub(crate) struct HttpConfig {
headers: HeaderMap,
params: Vec<(String, String)>,
data: Option<JsValue>,
}
impl<'js> FromJs<'js> for HttpConfig {
fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
let object = value.into_object().or_throw(ctx)?;
let headers_obj: Option<Object<'js>> = object.get("headers").or_throw(ctx)?;
let headers = if let Some(headers_obj) = headers_obj {
let mut header_map = HeaderMap::with_capacity(headers_obj.len());
for result in headers_obj.into_iter() {
let Ok((key, value)) = result else {
continue;
};
let value = JsValue::from_js(ctx, value)?;
let str_value = match value.0 {
serde_json::Value::Null => None,
serde_json::Value::Bool(b) => Some(b.to_string()),
serde_json::Value::Number(n) => Some(n.to_string()),
serde_json::Value::String(s) => Some(s),
serde_json::Value::Array(_) => None,
serde_json::Value::Object(_) => None,
};
let key_value = key.to_string()?;
let key = HeaderName::from_str(key_value.as_str()).or_throw(&ctx)?;
if let Some(str_value) = str_value {
header_map.insert(key, str_value.parse().or_throw(&ctx)?);
}
}
header_map
} else {
HeaderMap::default()
};
let params_obj: Option<Object<'js>> = object.get("params").or_throw(ctx)?;
let params = if let Some(params_obj) = params_obj {
let mut params = Vec::with_capacity(params_obj.len());
for result in params_obj.into_iter() {
let Ok((key, value)) = result else {
continue;
};
let value = JsValue::from_js(ctx, value)?;
let str_value = match value.0 {
serde_json::Value::Null => None,
serde_json::Value::Bool(b) => Some(b.to_string()),
serde_json::Value::Number(n) => Some(n.to_string()),
serde_json::Value::String(s) => Some(s),
serde_json::Value::Array(_) => None,
serde_json::Value::Object(_) => None,
};
let key = key.to_string()?;
if let Some(str_value) = str_value {
params.push((key, str_value));
}
}
params
} else {
Vec::default()
};
let data_obj: Option<Value<'js>> = object.get("data").ok();
let data = if let Some(data_obj) = data_obj {
Some(
JsValue::from_js(&ctx, data_obj)
.catch(&ctx)
.or_throw(&ctx)?,
)
} else {
None
};
Ok(Self {
headers,
params,
data,
})
}
}
async fn get<'js>(
ctx: Ctx<'js>,
url: String,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
execute_http(ctx, Method::GET, url, None, config.0).await
}
async fn post<'js>(
ctx: Ctx<'js>,
url: String,
data: JsValue,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
execute_http(ctx, Method::POST, url, Some(data), config.0).await
}
async fn patch<'js>(
ctx: Ctx<'js>,
url: String,
data: JsValue,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
execute_http(ctx, Method::PATCH, url, Some(data), config.0).await
}
async fn put<'js>(
ctx: Ctx<'js>,
url: String,
data: JsValue,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
execute_http(ctx, Method::PUT, url, Some(data), config.0).await
}
async fn delete<'js>(
ctx: Ctx<'js>,
url: String,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
execute_http(ctx, Method::DELETE, url, None, config.0).await
}
async fn head<'js>(
ctx: Ctx<'js>,
url: String,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
execute_http(ctx, Method::DELETE, url, None, config.0).await
}
pub(crate) struct HttpModule;
impl ModuleDef for HttpModule {
fn declare<'js>(decl: &Declarations<'js>) -> rquickjs::Result<()> {
decl.declare("get")?;
decl.declare("head")?;
decl.declare("post")?;
decl.declare("patch")?;
decl.declare("put")?;
decl.declare("delete")?;
decl.declare("default")?;
Ok(())
}
fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
export_default(ctx, exports, |default| {
default.set("get", Func::from(Async(get)))?;
default.set("head", Func::from(Async(head)))?;
default.set("post", Func::from(Async(post)))?;
default.set("patch", Func::from(Async(patch)))?;
default.set("put", Func::from(Async(put)))?;
default.set("delete", Func::from(Async(delete)))?;
Ok(())
})
}
}
@@ -0,0 +1,133 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::ops::DerefMut;
use std::rc::Rc;
use rquickjs::loader::{Bundle, Loader, ModuleLoader as MDLoader, Resolver};
use rquickjs::module::{Declared, Exports};
use rquickjs::{embed, Ctx, Error, Module, Object};
use crate::handler::function::module::http::HttpModule;
use crate::handler::function::module::zen::ZenModule;
pub(crate) mod console;
pub(crate) mod http;
pub(crate) mod zen;
static JS_BUNDLE: Bundle = embed! {
"dayjs": "js/dayjs.mjs",
"big.js": "js/big.mjs",
"zod": "js/zod.mjs"
};
#[derive(Clone)]
pub struct ModuleLoader(Rc<RefCell<BaseModuleLoader>>);
impl ModuleLoader {
pub fn new() -> Self {
Self(Rc::new(RefCell::new(BaseModuleLoader::new())))
}
pub fn add_module(&self, module: String) {
let reference = self.0.borrow_mut();
reference.add_module(module);
}
pub fn has_module(&self, module: &str) -> bool {
let reference = self.0.borrow();
reference.has_module(module)
}
}
impl Resolver for ModuleLoader {
fn resolve<'js>(&mut self, ctx: &Ctx<'js>, base: &str, name: &str) -> rquickjs::Result<String> {
let mut inner = self.0.borrow_mut();
inner.deref_mut().resolve(ctx, base, name)
}
}
impl Loader for ModuleLoader {
fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> rquickjs::Result<Module<'js, Declared>> {
let mut inner = self.0.borrow_mut();
inner.deref_mut().load(ctx, name)
}
}
struct BaseModuleLoader {
bundle: Bundle,
defined_modules: RefCell<HashSet<String>>,
md_loader: MDLoader,
}
impl BaseModuleLoader {
pub fn new() -> Self {
let mut hs = HashSet::from(["zen".to_string(), "http".to_string()]);
JS_BUNDLE.iter().for_each(|(key, _)| {
hs.insert(key.to_string());
});
Self {
bundle: JS_BUNDLE,
defined_modules: RefCell::new(hs),
md_loader: MDLoader::default()
.with_module("zen", ZenModule)
.with_module("http", HttpModule),
}
}
pub fn add_module(&self, value: String) {
let mut modules = self.defined_modules.borrow_mut();
modules.insert(value);
}
pub fn has_module(&self, value: &str) -> bool {
let modules = self.defined_modules.borrow();
modules.contains(value)
}
}
impl Resolver for &mut BaseModuleLoader {
fn resolve<'js>(&mut self, ctx: &Ctx<'js>, base: &str, name: &str) -> rquickjs::Result<String> {
if let Ok(b) = self.bundle.resolve(ctx, base, name) {
return Ok(b);
}
let defined_modules = self.defined_modules.borrow();
if defined_modules.contains(name) {
return Ok(name.to_string());
}
Err(Error::new_resolving(base, name))
}
}
impl Loader for &mut BaseModuleLoader {
fn load<'js>(&mut self, ctx: &Ctx<'js>, name: &str) -> rquickjs::Result<Module<'js, Declared>> {
self.bundle
.load(ctx, name)
.or_else(|_| self.md_loader.load(ctx, name))
}
}
pub(crate) fn export_default<'js, F>(
ctx: &Ctx<'js>,
exports: &Exports<'js>,
f: F,
) -> rquickjs::Result<()>
where
F: FnOnce(&Object<'js>) -> rquickjs::Result<()>,
{
let default = Object::new(ctx.clone())?;
f(&default)?;
for name in default.keys::<String>() {
let name = name?;
let value: rquickjs::Value = default.get(&name)?;
exports.export(name, value)?;
}
exports.export("default", default)?;
Ok(())
}
@@ -0,0 +1,144 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use rquickjs::module::{Declarations, Exports, ModuleDef};
use rquickjs::prelude::{Async, Func, Opt};
use rquickjs::{CatchResultExt, Ctx, Function, Object};
use crate::handler::custom_node_adapter::CustomNodeAdapter;
use crate::handler::function::error::{FunctionResult, ResultExt};
use crate::handler::function::listener::{RuntimeEvent, RuntimeListener};
use crate::handler::function::module::export_default;
use crate::handler::function::serde::JsValue;
use crate::handler::graph::{DecisionGraph, DecisionGraphConfig};
use crate::loader::DecisionLoader;
pub(crate) struct ZenListener<Loader, Adapter> {
pub loader: Arc<Loader>,
pub adapter: Arc<Adapter>,
}
impl<Loader: DecisionLoader + 'static, Adapter: CustomNodeAdapter + 'static> RuntimeListener
for ZenListener<Loader, Adapter>
{
fn on_event<'js>(
&self,
ctx: Ctx<'js>,
event: RuntimeEvent,
) -> Pin<Box<dyn Future<Output = FunctionResult> + 'js>> {
let loader = self.loader.clone();
let adapter = self.adapter.clone();
Box::pin(async move {
if event != RuntimeEvent::Startup {
return Ok(());
};
ctx.globals()
.set(
"__evaluate",
Func::from(Async(
move |ctx: Ctx<'js>,
key: String,
context: JsValue,
opts: Opt<Object<'js>>| {
let loader = loader.clone();
let adapter = adapter.clone();
async move {
let config: Object = ctx.globals().get("config").or_throw(&ctx)?;
let iteration: u8 = config.get("iteration").or_throw(&ctx)?;
let max_depth: u8 = config.get("maxDepth").or_throw(&ctx)?;
let trace = opts
.0
.map(|opt| opt.get::<_, bool>("trace").unwrap_or_default())
.unwrap_or_default();
let load_result = loader.load(key.as_str()).await;
let decision_content = load_result.or_throw(&ctx)?;
let mut sub_tree = DecisionGraph::try_new(DecisionGraphConfig {
content: &decision_content,
max_depth,
loader,
adapter,
iteration: iteration + 1,
trace,
})
.or_throw(&ctx)?;
let response =
sub_tree.evaluate(&context.0).await.or_throw(&ctx)?;
return rquickjs::Result::Ok(JsValue(
serde_json::to_value(response).or_throw(&ctx)?,
));
}
},
)),
)
.catch(&ctx)?;
Ok(())
})
}
}
fn evaluate_expression<'js>(
ctx: Ctx<'js>,
expression: String,
context: JsValue,
) -> rquickjs::Result<JsValue> {
let s = zen_expression::evaluate_expression(expression.as_str(), &context.0).or_throw(&ctx)?;
Ok(JsValue(s))
}
fn evaluate_unary_expression<'js>(
ctx: Ctx<'js>,
expression: String,
context: JsValue,
) -> rquickjs::Result<bool> {
let s = zen_expression::evaluate_unary_expression(expression.as_str(), &context.0)
.or_throw(&ctx)?;
Ok(s)
}
fn evaluate<'js>(
ctx: Ctx<'js>,
key: String,
context: JsValue,
opts: Opt<Object<'js>>,
) -> rquickjs::Result<rquickjs::Value<'js>> {
let s: Function = ctx.globals().get("__evaluate").or_throw(&ctx)?;
let result: rquickjs::Value = s.call((key, context, opts)).or_throw(&ctx)?;
Ok(result)
}
pub struct ZenModule;
impl ModuleDef for ZenModule {
fn declare<'js>(decl: &Declarations<'js>) -> rquickjs::Result<()> {
decl.declare("evaluateExpression")?;
decl.declare("evaluateUnaryExpression")?;
decl.declare("evaluate")?;
decl.declare("default")?;
Ok(())
}
fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> rquickjs::Result<()> {
export_default(ctx, exports, |default| {
default.set("evaluateExpression", Func::from(evaluate_expression))?;
default.set(
"evaluateUnaryExpression",
Func::from(evaluate_unary_expression),
)?;
default.set("evaluate", Func::from(evaluate))?;
Ok(())
})
}
}
@@ -1,16 +0,0 @@
use anyhow::Context;
use rquickjs::loader::Bundle;
use rquickjs::{embed, Runtime};
static JS_BUNDLE: Bundle = embed! {
"dayjs": "js/dayjs.js",
"big": "js/big.js",
"internals": "js/internals.js"
};
pub(crate) fn create_runtime() -> anyhow::Result<Runtime> {
let runtime = Runtime::new().context("Failed to create runtime")?;
runtime.set_loader(JS_BUNDLE, JS_BUNDLE);
Ok(runtime)
}
@@ -1,61 +0,0 @@
use std::fmt::Debug;
use anyhow::Context as _;
use rquickjs::{Context, Ctx, Error as QError, FromJs, Runtime};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::handler::function::js_value::JsValue;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EvaluateResponse {
pub output: Value,
pub log: Vec<Value>,
}
pub struct Script {
runtime: Runtime,
}
impl Script {
pub fn new(runtime: Runtime) -> Self {
Self { runtime }
}
pub async fn call<P>(&mut self, source: &str, args: &P) -> anyhow::Result<EvaluateResponse>
where
P: Serialize,
{
let runtime = &self.runtime;
let context = Context::full(&runtime).context("Failed to create context")?;
let args_str =
serde_json::to_string(args).context("Failed to serialize function arguments")?;
let json_response = context.with(|ctx| -> anyhow::Result<String> {
let _ = ctx
.clone()
.compile("main", "import 'internals'; globalThis.now = Date.now();")
.map_err(|e| map_js_error(&ctx, e))?;
let _ = ctx
.globals()
.set("log", Vec::<isize>::new())
.map_err(|e| map_js_error(&ctx, e))?;
ctx.eval::<String, _>(format!("{source};main({args_str})"))
.map_err(|e| map_js_error(&ctx, e))
})?;
serde_json::from_str(json_response.as_str()).context("Failed to parse function result")
}
}
fn map_js_error(ctx: &Ctx, e: QError) -> anyhow::Error {
let error = JsValue::from_js(&ctx, ctx.catch())
.map(|v| v.0)
.unwrap_or(Value::String(e.to_string()));
anyhow::Error::msg(error.to_string())
}
@@ -1,5 +1,6 @@
use crate::handler::function::error::ResultExt;
use itertools::Itertools;
use rquickjs::{Ctx, Error as QError, FromJs, Type, Value as QValue};
use rquickjs::{Ctx, FromJs, IntoAtom, IntoJs, Type, Value as QValue};
use serde_json::{json, Map, Number, Value};
#[derive(Debug)]
@@ -9,50 +10,46 @@ impl<'js> FromJs<'js> for JsValue {
fn from_js(ctx: &Ctx<'js>, v: QValue<'js>) -> rquickjs::Result<Self> {
let computed_value = match v.type_of() {
Type::Uninitialized | Type::Undefined | Type::Null => Value::Null,
Type::Bool => Value::Bool(
v.as_bool()
.ok_or_else(|| QError::new_from_js("bool", "bool"))?,
),
Type::Bool => Value::Bool(v.as_bool().or_throw_msg(ctx, "failed to convert to bool")?),
Type::Int => Value::Number(Number::from(
v.as_int()
.ok_or_else(|| QError::new_from_js("int", "number"))?,
v.as_int().or_throw_msg(ctx, "failed to convert to int")?,
)),
Type::BigInt => Value::Number(Number::from(
v.as_big_int()
.map(|b| b.clone().to_i64().ok())
.flatten()
.ok_or_else(|| QError::new_from_js("bigint", "number"))?,
.or_throw_msg(ctx, "failed to convert to number")?,
)),
Type::Float => Value::Number(
v.as_float()
.map(|n| Number::from_f64(n))
.flatten()
.ok_or_else(|| QError::new_from_js("float", "number"))?,
.or_throw_msg(ctx, "failed to convert to number")?,
),
Type::String => Value::String(
v.as_string()
.map(|s| s.to_string().ok())
.flatten()
.ok_or_else(|| QError::new_from_js("string", "string"))?,
.or_throw_msg(ctx, "failed to convert to string")?,
),
Type::Array => {
let arr = v
.as_array()
.ok_or_else(|| QError::new_from_js("array", "array"))?;
.or_throw_msg(ctx, "failed to convert to array")?;
let js_arr: Vec<Value> = arr
.iter::<QValue>()
.map_ok(|n| JsValue::from_js(ctx, n.clone()).map(|js_val| js_val.0))
.flatten()
.try_collect()
.map_err(|_| QError::new_from_js("array", "array"))?;
.or_throw(ctx)?;
Value::Array(js_arr)
}
Type::Object => {
let object = v
.as_object()
.ok_or_else(|| QError::new_from_js("object", "object"))?;
.or_throw_msg(ctx, "failed to convert to object")?;
let js_object: Map<String, Value> = object
.props::<String, QValue>()
@@ -61,18 +58,16 @@ impl<'js> FromJs<'js> for JsValue {
})
.flatten()
.try_collect()
.map_err(|_| QError::new_from_js("object", "object"))?;
.or_throw(ctx)?;
Value::Object(js_object)
}
Type::Exception => {
let exception = v
.as_exception()
.ok_or_else(|| QError::new_from_js("exception", "object"))?;
.or_throw_msg(ctx, "failed to convert to exception")?;
let message = exception
.message()
.ok_or_else(|| QError::new_from_js("exception", "object"))?;
let message = exception.message().unwrap_or_default();
let description = exception.to_string();
json!({ "message": message, "description": description })
@@ -82,8 +77,46 @@ impl<'js> FromJs<'js> for JsValue {
Type::Constructor => json!("[Constructor]"),
Type::Symbol => json!("[Symbol]"),
Type::Unknown => json!("[Unknown]"),
Type::Promise => {
let promise = v.as_promise().or_throw(ctx)?;
let val: JsValue = promise.finish()?;
val.0
}
};
Ok(JsValue(computed_value))
}
}
impl<'js> IntoJs<'js> for JsValue {
fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<QValue<'js>> {
let res = match self.0 {
Value::Null => QValue::new_null(ctx.clone()),
Value::Bool(b) => QValue::new_bool(ctx.clone(), b),
Value::Number(n) => QValue::new_number(
ctx.clone(),
n.as_f64()
.or_throw_msg(ctx, "failed to convert float to number")?,
),
Value::String(str) => str.into_js(ctx)?,
Value::Array(arr) => {
let qarr = rquickjs::Array::new(ctx.clone())?;
for (idx, item) in arr.into_iter().enumerate() {
qarr.set(idx, JsValue(item))?;
}
qarr.into_value()
}
Value::Object(map) => {
let qmap = rquickjs::Object::new(ctx.clone())?;
for (key, value) in map.into_iter() {
qmap.set(key.into_atom(ctx)?, JsValue(value))?;
}
qmap.into_value()
}
};
Ok(res)
}
}