Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
506
vendor/ruvector/examples/exo-ai-2025/crates/exo-wasm/src/lib.rs
vendored
Normal file
506
vendor/ruvector/examples/exo-ai-2025/crates/exo-wasm/src/lib.rs
vendored
Normal file
@@ -0,0 +1,506 @@
|
||||
//! WASM bindings for EXO-AI 2025 Cognitive Substrate
|
||||
//!
|
||||
//! This module provides browser bindings for the EXO substrate, enabling:
|
||||
//! - Pattern storage and retrieval
|
||||
//! - Similarity search with various distance metrics
|
||||
//! - Temporal memory coordination
|
||||
//! - Causal queries
|
||||
//! - Browser-based cognitive operations
|
||||
|
||||
use js_sys::{Array, Float32Array, Object, Promise, Reflect};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_wasm_bindgen::{from_value, to_value};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
mod types;
|
||||
mod utils;
|
||||
|
||||
pub use types::*;
|
||||
pub use utils::*;
|
||||
|
||||
/// Initialize panic hook and tracing for better error messages
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn init() {
|
||||
utils::set_panic_hook();
|
||||
tracing_wasm::set_as_global_default();
|
||||
}
|
||||
|
||||
/// WASM-specific error type that can cross the JS boundary
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ExoError {
|
||||
pub message: String,
|
||||
pub kind: String,
|
||||
}
|
||||
|
||||
impl ExoError {
|
||||
pub fn new(message: impl Into<String>, kind: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
kind: kind.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExoError> for JsValue {
|
||||
fn from(err: ExoError) -> Self {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"message".into(), &err.message.into()).unwrap();
|
||||
Reflect::set(&obj, &"kind".into(), &err.kind.into()).unwrap();
|
||||
obj.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ExoError {
|
||||
fn from(s: String) -> Self {
|
||||
ExoError::new(s, "Error")
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
type ExoResult<T> = Result<T, ExoError>;
|
||||
|
||||
/// Configuration for EXO substrate
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SubstrateConfig {
|
||||
/// Vector dimensions
|
||||
pub dimensions: usize,
|
||||
/// Distance metric (euclidean, cosine, dotproduct, manhattan)
|
||||
#[serde(default = "default_metric")]
|
||||
pub distance_metric: String,
|
||||
/// Enable HNSW index for faster search
|
||||
#[serde(default = "default_true")]
|
||||
pub use_hnsw: bool,
|
||||
/// Enable temporal memory coordination
|
||||
#[serde(default = "default_true")]
|
||||
pub enable_temporal: bool,
|
||||
/// Enable causal tracking
|
||||
#[serde(default = "default_true")]
|
||||
pub enable_causal: bool,
|
||||
}
|
||||
|
||||
fn default_metric() -> String {
|
||||
"cosine".to_string()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Pattern representation in the cognitive substrate
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone)]
|
||||
pub struct Pattern {
|
||||
inner: PatternInner,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct PatternInner {
|
||||
/// Vector embedding
|
||||
embedding: Vec<f32>,
|
||||
/// Metadata (stored as HashMap to match ruvector-core)
|
||||
metadata: Option<HashMap<String, serde_json::Value>>,
|
||||
/// Temporal timestamp (milliseconds since epoch)
|
||||
timestamp: f64,
|
||||
/// Pattern ID
|
||||
id: Option<String>,
|
||||
/// Causal antecedents (IDs of patterns that influenced this one)
|
||||
antecedents: Vec<String>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Pattern {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
embedding: Float32Array,
|
||||
metadata: Option<JsValue>,
|
||||
antecedents: Option<Vec<String>>,
|
||||
) -> Result<Pattern, JsValue> {
|
||||
let embedding_vec = embedding.to_vec();
|
||||
|
||||
if embedding_vec.is_empty() {
|
||||
return Err(JsValue::from_str("Embedding cannot be empty"));
|
||||
}
|
||||
|
||||
let metadata = if let Some(meta) = metadata {
|
||||
let json_val: serde_json::Value = from_value(meta)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?;
|
||||
// Convert to HashMap if it's an object, otherwise wrap it
|
||||
match json_val {
|
||||
serde_json::Value::Object(map) => Some(map.into_iter().collect()),
|
||||
other => {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("value".to_string(), other);
|
||||
Some(map)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Pattern {
|
||||
inner: PatternInner {
|
||||
embedding: embedding_vec,
|
||||
metadata,
|
||||
timestamp: js_sys::Date::now(),
|
||||
id: None,
|
||||
antecedents: antecedents.unwrap_or_default(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn id(&self) -> Option<String> {
|
||||
self.inner.id.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn embedding(&self) -> Float32Array {
|
||||
Float32Array::from(&self.inner.embedding[..])
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn metadata(&self) -> Option<JsValue> {
|
||||
self.inner.metadata.as_ref().map(|m| {
|
||||
let json_val = serde_json::Value::Object(m.clone().into_iter().collect());
|
||||
to_value(&json_val).unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn timestamp(&self) -> f64 {
|
||||
self.inner.timestamp
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn antecedents(&self) -> Vec<String> {
|
||||
self.inner.antecedents.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Search result from substrate query
|
||||
#[wasm_bindgen]
|
||||
pub struct SearchResult {
|
||||
inner: SearchResultInner,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct SearchResultInner {
|
||||
id: String,
|
||||
score: f32,
|
||||
pattern: Option<PatternInner>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SearchResult {
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn id(&self) -> String {
|
||||
self.inner.id.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn score(&self) -> f32 {
|
||||
self.inner.score
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn pattern(&self) -> Option<Pattern> {
|
||||
self.inner.pattern.clone().map(|p| Pattern { inner: p })
|
||||
}
|
||||
}
|
||||
|
||||
/// Main EXO substrate interface for browser deployment
|
||||
#[wasm_bindgen]
|
||||
pub struct ExoSubstrate {
|
||||
// Using ruvector-core as placeholder until exo-core is implemented
|
||||
db: Arc<Mutex<ruvector_core::vector_db::VectorDB>>,
|
||||
config: SubstrateConfig,
|
||||
dimensions: usize,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ExoSubstrate {
|
||||
/// Create a new EXO substrate instance
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `config` - Configuration object with dimensions, distance_metric, etc.
|
||||
///
|
||||
/// # Example
|
||||
/// ```javascript
|
||||
/// const substrate = new ExoSubstrate({
|
||||
/// dimensions: 384,
|
||||
/// distance_metric: "cosine",
|
||||
/// use_hnsw: true,
|
||||
/// enable_temporal: true,
|
||||
/// enable_causal: true
|
||||
/// });
|
||||
/// ```
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(config: JsValue) -> Result<ExoSubstrate, JsValue> {
|
||||
let config: SubstrateConfig =
|
||||
from_value(config).map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
|
||||
|
||||
// Validate configuration
|
||||
if config.dimensions == 0 {
|
||||
return Err(JsValue::from_str("Dimensions must be greater than 0"));
|
||||
}
|
||||
|
||||
// Create underlying vector database
|
||||
let distance_metric = match config.distance_metric.as_str() {
|
||||
"euclidean" => ruvector_core::types::DistanceMetric::Euclidean,
|
||||
"cosine" => ruvector_core::types::DistanceMetric::Cosine,
|
||||
"dotproduct" => ruvector_core::types::DistanceMetric::DotProduct,
|
||||
"manhattan" => ruvector_core::types::DistanceMetric::Manhattan,
|
||||
_ => {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Unknown distance metric: {}",
|
||||
config.distance_metric
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let hnsw_config = if config.use_hnsw {
|
||||
Some(ruvector_core::types::HnswConfig::default())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let db_options = ruvector_core::types::DbOptions {
|
||||
dimensions: config.dimensions,
|
||||
distance_metric,
|
||||
storage_path: ":memory:".to_string(), // WASM uses in-memory storage
|
||||
hnsw_config,
|
||||
quantization: None,
|
||||
};
|
||||
|
||||
let db = ruvector_core::vector_db::VectorDB::new(db_options)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to create substrate: {}", e)))?;
|
||||
|
||||
console::log_1(
|
||||
&format!(
|
||||
"EXO substrate initialized with {} dimensions",
|
||||
config.dimensions
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
|
||||
Ok(ExoSubstrate {
|
||||
db: Arc::new(Mutex::new(db)),
|
||||
dimensions: config.dimensions,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Store a pattern in the substrate
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `pattern` - Pattern object with embedding, metadata, and optional antecedents
|
||||
///
|
||||
/// # Returns
|
||||
/// Pattern ID as a string
|
||||
#[wasm_bindgen]
|
||||
pub fn store(&self, pattern: &Pattern) -> Result<String, JsValue> {
|
||||
if pattern.inner.embedding.len() != self.dimensions {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Pattern embedding dimension mismatch: expected {}, got {}",
|
||||
self.dimensions,
|
||||
pattern.inner.embedding.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let entry = ruvector_core::types::VectorEntry {
|
||||
id: pattern.inner.id.clone(),
|
||||
vector: pattern.inner.embedding.clone(),
|
||||
metadata: pattern.inner.metadata.clone(),
|
||||
};
|
||||
|
||||
let db = self.db.lock();
|
||||
let id = db
|
||||
.insert(entry)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to store pattern: {}", e)))?;
|
||||
|
||||
console::log_1(&format!("Pattern stored with ID: {}", id).into());
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Query the substrate for similar patterns
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `embedding` - Query embedding as Float32Array
|
||||
/// * `k` - Number of results to return
|
||||
///
|
||||
/// # Returns
|
||||
/// Promise that resolves to an array of SearchResult objects
|
||||
#[wasm_bindgen]
|
||||
pub fn query(&self, embedding: Float32Array, k: u32) -> Result<Promise, JsValue> {
|
||||
let query_vec = embedding.to_vec();
|
||||
|
||||
if query_vec.len() != self.dimensions {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Query embedding dimension mismatch: expected {}, got {}",
|
||||
self.dimensions,
|
||||
query_vec.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let db = self.db.clone();
|
||||
|
||||
let promise = future_to_promise(async move {
|
||||
let search_query = ruvector_core::types::SearchQuery {
|
||||
vector: query_vec,
|
||||
k: k as usize,
|
||||
filter: None,
|
||||
ef_search: None,
|
||||
};
|
||||
|
||||
let db_guard = db.lock();
|
||||
let results = db_guard
|
||||
.search(search_query)
|
||||
.map_err(|e| JsValue::from_str(&format!("Search failed: {}", e)))?;
|
||||
drop(db_guard);
|
||||
|
||||
let js_results: Vec<JsValue> = results
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let result = SearchResult {
|
||||
inner: SearchResultInner {
|
||||
id: r.id,
|
||||
score: r.score,
|
||||
pattern: None, // Can be populated if needed
|
||||
},
|
||||
};
|
||||
to_value(&result.inner).unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Array::from_iter(js_results).into())
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
/// Get substrate statistics
|
||||
///
|
||||
/// # Returns
|
||||
/// Object with substrate statistics
|
||||
#[wasm_bindgen]
|
||||
pub fn stats(&self) -> Result<JsValue, JsValue> {
|
||||
let db = self.db.lock();
|
||||
let count = db
|
||||
.len()
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get stats: {}", e)))?;
|
||||
|
||||
let stats = serde_json::json!({
|
||||
"dimensions": self.dimensions,
|
||||
"pattern_count": count,
|
||||
"distance_metric": self.config.distance_metric,
|
||||
"temporal_enabled": self.config.enable_temporal,
|
||||
"causal_enabled": self.config.enable_causal,
|
||||
});
|
||||
|
||||
to_value(&stats)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to serialize stats: {}", e)))
|
||||
}
|
||||
|
||||
/// Get a pattern by ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Pattern ID
|
||||
///
|
||||
/// # Returns
|
||||
/// Pattern object or null if not found
|
||||
#[wasm_bindgen]
|
||||
pub fn get(&self, id: &str) -> Result<Option<Pattern>, JsValue> {
|
||||
let db = self.db.lock();
|
||||
let entry = db
|
||||
.get(id)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get pattern: {}", e)))?;
|
||||
|
||||
Ok(entry.map(|e| Pattern {
|
||||
inner: PatternInner {
|
||||
embedding: e.vector,
|
||||
metadata: e.metadata,
|
||||
timestamp: js_sys::Date::now(),
|
||||
id: e.id,
|
||||
antecedents: vec![],
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
/// Delete a pattern by ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Pattern ID to delete
|
||||
///
|
||||
/// # Returns
|
||||
/// True if deleted, false if not found
|
||||
#[wasm_bindgen]
|
||||
pub fn delete(&self, id: &str) -> Result<bool, JsValue> {
|
||||
let db = self.db.lock();
|
||||
db.delete(id)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to delete pattern: {}", e)))
|
||||
}
|
||||
|
||||
/// Get the number of patterns in the substrate
|
||||
#[wasm_bindgen]
|
||||
pub fn len(&self) -> Result<usize, JsValue> {
|
||||
let db = self.db.lock();
|
||||
db.len()
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get length: {}", e)))
|
||||
}
|
||||
|
||||
/// Check if the substrate is empty
|
||||
#[wasm_bindgen(js_name = isEmpty)]
|
||||
pub fn is_empty(&self) -> Result<bool, JsValue> {
|
||||
let db = self.db.lock();
|
||||
db.is_empty()
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to check if empty: {}", e)))
|
||||
}
|
||||
|
||||
/// Get substrate dimensions
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn dimensions(&self) -> usize {
|
||||
self.dimensions
|
||||
}
|
||||
}
|
||||
|
||||
/// Get version information
|
||||
#[wasm_bindgen]
|
||||
pub fn version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
/// Detect SIMD support in the current environment
|
||||
#[wasm_bindgen(js_name = detectSIMD)]
|
||||
pub fn detect_simd() -> bool {
|
||||
#[cfg(target_feature = "simd128")]
|
||||
{
|
||||
true
|
||||
}
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_version() {
|
||||
assert!(!version().is_empty());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_detect_simd() {
|
||||
let _ = detect_simd();
|
||||
}
|
||||
}
|
||||
177
vendor/ruvector/examples/exo-ai-2025/crates/exo-wasm/src/types.rs
vendored
Normal file
177
vendor/ruvector/examples/exo-ai-2025/crates/exo-wasm/src/types.rs
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
//! Type conversions for JavaScript interoperability
|
||||
//!
|
||||
//! This module provides type conversions between Rust and JavaScript types
|
||||
//! for seamless WASM integration.
|
||||
|
||||
use js_sys::{Array, Float32Array, Object, Reflect};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// JavaScript-compatible query configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct QueryConfig {
|
||||
/// Query vector (will be converted from Float32Array)
|
||||
pub embedding: Vec<f32>,
|
||||
/// Number of results to return
|
||||
pub k: usize,
|
||||
/// Optional metadata filter
|
||||
pub filter: Option<serde_json::Value>,
|
||||
/// Optional ef_search parameter for HNSW
|
||||
pub ef_search: Option<usize>,
|
||||
}
|
||||
|
||||
/// Causal cone type for temporal queries
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CausalConeType {
|
||||
/// Past light cone (all events that could have influenced this point)
|
||||
Past,
|
||||
/// Future light cone (all events this point could influence)
|
||||
Future,
|
||||
/// Custom light cone with specified velocity
|
||||
LightCone,
|
||||
}
|
||||
|
||||
/// Causal query configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CausalQueryConfig {
|
||||
/// Base query configuration
|
||||
pub query: QueryConfig,
|
||||
/// Reference timestamp (milliseconds since epoch)
|
||||
pub reference_time: f64,
|
||||
/// Cone type
|
||||
pub cone_type: CausalConeType,
|
||||
/// Optional velocity parameter for light cone queries (in ms^-1)
|
||||
pub velocity: Option<f32>,
|
||||
}
|
||||
|
||||
/// Topological query types for advanced substrate operations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum TopologicalQuery {
|
||||
/// Find persistent homology features
|
||||
PersistentHomology {
|
||||
dimension: usize,
|
||||
epsilon_min: f32,
|
||||
epsilon_max: f32,
|
||||
},
|
||||
/// Compute Betti numbers (topological invariants)
|
||||
BettiNumbers { max_dimension: usize },
|
||||
/// Check sheaf consistency
|
||||
SheafConsistency { section_ids: Vec<String> },
|
||||
}
|
||||
|
||||
/// Result from causal query
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CausalResult {
|
||||
/// Pattern ID
|
||||
pub id: String,
|
||||
/// Similarity score
|
||||
pub score: f32,
|
||||
/// Causal distance (number of hops in causal graph)
|
||||
pub causal_distance: Option<usize>,
|
||||
/// Temporal distance (milliseconds)
|
||||
pub temporal_distance: f64,
|
||||
/// Optional pattern data
|
||||
pub pattern: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Convert JavaScript array to Rust Vec<f32>
|
||||
pub fn js_array_to_vec_f32(arr: &Array) -> Result<Vec<f32>, JsValue> {
|
||||
let mut vec = Vec::with_capacity(arr.length() as usize);
|
||||
for i in 0..arr.length() {
|
||||
let val = arr.get(i);
|
||||
if let Some(num) = val.as_f64() {
|
||||
vec.push(num as f32);
|
||||
} else {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Array element at index {} is not a number",
|
||||
i
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
/// Convert Rust Vec<f32> to JavaScript Float32Array
|
||||
pub fn vec_f32_to_js_array(vec: &[f32]) -> Float32Array {
|
||||
Float32Array::from(vec)
|
||||
}
|
||||
|
||||
/// Convert JavaScript object to JSON value
|
||||
pub fn js_object_to_json(obj: &JsValue) -> Result<serde_json::Value, JsValue> {
|
||||
serde_wasm_bindgen::from_value(obj.clone())
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert to JSON: {}", e)))
|
||||
}
|
||||
|
||||
/// Convert JSON value to JavaScript object
|
||||
pub fn json_to_js_object(value: &serde_json::Value) -> Result<JsValue, JsValue> {
|
||||
serde_wasm_bindgen::to_value(value)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to convert from JSON: {}", e)))
|
||||
}
|
||||
|
||||
/// Helper to create JavaScript error objects
|
||||
pub fn create_js_error(message: &str, kind: &str) -> JsValue {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"message".into(), &message.into()).unwrap();
|
||||
Reflect::set(&obj, &"kind".into(), &kind.into()).unwrap();
|
||||
Reflect::set(&obj, &"name".into(), &"ExoError".into()).unwrap();
|
||||
obj.into()
|
||||
}
|
||||
|
||||
/// Helper to validate vector dimensions
|
||||
pub fn validate_dimensions(vec: &[f32], expected: usize) -> Result<(), JsValue> {
|
||||
if vec.len() != expected {
|
||||
return Err(create_js_error(
|
||||
&format!(
|
||||
"Dimension mismatch: expected {}, got {}",
|
||||
expected,
|
||||
vec.len()
|
||||
),
|
||||
"DimensionError",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to validate vector is not empty
|
||||
pub fn validate_not_empty(vec: &[f32]) -> Result<(), JsValue> {
|
||||
if vec.is_empty() {
|
||||
return Err(create_js_error("Vector cannot be empty", "ValidationError"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to validate k parameter
|
||||
pub fn validate_k(k: usize) -> Result<(), JsValue> {
|
||||
if k == 0 {
|
||||
return Err(create_js_error(
|
||||
"k must be greater than 0",
|
||||
"ValidationError",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_causal_cone_type_serialization() {
|
||||
let cone = CausalConeType::Past;
|
||||
let json = serde_json::to_string(&cone).unwrap();
|
||||
assert_eq!(json, "\"past\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_topological_query_serialization() {
|
||||
let query = TopologicalQuery::PersistentHomology {
|
||||
dimension: 2,
|
||||
epsilon_min: 0.1,
|
||||
epsilon_max: 1.0,
|
||||
};
|
||||
let json = serde_json::to_value(&query).unwrap();
|
||||
assert_eq!(json["type"], "persistent_homology");
|
||||
}
|
||||
}
|
||||
180
vendor/ruvector/examples/exo-ai-2025/crates/exo-wasm/src/utils.rs
vendored
Normal file
180
vendor/ruvector/examples/exo-ai-2025/crates/exo-wasm/src/utils.rs
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
//! Utility functions for WASM runtime
|
||||
//!
|
||||
//! This module provides utility functions for panic handling, logging,
|
||||
//! and browser environment detection.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::console;
|
||||
|
||||
/// Set up panic hook for better error messages in browser console
|
||||
pub fn set_panic_hook() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
/// Log a message to the browser console
|
||||
pub fn log(message: &str) {
|
||||
console::log_1(&JsValue::from_str(message));
|
||||
}
|
||||
|
||||
/// Log a warning to the browser console
|
||||
pub fn warn(message: &str) {
|
||||
console::warn_1(&JsValue::from_str(message));
|
||||
}
|
||||
|
||||
/// Log an error to the browser console
|
||||
pub fn error(message: &str) {
|
||||
console::error_1(&JsValue::from_str(message));
|
||||
}
|
||||
|
||||
/// Log debug information (includes timing)
|
||||
pub fn debug(message: &str) {
|
||||
console::debug_1(&JsValue::from_str(message));
|
||||
}
|
||||
|
||||
/// Measure execution time of a function
|
||||
pub fn measure_time<F, R>(name: &str, f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
let start = js_sys::Date::now();
|
||||
let result = f();
|
||||
let elapsed = js_sys::Date::now() - start;
|
||||
log(&format!("{} took {:.2}ms", name, elapsed));
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if running in a Web Worker context
|
||||
#[wasm_bindgen]
|
||||
pub fn is_web_worker() -> bool {
|
||||
js_sys::eval("typeof WorkerGlobalScope !== 'undefined'")
|
||||
.map(|v| v.is_truthy())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if running in a browser with WebAssembly support
|
||||
#[wasm_bindgen]
|
||||
pub fn is_wasm_supported() -> bool {
|
||||
js_sys::eval("typeof WebAssembly !== 'undefined'")
|
||||
.map(|v| v.is_truthy())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get browser performance metrics
|
||||
#[wasm_bindgen]
|
||||
pub fn get_performance_metrics() -> Result<JsValue, JsValue> {
|
||||
let window = web_sys::window().ok_or_else(|| JsValue::from_str("No window object"))?;
|
||||
let performance = window
|
||||
.performance()
|
||||
.ok_or_else(|| JsValue::from_str("No performance object"))?;
|
||||
|
||||
let timing = performance.timing();
|
||||
|
||||
let metrics = serde_json::json!({
|
||||
"navigation_start": timing.navigation_start(),
|
||||
"dom_complete": timing.dom_complete(),
|
||||
"load_event_end": timing.load_event_end(),
|
||||
});
|
||||
|
||||
serde_wasm_bindgen::to_value(&metrics)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to serialize metrics: {}", e)))
|
||||
}
|
||||
|
||||
/// Get available memory (if supported by browser)
|
||||
#[wasm_bindgen]
|
||||
pub fn get_memory_info() -> Result<JsValue, JsValue> {
|
||||
// Try to access performance.memory (Chrome only)
|
||||
let window = web_sys::window().ok_or_else(|| JsValue::from_str("No window object"))?;
|
||||
let performance = window
|
||||
.performance()
|
||||
.ok_or_else(|| JsValue::from_str("No performance object"))?;
|
||||
|
||||
// This is non-standard and may not be available
|
||||
let result = js_sys::Reflect::get(&performance, &JsValue::from_str("memory"));
|
||||
|
||||
if let Ok(memory) = result {
|
||||
if !memory.is_undefined() {
|
||||
return Ok(memory);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return empty object
|
||||
Ok(js_sys::Object::new().into())
|
||||
}
|
||||
|
||||
/// Format bytes to human-readable string
|
||||
pub fn format_bytes(bytes: f64) -> String {
|
||||
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
|
||||
let mut size = bytes;
|
||||
let mut unit_index = 0;
|
||||
|
||||
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
|
||||
size /= 1024.0;
|
||||
unit_index += 1;
|
||||
}
|
||||
|
||||
format!("{:.2} {}", size, UNITS[unit_index])
|
||||
}
|
||||
|
||||
/// Generate a random UUID v4
|
||||
#[wasm_bindgen]
|
||||
pub fn generate_uuid() -> String {
|
||||
// Use crypto.randomUUID if available, otherwise fallback
|
||||
let result = js_sys::eval(
|
||||
"typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : null"
|
||||
);
|
||||
|
||||
if let Ok(uuid) = result {
|
||||
if let Some(uuid_str) = uuid.as_string() {
|
||||
return uuid_str;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: simple UUID generation
|
||||
use getrandom::getrandom;
|
||||
let mut bytes = [0u8; 16];
|
||||
if getrandom(&mut bytes).is_ok() {
|
||||
// Set version (4) and variant bits
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
format!(
|
||||
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
|
||||
bytes[0], bytes[1], bytes[2], bytes[3],
|
||||
bytes[4], bytes[5],
|
||||
bytes[6], bytes[7],
|
||||
bytes[8], bytes[9],
|
||||
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
|
||||
)
|
||||
} else {
|
||||
// Ultimate fallback: timestamp-based ID
|
||||
format!("{}-{}", js_sys::Date::now(), js_sys::Math::random())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if localStorage is available
|
||||
#[wasm_bindgen]
|
||||
pub fn is_local_storage_available() -> bool {
|
||||
js_sys::eval("typeof localStorage !== 'undefined'")
|
||||
.map(|v| v.is_truthy())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if IndexedDB is available
|
||||
#[wasm_bindgen]
|
||||
pub fn is_indexed_db_available() -> bool {
|
||||
js_sys::eval("typeof indexedDB !== 'undefined'")
|
||||
.map(|v| v.is_truthy())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_bytes() {
|
||||
assert_eq!(format_bytes(100.0), "100.00 B");
|
||||
assert_eq!(format_bytes(1024.0), "1.00 KB");
|
||||
assert_eq!(format_bytes(1024.0 * 1024.0), "1.00 MB");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user