WebAssembly Runtime
EvidentSource uses WebAssembly (WASM) as its extensibility mechanism, allowing users to write custom business logic in any language that compiles to WebAssembly. This provides a secure, performant, and language-agnostic way to extend the platform.
Why WebAssembly?
Section titled “Why WebAssembly?”Key Benefits
Section titled “Key Benefits”- Language Agnostic: Write components in Rust, Go, C++, AssemblyScript, or any WASM-targeting language
- Sandboxed Execution: Components run in isolation with no access to host resources
- Predictable Performance: Near-native execution speed with deterministic behavior
- Portable: Same component works across all platforms and deployments
- Version Compatible: Components remain compatible across EvidentSource versions
Use Cases in EvidentSource
Section titled “Use Cases in EvidentSource”- State Views: Transform events into materialized views
- State Changes: Process commands and emit events
- Custom Validators: Implement domain-specific validation
- Data Transformations: Convert between formats
- Business Rules: Encode complex domain logic
Runtime Architecture
Section titled “Runtime Architecture”Component Model
Section titled “Component Model”EvidentSource uses the WebAssembly Component Model (WASI Preview 2):
graph TB subgraph "EvidentSource Host" A[WASM Runtime] B[Component Registry] C[Resource Manager] D[Security Sandbox] end
subgraph "WASM Component" E[Exports] F[Imports] G[Linear Memory] H[Component State] end
A --> E F --> A A --> G D --> G B --> A C --> AWasmtime Integration
Section titled “Wasmtime Integration”EvidentSource uses Wasmtime, a production-ready WebAssembly runtime:
// Runtime configurationlet config = Config::new();config.wasm_component_model(true);config.async_support(true);config.consume_fuel(true);config.epoch_interruption(true);
let engine = Engine::new(&config)?;let linker = Linker::new(&engine);
// Component instantiationlet component = Component::from_file(&engine, "state-view.wasm")?;let instance = linker.instantiate(&mut store, &component)?;Component Interfaces
Section titled “Component Interfaces”State View Interface
Section titled “State View Interface”State Views implement this WIT interface:
package evident:db@0.1.0;
interface state-view { use types.{event, state};
// Core reduce function reduce: func( state: state, event: event, parameters: list<tuple<string, string>> ) -> result<state, string>;
// Optional: Initialize state initialize: func( parameters: list<tuple<string, string>> ) -> result<state, string>;}State Change Interface
Section titled “State Change Interface”State Changes implement this interface:
package evident:db@0.1.0;
interface state-change { use types.{event, state, command};
// Process command and emit events process: func( state: state, command: command, parameters: list<tuple<string, string>> ) -> result<list<event>, error>;
// Validate command without side effects validate: func( state: state, command: command ) -> result<bool, string>;}Type Definitions
Section titled “Type Definitions”Common types used across interfaces:
interface types { // CloudEvent representation record event { id: string, source: string, spec-version: string, type: string, datacontenttype: option<string>, dataschema: option<string>, subject: option<string>, time: option<string>, data: option<list<u8>>, extensions: list<tuple<string, string>>, }
// State representation record state { content-type: string, schema-id: option<string>, bytes: list<u8>, }
// Command representation record command { content-type: string, schema-id: option<string>, bytes: list<u8>, }
// Error information record error { code: string, message: string, details: option<list<u8>>, }}Writing Components
Section titled “Writing Components”Rust Example
Section titled “Rust Example”A State View that counts events by type:
use wit_bindgen::generate;
generate!({ world: "state-view", exports: { "evident:db/state-view": Component, },});
use exports::evident::db::state_view::{Event, State};use serde::{Deserialize, Serialize};use std::collections::HashMap;
#[derive(Serialize, Deserialize, Default)]struct EventCounts { by_type: HashMap<String, u64>, total: u64,}
struct Component;
impl exports::evident::db::state_view::Guest for Component { fn reduce( state: State, event: Event, _parameters: Vec<(String, String)>, ) -> Result<State, String> { // Deserialize current state let mut counts: EventCounts = if state.bytes.is_empty() { EventCounts::default() } else { serde_json::from_slice(&state.bytes) .map_err(|e| format!("Failed to parse state: {}", e))? };
// Update counts counts.total += 1; *counts.by_type.entry(event.type_.clone()).or_insert(0) += 1;
// Serialize and return new state let bytes = serde_json::to_vec(&counts) .map_err(|e| format!("Failed to serialize: {}", e))?;
Ok(State { content_type: "application/json".to_string(), schema_id: None, bytes, }) }
fn initialize( _parameters: Vec<(String, String)>, ) -> Result<State, String> { let counts = EventCounts::default(); let bytes = serde_json::to_vec(&counts) .map_err(|e| format!("Failed to serialize: {}", e))?;
Ok(State { content_type: "application/json".to_string(), schema_id: None, bytes, }) }}Building Components
Section titled “Building Components”Component development requires the EvidentSource SDK (available in the customer portal). The SDK includes:
- Component templates for various languages
- Build tools and optimizers
- Testing framework
- Documentation and examples
For custom component development:
- Download the SDK from the customer portal
- Follow the included development guide
- Use provided build tools for optimization
- Test with the included test harness
Professional services are available for custom component development.
Runtime Features
Section titled “Runtime Features”Memory Management
Section titled “Memory Management”Components have isolated linear memory:
- Initial Size: 1MB default
- Maximum Size: 4GB limit
- Growth: Automatic based on usage
- Isolation: No shared memory between components
// Configure memory limitslet mut store = Store::new(&engine, ());store.limiter(|_| &mut StoreLimits { memory_size: 100 * 1024 * 1024, // 100MB max table_elements: 10000, instances: 10, memories: 1,});Resource Limits
Section titled “Resource Limits”Prevent runaway components:
// Fuel-based execution limitsstore.add_fuel(1_000_000)?; // 1M fuel unitslet remaining = store.consume_fuel(0)?;
// Time-based limitsstore.set_epoch_deadline(1); // 1 epoch tickengine.increment_epoch(); // In separate threadError Handling
Section titled “Error Handling”Components can return errors:
fn reduce(state: State, event: Event) -> Result<State, String> { if event.data.is_none() { return Err("Event missing required data".to_string()); }
// Process event... Ok(new_state)}Host handles errors gracefully:
match instance.call_reduce(&mut store, &state, &event) { Ok(new_state) => { // Update state } Err(e) => { // Log error, maintain previous state error!("Component error: {}", e); }}Security Model
Section titled “Security Model”Capability-Based Security
Section titled “Capability-Based Security”Components have no capabilities by default:
- ❌ No filesystem access
- ❌ No network access
- ❌ No system calls
- ❌ No random number generation
- ✅ Only computation on provided data
Sandboxing
Section titled “Sandboxing”Multiple layers of isolation:
- Memory Isolation: Separate linear memory
- Stack Isolation: Independent call stacks
- Resource Limits: CPU and memory bounds
- Type Safety: Interface type checking
Trust Boundaries
Section titled “Trust Boundaries”graph LR subgraph "Trusted" A[EvidentSource Core] B[Storage Layer] end
subgraph "Untrusted" C[User Component] D[Component Memory] end
A -->|"Controlled API"| C C -->|"Return Values Only"| A C --> DPerformance
Section titled “Performance”Execution Performance
Section titled “Execution Performance”Near-native performance characteristics:
- Startup: ~1ms component instantiation
- Execution: 80-95% of native speed
- Memory: Efficient linear memory model
- Compilation: AOT compilation via Wasmtime
Optimization Strategies
Section titled “Optimization Strategies”- Minimize Allocations: Reuse buffers
- Batch Operations: Process multiple items
- Efficient Serialization: Use binary formats
- Avoid Syscalls: Pure computation only
Example optimization:
// Inefficient: Allocates on every callfn process(data: Vec<u8>) -> Vec<u8> { let parsed = parse(&data); // Allocation let result = transform(parsed); // Allocation serialize(result) // Allocation}
// Efficient: Reuse buffersfn process(data: &[u8], output: &mut Vec<u8>) { output.clear(); transform_in_place(data, output); // No allocations}Benchmarking
Section titled “Benchmarking”Measure component performance:
#[cfg(test)]mod benches { use criterion::{criterion_group, Criterion};
fn bench_reduce(c: &mut Criterion) { c.bench_function("reduce", |b| { b.iter(|| { component.reduce(state.clone(), event.clone()) }); }); }
criterion_group!(benches, bench_reduce);}Debugging Components
Section titled “Debugging Components”Logging
Section titled “Logging”Components can emit logs via imports:
interface logging { log: func(level: log-level, message: string);
enum log-level { error, warn, info, debug, trace, }}Usage in component:
logging::log(LogLevel::Info, &format!("Processing event: {}", event.id));Development Tools
Section titled “Development Tools”-
WABT: WebAssembly Binary Toolkit
Terminal window wasm2wat component.wasm > component.watwasm-validate component.wasm -
Wasmtime CLI: Testing and debugging
Terminal window wasmtime run --invoke reduce component.wasm -
Component Inspector: Analyze components
Terminal window wasm-tools component wit component.wasm
Best Practices
Section titled “Best Practices”Component Design
Section titled “Component Design”- Stateless Functions: Avoid global state
- Deterministic: Same input → same output
- Error Handling: Return meaningful errors
- Efficient Serialization: Choose appropriate formats
- Version Compatibility: Plan for schema evolution
Testing Components
Section titled “Testing Components”Comprehensive test coverage:
#[test]fn test_reduce_empty_state() { let state = State::default(); let event = create_test_event();
let result = Component::reduce(state, event, vec![]).unwrap();
assert_eq!(result.content_type, "application/json"); let counts: EventCounts = serde_json::from_slice(&result.bytes).unwrap(); assert_eq!(counts.total, 1);}
#[test]fn test_reduce_error_handling() { let invalid_state = State { bytes: b"invalid json".to_vec(), ..Default::default() };
let result = Component::reduce(invalid_state, event, vec![]); assert!(result.is_err());}Deployment Considerations
Section titled “Deployment Considerations”- Component Size: Keep under 10MB
- Memory Usage: Monitor and limit
- CPU Usage: Set appropriate fuel limits
- Error Rates: Monitor and alert
- Version Management: Track deployed versions
Future Enhancements
Section titled “Future Enhancements”Planned Features
Section titled “Planned Features”- Component Composition: Combine multiple components
- Async Support: Non-blocking I/O operations
- WASI Preview 3: Enhanced capabilities
- Component Registry: Discover and share components
- Hot Reloading: Update without downtime
Component Ecosystem
Section titled “Component Ecosystem”Building a rich ecosystem:
- Standard Library: Common utilities
- Component Templates: Quick start examples
- Testing Framework: Component test harness
- Performance Profiler: Optimization tools
- Component Marketplace: Share components
Next Steps
Section titled “Next Steps”- Learn to write State Views
- Explore State Changes
- Download the SDK from the Customer Portal
- Read WASM Component Model specification