Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 52 additions & 52 deletions bindings/nodejs/index.js

Large diffs are not rendered by default.

255 changes: 255 additions & 0 deletions bindings/nodejs/src/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
use std::collections::HashMap;
use std::os::raw::c_char;
use std::ptr;
use std::rc::Rc;

use napi::bindgen_prelude::ToNapiValue;
use napi::sys;
use napi::sys::{napi_env, napi_value};
use serde_json::Value;
use zen_engine::{DecisionGraphResponse, EvaluationTrace, EvaluationTraceKind};
use zen_expression::variable::ToVariable;
use zen_expression::Variable;

enum PNode {
Null,
Bool(bool),
Num(f64),
Str(Box<str>),
Arr(Vec<u32>),
Obj(Vec<(Box<str>, u32)>),
Json(Value),
}

pub struct PortableArena {
nodes: Vec<PNode>,
}

impl PortableArena {
pub fn new() -> Self {
Self { nodes: Vec::new() }
}

pub fn add(&mut self, var: &Variable, memo: &mut HashMap<usize, u32>) -> u32 {
let addr = match var {
Variable::Array(a) => Some(Rc::as_ptr(a) as *const () as usize),
Variable::Object(o) => Some(Rc::as_ptr(o) as *const () as usize),
_ => None,
};

if let Some(addr) = addr {
if let Some(id) = memo.get(&addr) {
return *id;
}
}

let node = match var {
Variable::Null => PNode::Null,
Variable::Bool(b) => PNode::Bool(*b),
Variable::Number(n) => {
PNode::Num(n.normalize().to_string().parse::<f64>().unwrap_or(f64::NAN))
}
Variable::String(s) => PNode::Str(Box::from(s.as_ref())),
Variable::Array(a) => {
let borrowed = a.borrow();
let mut children = Vec::with_capacity(borrowed.len());
for item in borrowed.iter() {
children.push(self.add(item, memo));
}

PNode::Arr(children)
}
Variable::Object(o) => {
let borrowed = o.borrow();
let mut entries = Vec::with_capacity(borrowed.len());
for (key, value) in borrowed.iter() {
let child = self.add(value, memo);
entries.push((Box::from(key.as_ref()), child));
}

PNode::Obj(entries)
}
Variable::Dynamic(d) => PNode::Json(d.to_value()),
};

let id = self.nodes.len() as u32;
self.nodes.push(node);
if let Some(addr) = addr {
memo.insert(addr, id);
}

id
}
}

pub enum TraceField {
None,
Arena(u32),
Json(Value),
}

pub struct NodeEvalResponse {
pub performance: String,
pub arena: PortableArena,
pub result_root: u32,
pub trace: TraceField,
}

impl NodeEvalResponse {
pub fn build(response: DecisionGraphResponse, mode: EvaluationTraceKind) -> Self {
let mut arena = PortableArena::new();
let mut memo = HashMap::new();

let result_root = arena.add(&response.result, &mut memo);

let trace = match response.trace {
None => TraceField::None,
Some(EvaluationTrace::Graph(graph)) => {
let variable = graph.to_variable();
match mode {
EvaluationTraceKind::Default => {
TraceField::Arena(arena.add(&variable, &mut memo))
}
other => TraceField::Json(other.serialize_trace(&variable)),
}
}
Some(EvaluationTrace::Policy(policy)) => {
let value = match mode {
EvaluationTraceKind::String | EvaluationTraceKind::ReferenceString => {
Value::String(serde_json::to_string(&policy).unwrap_or_default())
}
_ => serde_json::to_value(&policy).unwrap_or_default(),
};

TraceField::Json(value)
}
};

Self {
performance: response.performance,
arena,
result_root,
trace,
}
}
}

struct Materializer<'a> {
env: napi_env,
nodes: &'a [PNode],
cache: Vec<napi_value>,
keys: HashMap<&'a str, napi_value>,
}

