WASM Guide
This guide explains how to build and run the choreography runtime on wasm32. It covers feature flags, build tooling, handler compatibility, and testing.
Overview
The telltale-runtime crate supports WASM targets. Core effects, handlers, and timeouts compile under wasm32 using wasm-bindgen-futures and wasm-timer. The same Program and interpret API used on native targets works without modification. Platform differences are handled internally by the runtime module.
What Works
In WASM builds you can use Program, interpret, and effect handlers. InMemoryHandler and TelltaleHandler are WASM compatible for local or custom transports. Middleware such as Trace, Metrics, and Retry is WASM compatible. FaultInjection is available with the test-utils feature.
Protocol definitions written with tell! produce the same projected types on
both platforms when the protocol is session-projectable. The proof-status and
effect surfaces are platform-agnostic. Only the lowest-level spawn and timer
calls differ between native and WASM.
Limitations
WASM is single threaded. Concurrency is async only. Direct std::net sockets are not available. Network transports must use browser APIs or host provided bindings.
Tokio-specific features such as tokio::spawn are not available. Use the runtime::spawn abstraction instead. File system access is also unavailable unless the host provides a binding.
Enable WASM
Enable the wasm feature on the choreography crate.
[dependencies]
telltale-runtime = { version = "11.3.0", features = ["wasm"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
The wasm feature enables getrandom support and pulls in the WASM runtime dependencies. Use a path dependency only for local workspace development.
Build with wasm-pack or cargo targets. wasm-pack produces a ready-to-use JavaScript package. Direct cargo build --target wasm32-unknown-unknown works for library crates that do not need JS bindings.
For reproducible local setup, install the same tool version used in CI.
cargo install wasm-pack --version 0.14.0 --locked
This installs the same wasm-pack version used in CI for reproducible builds.
wasm-pack build --target web
This produces a pkg directory with JavaScript bindings.
Minimal Example
This example runs a simple request response program using InMemoryHandler. It defines the role, label, and message types needed by the effect system. It then builds two programs and runs them concurrently with shared channels.
The first section defines the Role and Label enums. LabelId requires string round-tripping via as_str and from_str. RoleId associates a label type and provides canonical names through role_name.
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use telltale_runtime::{interpret, InMemoryHandler, LabelId, Program, RoleId, RoleName};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Role {
Client,
Server,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Label {
Ok,
}
impl LabelId for Label {
fn as_str(&self) -> &'static str {
match self {
Label::Ok => "ok",
}
}
fn from_str(label: &str) -> Option<Self> {
match label {
"ok" => Some(Label::Ok),
_ => None,
}
}
}
impl RoleId for Role {
type Label = Label;
fn role_name(&self) -> RoleName {
match self {
Role::Client => RoleName::from_static("Client"),
Role::Server => RoleName::from_static("Server"),
}
}
}
}
The Message enum carries the payload variants exchanged between roles. Both variants use String here, but any Serialize + Deserialize type works.
#![allow(unused)]
fn main() {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
enum Message {
Ping(String),
Pong(String),
}
}
The canonical WASM example now lives in examples/wasm/ and uses
tell! plus generated effect traits as the public surface. Browser-facing
code should keep protocol structure in the DSL and host integration in the
generated Protocol::effects boundary, rather than manually assembling
low-level effect programs. Use ./harness.sh build to emit the browser
package, ./harness.sh smoke for a deterministic Node-based end-to-end
check, and ./harness.sh run to build, verify, and serve the demo locally.
For multi role tests, share channels by using InMemoryHandler::with_channels and a shared channel map. The WASM test suite in rust/runtime/tests/wasm_integration.rs shows larger examples. Each handler must reference the same Arc<Mutex<BTreeMap>> instances for messages to route correctly. The unit endpoint () is sufficient when no external state is needed.
TelltaleHandler in WASM
TelltaleHandler works in WASM with TelltaleSession pairs or custom sessions. This handler manages typed endpoints and routes messages through registered sessions. It is the recommended handler for integration-level WASM tests.
#![allow(unused)]
fn main() {
use telltale_runtime::{TelltaleEndpoint, TelltaleHandler, TelltaleSession};
let (alice_session, bob_session) = TelltaleSession::pair();
let mut alice_ep = TelltaleEndpoint::new(Role::Client);
let mut bob_ep = TelltaleEndpoint::new(Role::Server);
alice_ep.register_session(Role::Server, alice_session);
bob_ep.register_session(Role::Client, bob_session);
let mut handler = TelltaleHandler::<Role, Message>::new();
}
Use your protocol message type for Message. The role must implement both telltale::Role and RoleId. For browser transports, build a TelltaleSession from a sink and stream. Register it with TelltaleEndpoint::register_session.
Runtime Utilities
The runtime provides WASM aware task spawning helpers. These abstractions let the same protocol code run on both native and browser targets without conditional compilation at the call site.
#![allow(unused)]
fn main() {
use telltale_runtime::runtime::spawn;
spawn(async move {
// task body
});
}
spawn uses Tokio on native targets and wasm_bindgen_futures::spawn_local on WASM.
Testing
Use wasm-bindgen-test for WASM tests. Import the crate and annotate test functions with #[wasm_bindgen_test]. Tests marked with #[wasm_bindgen_test(unsupported = test)] run as standard #[test] on native targets and as WASM tests on wasm32.
#![allow(unused)]
fn main() {
use wasm_bindgen_test::*;
}
Repository-managed tests run under Node, not a browser driver. This avoids the need for a headless browser in CI.
just wasm-test-all
For ad hoc crate-level runs, use wasm-pack test --node. This compiles the crate for wasm32 and executes tests in a Node environment.
See Choreography Effect Handlers for handler details. See Using Telltale Handlers for the channel based API.