briefcase-wasm 2.4.1

WebAssembly bindings for Briefcase AI
Documentation
//! # Briefcase AI WebAssembly Bindings
//!
//! High-performance AI observability and decision tracking for JavaScript and TypeScript applications
//! running in browsers or Node.js environments.
//!
//! ## Features
//!
//! - **AI Decision Tracking**: Capture inputs, outputs, and context for every AI decision
//! - **High Performance**: Rust-powered WebAssembly for maximum speed
//! - **Browser Compatible**: Works in all modern browsers and Node.js
//! - **Sync APIs**: Simplified synchronous APIs optimized for WebAssembly
//! - **TypeScript Support**: Full type definitions included
//! - **Zero Dependencies**: No external JavaScript dependencies required
//!
//! ## Installation
//!
//! ```bash
//! npm install briefcase-wasm
//! ```
//!
//! ## Usage
//!
//! ```javascript
//! import { init, DecisionSnapshot, Input, Output } from 'briefcase-wasm';
//!
//! // Initialize the WASM module
//! await init();
//!
//! // Create a decision snapshot
//! const decision = new DecisionSnapshot("ai_function");
//! const input = new Input("query", "Hello world", "string");
//! const output = new Output("response", "Hello back!", "string");
//!
//! decision.addInput(input);
//! decision.addOutput(output);
//! ```
//!
//! ## Browser Usage
//!
//! For browser environments, you may need to configure your bundler to handle WASM files.
//! Most modern bundlers like Vite, Webpack 5, and Parcel support WebAssembly out of the box.
//!
//! ## Node.js Usage
//!
//! The package works seamlessly in Node.js environments with ES modules or CommonJS.

use briefcase_core::models::{DecisionSnapshot, Input, Output};
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
use web_sys::console;

// Set up panic hook for better error messages in WASM
#[cfg(feature = "console_error_panic_hook")]
#[wasm_bindgen(start)]
pub fn main() {
    console_error_panic_hook::set_once();
}

/// Macro for console logging from WASM
macro_rules! log {
    ( $( $t:tt )* ) => {
        console::log_1(&format!( $( $t )* ).into());
    }
}

/// Initialize the Briefcase AI WASM module
///
/// This function sets up panic hooks for better error reporting in WebAssembly environments.
/// Call this function once at the start of your application.
///
/// # Examples
///
/// ```javascript
/// import { init } from 'briefcase-wasm';
///
/// await init();
/// console.log('Briefcase AI ready!');
/// ```
#[wasm_bindgen]
pub fn init() {
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();

    log!("🦀 Briefcase AI WASM module initialized!");
}

/// JavaScript-friendly wrapper for DecisionSnapshot
///
/// Represents a snapshot of an AI decision with inputs, outputs, and metadata.
/// This is the main data structure for tracking AI decisions in JavaScript/TypeScript.
///
/// # Examples
///
/// ```javascript
/// import { JsDecisionSnapshot, JsInput, JsOutput } from 'briefcase-wasm';
///
/// const decision = new JsDecisionSnapshot("my_ai_function");
/// const input = new JsInput("user_query", "Hello", "string");
/// const output = new JsOutput("ai_response", "Hi there!", "string");
///
/// decision.add_input(input);
/// decision.add_output(output);
/// decision.set_execution_time(123.5);
/// ```
#[wasm_bindgen]
pub struct JsDecisionSnapshot {
    inner: DecisionSnapshot,
}

#[wasm_bindgen]
impl JsDecisionSnapshot {
    #[wasm_bindgen(constructor)]
    pub fn new(function_name: &str) -> JsDecisionSnapshot {
        JsDecisionSnapshot {
            inner: DecisionSnapshot::new(function_name),
        }
    }

    #[wasm_bindgen]
    pub fn with_module(mut self, module_name: &str) -> JsDecisionSnapshot {
        self.inner = self.inner.with_module(module_name);
        self
    }

    #[wasm_bindgen]
    pub fn add_input(
        mut self,
        name: &str,
        value: &JsValue,
        data_type: &str,
    ) -> Result<JsDecisionSnapshot, JsError> {
        let json_value: serde_json::Value = serde_wasm_bindgen::from_value(value.clone())
            .map_err(|e| JsError::new(&format!("Failed to convert input value: {}", e)))?;

        let input = Input::new(name, json_value, data_type);
        self.inner = self.inner.add_input(input);
        Ok(self)
    }

    #[wasm_bindgen]
    pub fn add_output(
        mut self,
        name: &str,
        value: &JsValue,
        data_type: &str,
    ) -> Result<JsDecisionSnapshot, JsError> {
        let json_value: serde_json::Value = serde_wasm_bindgen::from_value(value.clone())
            .map_err(|e| JsError::new(&format!("Failed to convert output value: {}", e)))?;

        let output = Output::new(name, json_value, data_type);
        self.inner = self.inner.add_output(output);
        Ok(self)
    }

    #[wasm_bindgen]
    pub fn add_tag(mut self, key: &str, value: &str) -> JsDecisionSnapshot {
        self.inner = self.inner.add_tag(key, value);
        self
    }

    #[wasm_bindgen]
    pub fn to_json(&self) -> Result<JsValue, JsError> {
        serde_wasm_bindgen::to_value(&self.inner)
            .map_err(|e| JsError::new(&format!("Failed to serialize decision: {}", e)))
    }
}

/// Simple in-memory storage for demo purposes
#[wasm_bindgen]
pub struct JsMemoryStorage {
    decisions: HashMap<String, DecisionSnapshot>,
}