impl<'a> Materializer<'a> {
unsafe fn string(&self, s: &str) -> napi_value {
let mut out = ptr::null_mut();
sys::napi_create_string_utf8(
self.env,
s.as_ptr() as *const c_char,
s.len() as isize,
&mut out,
);
out
}

unsafe fn intern_key(&mut self, key: &'a str) -> napi_value {
if let Some(existing) = self.keys.get(key) {
return *existing;
}

let created = self.string(key);
self.keys.insert(key, created);
created
}

unsafe fn value(&mut self, id: u32) -> napi::Result<napi_value> {
let cached = self.cache[id as usize];
if !cached.is_null() {
return Ok(cached);
}

let built = self.build(id)?;
self.cache[id as usize] = built;
Ok(built)
}

unsafe fn build(&mut self, id: u32) -> napi::Result<napi_value> {
let nodes = self.nodes;
match &nodes[id as usize] {
PNode::Null => {
let mut out = ptr::null_mut();
sys::napi_get_null(self.env, &mut out);
Ok(out)
}
PNode::Bool(b) => {
let mut out = ptr::null_mut();
sys::napi_get_boolean(self.env, *b, &mut out);
Ok(out)
}
PNode::Num(n) => {
let mut out = ptr::null_mut();
sys::napi_create_double(self.env, *n, &mut out);
Ok(out)
}
PNode::Str(s) => Ok(self.string(s)),
PNode::Arr(children) => {
let mut arr = ptr::null_mut();
sys::napi_create_array_with_length(self.env, children.len(), &mut arr);
for (index, child) in children.iter().enumerate() {
let value = self.value(*child)?;
sys::napi_set_element(self.env, arr, index as u32, value);
}

Ok(arr)
}
PNode::Obj(entries) => {
let mut obj = ptr::null_mut();
sys::napi_create_object(self.env, &mut obj);
for (key, child) in entries.iter() {
let key_value = self.intern_key(key);
let value = self.value(*child)?;
sys::napi_set_property(self.env, obj, key_value, value);
}

Ok(obj)
}
PNode::Json(value) => ToNapiValue::to_napi_value(self.env, value.clone()),
}
}
}

impl ToNapiValue for NodeEvalResponse {
unsafe fn to_napi_value(env: napi_env, val: Self) -> napi::Result<napi_value> {
let mut materializer = Materializer {
env,
nodes: &val.arena.nodes,
cache: vec![ptr::null_mut(); val.arena.nodes.len()],
keys: HashMap::new(),
};

let mut obj = ptr::null_mut();
sys::napi_create_object(env, &mut obj);

let performance = materializer.string(&val.performance);
let performance_key = materializer.string("performance");
sys::napi_set_property(env, obj, performance_key, performance);

let result = materializer.value(val.result_root)?;
let result_key = materializer.string("result");
sys::napi_set_property(env, obj, result_key, result);

let trace = match &val.trace {
TraceField::None => None,
TraceField::Arena(root) => Some(materializer.value(*root)?),
TraceField::Json(value) => Some(ToNapiValue::to_napi_value(env, value.clone())?),
};

if let Some(trace) = trace {
let trace_key = materializer.string("trace");
sys::napi_set_property(env, obj, trace_key, trace);
}

Ok(obj)
}
}
55 changes: 46 additions & 9 deletions bindings/nodejs/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use napi_derive::napi;
use serde_json::Value;

use crate::content::ZenDecisionContent;
use crate::convert::NodeEvalResponse;
use crate::custom_node::{CustomNode, CustomNodeTsfn};
use crate::decision::ZenDecision;
use crate::dispose::DisposeThreadsafeHandler;
Expand All @@ -23,7 +24,9 @@ use crate::safe_result::SafeResult;
use crate::types::{ZenEngineHandlerRequest, ZenEngineHandlerResponse};
use zen_engine::loader::{DynamicLoader, LoaderConfig};
use zen_engine::model::DecisionContent;
use zen_engine::{DecisionEngine, EvaluationSerializedOptions, EvaluationTraceKind};
use zen_engine::{
DecisionEngine, EvaluationOptions, EvaluationSerializedOptions, EvaluationTraceKind,
};

