diff --git a/bindings/nodejs/Cargo.toml b/bindings/nodejs/Cargo.toml index 31311890..fb75d66a 100644 --- a/bindings/nodejs/Cargo.toml +++ b/bindings/nodejs/Cargo.toml @@ -10,8 +10,8 @@ crate-type = ["cdylib"] [dependencies] async-trait = { workspace = true } -napi = { version = "2.16", features = ["serde-json", "error_anyhow", "tokio_rt"] } -napi-derive = "2.16" +napi = { version = "2.16.2", features = ["serde-json", "error_anyhow", "tokio_rt"] } +napi-derive = "2.16.2" serde_json = { workspace = true } futures = { workspace = true } zen-engine = { path = "../../core/engine" } diff --git a/bindings/nodejs/README.md b/bindings/nodejs/README.md index 76c7e970..2f957a30 100644 --- a/bindings/nodejs/README.md +++ b/bindings/nodejs/README.md @@ -40,6 +40,7 @@ import fs from 'fs/promises'; const decision = engine.createDecision(content); const result = await decision.evaluate({ input: 15 }); + engine.dispose(); })(); ``` @@ -58,6 +59,7 @@ const loader = async (key: string) => fs.readFile(path.join(testDataRoot, key))( const engine = new ZenEngine({ loader }); const result = await engine.evaluate('jdm_graph1.json', { input: 5 }); + engine.dispose(); })(); ``` diff --git a/bindings/nodejs/index.d.ts b/bindings/nodejs/index.d.ts index 49c0445d..55989d60 100644 --- a/bindings/nodejs/index.d.ts +++ b/bindings/nodejs/index.d.ts @@ -54,6 +54,11 @@ export class ZenEngine { evaluate(key: string, context: any, opts?: ZenEvaluateOptions | undefined | null): Promise createDecision(content: ZenDecisionContent | Buffer | object): ZenDecision getDecision(key: string): Promise + /** + * Function used to dispose memory allocated for loaders + * In the future, it will likely be removed and made automatic + */ + dispose(): void } export class ZenEngineHandlerRequest { input: any diff --git a/bindings/nodejs/src/custom_node.rs b/bindings/nodejs/src/custom_node.rs index 445f7641..481d5331 100644 --- a/bindings/nodejs/src/custom_node.rs +++ b/bindings/nodejs/src/custom_node.rs @@ -1,35 +1,22 @@ use napi::anyhow::anyhow; use napi::bindgen_prelude::Promise; -use napi::threadsafe_function::{ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction}; -use napi::{Env, JsFunction}; +use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction}; use zen_engine::handler::custom_node_adapter::{CustomNodeAdapter, CustomNodeRequest}; use zen_engine::handler::node::{NodeResponse, NodeResult}; use crate::types::{ZenEngineHandlerRequest, ZenEngineHandlerResponse}; +#[derive(Default)] pub(crate) struct CustomNode { function: Option>, } -impl Default for CustomNode { - fn default() -> Self { - Self { function: None } - } -} - impl CustomNode { - pub fn try_new(env: &mut Env, function: JsFunction) -> napi::Result { - let mut tsf = function.create_threadsafe_function( - 0, - |cx: ThreadSafeCallContext| Ok(vec![cx.value]), - )?; - - tsf.unref(env)?; - - Ok(Self { + pub fn new(tsf: ThreadsafeFunction) -> Self { + Self { function: Some(tsf), - }) + } } } diff --git a/bindings/nodejs/src/engine.rs b/bindings/nodejs/src/engine.rs index cc35012a..fbd49354 100644 --- a/bindings/nodejs/src/engine.rs +++ b/bindings/nodejs/src/engine.rs @@ -2,9 +2,10 @@ use crate::content::ZenDecisionContent; use crate::custom_node::CustomNode; use crate::decision::ZenDecision; use crate::loader::DecisionLoader; -use crate::types::ZenEngineResponse; +use crate::types::{ZenEngineHandlerRequest, ZenEngineResponse}; use napi::anyhow::{anyhow, Context}; use napi::bindgen_prelude::{Buffer, Either3}; +use napi::threadsafe_function::{ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction}; use napi::{tokio, Env, JsFunction, JsObject}; use napi_derive::napi; use serde_json::Value; @@ -15,6 +16,9 @@ use zen_engine::{DecisionEngine, EvaluationOptions}; #[napi] pub struct ZenEngine { graph: Arc>, + + loader_ref: Option>, + custom_handler_ref: Option>, } #[napi(object)] @@ -44,7 +48,7 @@ pub struct ZenEngineOptions { #[napi] impl ZenEngine { #[napi(constructor)] - pub fn new(mut env: Env, options: Option) -> napi::Result { + pub fn new(options: Option) -> napi::Result { let Some(opts) = options else { return Ok(Self { graph: DecisionEngine::new( @@ -52,21 +56,45 @@ impl ZenEngine { CustomNode::default().into(), ) .into(), + + loader_ref: None, + custom_handler_ref: None, }); }; - let loader = match opts.loader { - None => DecisionLoader::default(), - Some(loader_fn) => DecisionLoader::try_new(&mut env, loader_fn)?, + let loader_ref = match opts.loader { + None => None, + Some(l) => Some(l.create_threadsafe_function( + 0, + |cx: ThreadSafeCallContext| { + cx.env.create_string(cx.value.as_str()).map(|v| vec![v]) + }, + )?), }; - let custom_handler = match opts.custom_handler { + let loader = match &loader_ref { + None => DecisionLoader::default(), + Some(loader_fn) => DecisionLoader::new(loader_fn.clone())?, + }; + + let custom_handler_ref = match opts.custom_handler { + None => None, + Some(custom_handler_fn) => Some(custom_handler_fn.create_threadsafe_function( + 0, + |cx: ThreadSafeCallContext| Ok(vec![cx.value]), + )?), + }; + + let custom_handler = match &custom_handler_ref { None => CustomNode::default(), - Some(custom_fn) => CustomNode::try_new(&mut env, custom_fn)?, + Some(custom_fn) => CustomNode::new(custom_fn.clone()), }; Ok(Self { graph: DecisionEngine::new(loader.into(), custom_handler.into()).into(), + + loader_ref, + custom_handler_ref, }) } @@ -128,4 +156,17 @@ impl ZenEngine { Ok(ZenDecision::from(decision)) } + + /// Function used to dispose memory allocated for loaders + /// In the future, it will likely be removed and made automatic + #[napi] + pub fn dispose(&self) { + if let Some(loader) = self.loader_ref.clone() { + let _ = loader.abort(); + } + + if let Some(loader) = self.custom_handler_ref.clone() { + let _ = loader.abort(); + } + } } diff --git a/bindings/nodejs/src/loader.rs b/bindings/nodejs/src/loader.rs index 3e76ed70..d39875f5 100644 --- a/bindings/nodejs/src/loader.rs +++ b/bindings/nodejs/src/loader.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use async_trait::async_trait; use napi::anyhow::anyhow; use napi::bindgen_prelude::{Buffer, Promise}; -use napi::threadsafe_function::{ErrorStrategy, ThreadSafeCallContext, ThreadsafeFunction}; -use napi::{Either, Env, JsFunction}; +use napi::threadsafe_function::{ErrorStrategy, ThreadsafeFunction}; +use napi::Either; use zen_engine::loader::{DecisionLoader as DecisionLoaderTrait, LoaderError, LoaderResult}; use zen_engine::model::DecisionContent; @@ -17,14 +17,7 @@ pub(crate) struct DecisionLoader { } impl DecisionLoader { - pub fn try_new(env: &mut Env, function: JsFunction) -> napi::Result { - let mut tsf = - function.create_threadsafe_function(0, |cx: ThreadSafeCallContext| { - cx.env.create_string(cx.value.as_str()).map(|v| vec![v]) - })?; - - tsf.unref(env)?; - + pub fn new(tsf: ThreadsafeFunction) -> napi::Result { Ok(Self { function: Some(tsf), }) diff --git a/bindings/nodejs/test/decision.spec.ts b/bindings/nodejs/test/decision.spec.ts index 76ad4ce8..19292c1b 100644 --- a/bindings/nodejs/test/decision.spec.ts +++ b/bindings/nodejs/test/decision.spec.ts @@ -29,6 +29,8 @@ describe('ZenEngine', () => { expect(r1.result.output).toEqual(10); expect(r2.result.output).toEqual(0); expect(r3.result.output).toEqual(10); + + engine.dispose(); }, 10000); it('Evaluates decisions using getDecision', async () => { @@ -46,6 +48,8 @@ describe('ZenEngine', () => { expect(r1.result.output).toEqual(20); expect(r2.result.output).toEqual(0); expect(r3.result.output).toEqual(10); + + engine.dispose(); }, 10000); it('Creates a decision from contents', async () => { @@ -80,6 +84,8 @@ describe('ZenEngine', () => { const r = await engine.evaluate('custom.json', {a: 5}); expect(r.result.data).toEqual(25); + + engine.dispose(); }); it('Parses ZenDecisionContent', async () => {