//! # RuVector Delta WASM //! //! WASM bindings for delta operations on vectors. //! Provides high-performance delta capture, application, and SIMD-accelerated operations. //! //! ## Features //! //! - Delta capture from vector pairs //! - Efficient delta application //! - SIMD acceleration (when available) //! - Shared memory for zero-copy operations //! - Streaming delta support //! //! ## Example (JavaScript) //! //! ```javascript //! import { DeltaEngine, vectorDelta } from 'ruvector-delta-wasm'; //! //! const engine = new DeltaEngine(384); //! //! const oldVec = new Float32Array([1.0, 2.0, 3.0, ...]); //! const newVec = new Float32Array([1.1, 2.0, 3.5, ...]); //! //! const delta = engine.capture(oldVec, newVec); //! console.log('Delta sparsity:', delta.sparsity); //! //! engine.apply(oldVec, delta); //! // oldVec now equals newVec //! ``` mod apply; mod capture; mod memory; mod simd; pub use apply::*; pub use capture::*; pub use memory::*; pub use simd::*; use js_sys::{Array, Float32Array, Object, Reflect, Uint8Array}; use parking_lot::RwLock; use ruvector_delta_core::{ Delta, DeltaEncoding, DeltaOp, DeltaStream, DeltaValue, DeltaWindow, HybridEncoding, SparseEncoding, VectorDelta, WindowConfig, WindowType, }; use serde::{Deserialize, Serialize}; use serde_wasm_bindgen::{from_value, to_value}; use std::sync::Arc; use wasm_bindgen::prelude::*; /// Initialize panic hook for better error messages #[wasm_bindgen(start)] pub fn init() { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); tracing_wasm::set_as_global_default(); } /// Get WASM module version #[wasm_bindgen] pub fn version() -> String { env!("CARGO_PKG_VERSION").to_string() } /// Check for SIMD support #[wasm_bindgen(js_name = hasSIMD)] pub fn has_simd() -> bool { #[cfg(target_feature = "simd128")] { true } #[cfg(not(target_feature = "simd128"))] { false } } /// JavaScript-friendly delta representation #[wasm_bindgen] pub struct JsDelta { inner: VectorDelta, } #[wasm_bindgen] impl JsDelta { /// Get the dimensions of this delta #[wasm_bindgen(getter)] pub fn dimensions(&self) -> usize { self.inner.dimensions } /// Check if this is an identity (no change) delta #[wasm_bindgen(getter, js_name = isIdentity)] pub fn is_identity(&self) -> bool { self.inner.is_identity() } /// Get the sparsity ratio (0.0 = dense, 1.0 = fully sparse) #[wasm_bindgen(getter)] pub fn sparsity(&self) -> f32 { let nnz = self.inner.value.nnz(); if self.inner.dimensions == 0 { 1.0 } else { 1.0 - (nnz as f32 / self.inner.dimensions as f32) } } /// Get the L2 norm of the delta #[wasm_bindgen(js_name = l2Norm)] pub fn l2_norm(&self) -> f32 { self.inner.l2_norm() } /// Get the L1 norm of the delta #[wasm_bindgen(js_name = l1Norm)] pub fn l1_norm(&self) -> f32 { self.inner.l1_norm() } /// Get the number of non-zero elements #[wasm_bindgen(getter)] pub fn nnz(&self) -> usize { self.inner.value.nnz() } /// Get byte size of this delta #[wasm_bindgen(getter, js_name = byteSize)] pub fn byte_size(&self) -> usize { self.inner.byte_size() } /// Scale the delta by a factor pub fn scale(&self, factor: f32) -> JsDelta { JsDelta { inner: self.inner.scale(factor), } } /// Clip delta values to a range pub fn clip(&self, min: f32, max: f32) -> JsDelta { JsDelta { inner: self.inner.clip(min, max), } } /// Compose with another delta pub fn compose(&self, other: &JsDelta) -> JsDelta { JsDelta { inner: self.inner.clone().compose(other.inner.clone()), } } /// Get the inverse delta pub fn inverse(&self) -> JsDelta { JsDelta { inner: self.inner.inverse(), } } /// Export to dense Float32Array #[wasm_bindgen(js_name = toDense)] pub fn to_dense(&self) -> Float32Array { let dense = self.inner.value.to_dense(self.inner.dimensions); match dense { DeltaValue::Dense(values) => Float32Array::from(&values[..]), _ => Float32Array::new_with_length(self.inner.dimensions as u32), } } /// Export sparse representation as array of {index, value} #[wasm_bindgen(js_name = toSparse)] pub fn to_sparse(&self) -> Result { let ops = match &self.inner.value { DeltaValue::Identity => Vec::new(), DeltaValue::Sparse(ops) => ops .iter() .map(|op| SparseEntry { index: op.index, value: op.value, }) .collect(), DeltaValue::Dense(values) | DeltaValue::Replace(values) => values .iter() .enumerate() .filter(|(_, v)| **v != 0.0) .map(|(i, v)| SparseEntry { index: i as u32, value: *v, }) .collect(), }; to_value(&ops).map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))) } /// Serialize to bytes #[wasm_bindgen(js_name = toBytes)] pub fn to_bytes(&self) -> Result { let encoding = HybridEncoding::default(); let bytes = encoding .encode(&self.inner) .map_err(|e| JsValue::from_str(&format!("Encoding error: {}", e)))?; Ok(Uint8Array::from(&bytes[..])) } /// Deserialize from bytes #[wasm_bindgen(js_name = fromBytes)] pub fn from_bytes(bytes: Uint8Array) -> Result { let data = bytes.to_vec(); let encoding = HybridEncoding::default(); let inner = encoding .decode(&data) .map_err(|e| JsValue::from_str(&format!("Decoding error: {}", e)))?; Ok(JsDelta { inner }) } } #[derive(Serialize, Deserialize)] struct SparseEntry { index: u32, value: f32, } /// Main delta engine for vector operations #[wasm_bindgen] pub struct DeltaEngine { dimensions: usize, sparsity_threshold: f32, } #[wasm_bindgen] impl DeltaEngine { /// Create a new delta engine #[wasm_bindgen(constructor)] pub fn new(dimensions: usize) -> DeltaEngine { DeltaEngine { dimensions, sparsity_threshold: 0.7, } } /// Set sparsity threshold (0.0 to 1.0) #[wasm_bindgen(js_name = setSparsityThreshold)] pub fn set_sparsity_threshold(&mut self, threshold: f32) { self.sparsity_threshold = threshold.clamp(0.0, 1.0); } /// Capture delta between two vectors pub fn capture( &self, old_vec: Float32Array, new_vec: Float32Array, ) -> Result { if old_vec.length() != new_vec.length() { return Err(JsValue::from_str("Vectors must have same length")); } if old_vec.length() as usize != self.dimensions { return Err(JsValue::from_str(&format!( "Vector length {} doesn't match engine dimensions {}", old_vec.length(), self.dimensions ))); } let old: Vec = old_vec.to_vec(); let new: Vec = new_vec.to_vec(); let inner = VectorDelta::compute(&old, &new); Ok(JsDelta { inner }) } /// Apply delta to a vector in-place pub fn apply(&self, vec: Float32Array, delta: &JsDelta) -> Result<(), JsValue> { if vec.length() as usize != self.dimensions { return Err(JsValue::from_str("Vector length mismatch")); } let mut data: Vec = vec.to_vec(); delta .inner .apply(&mut data) .map_err(|e| JsValue::from_str(&format!("Apply error: {}", e)))?; // Copy back to Float32Array vec.copy_from(&data); Ok(()) } /// Apply delta and return new vector #[wasm_bindgen(js_name = applyClone)] pub fn apply_clone(&self, vec: Float32Array, delta: &JsDelta) -> Result { let mut data: Vec = vec.to_vec(); delta .inner .apply(&mut data) .map_err(|e| JsValue::from_str(&format!("Apply error: {}", e)))?; Ok(Float32Array::from(&data[..])) } /// Create delta from sparse entries #[wasm_bindgen(js_name = fromSparse)] pub fn from_sparse(&self, entries: JsValue) -> Result { let sparse: Vec = from_value(entries).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?; let ops: smallvec::SmallVec<[DeltaOp; 8]> = sparse .into_iter() .map(|e| DeltaOp::new(e.index, e.value)) .collect(); let inner = VectorDelta::from_sparse(ops, self.dimensions); Ok(JsDelta { inner }) } /// Create delta from dense array #[wasm_bindgen(js_name = fromDense)] pub fn from_dense(&self, values: Float32Array) -> Result { if values.length() as usize != self.dimensions { return Err(JsValue::from_str("Values length doesn't match dimensions")); } let inner = VectorDelta::from_dense(values.to_vec()); Ok(JsDelta { inner }) } /// Create identity (no change) delta #[wasm_bindgen(js_name = identity)] pub fn identity(&self) -> JsDelta { JsDelta { inner: VectorDelta::new(self.dimensions), } } /// Batch capture deltas for multiple vector pairs #[wasm_bindgen(js_name = captureBatch)] pub fn capture_batch( &self, old_vecs: JsValue, new_vecs: JsValue, ) -> Result { let old_array: js_sys::Array = old_vecs .dyn_into() .map_err(|_| JsValue::from_str("old_vecs must be array"))?; let new_array: js_sys::Array = new_vecs .dyn_into() .map_err(|_| JsValue::from_str("new_vecs must be array"))?; if old_array.length() != new_array.length() { return Err(JsValue::from_str("Arrays must have same length")); } let result = js_sys::Array::new(); for i in 0..old_array.length() { let old_vec: Float32Array = old_array .get(i) .dyn_into() .map_err(|_| JsValue::from_str("Expected Float32Array"))?; let new_vec: Float32Array = new_array .get(i) .dyn_into() .map_err(|_| JsValue::from_str("Expected Float32Array"))?; let delta = self.capture(old_vec, new_vec)?; result.push(&delta.into()); } Ok(result) } /// Compose two deltas into one /// For composing multiple deltas, call this method repeatedly #[wasm_bindgen(js_name = composeTwo)] pub fn compose_two(&self, first: &JsDelta, second: &JsDelta) -> JsDelta { let result = first.inner.clone().compose(second.inner.clone()); JsDelta { inner: result } } } /// Delta stream for event sourcing #[wasm_bindgen] pub struct JsDeltaStream { inner: DeltaStream, dimensions: usize, } #[wasm_bindgen] impl JsDeltaStream { /// Create a new delta stream #[wasm_bindgen(constructor)] pub fn new(dimensions: usize) -> JsDeltaStream { JsDeltaStream { inner: DeltaStream::for_vectors(dimensions), dimensions, } } /// Push a delta to the stream pub fn push(&mut self, delta: &JsDelta) { self.inner.push(delta.inner.clone()); } /// Get the current sequence number #[wasm_bindgen(getter)] pub fn sequence(&self) -> u32 { self.inner.sequence() as u32 } /// Get the number of deltas #[wasm_bindgen(getter)] pub fn length(&self) -> usize { self.inner.len() } /// Replay from initial state pub fn replay(&self, initial: Float32Array) -> Result { let init: Vec = initial.to_vec(); let result = self .inner .replay(init) .map_err(|e| JsValue::from_str(&format!("Replay error: {}", e)))?; Ok(Float32Array::from(&result[..])) } /// Create a checkpoint #[wasm_bindgen(js_name = createCheckpoint)] pub fn create_checkpoint(&mut self, value: Float32Array) { self.inner.create_checkpoint(value.to_vec()); } /// Get number of checkpoints #[wasm_bindgen(getter, js_name = checkpointCount)] pub fn checkpoint_count(&self) -> usize { self.inner.checkpoint_count() } /// Replay from checkpoint #[wasm_bindgen(js_name = replayFromCheckpoint)] pub fn replay_from_checkpoint(&self, checkpoint_idx: usize) -> Result { let result = self .inner .replay_from_checkpoint(checkpoint_idx) .ok_or_else(|| JsValue::from_str("Checkpoint index out of bounds"))? .map_err(|e| JsValue::from_str(&format!("Replay error: {:?}", e)))?; Ok(Float32Array::from(&result[..])) } /// Compact the stream pub fn compact(&mut self) -> usize { self.inner.compact().unwrap_or(0) } /// Clear all deltas pub fn clear(&mut self) { self.inner.clear(); } } /// Delta window for time-bounded aggregation #[wasm_bindgen] pub struct JsDeltaWindow { inner: DeltaWindow, dimensions: usize, } #[wasm_bindgen] impl JsDeltaWindow { /// Create a tumbling window (size in milliseconds) #[wasm_bindgen(js_name = tumbling)] pub fn tumbling(dimensions: usize, size_ms: u32) -> JsDeltaWindow { let size_ns = (size_ms as u64) * 1_000_000; JsDeltaWindow { inner: DeltaWindow::tumbling(size_ns), dimensions, } } /// Create a sliding window #[wasm_bindgen(js_name = sliding)] pub fn sliding(dimensions: usize, size_ms: u32, slide_ms: u32) -> JsDeltaWindow { let size_ns = (size_ms as u64) * 1_000_000; let slide_ns = (slide_ms as u64) * 1_000_000; JsDeltaWindow { inner: DeltaWindow::sliding(size_ns, slide_ns), dimensions, } } /// Create a count-based window #[wasm_bindgen(js_name = countBased)] pub fn count_based(dimensions: usize, count: usize) -> JsDeltaWindow { JsDeltaWindow { inner: DeltaWindow::count_based(count), dimensions, } } /// Add a delta with timestamp (milliseconds) pub fn add(&mut self, delta: &JsDelta, timestamp_ms: f64) { let timestamp_ns = (timestamp_ms * 1_000_000.0) as u64; self.inner.add(delta.inner.clone(), timestamp_ns); } /// Check if window is complete #[wasm_bindgen(js_name = isComplete)] pub fn is_complete(&self, current_ms: f64) -> bool { let current_ns = (current_ms * 1_000_000.0) as u64; self.inner.is_complete(current_ns) } /// Emit aggregated window result pub fn emit(&mut self) -> Option { self.inner.emit().map(|r| JsDelta { inner: r.delta }) } /// Get number of entries in window #[wasm_bindgen(getter)] pub fn length(&self) -> usize { self.inner.len() } /// Clear the window pub fn clear(&mut self) { self.inner.clear(); } } #[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_delta_engine_capture() { let engine = DeltaEngine::new(4); let old = Float32Array::from(&[1.0f32, 2.0, 3.0, 4.0][..]); let new = Float32Array::from(&[1.5f32, 2.0, 3.5, 4.0][..]); let delta = engine.capture(old, new).unwrap(); assert!(!delta.is_identity()); assert_eq!(delta.dimensions(), 4); } #[wasm_bindgen_test] fn test_delta_apply() { let engine = DeltaEngine::new(3); let old = Float32Array::from(&[1.0f32, 2.0, 3.0][..]); let new = Float32Array::from(&[2.0f32, 2.0, 4.0][..]); let delta = engine.capture(old.clone(), new.clone()).unwrap(); let mut test_vec = Float32Array::from(&[1.0f32, 2.0, 3.0][..]); engine.apply(test_vec.clone(), &delta).unwrap(); // Note: can't easily verify Float32Array equality in WASM tests } #[wasm_bindgen_test] fn test_identity_delta() { let engine = DeltaEngine::new(10); let delta = engine.identity(); assert!(delta.is_identity()); assert_eq!(delta.sparsity(), 1.0); } #[wasm_bindgen_test] fn test_delta_compose() { let engine = DeltaEngine::new(3); let d1 = engine .from_dense(Float32Array::from(&[1.0f32, 0.0, 0.0][..])) .unwrap(); let d2 = engine .from_dense(Float32Array::from(&[0.0f32, 1.0, 0.0][..])) .unwrap(); let composed = d1.compose(&d2); assert!(!composed.is_identity()); } }