From ac329e3fa8e97c675000b998d452250537ce88b7 Mon Sep 17 00:00:00 2001 From: Stefan Date: Fri, 21 Nov 2025 01:39:20 +0100 Subject: [PATCH] fix: nodejs add dispose method --- bindings/nodejs/Cargo.toml | 1 + bindings/nodejs/index.d.ts | 7 ++-- bindings/nodejs/src/custom_node.rs | 2 +- bindings/nodejs/src/decision.rs | 4 +- bindings/nodejs/src/dispose.rs | 24 +++++++++++ bindings/nodejs/src/engine.rs | 65 ++++++++++++++++++++++++----- bindings/nodejs/src/http_handler.rs | 2 +- bindings/nodejs/src/lib.rs | 1 + bindings/nodejs/src/loader.rs | 2 +- 9 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 bindings/nodejs/src/dispose.rs diff --git a/bindings/nodejs/Cargo.toml b/bindings/nodejs/Cargo.toml index d83f9060..f7488dd9 100644 --- a/bindings/nodejs/Cargo.toml +++ b/bindings/nodejs/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] [dependencies] ahash = { workspace = true } napi = { version = "3", features = ["serde-json", "error_anyhow", "tokio_rt"] } +napi-sys = "3" napi-derive = "3" tokio-util = { workspace = true, features = ["rt"] } serde_json = { workspace = true } diff --git a/bindings/nodejs/index.d.ts b/bindings/nodejs/index.d.ts index dc5b2519..591b8349 100644 --- a/bindings/nodejs/index.d.ts +++ b/bindings/nodejs/index.d.ts @@ -3,7 +3,7 @@ export declare class ZenDecision { constructor() evaluate(context: any, opts?: ZenEvaluateOptions | undefined | null): Promise - safeEvaluate(context: any, opts?: ZenEvaluateOptions | undefined | null): Promise> + safeEvaluate(context: any, opts?: ZenEvaluateOptions | undefined | null): { success: true, data: ZenEngineResponse } | { success: false; error: any; } validate(): void } @@ -17,8 +17,9 @@ export declare class ZenEngine { evaluate(key: string, context: any, opts?: ZenEvaluateOptions | undefined | null): Promise createDecision(content: ZenDecisionContent | Buffer | object): ZenDecision getDecision(key: string): Promise - safeEvaluate(key: string, context: any, opts?: ZenEvaluateOptions | undefined | null): Promise> - safeGetDecision(key: string): Promise> + safeEvaluate(key: string, context: any, opts?: ZenEvaluateOptions | undefined | null): { success: true, data: ZenEngineResponse } | { success: false; error: any; } + safeGetDecision(key: string): { success: true, data: ZenDecision } | { success: false; error: any; } + dispose(): void } export declare class ZenEngineHandlerRequest { diff --git a/bindings/nodejs/src/custom_node.rs b/bindings/nodejs/src/custom_node.rs index 09509ac7..4b295f12 100644 --- a/bindings/nodejs/src/custom_node.rs +++ b/bindings/nodejs/src/custom_node.rs @@ -10,7 +10,7 @@ use zen_engine::nodes::custom::{CustomNodeAdapter, CustomNodeRequest}; use zen_engine::nodes::{NodeError, NodeResponse, NodeResult}; use zen_engine::Variable; -type CustomNodeTsfn = Arc< +pub(crate) type CustomNodeTsfn = Arc< ThreadsafeFunction< ZenEngineHandlerRequest, Promise, diff --git a/bindings/nodejs/src/decision.rs b/bindings/nodejs/src/decision.rs index 74eb8023..6ca3839c 100644 --- a/bindings/nodejs/src/decision.rs +++ b/bindings/nodejs/src/decision.rs @@ -46,7 +46,9 @@ impl ZenDecision { Ok(result) } - #[napi(ts_return_type = "Promise>")] + #[napi( + ts_return_type = "{ success: true, data: ZenEngineResponse } | { success: false; error: any; }" + )] pub async fn safe_evaluate( &self, context: Value, diff --git a/bindings/nodejs/src/dispose.rs b/bindings/nodejs/src/dispose.rs new file mode 100644 index 00000000..e9d34d40 --- /dev/null +++ b/bindings/nodejs/src/dispose.rs @@ -0,0 +1,24 @@ +use napi::check_status; +use napi::threadsafe_function::ThreadsafeFunctionHandle; + +pub trait DisposeThreadsafeHandler { + fn dispose(&self) -> napi::Result<()>; +} + +impl DisposeThreadsafeHandler for ThreadsafeFunctionHandle { + fn dispose(&self) -> napi::Result<()> { + self.with_write_aborted(|mut aborted_guard| { + if !*aborted_guard { + check_status!(unsafe { + napi_sys::napi_release_threadsafe_function( + self.get_raw(), + napi_sys::ThreadsafeFunctionReleaseMode::abort, + ) + })?; + *aborted_guard = true; + } + + Ok(()) + }) + } +} diff --git a/bindings/nodejs/src/engine.rs b/bindings/nodejs/src/engine.rs index c3dfbcd7..4665ca8b 100644 --- a/bindings/nodejs/src/engine.rs +++ b/bindings/nodejs/src/engine.rs @@ -10,21 +10,27 @@ use napi::{Env, JsValue, Unknown, ValueType}; use napi_derive::napi; use serde_json::Value; -use zen_engine::model::DecisionContent; -use zen_engine::{DecisionEngine, EvaluationSerializedOptions, EvaluationTraceKind}; - use crate::content::ZenDecisionContent; -use crate::custom_node::CustomNode; +use crate::custom_node::{CustomNode, CustomNodeTsfn}; use crate::decision::ZenDecision; -use crate::http_handler::{NodeHttpHandler, ZenHttpHandlerRequest, ZenHttpHandlerResponse}; -use crate::loader::DecisionLoader; +use crate::dispose::DisposeThreadsafeHandler; +use crate::http_handler::{ + HttpHandlerTsfn, NodeHttpHandler, ZenHttpHandlerRequest, ZenHttpHandlerResponse, +}; +use crate::loader::{DecisionLoader, LoaderTsfn}; use crate::mt::spawn_worker; use crate::safe_result::SafeResult; use crate::types::{ZenEngineHandlerRequest, ZenEngineHandlerResponse}; +use zen_engine::model::DecisionContent; +use zen_engine::{DecisionEngine, EvaluationSerializedOptions, EvaluationTraceKind}; #[napi] pub struct ZenEngine { graph: Arc, + + custom_node_tsfn: Option, + loader_tsfn: Option, + http_handler_tsfn: Option, } #[derive(Debug, Default)] @@ -124,9 +130,17 @@ impl ZenEngine { Arc::new(CustomNode::default()), ) .into(), + + loader_tsfn: None, + http_handler_tsfn: None, + custom_node_tsfn: None, }); }; + let mut loader_tsfn_opt: Option = None; + let mut http_handler_tsfn_opt: Option = None; + let mut custom_node_tsfn_opt: Option = None; + let loader = match opts.loader { None => DecisionLoader::default(), Some(l) => { @@ -137,7 +151,9 @@ impl ZenEngine { .weak() .build()?; - DecisionLoader::new(Arc::new(loader_tsfn)) + let arc_loader_tsfn = Arc::new(loader_tsfn); + loader_tsfn_opt = Some(arc_loader_tsfn.clone()); + DecisionLoader::new(arc_loader_tsfn) } }; @@ -151,7 +167,9 @@ impl ZenEngine { .weak() .build()?; - CustomNode::new(Arc::new(custom_tfsn)) + let arc_custom_node = Arc::new(custom_tfsn); + custom_node_tsfn_opt = Some(arc_custom_node.clone()); + CustomNode::new(arc_custom_node) } }; @@ -164,12 +182,18 @@ impl ZenEngine { .weak() .build()?; + let arc_http_handler_tsfn = Arc::new(http_tsfn); + http_handler_tsfn_opt = Some(arc_http_handler_tsfn.clone()); decision_engine = decision_engine - .with_http_handler(Some(Arc::new(NodeHttpHandler::new(Arc::new(http_tsfn))))); + .with_http_handler(Some(Arc::new(NodeHttpHandler::new(arc_http_handler_tsfn)))); } Ok(Self { graph: Arc::new(decision_engine), + + loader_tsfn: loader_tsfn_opt, + http_handler_tsfn: http_handler_tsfn_opt, + custom_node_tsfn: custom_node_tsfn_opt, }) } @@ -228,7 +252,9 @@ impl ZenEngine { Ok(ZenDecision::from(decision)) } - #[napi(ts_return_type = "Promise>")] + #[napi( + ts_return_type = "{ success: true, data: ZenEngineResponse } | { success: false; error: any; }" + )] pub async fn safe_evaluate( &self, key: String, @@ -238,8 +264,25 @@ impl ZenEngine { self.evaluate(key, context, opts).await.into() } - #[napi(ts_return_type = "Promise>")] + #[napi( + ts_return_type = "{ success: true, data: ZenDecision } | { success: false; error: any; }" + )] pub async fn safe_get_decision(&self, key: String) -> SafeResult { self.get_decision(key).await.into() } + + #[napi] + pub fn dispose(&self) { + if let Some(loader_tsfn) = &self.loader_tsfn { + let _ = loader_tsfn.handle.dispose(); + } + + if let Some(http_handler_tsfn) = &self.http_handler_tsfn { + let _ = http_handler_tsfn.handle.dispose(); + } + + if let Some(custom_node) = &self.custom_node_tsfn { + let _ = custom_node.handle.dispose(); + } + } } diff --git a/bindings/nodejs/src/http_handler.rs b/bindings/nodejs/src/http_handler.rs index c00031c7..26f25a62 100644 --- a/bindings/nodejs/src/http_handler.rs +++ b/bindings/nodejs/src/http_handler.rs @@ -14,7 +14,7 @@ use zen_engine::nodes::http_handler::{ HttpHandlerResponse as EngineHttpHandlerResponse, }; -type HttpHandlerTsfn = Arc< +pub(crate) type HttpHandlerTsfn = Arc< ThreadsafeFunction< ZenHttpHandlerRequest, Promise, diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs index 2523ccd3..b854cd09 100644 --- a/bindings/nodejs/src/lib.rs +++ b/bindings/nodejs/src/lib.rs @@ -2,6 +2,7 @@ mod config; mod content; mod custom_node; mod decision; +mod dispose; mod engine; mod expression; mod http_handler; diff --git a/bindings/nodejs/src/loader.rs b/bindings/nodejs/src/loader.rs index 1230a0dd..559249a6 100644 --- a/bindings/nodejs/src/loader.rs +++ b/bindings/nodejs/src/loader.rs @@ -15,7 +15,7 @@ use zen_engine::model::DecisionContent; use crate::content::ZenDecisionContent; -type LoaderTsfn = Arc< +pub(crate) type LoaderTsfn = Arc< ThreadsafeFunction< String, Promise>>,