ruvector_tiny_dancer_wasm/
lib.rs

1//! WASM bindings for Tiny Dancer neural routing
2
3use wasm_bindgen::prelude::*;
4use ruvector_tiny_dancer_core::{
5    Router as CoreRouter,
6    types::{
7        RouterConfig as CoreRouterConfig,
8        RoutingRequest as CoreRoutingRequest,
9        RoutingResponse as CoreRoutingResponse,
10        Candidate as CoreCandidate,
11    },
12};
13use std::collections::HashMap;
14
15/// Initialize panic hook for better error messages in WASM
16#[wasm_bindgen(start)]
17pub fn init() {
18    #[cfg(feature = "console_error_panic_hook")]
19    console_error_panic_hook::set_once();
20}
21
22/// Router configuration for WASM
23#[wasm_bindgen]
24#[derive(Clone)]
25pub struct RouterConfig {
26    model_path: String,
27    confidence_threshold: f32,
28    max_uncertainty: f32,
29    enable_circuit_breaker: bool,
30    circuit_breaker_threshold: u32,
31    enable_quantization: bool,
32}
33
34#[wasm_bindgen]
35impl RouterConfig {
36    #[wasm_bindgen(constructor)]
37    pub fn new() -> Self {
38        Self {
39            model_path: "./models/fastgrnn.safetensors".to_string(),
40            confidence_threshold: 0.85,
41            max_uncertainty: 0.15,
42            enable_circuit_breaker: true,
43            circuit_breaker_threshold: 5,
44            enable_quantization: true,
45        }
46    }
47
48    #[wasm_bindgen(setter)]
49    pub fn set_model_path(&mut self, path: String) {
50        self.model_path = path;
51    }
52
53    #[wasm_bindgen(setter)]
54    pub fn set_confidence_threshold(&mut self, threshold: f32) {
55        self.confidence_threshold = threshold;
56    }
57
58    #[wasm_bindgen(setter)]
59    pub fn set_max_uncertainty(&mut self, uncertainty: f32) {
60        self.max_uncertainty = uncertainty;
61    }
62}
63
64impl From<RouterConfig> for CoreRouterConfig {
65    fn from(config: RouterConfig) -> Self {
66        CoreRouterConfig {
67            model_path: config.model_path,
68            confidence_threshold: config.confidence_threshold,
69            max_uncertainty: config.max_uncertainty,
70            enable_circuit_breaker: config.enable_circuit_breaker,
71            circuit_breaker_threshold: config.circuit_breaker_threshold,
72            enable_quantization: config.enable_quantization,
73            database_path: None,
74        }
75    }
76}
77
78/// Candidate for routing
79#[wasm_bindgen]
80pub struct Candidate {
81    id: String,
82    embedding: Vec<f32>,
83    metadata: String,
84    created_at: i64,
85    access_count: u64,
86    success_rate: f32,
87}
88
89#[wasm_bindgen]
90impl Candidate {
91    #[wasm_bindgen(constructor)]
92    pub fn new(
93        id: String,
94        embedding: Vec<f32>,
95        metadata: String,
96        created_at: i64,
97        access_count: u64,
98        success_rate: f32,
99    ) -> Self {
100        Self {
101            id,
102            embedding,
103            metadata,
104            created_at,
105            access_count,
106            success_rate,
107        }
108    }
109}
110
111impl TryFrom<Candidate> for CoreCandidate {
112    type Error = JsValue;
113
114    fn try_from(candidate: Candidate) -> Result<Self, Self::Error> {
115        let metadata: HashMap<String, serde_json::Value> =
116            serde_json::from_str(&candidate.metadata)
117                .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
118
119        Ok(CoreCandidate {
120            id: candidate.id,
121            embedding: candidate.embedding,
122            metadata,
123            created_at: candidate.created_at,
124            access_count: candidate.access_count,
125            success_rate: candidate.success_rate,
126        })
127    }
128}
129
130/// Routing request
131#[wasm_bindgen]
132pub struct RoutingRequest {
133    query_embedding: Vec<f32>,
134    candidates: Vec<Candidate>,
135    metadata: Option<String>,
136}
137
138#[wasm_bindgen]
139impl RoutingRequest {
140    #[wasm_bindgen(constructor)]
141    pub fn new(query_embedding: Vec<f32>, candidates: Vec<Candidate>) -> Self {
142        Self {
143            query_embedding,
144            candidates,
145            metadata: None,
146        }
147    }
148
149    #[wasm_bindgen(setter)]
150    pub fn set_metadata(&mut self, metadata: String) {
151        self.metadata = Some(metadata);
152    }
153}
154
155impl TryFrom<RoutingRequest> for CoreRoutingRequest {
156    type Error = JsValue;
157
158    fn try_from(request: RoutingRequest) -> Result<Self, Self::Error> {
159        let candidates: Result<Vec<CoreCandidate>, JsValue> = request
160            .candidates
161            .into_iter()
162            .map(|c| c.try_into())
163            .collect();
164
165        let metadata = if let Some(meta_str) = request.metadata {
166            Some(serde_json::from_str(&meta_str)
167                .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?)
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}