ruvector_tiny_dancer_wasm/
lib.rs

1//! WASM bindings for Tiny Dancer neural routing
2
3use ruvector_tiny_dancer_core::{
4    types::{
5        Candidate as CoreCandidate, RouterConfig as CoreRouterConfig,
6        RoutingRequest as CoreRoutingRequest, RoutingResponse as CoreRoutingResponse,
7    },
8    Router as CoreRouter,
9};
10use std::collections::HashMap;
11use wasm_bindgen::prelude::*;
12
13/// Initialize panic hook for better error messages in WASM
14#[wasm_bindgen(start)]
15pub fn init() {
16    #[cfg(feature = "console_error_panic_hook")]
17    console_error_panic_hook::set_once();
18}
19
20/// Router configuration for WASM
21#[wasm_bindgen]
22#[derive(Clone)]
23pub struct RouterConfig {
24    model_path: String,
25    confidence_threshold: f32,
26    max_uncertainty: f32,
27    enable_circuit_breaker: bool,
28    circuit_breaker_threshold: u32,
29    enable_quantization: bool,
30}
31
32#[wasm_bindgen]
33impl RouterConfig {
34    #[wasm_bindgen(constructor)]
35    pub fn new() -> Self {
36        Self {
37            model_path: "./models/fastgrnn.safetensors".to_string(),
38            confidence_threshold: 0.85,
39            max_uncertainty: 0.15,
40            enable_circuit_breaker: true,
41            circuit_breaker_threshold: 5,
42            enable_quantization: true,
43        }
44    }
45
46    #[wasm_bindgen(setter)]
47    pub fn set_model_path(&mut self, path: String) {
48        self.model_path = path;
49    }
50
51    #[wasm_bindgen(setter)]
52    pub fn set_confidence_threshold(&mut self, threshold: f32) {
53        self.confidence_threshold = threshold;
54    }
55
56    #[wasm_bindgen(setter)]
57    pub fn set_max_uncertainty(&mut self, uncertainty: f32) {
58        self.max_uncertainty = uncertainty;
59    }
60}
61
62impl From<RouterConfig> for CoreRouterConfig {
63    fn from(config: RouterConfig) -> Self {
64        CoreRouterConfig {
65            model_path: config.model_path,
66            confidence_threshold: config.confidence_threshold,
67            max_uncertainty: config.max_uncertainty,
68            enable_circuit_breaker: config.enable_circuit_breaker,
69            circuit_breaker_threshold: config.circuit_breaker_threshold,
70            enable_quantization: config.enable_quantization,
71            database_path: None,
72        }
73    }
74}
75
76/// Candidate for routing
77#[wasm_bindgen]
78pub struct Candidate {
79    id: String,
80    embedding: Vec<f32>,
81    metadata: String,
82    created_at: i64,
83    access_count: u64,
84    success_rate: f32,
85}
86
87#[wasm_bindgen]
88impl Candidate {
89    #[wasm_bindgen(constructor)]
90    pub fn new(
91        id: String,
92        embedding: Vec<f32>,
93        metadata: String,
94        created_at: i64,
95        access_count: u64,
96        success_rate: f32,
97    ) -> Self {
98        Self {
99            id,
100            embedding,
101            metadata,
102            created_at,
103            access_count,
104            success_rate,
105        }
106    }
107}
108
109impl TryFrom<Candidate> for CoreCandidate {
110    type Error = JsValue;
111
112    fn try_from(candidate: Candidate) -> Result<Self, Self::Error> {
113        let metadata: HashMap<String, serde_json::Value> =
114            serde_json::from_str(&candidate.metadata)
115                .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
116
117        Ok(CoreCandidate {
118            id: candidate.id,
119            embedding: candidate.embedding,
120            metadata,
121            created_at: candidate.created_at,
122            access_count: candidate.access_count,
123            success_rate: candidate.success_rate,
124        })
125    }
126}
127
128/// Routing request
129#[wasm_bindgen]
130pub struct RoutingRequest {
131    query_embedding: Vec<f32>,
132    candidates: Vec<Candidate>,
133    metadata: Option<String>,
134}
135
136#[wasm_bindgen]
137impl RoutingRequest {
138    #[wasm_bindgen(constructor)]
139    pub fn new(query_embedding: Vec<f32>, candidates: Vec<Candidate>) -> Self {
140        Self {
141            query_embedding,
142            candidates,
143            metadata: None,
144        }
145    }
146
147    #[wasm_bindgen(setter)]
148    pub fn set_metadata(&mut self, metadata: String) {
149        self.metadata = Some(metadata);
150    }
151}
152
153impl TryFrom<RoutingRequest> for CoreRoutingRequest {
154    type Error = JsValue;
155
156    fn try_from(request: RoutingRequest) -> Result<Self, Self::Error> {
157        let candidates: Result<Vec<CoreCandidate>, JsValue> = request
158            .candidates
159            .into_iter()
160            .map(|c| c.try_into())
161            .collect();
162
163        let metadata = if let Some(meta_str) = request.metadata {
164            Some(
165                serde_json::from_str(&meta_str)
166                    .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?,
167            )
168        } else {
169            None
170        };
171
172        Ok(CoreRoutingRequest {
173            query_embedding: request.query_embedding,
174            candidates: candidates?,
175            metadata,
176        })
177    }
178}
179
180/// Routing response
181#[wasm_bindgen]
182pub struct RoutingResponse {
183    decisions_json: String,
184    inference_time_us: u64,
185    candidates_processed: usize,
186    feature_time_us: u64,
187}
188
189#[wasm_bindgen]
190impl RoutingResponse {
191    #[wasm_bindgen(getter)]
192    pub fn decisions_json(&self) -> String {
193        self.decisions_json.clone()
194    }
195
196    #[wasm_bindgen(getter)]
197    pub fn inference_time_us(&self) -> u64 {
198        self.inference_time_us
199    }
200
201    #[wasm_bindgen(getter)]
202    pub fn candidates_processed(&self) -> usize {
203        self.candidates_processed
204    }
205
206    #[wasm_bindgen(getter)]
207    pub fn feature_time_us(&self) -> u64 {
208        self.feature_time_us
209    }
210}
211
212impl From<CoreRoutingResponse> for RoutingResponse {
213    fn from(response: CoreRoutingResponse) -> Self {
214        let decisions_json = serde_json::to_string(&response.decisions).unwrap_or_default();
215
216        Self {
217            decisions_json,
218            inference_time_us: response.inference_time_us,
219            candidates_processed: response.candidates_processed,
220            feature_time_us: response.feature_time_us,
221        }
222    }
223}
224
225/// Tiny Dancer router for WASM
226#[wasm_bindgen]
227pub struct Router {
228    inner: CoreRouter,
229}
230
231#[wasm_bindgen]
232impl Router {
233    /// Create a new router with configuration
234    #[wasm_bindgen(constructor)]
235    pub fn new(config: RouterConfig) -> Result<Router, JsValue> {
236        let core_config: CoreRouterConfig = config.into();
237        let router = CoreRouter::new(core_config)
238            .map_err(|e| JsValue::from_str(&format!("Failed to create router: {}", e)))?;
239
240        Ok(Router { inner: router })
241    }
242
243    /// Route a request
244    pub fn route(&self, request: RoutingRequest) -> Result<RoutingResponse, JsValue> {
245        let core_request: CoreRoutingRequest = request.try_into()?;
246        let core_response = self
247            .inner
248            .route(core_request)
249            .map_err(|e| JsValue::from_str(&format!("Routing failed: {}", e)))?;
250
251        Ok(core_response.into())
252    }
253
254    /// Check circuit breaker status
255    pub fn circuit_breaker_status(&self) -> Option<bool> {
256        self.inner.circuit_breaker_status()
257    }
258}
259
260/// Get library version
261#[wasm_bindgen]
262pub fn version() -> String {
263    env!("CARGO_PKG_VERSION").to_string()
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_version() {
272        assert!(!version().is_empty());
273    }
274}