ruvector_tiny_dancer_node/
lib.rs

1//! Node.js bindings for Tiny Dancer neural routing via NAPI-RS
2//!
3//! High-performance Rust neural routing with zero-copy buffer sharing,
4//! async/await support, and complete TypeScript type definitions.
5
6#![deny(clippy::all)]
7#![warn(clippy::pedantic)]
8
9use napi::bindgen_prelude::*;
10use napi_derive::napi;
11use parking_lot::RwLock;
12use ruvector_tiny_dancer_core::{
13    types::{
14        Candidate as CoreCandidate, RouterConfig as CoreRouterConfig,
15        RoutingDecision as CoreRoutingDecision, RoutingRequest as CoreRoutingRequest,
16        RoutingResponse as CoreRoutingResponse,
17    },
18    Router as CoreRouter,
19};
20use std::collections::HashMap;
21use std::sync::Arc;
22
23/// Router configuration
24#[napi(object)]
25#[derive(Debug, Clone)]
26pub struct RouterConfig {
27    /// Model path
28    pub model_path: String,
29    /// Confidence threshold (0.0 to 1.0)
30    pub confidence_threshold: Option<f64>,
31    /// Maximum uncertainty (0.0 to 1.0)
32    pub max_uncertainty: Option<f64>,
33    /// Enable circuit breaker
34    pub enable_circuit_breaker: Option<bool>,
35    /// Circuit breaker threshold
36    pub circuit_breaker_threshold: Option<u32>,
37    /// Enable quantization
38    pub enable_quantization: Option<bool>,
39    /// Database path
40    pub database_path: Option<String>,
41}
42
43impl From<RouterConfig> for CoreRouterConfig {
44    fn from(config: RouterConfig) -> Self {
45        CoreRouterConfig {
46            model_path: config.model_path,
47            confidence_threshold: config.confidence_threshold.unwrap_or(0.85) as f32,
48            max_uncertainty: config.max_uncertainty.unwrap_or(0.15) as f32,
49            enable_circuit_breaker: config.enable_circuit_breaker.unwrap_or(true),
50            circuit_breaker_threshold: config.circuit_breaker_threshold.unwrap_or(5),
51            enable_quantization: config.enable_quantization.unwrap_or(true),
52            database_path: config.database_path,
53        }
54    }
55}
56
57/// Candidate for routing
58#[napi(object)]
59#[derive(Clone)]
60pub struct Candidate {
61    /// Candidate ID
62    pub id: String,
63    /// Embedding vector
64    pub embedding: Float32Array,
65    /// Metadata (JSON string)
66    pub metadata: Option<String>,
67    /// Creation timestamp
68    pub created_at: Option<i64>,
69    /// Access count
70    pub access_count: Option<u32>,
71    /// Success rate (0.0 to 1.0)
72    pub success_rate: Option<f64>,
73}
74
75impl Candidate {
76    fn to_core(&self) -> Result<CoreCandidate> {
77        let metadata: HashMap<String, serde_json::Value> = if let Some(ref meta_str) = self.metadata
78        {
79            serde_json::from_str(meta_str)
80                .map_err(|e| Error::from_reason(format!("Invalid metadata JSON: {}", e)))?
81        } else {
82            HashMap::new()
83        };
84
85        Ok(CoreCandidate {
86            id: self.id.clone(),
87            embedding: self.embedding.to_vec(),
88            metadata,
89            created_at: self
90                .created_at
91                .unwrap_or_else(|| chrono::Utc::now().timestamp()),
92            access_count: self.access_count.unwrap_or(0) as u64,
93            success_rate: self.success_rate.unwrap_or(0.0) as f32,
94        })
95    }
96}
97
98/// Routing request
99#[napi(object)]
100pub struct RoutingRequest {
101    /// Query embedding
102    pub query_embedding: Float32Array,
103    /// Candidates to score
104    pub candidates: Vec<Candidate>,
105    /// Optional metadata (JSON string)
106    pub metadata: Option<String>,
107}
108
109impl RoutingRequest {
110    fn to_core(&self) -> Result<CoreRoutingRequest> {
111        let candidates: Result<Vec<CoreCandidate>> =
112            self.candidates.iter().map(|c| c.to_core()).collect();
113
114        let metadata = if let Some(ref meta_str) = self.metadata {
115            Some(
116                serde_json::from_str(meta_str)
117                    .map_err(|e| Error::from_reason(format!("Invalid metadata JSON: {}", e)))?,
118            )
119        } else {
120            None
121        };
122
123        Ok(CoreRoutingRequest {
124            query_embedding: self.query_embedding.to_vec(),
125            candidates: candidates?,
126            metadata,
127        })
128    }
129}
130
131/// Routing decision
132#[napi(object)]
133#[derive(Debug, Clone)]
134pub struct RoutingDecision {
135    /// Candidate ID
136    pub candidate_id: String,
137    /// Confidence score (0.0 to 1.0)
138    pub confidence: f64,
139    /// Whether to use lightweight model
140    pub use_lightweight: bool,
141    /// Uncertainty estimate (0.0 to 1.0)
142    pub uncertainty: f64,
143}
144
145impl From<CoreRoutingDecision> for RoutingDecision {
146    fn from(decision: CoreRoutingDecision) -> Self {
147        Self {
148            candidate_id: decision.candidate_id,
149            confidence: decision.confidence as f64,
150            use_lightweight: decision.use_lightweight,
151            uncertainty: decision.uncertainty as f64,
152        }
153    }
154}
155
156/// Routing response
157#[napi(object)]
158#[derive(Debug, Clone)]
159pub struct RoutingResponse {
160    /// Routing decisions
161    pub decisions: Vec<RoutingDecision>,
162    /// Total inference time in microseconds
163    pub inference_time_us: u32,
164    /// Number of candidates processed
165    pub candidates_processed: u32,
166    /// Feature engineering time in microseconds
167    pub feature_time_us: u32,
168}
169
170impl From<CoreRoutingResponse> for RoutingResponse {
171    fn from(response: CoreRoutingResponse) -> Self {
172        Self {
173            decisions: response.decisions.into_iter().map(Into::into).collect(),
174            inference_time_us: response.inference_time_us as u32,
175            candidates_processed: response.candidates_processed as u32,
176            feature_time_us: response.feature_time_us as u32,
177        }
178    }
179}
180
181/// Tiny Dancer neural router
182#[napi]
183pub struct Router {
184    inner: Arc<RwLock<CoreRouter>>,
185}
186
187#[napi]
188impl Router {
189    /// Create a new router with configuration
190    ///
191    /// # Example
192    /// ```javascript
193    /// const router = new Router({
194    ///   modelPath: './models/fastgrnn.safetensors',
195    ///   confidenceThreshold: 0.85,
196    ///   maxUncertainty: 0.15,
197    ///   enableCircuitBreaker: true
198    /// });
199    /// ```
200    #[napi(constructor)]
201    pub fn new(config: RouterConfig) -> Result<Self> {
202        let core_config: CoreRouterConfig = config.into();
203        let router = CoreRouter::new(core_config)
204            .map_err(|e| Error::from_reason(format!("Failed to create router: {}", e)))?;
205
206        Ok(Self {
207            inner: Arc::new(RwLock::new(router)),
208        })
209    }
210
211    /// Route a request through the neural routing system
212    ///
213    /// Returns routing decisions with confidence scores and model recommendations
214    ///
215    /// # Example
216    /// ```javascript
217    /// const response = await router.route({
218    ///   queryEmbedding: new Float32Array([0.1, 0.2, ...]),
219    ///   candidates: [
220    ///     { id: '1', embedding: new Float32Array([...]) },
221    ///     { id: '2', embedding: new Float32Array([...]) }
222    ///   ]
223    /// });
224    /// console.log('Top decision:', response.decisions[0]);
225    /// console.log('Inference time:', response.inferenceTimeUs, 'μs');
226    /// ```
227    #[napi]
228    pub async fn route(&self, request: RoutingRequest) -> Result<RoutingResponse> {
229        let core_request = request.to_core()?;
230        let router = self.inner.clone();
231
232        tokio::task::spawn_blocking(move || {
233            let router = router.read();
234            router.route(core_request)
235        })
236        .await
237        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
238        .map_err(|e| Error::from_reason(format!("Routing failed: {}", e)))
239        .map(Into::into)
240    }
241
242    /// Reload the model from disk (hot-reload)
243    ///
244    /// # Example
245    /// ```javascript
246    /// await router.reloadModel();
247    /// ```
248    #[napi]
249    pub async fn reload_model(&self) -> Result<()> {
250        let router = self.inner.clone();
251
252        tokio::task::spawn_blocking(move || {
253            let router = router.read();
254            router.reload_model()
255        })
256        .await
257        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
258        .map_err(|e| Error::from_reason(format!("Model reload failed: {}", e)))
259    }
260
261    /// Check circuit breaker status
262    ///
263    /// Returns true if the circuit is closed (healthy), false if open (unhealthy)
264    ///
265    /// # Example
266    /// ```javascript
267    /// const isHealthy = router.circuitBreakerStatus();
268    /// ```
269    #[napi]
270    pub fn circuit_breaker_status(&self) -> Option<bool> {
271        let router = self.inner.read();
272        router.circuit_breaker_status()
273    }
274}
275
276/// Get the version of the Tiny Dancer library
277#[napi]
278pub fn version() -> String {
279    env!("CARGO_PKG_VERSION").to_string()
280}
281
282/// Hello function for testing bindings
283#[napi]
284pub fn hello() -> String {
285    "Hello from Tiny Dancer Node.js bindings!".to_string()
286}