1use js_sys::{Array, Float32Array, Object, Promise, Reflect};
11use parking_lot::Mutex;
12use serde::{Deserialize, Serialize};
13use serde_wasm_bindgen::{from_value, to_value};
14use std::collections::HashMap;
15use std::sync::Arc;
16use wasm_bindgen::prelude::*;
17use wasm_bindgen_futures::future_to_promise;
18use web_sys::console;
19
20mod types;
21mod utils;
22
23pub use types::*;
24pub use utils::*;
25
26#[wasm_bindgen(start)]
28pub fn init() {
29 utils::set_panic_hook();
30 tracing_wasm::set_as_global_default();
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ExoError {
36 pub message: String,
37 pub kind: String,
38}
39
40impl ExoError {
41 pub fn new(message: impl Into<String>, kind: impl Into<String>) -> Self {
42 Self {
43 message: message.into(),
44 kind: kind.into(),
45 }
46 }
47}
48
49impl From<ExoError> for JsValue {
50 fn from(err: ExoError) -> Self {
51 let obj = Object::new();
52 Reflect::set(&obj, &"message".into(), &err.message.into()).unwrap();
53 Reflect::set(&obj, &"kind".into(), &err.kind.into()).unwrap();
54 obj.into()
55 }
56}
57
58impl From<String> for ExoError {
59 fn from(s: String) -> Self {
60 ExoError::new(s, "Error")
61 }
62}
63
64type ExoResult<T> = Result<T, ExoError>;
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SubstrateConfig {
69 pub dimensions: usize,
71 #[serde(default = "default_metric")]
73 pub distance_metric: String,
74 #[serde(default = "default_true")]
76 pub use_hnsw: bool,
77 #[serde(default = "default_true")]
79 pub enable_temporal: bool,
80 #[serde(default = "default_true")]
82 pub enable_causal: bool,
83}
84
85fn default_metric() -> String {
86 "cosine".to_string()
87}
88
89fn default_true() -> bool {
90 true
91}
92
93#[wasm_bindgen]
95#[derive(Clone)]
96pub struct Pattern {
97 inner: PatternInner,
98}
99
100#[derive(Clone, Serialize, Deserialize)]
101struct PatternInner {
102 embedding: Vec<f32>,
104 metadata: Option<HashMap<String, serde_json::Value>>,
106 timestamp: f64,
108 id: Option<String>,
110 antecedents: Vec<String>,
112}
113
114#[wasm_bindgen]
115impl Pattern {
116 #[wasm_bindgen(constructor)]
117 pub fn new(
118 embedding: Float32Array,
119 metadata: Option<JsValue>,
120 antecedents: Option<Vec<String>>,
121 ) -> Result<Pattern, JsValue> {
122 let embedding_vec = embedding.to_vec();
123
124 if embedding_vec.is_empty() {
125 return Err(JsValue::from_str("Embedding cannot be empty"));
126 }
127
128 let metadata = if let Some(meta) = metadata {
129 let json_val: serde_json::Value = from_value(meta)
130 .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
131 match json_val {
133 serde_json::Value::Object(map) => Some(map.into_iter().collect()),
134 other => {
135 let mut map = HashMap::new();
136 map.insert("value".to_string(), other);
137 Some(map)
138 }
139 }
140 } else {
141 None
142 };
143
144 Ok(Pattern {
145 inner: PatternInner {
146 embedding: embedding_vec,
147 metadata,
148 timestamp: js_sys::Date::now(),
149 id: None,
150 antecedents: antecedents.unwrap_or_default(),
151 },
152 })
153 }
154
155 #[wasm_bindgen(getter)]
156 pub fn id(&self) -> Option<String> {
157 self.inner.id.clone()
158 }
159
160 #[wasm_bindgen(getter)]
161 pub fn embedding(&self) -> Float32Array {
162 Float32Array::from(&self.inner.embedding[..])
163 }
164
165 #[wasm_bindgen(getter)]
166 pub fn metadata(&self) -> Option<JsValue> {
167 self.inner.metadata.as_ref().map(|m| {
168 let json_val = serde_json::Value::Object(m.clone().into_iter().collect());
169 to_value(&json_val).unwrap()
170 })
171 }
172
173 #[wasm_bindgen(getter)]
174 pub fn timestamp(&self) -> f64 {
175 self.inner.timestamp
176 }
177
178 #[wasm_bindgen(getter)]
179 pub fn antecedents(&self) -> Vec<String> {
180 self.inner.antecedents.clone()
181 }
182}
183
184#[wasm_bindgen]
186pub struct SearchResult {
187 inner: SearchResultInner,
188}
189
190#[derive(Clone, Serialize, Deserialize)]
191struct SearchResultInner {
192 id: String,
193 score: f32,
194 pattern: Option<PatternInner>,
195}
196
197#[wasm_bindgen]
198impl SearchResult {
199 #[wasm_bindgen(getter)]
200 pub fn id(&self) -> String {
201 self.inner.id.clone()
202 }
203
204 #[wasm_bindgen(getter)]
205 pub fn score(&self) -> f32 {
206 self.inner.score
207 }
208
209 #[wasm_bindgen(getter)]
210 pub fn pattern(&self) -> Option<Pattern> {
211 self.inner.pattern.clone().map(|p| Pattern { inner: p })
212 }
213}
214
215#[wasm_bindgen]
217pub struct ExoSubstrate {
218 db: Arc<Mutex<ruvector_core::vector_db::VectorDB>>,
220 config: SubstrateConfig,
221 dimensions: usize,
222}
223
224#[wasm_bindgen]
225impl ExoSubstrate {
226 #[wasm_bindgen(constructor)]
242 pub fn new(config: JsValue) -> Result<ExoSubstrate, JsValue> {
243 let config: SubstrateConfig = from_value(config)
244 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
245
246 if config.dimensions == 0 {
248 return Err(JsValue::from_str("Dimensions must be greater than 0"));
249 }
250
251 let distance_metric = match config.distance_metric.as_str() {
253 "euclidean" => ruvector_core::types::DistanceMetric::Euclidean,
254 "cosine" => ruvector_core::types::DistanceMetric::Cosine,
255 "dotproduct" => ruvector_core::types::DistanceMetric::DotProduct,
256 "manhattan" => ruvector_core::types::DistanceMetric::Manhattan,
257 _ => return Err(JsValue::from_str(&format!("Unknown distance metric: {}", config.distance_metric))),
258 };
259
260 let hnsw_config = if config.use_hnsw {
261 Some(ruvector_core::types::HnswConfig::default())
262 } else {
263 None
264 };
265
266 let db_options = ruvector_core::types::DbOptions {
267 dimensions: config.dimensions,
268 distance_metric,
269 storage_path: ":memory:".to_string(), hnsw_config,
271 quantization: None,
272 };
273
274 let db = ruvector_core::vector_db::VectorDB::new(db_options)
275 .map_err(|e| JsValue::from_str(&format!("Failed to create substrate: {}", e)))?;
276
277 console::log_1(&format!("EXO substrate initialized with {} dimensions", config.dimensions).into());
278
279 Ok(ExoSubstrate {
280 db: Arc::new(Mutex::new(db)),
281 dimensions: config.dimensions,
282 config,
283 })
284 }
285
286 #[wasm_bindgen]
294 pub fn store(&self, pattern: &Pattern) -> Result<String, JsValue> {
295 if pattern.inner.embedding.len() != self.dimensions {
296 return Err(JsValue::from_str(&format!(
297 "Pattern embedding dimension mismatch: expected {}, got {}",
298 self.dimensions,
299 pattern.inner.embedding.len()
300 )));
301 }
302
303 let entry = ruvector_core::types::VectorEntry {
304 id: pattern.inner.id.clone(),
305 vector: pattern.inner.embedding.clone(),
306 metadata: pattern.inner.metadata.clone(),
307 };
308
309 let db = self.db.lock();
310 let id = db.insert(entry)
311 .map_err(|e| JsValue::from_str(&format!("Failed to store pattern: {}", e)))?;
312
313 console::log_1(&format!("Pattern stored with ID: {}", id).into());
314 Ok(id)
315 }
316
317 #[wasm_bindgen]
326 pub fn query(&self, embedding: Float32Array, k: u32) -> Result<Promise, JsValue> {
327 let query_vec = embedding.to_vec();
328
329 if query_vec.len() != self.dimensions {
330 return Err(JsValue::from_str(&format!(
331 "Query embedding dimension mismatch: expected {}, got {}",
332 self.dimensions,
333 query_vec.len()
334 )));
335 }
336
337 let db = self.db.clone();
338
339 let promise = future_to_promise(async move {
340 let search_query = ruvector_core::types::SearchQuery {
341 vector: query_vec,
342 k: k as usize,
343 filter: None,
344 ef_search: None,
345 };
346
347 let db_guard = db.lock();
348 let results = db_guard.search(search_query)
349 .map_err(|e| JsValue::from_str(&format!("Search failed: {}", e)))?;
350 drop(db_guard);
351
352 let js_results: Vec<JsValue> = results
353 .into_iter()
354 .map(|r| {
355 let result = SearchResult {
356 inner: SearchResultInner {
357 id: r.id,
358 score: r.score,
359 pattern: None, },
361 };
362 to_value(&result.inner).unwrap()
363 })
364 .collect();
365
366 Ok(Array::from_iter(js_results).into())
367 });
368
369 Ok(promise)
370 }
371
372 #[wasm_bindgen]
377 pub fn stats(&self) -> Result<JsValue, JsValue> {
378 let db = self.db.lock();
379 let count = db.len()
380 .map_err(|e| JsValue::from_str(&format!("Failed to get stats: {}", e)))?;
381
382 let stats = serde_json::json!({
383 "dimensions": self.dimensions,
384 "pattern_count": count,
385 "distance_metric": self.config.distance_metric,
386 "temporal_enabled": self.config.enable_temporal,
387 "causal_enabled": self.config.enable_causal,
388 });
389
390 to_value(&stats).map_err(|e| JsValue::from_str(&format!("Failed to serialize stats: {}", e)))
391 }
392
393 #[wasm_bindgen]
401 pub fn get(&self, id: &str) -> Result<Option<Pattern>, JsValue> {
402 let db = self.db.lock();
403 let entry = db.get(id)
404 .map_err(|e| JsValue::from_str(&format!("Failed to get pattern: {}", e)))?;
405
406 Ok(entry.map(|e| Pattern {
407 inner: PatternInner {
408 embedding: e.vector,
409 metadata: e.metadata,
410 timestamp: js_sys::Date::now(),
411 id: e.id,
412 antecedents: vec![],
413 },
414 }))
415 }
416
417 #[wasm_bindgen]
425 pub fn delete(&self, id: &str) -> Result<bool, JsValue> {
426 let db = self.db.lock();
427 db.delete(id)
428 .map_err(|e| JsValue::from_str(&format!("Failed to delete pattern: {}", e)))
429 }
430
431 #[wasm_bindgen]
433 pub fn len(&self) -> Result<usize, JsValue> {
434 let db = self.db.lock();
435 db.len()
436 .map_err(|e| JsValue::from_str(&format!("Failed to get length: {}", e)))
437 }
438
439 #[wasm_bindgen(js_name = isEmpty)]
441 pub fn is_empty(&self) -> Result<bool, JsValue> {
442 let db = self.db.lock();
443 db.is_empty()
444 .map_err(|e| JsValue::from_str(&format!("Failed to check if empty: {}", e)))
445 }
446
447 #[wasm_bindgen(getter)]
449 pub fn dimensions(&self) -> usize {
450 self.dimensions
451 }
452}
453
454#[wasm_bindgen]
456pub fn version() -> String {
457 env!("CARGO_PKG_VERSION").to_string()
458}
459
460#[wasm_bindgen(js_name = detectSIMD)]
462pub fn detect_simd() -> bool {
463 #[cfg(target_feature = "simd128")]
464 {
465 true
466 }
467 #[cfg(not(target_feature = "simd128"))]
468 {
469 false
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use wasm_bindgen_test::*;
477
478 wasm_bindgen_test_configure!(run_in_browser);
479
480 #[wasm_bindgen_test]
481 fn test_version() {
482 assert!(!version().is_empty());
483 }
484
485 #[wasm_bindgen_test]
486 fn test_detect_simd() {
487 let _ = detect_simd();
488 }
489}