Skip to main content

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            // VoI escalation gate (ADR-331) is not yet exposed over wasm.
73            voi: 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(
167                serde_json::from_str(&meta_str)
168                    .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?,
169            )
170        } else {
171            None
172        };
173
174        Ok(CoreRoutingRequest {
175            query_embedding: request.query_embedding,
176            candidates: candidates?,
177            metadata,
178        })
179    }
180}
181
182/// Routing response
183#[wasm_bindgen]
184pub struct RoutingResponse {
185    decisions_json: String,
186    inference_time_us: u64,
187    candidates_processed: usize,
188    feature_time_us: u64,
189}
190
191#[wasm_bindgen]
192impl RoutingResponse {
193    #[wasm_bindgen(getter)]
194    pub fn decisions_json(&self) -> String {
195        self.decisions_json.clone()
196    }
197
198    #[wasm_bindgen(getter)]
199    pub fn inference_time_us(&self) -> u64 {
200        self.inference_time_us
201    }
202
203    #[wasm_bindgen(getter)]
204    pub fn candidates_processed(&self) -> usize {
205        self.candidates_processed
206    }
207
208    #[wasm_bindgen(getter)]
209    pub fn feature_time_us(&self) -> u64 {
210        self.feature_time_us
211    }
212}
213
214impl From<CoreRoutingResponse> for RoutingResponse {
215    fn from(response: CoreRoutingResponse) -> Self {
216        let decisions_json = serde_json::to_string(&response.decisions).unwrap_or_default();
217
218        Self {
219            decisions_json,
220            inference_time_us: response.inference_time_us,
221            candidates_processed: response.candidates_processed,
222            feature_time_us: response.feature_time_us,
223        }
224    }
225}
226
227/// Tiny Dancer router for WASM
228#[wasm_bindgen]
229pub struct Router {
230    inner: CoreRouter,
231}
232
233#[wasm_bindgen]
234impl Router {
235    /// Create a new router with configuration
236    #[wasm_bindgen(constructor)]
237    pub fn new(config: RouterConfig) -> Result<Router, JsValue> {
238        let core_config: CoreRouterConfig = config.into();
239        let router = CoreRouter::new(core_config)
240            .map_err(|e| JsValue::from_str(&format!("Failed to create router: {}", e)))?;
241
242        Ok(Router { inner: router })
243    }
244
245    /// Route a request
246    pub fn route(&self, request: RoutingRequest) -> Result<RoutingResponse, JsValue> {
247        let core_request: CoreRoutingRequest = request.try_into()?;
248        let core_response = self
249            .inner
250            .route(core_request)
251            .map_err(|e| JsValue::from_str(&format!("Routing failed: {}", e)))?;
252
253        Ok(core_response.into())
254    }
255
256    /// Check circuit breaker status
257    pub fn circuit_breaker_status(&self) -> Option<bool> {
258        self.inner.circuit_breaker_status()
259    }
260}
261
262/// Get library version
263#[wasm_bindgen]
264pub fn version() -> String {
265    env!("CARGO_PKG_VERSION").to_string()
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_version() {
274        assert!(!version().is_empty());
275    }
276}