impl Default for JsMemoryStorage {
    fn default() -> Self {
        Self::new()
    }
}

#[wasm_bindgen]
impl JsMemoryStorage {
    #[wasm_bindgen(constructor)]
    pub fn new() -> JsMemoryStorage {
        JsMemoryStorage {
            decisions: HashMap::new(),
        }
    }

    #[wasm_bindgen]
    pub fn save_decision(&mut self, decision: &JsDecisionSnapshot) -> String {
        let id = format!("decision_{}", uuid::Uuid::new_v4());
        self.decisions.insert(id.clone(), decision.inner.clone());
        id
    }

    #[wasm_bindgen]
    pub fn load_decision(&self, decision_id: &str) -> Result<JsDecisionSnapshot, JsError> {
        let decision = self
            .decisions
            .get(decision_id)
            .ok_or_else(|| JsError::new(&format!("Decision not found: {}", decision_id)))?;

        Ok(JsDecisionSnapshot {
            inner: decision.clone(),
        })
    }

    #[wasm_bindgen]
    pub fn health_check(&self) -> bool {
        true
    }
}

/// Utility function to get the version of the WASM module
#[wasm_bindgen]
pub fn version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

/// Utility function for testing WASM functionality
#[wasm_bindgen]
pub fn test_functionality() -> Result<JsValue, JsError> {
    // Create a test decision
    let decision = JsDecisionSnapshot::new("test_function")
        .with_module("test_module")
        .add_tag("env", "test");

    // Test storage
    let mut storage = JsMemoryStorage::new();
    let decision_id = storage.save_decision(&decision);
    let _loaded_decision = storage.load_decision(&decision_id)?;

    let result = serde_json::json!({
        "decision_id": decision_id,
        "storage_health": storage.health_check(),
        "version": version(),
    });

    serde_wasm_bindgen::to_value(&result)
        .map_err(|e| JsError::new(&format!("Failed to serialize test result: {}", e)))
}

// Authenticated client (pre-validated for WASM)
pub mod client;

// Re-export modules so they're accessible
pub mod cost;
pub mod drift;
pub mod models;
pub mod sanitization;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_version_returns_package_version() {
        let v = version();
        assert!(!v.is_empty());
        // Should be a valid semver-like string
        assert!(v.contains('.'), "Version '{}' should contain dots", v);
    }

    #[test]
    fn test_js_decision_snapshot_construction() {
        let decision = JsDecisionSnapshot::new("test_fn");
        assert_eq!(decision.inner.function_name, "test_fn");
        assert!(decision.inner.module_name.is_none());
    }

    #[test]
    fn test_js_decision_snapshot_with_module() {
        let decision = JsDecisionSnapshot::new("test_fn").with_module("test_mod");
        assert_eq!(decision.inner.module_name, Some("test_mod".to_string()));
    }

    #[test]
    fn test_js_decision_snapshot_add_tag() {
        let decision = JsDecisionSnapshot::new("test_fn")
            .add_tag("env", "production")
            .add_tag("version", "1.0");
        assert_eq!(
            decision.inner.tags.get("env"),
            Some(&"production".to_string())
        );
        assert_eq!(decision.inner.tags.get("version"), Some(&"1.0".to_string()));
    }

    #[test]
    fn test_js_memory_storage_new() {
        let storage = JsMemoryStorage::new();
        assert!(storage.health_check());
    }

    #[test]
    fn test_js_memory_storage_save_and_load() {
        let decision = JsDecisionSnapshot::new("persist_fn")
            .with_module("persist_mod")
            .add_tag("key", "value");

        let mut storage = JsMemoryStorage::new();
        let id = storage.save_decision(&decision);

        assert!(id.starts_with("decision_"));

        let loaded = storage.load_decision(&id).unwrap();
        assert_eq!(loaded.inner.function_name, "persist_fn");
        assert_eq!(loaded.inner.module_name, Some("persist_mod".to_string()));
        assert_eq!(loaded.inner.tags.get("key"), Some(&"value".to_string()));
    }

    #[test]
    #[cfg(target_arch = "wasm32")]
    fn test_js_memory_storage_load_nonexistent() {
        let storage = JsMemoryStorage::new();
        let result = storage.load_decision("nonexistent_id");
        assert!(result.is_err());
    }

    #[test]
    fn test_js_memory_storage_multiple_decisions() {
        let mut storage = JsMemoryStorage::new();

        let d1 = JsDecisionSnapshot::new("fn_1");
        let d2 = JsDecisionSnapshot::new("fn_2");
        let d3 = JsDecisionSnapshot::new("fn_3");

        let id1 = storage.save_decision(&d1);
        let id2 = storage.save_decision(&d2);
        let id3 = storage.save_decision(&d3);

        // All IDs should be unique
        assert_ne!(id1, id2);
        assert_ne!(id2, id3);
        assert_ne!(id1, id3);

        // All should be loadable
        assert_eq!(
            storage.load_decision(&id1).unwrap().inner.function_name,
            "fn_1"
        );
        assert_eq!(
            storage.load_decision(&id2).unwrap().inner.function_name,
            "fn_2"
        );
        assert_eq!(
            storage.load_decision(&id3).unwrap().inner.function_name,
            "fn_3"
        );
    }

    #[test]
    fn test_js_memory_storage_default_trait() {
        let storage = JsMemoryStorage::default();
        assert!(storage.health_check());
    }
}