#[napi]
pub struct ZenEngine {
Expand Down Expand Up @@ -110,13 +113,23 @@ pub struct EvaluateBatchRequest {
pub context: Value,
}

#[napi(object)]
pub struct EvaluateBatchResult {
pub success: bool,
pub data: Option<Value>,
pub data: Option<NodeEvalResponse>,
pub error: Option<Value>,
}

impl ToNapiValue for EvaluateBatchResult {
unsafe fn to_napi_value(env: napi_env, val: Self) -> napi::Result<napi_value> {
let env_wrapper = &Env::from(env);
let mut obj = Object::new(env_wrapper)?;
obj.set("success", val.success)?;
obj.set("data", val.data)?;
obj.set("error", val.error)?;
Object::to_napi_value(env, obj)
}
}

#[napi(object)]
pub struct ZenEngineOptions {
#[napi(
Expand Down Expand Up @@ -240,15 +253,25 @@ impl ZenEngine {
key: String,
context: Value,
opts: Option<ZenEvaluateOptions>,
) -> napi::Result<Value> {
) -> napi::Result<NodeEvalResponse> {
let graph = self.graph.clone();
let result = spawn_worker(|| {
let options = opts.unwrap_or_default();
let serialized: EvaluationSerializedOptions = opts.unwrap_or_default().into();
let mode = serialized.trace;
let options = EvaluationOptions {
trace: mode != EvaluationTraceKind::None,
max_depth: serialized.max_depth,
};

async move {
graph
.evaluate_serialized(key, context.into(), options.into())
.evaluate_with_opts(key, context.into(), options)
.await
.map(|response| NodeEvalResponse::build(response, mode))
.map_err(|e| {
e.serialize_with_mode(serde_json::value::Serializer, mode)
.unwrap_or_default()
})
}
})
.await
Expand Down Expand Up @@ -301,7 +324,7 @@ impl ZenEngine {
key: String,
context: Value,
opts: Option<ZenEvaluateOptions>,
) -> SafeResult<Value> {
) -> SafeResult<NodeEvalResponse> {
self.evaluate(key, context, opts).await.into()
}

Expand All @@ -312,22 +335,36 @@ impl ZenEngine {
self.get_decision(key).await.into()
}

#[napi]
#[napi(
ts_return_type = "Promise<Array<{ success: true; data: ZenEngineResponse } | { success: false; error: any }>>"
)]
pub async fn evaluate_batch(
&self,
requests: Vec<EvaluateBatchRequest>,
opts: Option<ZenEvaluateOptions>,
) -> napi::Result<Vec<EvaluateBatchResult>> {
let options: EvaluationSerializedOptions = opts.unwrap_or_default().into();
let mode = options.trace;
let max_depth = options.max_depth;

let mut handles = Vec::with_capacity(requests.len());
for req in requests {
let engine = self.graph.clone();
let EvaluateBatchRequest { key, context } = req;
handles.push(spawn_worker(move || async move {
let eval_opts = EvaluationOptions {
trace: mode != EvaluationTraceKind::None,
max_depth,
};

engine
.evaluate_serialized(&key, context.into(), options)
.evaluate_with_opts(key, context.into(), eval_opts)
.await
.map(|response| NodeEvalResponse::build(response, mode))
.map_err(|e| {
e.serialize_with_mode(serde_json::value::Serializer, mode)
.unwrap_or_default()
})
}));
}

Expand Down
1 change: 1 addition & 0 deletions bindings/nodejs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod config;
mod content;
mod convert;
mod custom_node;
mod decision;
mod dispose;
Expand Down
Loading
Loading