ruvector_tiny_dancer_node/
lib.rs1#![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#[napi(object)]
25#[derive(Debug, Clone)]
26pub struct RouterConfig {
27 pub model_path: String,
29 pub confidence_threshold: Option<f64>,
31 pub max_uncertainty: Option<f64>,
33 pub enable_circuit_breaker: Option<bool>,
35 pub circuit_breaker_threshold: Option<u32>,
37 pub enable_quantization: Option<bool>,
39 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#[napi(object)]
59#[derive(Clone)]
60pub struct Candidate {
61 pub id: String,
63 pub embedding: Float32Array,
65 pub metadata: Option<String>,
67 pub created_at: Option<i64>,
69 pub access_count: Option<u32>,
71 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#[napi(object)]
100pub struct RoutingRequest {
101 pub query_embedding: Float32Array,
103 pub candidates: Vec<Candidate>,
105 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#[napi(object)]
133#[derive(Debug, Clone)]
134pub struct RoutingDecision {
135 pub candidate_id: String,
137 pub confidence: f64,
139 pub use_lightweight: bool,
141 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#[napi(object)]
158#[derive(Debug, Clone)]
159pub struct RoutingResponse {
160 pub decisions: Vec<RoutingDecision>,
162 pub inference_time_us: u32,
164 pub candidates_processed: u32,
166 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#[napi]
183pub struct Router {
184 inner: Arc<RwLock<CoreRouter>>,
185}
186
187#[napi]
188impl Router {
189 #[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 #[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 #[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 #[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#[napi]
278pub fn version() -> String {
279 env!("CARGO_PKG_VERSION").to_string()
280}
281
282#[napi]
284pub fn hello() -> String {
285 "Hello from Tiny Dancer Node.js bindings!".to_string()
286}