Skip to main content

briefcase_wasm/
lib.rs

1//! # Briefcase AI WebAssembly Bindings
2//!
3//! High-performance AI observability and decision tracking for JavaScript and TypeScript applications
4//! running in browsers or Node.js environments.
5//!
6//! ## Features
7//!
8//! - **AI Decision Tracking**: Capture inputs, outputs, and context for every AI decision
9//! - **High Performance**: Rust-powered WebAssembly for maximum speed
10//! - **Browser Compatible**: Works in all modern browsers and Node.js
11//! - **Sync APIs**: Simplified synchronous APIs optimized for WebAssembly
12//! - **TypeScript Support**: Full type definitions included
13//! - **Zero Dependencies**: No external JavaScript dependencies required
14//!
15//! ## Installation
16//!
17//! ```bash
18//! npm install briefcase-wasm
19//! ```
20//!
21//! ## Usage
22//!
23//! ```javascript
24//! import { init, DecisionSnapshot, Input, Output } from 'briefcase-wasm';
25//!
26//! // Initialize the WASM module
27//! await init();
28//!
29//! // Create a decision snapshot
30//! const decision = new DecisionSnapshot("ai_function");
31//! const input = new Input("query", "Hello world", "string");
32//! const output = new Output("response", "Hello back!", "string");
33//!
34//! decision.addInput(input);
35//! decision.addOutput(output);
36//! ```
37//!
38//! ## Browser Usage
39//!
40//! For browser environments, you may need to configure your bundler to handle WASM files.
41//! Most modern bundlers like Vite, Webpack 5, and Parcel support WebAssembly out of the box.
42//!
43//! ## Node.js Usage
44//!
45//! The package works seamlessly in Node.js environments with ES modules or CommonJS.
46
47use briefcase_core::models::{DecisionSnapshot, Input, Output};
48use std::collections::HashMap;
49use wasm_bindgen::prelude::*;
50use web_sys::console;
51
52// Set up panic hook for better error messages in WASM
53#[cfg(feature = "console_error_panic_hook")]
54#[wasm_bindgen(start)]
55pub fn main() {
56    console_error_panic_hook::set_once();
57}
58
59/// Macro for console logging from WASM
60macro_rules! log {
61    ( $( $t:tt )* ) => {
62        console::log_1(&format!( $( $t )* ).into());
63    }
64}
65
66/// Initialize the Briefcase AI WASM module
67///
68/// This function sets up panic hooks for better error reporting in WebAssembly environments.
69/// Call this function once at the start of your application.
70///
71/// # Examples
72///
73/// ```javascript
74/// import { init } from 'briefcase-wasm';
75///
76/// await init();
77/// console.log('Briefcase AI ready!');
78/// ```
79#[wasm_bindgen]
80pub fn init() {
81    #[cfg(feature = "console_error_panic_hook")]
82    console_error_panic_hook::set_once();
83
84    log!("🦀 Briefcase AI WASM module initialized!");
85}
86
87/// JavaScript-friendly wrapper for DecisionSnapshot
88///
89/// Represents a snapshot of an AI decision with inputs, outputs, and metadata.
90/// This is the main data structure for tracking AI decisions in JavaScript/TypeScript.
91///
92/// # Examples
93///
94/// ```javascript
95/// import { JsDecisionSnapshot, JsInput, JsOutput } from 'briefcase-wasm';
96///
97/// const decision = new JsDecisionSnapshot("my_ai_function");
98/// const input = new JsInput("user_query", "Hello", "string");
99/// const output = new JsOutput("ai_response", "Hi there!", "string");
100///
101/// decision.add_input(input);
102/// decision.add_output(output);
103/// decision.set_execution_time(123.5);
104/// ```
105#[wasm_bindgen]
106pub struct JsDecisionSnapshot {
107    inner: DecisionSnapshot,
108}
109
110#[wasm_bindgen]
111impl JsDecisionSnapshot {
112    #[wasm_bindgen(constructor)]
113    pub fn new(function_name: &str) -> JsDecisionSnapshot {
114        JsDecisionSnapshot {
115            inner: DecisionSnapshot::new(function_name),
116        }
117    }
118
119    #[wasm_bindgen]
120    pub fn with_module(mut self, module_name: &str) -> JsDecisionSnapshot {
121        self.inner = self.inner.with_module(module_name);
122        self
123    }
124
125    #[wasm_bindgen]
126    pub fn add_input(
127        mut self,
128        name: &str,
129        value: &JsValue,
130        data_type: &str,
131    ) -> Result<JsDecisionSnapshot, JsError> {
132        let json_value: serde_json::Value = serde_wasm_bindgen::from_value(value.clone())
133            .map_err(|e| JsError::new(&format!("Failed to convert input value: {}", e)))?;
134
135        let input = Input::new(name, json_value, data_type);
136        self.inner = self.inner.add_input(input);
137        Ok(self)
138    }
139
140    #[wasm_bindgen]
141    pub fn add_output(
142        mut self,
143        name: &str,
144        value: &JsValue,
145        data_type: &str,
146    ) -> Result<JsDecisionSnapshot, JsError> {
147        let json_value: serde_json::Value = serde_wasm_bindgen::from_value(value.clone())
148            .map_err(|e| JsError::new(&format!("Failed to convert output value: {}", e)))?;
149
150        let output = Output::new(name, json_value, data_type);
151        self.inner = self.inner.add_output(output);
152        Ok(self)
153    }
154
155    #[wasm_bindgen]
156    pub fn add_tag(mut self, key: &str, value: &str) -> JsDecisionSnapshot {
157        self.inner = self.inner.add_tag(key, value);
158        self
159    }
160
161    #[wasm_bindgen]
162    pub fn to_json(&self) -> Result<JsValue, JsError> {
163        serde_wasm_bindgen::to_value(&self.inner)
164            .map_err(|e| JsError::new(&format!("Failed to serialize decision: {}", e)))
165    }
166}
167
168/// Simple in-memory storage for demo purposes
169#[wasm_bindgen]
170pub struct JsMemoryStorage {
171    decisions: HashMap<String, DecisionSnapshot>,
172}
173
174impl Default for JsMemoryStorage {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180#[wasm_bindgen]
181impl JsMemoryStorage {
182    #[wasm_bindgen(constructor)]
183    pub fn new() -> JsMemoryStorage {
184        JsMemoryStorage {
185            decisions: HashMap::new(),
186        }
187    }
188
189    #[wasm_bindgen]
190    pub fn save_decision(&mut self, decision: &JsDecisionSnapshot) -> String {
191        let id = format!("decision_{}", uuid::Uuid::new_v4());
192        self.decisions.insert(id.clone(), decision.inner.clone());
193        id
194    }
195
196    #[wasm_bindgen]
197    pub fn load_decision(&self, decision_id: &str) -> Result<JsDecisionSnapshot, JsError> {
198        let decision = self
199            .decisions
200            .get(decision_id)
201            .ok_or_else(|| JsError::new(&format!("Decision not found: {}", decision_id)))?;
202
203        Ok(JsDecisionSnapshot {
204            inner: decision.clone(),
205        })
206    }
207
208    #[wasm_bindgen]
209    pub fn health_check(&self) -> bool {
210        true
211    }
212}
213
214/// Utility function to get the version of the WASM module
215#[wasm_bindgen]
216pub fn version() -> String {
217    env!("CARGO_PKG_VERSION").to_string()
218}
219
220/// Utility function for testing WASM functionality
221#[wasm_bindgen]
222pub fn test_functionality() -> Result<JsValue, JsError> {
223    // Create a test decision
224    let decision = JsDecisionSnapshot::new("test_function")
225        .with_module("test_module")
226        .add_tag("env", "test");
227
228    // Test storage
229    let mut storage = JsMemoryStorage::new();
230    let decision_id = storage.save_decision(&decision);
231    let _loaded_decision = storage.load_decision(&decision_id)?;
232
233    let result = serde_json::json!({
234        "decision_id": decision_id,
235        "storage_health": storage.health_check(),
236        "version": version(),
237    });
238
239    serde_wasm_bindgen::to_value(&result)
240        .map_err(|e| JsError::new(&format!("Failed to serialize test result: {}", e)))
241}
242
243// Authenticated client (pre-validated for WASM)
244pub mod client;
245
246// Re-export modules so they're accessible
247pub mod cost;
248pub mod drift;
249pub mod models;
250pub mod sanitization;
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_version_returns_package_version() {
258        let v = version();
259        assert!(!v.is_empty());
260        // Should be a valid semver-like string
261        assert!(v.contains('.'), "Version '{}' should contain dots", v);
262    }
263
264    #[test]
265    fn test_js_decision_snapshot_construction() {
266        let decision = JsDecisionSnapshot::new("test_fn");
267        assert_eq!(decision.inner.function_name, "test_fn");
268        assert!(decision.inner.module_name.is_none());
269    }
270
271    #[test]
272    fn test_js_decision_snapshot_with_module() {
273        let decision = JsDecisionSnapshot::new("test_fn").with_module("test_mod");
274        assert_eq!(decision.inner.module_name, Some("test_mod".to_string()));
275    }
276
277    #[test]
278    fn test_js_decision_snapshot_add_tag() {
279        let decision = JsDecisionSnapshot::new("test_fn")
280            .add_tag("env", "production")
281            .add_tag("version", "1.0");
282        assert_eq!(
283            decision.inner.tags.get("env"),
284            Some(&"production".to_string())
285        );
286        assert_eq!(decision.inner.tags.get("version"), Some(&"1.0".to_string()));
287    }
288
289    #[test]
290    fn test_js_memory_storage_new() {
291        let storage = JsMemoryStorage::new();
292        assert!(storage.health_check());
293    }
294
295    #[test]
296    fn test_js_memory_storage_save_and_load() {
297        let decision = JsDecisionSnapshot::new("persist_fn")
298            .with_module("persist_mod")
299            .add_tag("key", "value");
300
301        let mut storage = JsMemoryStorage::new();
302        let id = storage.save_decision(&decision);
303
304        assert!(id.starts_with("decision_"));
305
306        let loaded = storage.load_decision(&id).unwrap();
307        assert_eq!(loaded.inner.function_name, "persist_fn");
308        assert_eq!(loaded.inner.module_name, Some("persist_mod".to_string()));
309        assert_eq!(loaded.inner.tags.get("key"), Some(&"value".to_string()));
310    }
311
312    #[test]
313    #[cfg(target_arch = "wasm32")]
314    fn test_js_memory_storage_load_nonexistent() {
315        let storage = JsMemoryStorage::new();
316        let result = storage.load_decision("nonexistent_id");
317        assert!(result.is_err());
318    }
319
320    #[test]
321    fn test_js_memory_storage_multiple_decisions() {
322        let mut storage = JsMemoryStorage::new();
323
324        let d1 = JsDecisionSnapshot::new("fn_1");
325        let d2 = JsDecisionSnapshot::new("fn_2");
326        let d3 = JsDecisionSnapshot::new("fn_3");
327
328        let id1 = storage.save_decision(&d1);
329        let id2 = storage.save_decision(&d2);
330        let id3 = storage.save_decision(&d3);
331
332        // All IDs should be unique
333        assert_ne!(id1, id2);
334        assert_ne!(id2, id3);
335        assert_ne!(id1, id3);
336
337        // All should be loadable
338        assert_eq!(
339            storage.load_decision(&id1).unwrap().inner.function_name,
340            "fn_1"
341        );
342        assert_eq!(
343            storage.load_decision(&id2).unwrap().inner.function_name,
344            "fn_2"
345        );
346        assert_eq!(
347            storage.load_decision(&id3).unwrap().inner.function_name,
348            "fn_3"
349        );
350    }
351
352    #[test]
353    fn test_js_memory_storage_default_trait() {
354        let storage = JsMemoryStorage::default();
355        assert!(storage.health_check());
356    }
357}