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 ruvector_tiny_dancer_core::{
12    Router as CoreRouter,
13    types::{
14        RouterConfig as CoreRouterConfig,
15        RoutingRequest as CoreRoutingRequest,
16        RoutingResponse as CoreRoutingResponse,
17        RoutingDecision as CoreRoutingDecision,
18        Candidate as CoreCandidate,
19    },
20};
21use std::collections::HashMap;
22use std::sync::Arc;
23use parking_lot::RwLock;
24
25/// Router configuration
26#[napi(object)]
27#[derive(Debug, Clone)]
28pub struct RouterConfig {
29    /// Model path
30    pub model_path: String,
31    /// Confidence threshold (0.0 to 1.0)
32    pub confidence_threshold: Option<f64>,
33    /// Maximum uncertainty (0.0 to 1.0)
34    pub max_uncertainty: Option<f64>,
35    /// Enable circuit breaker
36    pub enable_circuit_breaker: Option<bool>,
37    /// Circuit breaker threshold
38    pub circuit_breaker_threshold: Option<u32>,
39    /// Enable quantization
40    pub enable_quantization: Option<bool>,
41    /// Database path
42    pub database_path: Option<String>,
43}
44
45impl From<RouterConfig> for CoreRouterConfig {
46    fn from(config: RouterConfig) -> Self {
47        CoreRouterConfig {
48            model_path: config.model_path,
49            confidence_threshold: config.confidence_threshold.unwrap_or(0.85) as f32,
50            max_uncertainty: config.max_uncertainty.unwrap_or(0.15) as f32,
51            enable_circuit_breaker: config.enable_circuit_breaker.unwrap_or(true),
52            circuit_breaker_threshold: config.circuit_breaker_threshold.unwrap_or(5),
53            enable_quantization: config.enable_quantization.unwrap_or(true),
54            database_path: config.database_path,
55        }
56    }
57}
58
59/// Candidate for routing
60#[napi(object)]
61#[derive(Clone)]
62pub struct Candidate {
63    /// Candidate ID
64    pub id: String,
65    /// Embedding vector
66    pub embedding: Float32Array,
67    /// Metadata (JSON string)
68    pub metadata: Option<String>,
69    /// Creation timestamp
70    pub created_at: Option<i64>,
71    /// Access count
72    pub access_count: Option<u32>,
73    /// Success rate (0.0 to 1.0)
74    pub success_rate: Option<f64>,
75}
76
77impl Candidate {
78    fn to_core(&self) -> Result<CoreCandidate> {
79        let metadata: HashMap<String, serde_json::Value> = if let Some(ref meta_str) = self.metadata {
80            serde_json::from_str(meta_str)
81                .map_err(|e| Error::from_reason(format!("Invalid metadata JSON: {}", e)))?
82        } else {
83            HashMap::new()
84        };
85
86        Ok(CoreCandidate {
87            id: self.id.clone(),
88            embedding: self.embedding.to_vec(),
89            metadata,
90            created_at: self.created_at.unwrap_or_else(|| chrono::Utc::now().timestamp()),
91            access_count: self.access_count.unwrap_or(0) as u64,
92            success_rate: self.success_rate.unwrap_or(0.0) as f32,
93        })
94    }
95}
96
97/// Routing request
98#[napi(object)]
99pub struct RoutingRequest {
100    /// Query embedding
101    pub query_embedding: Float32Array,
102    /// Candidates to score
103    pub candidates: Vec<Candidate>,
104    /// Optional metadata (JSON string)
105    pub metadata: Option<String>,
106}
107
108impl RoutingRequest {
109    fn to_core(&self) -> Result<CoreRoutingRequest> {
110        let candidates: Result<Vec<CoreCandidate>> = self
111            .candidates
112            .iter()
113            .map(|c| c.to_core())
114            .collect();
115
116        let metadata = if let Some(ref meta_str) = self.metadata {
117            Some(serde_json::from_str(meta_str)
118                .map_err(|e| Error::from_reason(format!("Invalid metadata JSON: {}", e)))?)
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}