Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WASM Guide

Overview

Rumpsteak’s choreographic programming system compiles to WebAssembly. The core library and effect handlers work in browser environments.

WASM support enables choreographic protocols in web applications, browser-based distributed systems, and serverless edge computing.

What Works in WASM

The following features compile and run in WASM:

Core session types and choreography system work fully. The InMemoryHandler provides local message passing for testing protocols. RumpsteakHandler now compiles for WASM and can be used with custom network transports (WebSocket, fetch API, etc.). All middleware (Trace, Metrics, Retry, FaultInjection) functions correctly. Effect system and interpreter execute normally. Timeouts use wasm-timer for cross-platform support.

What Does Not Work in WASM

Multi-threading is unavailable since WASM runs single-threaded.

Native file system access requires browser File APIs.

Building for WASM

Add the wasm feature to your dependencies:

[dependencies]
rumpsteak-aura-choreography = { version = "0.7", features = ["wasm"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"

Build using wasm-pack:

wasm-pack build --target web

This generates a pkg directory with JavaScript bindings.

Example Protocol

The wasm-ping-pong example demonstrates a complete browser protocol:

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
use rumpsteak_aura_choreography::{InMemoryHandler, Program, interpret};

#[wasm_bindgen]
pub async fn run_protocol(message: String) -> Result<String, JsValue> {
    let mut handler = InMemoryHandler::new(Role::Alice);
    
    let program = Program::new()
        .send(Role::Bob, Message::Ping(message))
        .recv::<Message>(Role::Bob)
        .end();
    
    let mut endpoint = ();
    let result = interpret(&mut handler, &mut endpoint, program).await
        .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?;
    
    match result.received_values.first() {
        Some(Message::Pong(response)) => Ok(response.clone()),
        _ => Err(JsValue::from_str("Expected Pong message")),
    }
}
}

The wasm_bindgen attribute exposes the function to JavaScript.

Build and run the example:

cd examples/wasm-ping-pong
./build.sh
python3 -m http.server 8000

Open http://localhost:8000 in a browser to run the protocol.

Using RumpsteakHandler in WASM

RumpsteakHandler now compiles for WASM. To use it with real network transport:

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;
use rumpsteak_aura_choreography::{RumpsteakHandler, RumpsteakEndpoint, SimpleChannel};

#[wasm_bindgen]
pub async fn run_distributed_protocol() -> Result<(), JsValue> {
    // Create endpoints
    let mut alice_ep = RumpsteakEndpoint::new(Role::Alice);
    let mut bob_ep = RumpsteakEndpoint::new(Role::Bob);
    
    // Option 1: use SimpleChannel (works natively and in WASM)
    let (alice_ch, bob_ch) = SimpleChannel::pair();
    alice_ep.register_channel(Role::Bob, alice_ch);
    bob_ep.register_channel(Role::Alice, bob_ch);

    // Option 2: wrap browser transports directly
    // let ws_session = RumpsteakSession::from_sink_stream(ws_writer, ws_reader);
    // alice_ep.register_session(Role::Bob, ws_session);
    
    // Create handler
    let mut handler = RumpsteakHandler::new();
    
    // Use with choreography operations
    handler.send(&mut alice_ep, Role::Bob, &message).await?;
    let response = handler.recv(&mut bob_ep, Role::Alice).await?;
    
    Ok(())
}
}

SimpleChannel uses futures::channel::mpsc which is WASM-compatible. For distributed WASM applications, implement custom channels using browser APIs.

Async traits in WASM

The handler traits continue to use the async_trait macro rather than native async fn in traits. This keeps the traits object-safe (needed for middleware stacks such as Trace<Retry<H>>) and lets us share a single implementation strategy across native and WASM targets. The generated futures are still Send, so handlers that run under multithreaded executors behave the same way as handlers compiled for single-threaded WASM.

Custom Network Transport

InMemoryHandler works for single-context protocols. Real distributed WASM applications need network transport.

Implement ChoreoHandler with WebSocket or fetch APIs:

#![allow(unused)]
fn main() {
use web_sys::WebSocket;
use wasm_bindgen::JsCast;

pub struct WebSocketHandler {
    role: Role,
    socket: WebSocket,
    incoming: mpsc::UnboundedReceiver<Vec<u8>>,
}

impl WebSocketHandler {
    pub fn new(role: Role, url: &str) -> Result<Self, JsValue> {
        let socket = WebSocket::new(url)?;
        let (tx, rx) = mpsc::unbounded();
        
        let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| {
            // Handle incoming messages
            if let Ok(buffer) = e.data().dyn_into::<js_sys::ArrayBuffer>() {
                let bytes = js_sys::Uint8Array::new(&buffer).to_vec();
                tx.unbounded_send(bytes).ok();
            }
        }) as Box<dyn FnMut(MessageEvent)>);
        
        socket.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
        onmessage.forget();
        
        Ok(Self { role, socket, incoming: rx })
    }
}
}

With the Phase 3 handler you can skip the custom ChoreoHandler. Wrap this WebSocketHandler with RumpsteakSession::from_sink_stream and register it on the endpoint instead.

For HTTP-based protocols, use the fetch API:

#![allow(unused)]
fn main() {
use web_sys::window;

async fn send_http(to: Role, msg: &Message) -> Result<()> {
    let window = window().ok_or("no window")?;
    let resp = JsFuture::from(
        window.fetch_with_str(&format!("http://server/{}", to))
    ).await?;
    Ok(())
}
}

The pattern is the same: implement ChoreoHandler using browser APIs for your transport.

Platform Differences

The runtime module provides platform-specific functions:

#![allow(unused)]
fn main() {
use rumpsteak_aura_choreography::runtime::{spawn, spawn_local};

spawn(async { /* task */ });  // Works on native and WASM
}

On native targets, spawn uses tokio. On WASM, it uses wasm-bindgen-futures.

Timeouts use conditional compilation:

#![allow(unused)]
fn main() {
// Native
tokio::time::timeout(duration, future).await

// WASM
use wasm_timer::Delay;
futures::select! {
    result = future => result,
    _ = Delay::new(duration) => Err(Timeout),
}
}

The library handles this automatically. Your code works on both platforms.

Testing in WASM

Use wasm-bindgen-test for browser tests:

#![allow(unused)]
fn main() {
use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
async fn test_protocol() {
    let mut handler = InMemoryHandler::new(Role::Alice);
    let program = Program::new().send(Role::Bob, Message::Test).end();
    let mut endpoint = ();
    let result = interpret(&mut handler, &mut endpoint, program).await;
    assert!(result.is_ok());
}
}

Run tests with wasm-pack:

wasm-pack test --headless --chrome

The tests execute in a headless browser.

Deployment

For production WASM deployments:

Build with release optimizations:

wasm-pack build --target web --release

The generated WASM is optimized for size and performance.

Serve the pkg directory with your web application. Import the JavaScript bindings:

<script type="module">
    import init, { run_protocol } from './pkg/my_protocol.js';
    
    async function main() {
        await init();
        const result = await run_protocol("data");
        console.log(result);
    }
    
    main();
</script>

The init function loads the WASM module. Protocol functions then become available.

Browser Compatibility

WASM support works in modern browsers:

Chrome/Chromium 90+, Firefox 88+, Safari 14+, Edge 90+.

Older browsers may require polyfills or transpilation.

Performance

WASM execution is near-native speed for computation. Network operations have typical browser fetch/WebSocket latency.

InMemoryHandler has negligible overhead in WASM. Custom network handlers add latency from browser APIs.

The generated WASM binary size is approximately 200-300KB after optimization.

Limitations

WASM is single-threaded. Parallel protocol execution uses async concurrency, not OS threads.

Browser security restricts some operations. Cross-origin requests need CORS configuration. WebSocket connections require proper server setup.

File system access uses browser APIs which differ from native file operations.

Example Projects

The examples/wasm-ping-pong directory contains a complete working example.

See the build script for compilation details and the README for deployment instructions.