WASM Interface
Views are WebAssembly components that implement the primatomic:machine interface. This document specifies the interface contract and requirements for view implementations.
Interface Definition (WIT)
Section titled “Interface Definition (WIT)”Fetch the WIT interface using wkg:
This downloads the interface files into your project’s wit/ directory. The interface defines five functions:
package primatomic:machine@0.0.1;
interface machine { /// Initialize the state machine. /// Called once before any append/query. init: func();
/// Apply a single log entry to the state machine. append: func(event: list<u8>);
/// Run a query against the current in-memory state. /// Input and output formats are up to the guest. query: func(input: list<u8>) -> list<u8>;
/// Serialize the current state into a snapshot. /// Host persists this blob and passes it back to `restore`. snapshot: func() -> list<u8>;
/// Restore the state machine from a previously-produced snapshot. /// Must fully replace any existing in-memory state. restore: func(snapshot: list<u8>);}
world state-machine { export machine;}Function Contracts
Section titled “Function Contracts”init()
Section titled “init()”Initializes the state machine.
| Aspect | Specification |
|---|---|
| Invocation | Called exactly once before any append or query calls |
| Side Effects | Should initialize internal state to a known starting point |
| Return | None |
append(event: list<u8>)
Section titled “append(event: list<u8>)”Applies a single log event to the state machine.
| Aspect | Specification |
|---|---|
| Invocation | Called for each event in sequence order |
| Ordering | Events are delivered in strictly ascending sequence order |
| Delivery | Each event is delivered exactly once during normal operation |
| Input | Raw event bytes as appended to the log |
| Side Effects | May update internal state based on the event (handlers may ignore certain events) |
| Return | None |
query(input: list<u8>) -> list<u8>
Section titled “query(input: list<u8>) -> list<u8>”Queries the current state.
| Aspect | Specification |
|---|---|
| Concurrency | May be called concurrently with append |
| Input | Query parameters; format is defined by the view implementation |
| Output | Query result; format is defined by the view implementation |
| Side Effects | Does not modify state |
| Determinism | Given the same state and input, returns the same output |
snapshot() -> list<u8>
Section titled “snapshot() -> list<u8>”Serializes the current state for persistence.
| Aspect | Specification |
|---|---|
| Invocation | May be called periodically by the host |
| Timing | Called only after event processing, never during |
| Output | Serialized state that restore can deserialize |
| Consistency | Output represents the complete state after processing all prior events |
restore(snapshot: list<u8>)
Section titled “restore(snapshot: list<u8>)”Restores state from a previous snapshot.
| Aspect | Specification |
|---|---|
| Invocation | Called before any append calls (but after init) |
| Input | Bytes previously returned by snapshot |
| Side Effects | Completely replaces any existing state |
| Recovery | After restore, subsequent append calls continue from the snapshot state |
Lifecycle Guarantees
Section titled “Lifecycle Guarantees”The host calls functions in one of these sequences:
Fresh start (no snapshot):
init -> append* -> query* ↑ | └──────────┘Recovery from snapshot:
init -> restore -> append* -> query* ↑ | └──────────┘Snapshot capture:
... -> append -> snapshot -> append -> ...Example: Counter View
Section titled “Example: Counter View”A JSON-based counter that accepts {"inc": N} and {"dec": N} commands and returns {"count": N}:
[package]name = "counter"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib"]
[dependencies]wit-bindgen = "0.41"serde = { version = "1", features = ["derive"] }serde_json = "1"
[profile.release]opt-level = "s"lto = truewit_bindgen::generate!({ path: "wit", world: "state-machine",});
use exports::primatomic::machine::machine::Guest;use serde::{Deserialize, Serialize};use std::cell::RefCell;
#[derive(Deserialize)]#[serde(rename_all = "lowercase")]enum Command { Inc(i64), Dec(i64),}
#[derive(Serialize)]struct QueryResponse { count: i64,}
struct Counter;
thread_local! { static STATE: RefCell<i64> = const { RefCell::new(0) };}
export!(Counter);
impl Guest for Counter { fn init() { STATE.with(|s| *s.borrow_mut() = 0); }
fn append(event: Vec<u8>) { let delta = match serde_json::from_slice::<Command>(&event) { Ok(Command::Inc(n)) => n, Ok(Command::Dec(n)) => -n, Err(_) => 1, }; STATE.with(|s| *s.borrow_mut() += delta); }
fn query(_input: Vec<u8>) -> Vec<u8> { STATE.with(|s| { serde_json::to_vec(&QueryResponse { count: *s.borrow() }).unwrap() }) }
fn snapshot() -> Vec<u8> { STATE.with(|s| s.borrow().to_le_bytes().to_vec()) }
fn restore(snapshot: Vec<u8>) { if snapshot.len() >= 8 { let bytes: [u8; 8] = snapshot[..8].try_into().unwrap(); STATE.with(|s| *s.borrow_mut() = i64::from_le_bytes(bytes)); } }}Build Requirements
Section titled “Build Requirements”| Requirement | Specification |
|---|---|
| Target | Compile to wasm32-unknown-unknown (no WASI imports) |
| Tool | Use cargo-component for Rust |
| WIT path | Fetch with wkg get --format wit primatomic:[email protected] --output ./wit |
| Bindings | Use wit-bindgen for Rust |
Build Commands
Section titled “Build Commands”# Install cargo-component (one-time)cargo install cargo-component
# Add the wasm32-unknown-unknown targetrustup target add wasm32-unknown-unknown
# Buildcargo component build --release --target wasm32-unknown-unknownValidate
Section titled “Validate”# Verify no WASI imports (should produce no output)wasm-tools print target/wasm32-unknown-unknown/release/counter.wasm | grep -i wasiDeployment
Section titled “Deployment”curl -X POST https://api.primatomic.com/logs/$LOG_ID/views/counter \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @target/wasm32-unknown-unknown/release/counter.wasmRuntime Constraints
Section titled “Runtime Constraints”| Constraint | Specification |
|---|---|
| I/O Access | Views cannot perform I/O (filesystem, network, environment) |
| Memory | Views are limited to 64 MB of memory by default; exceeding this limit terminates the view |
| Binary Size | WASM binaries must be under 100 MB |
| Query Input | Query request bodies are limited to 1 MB |
| Threading | Views execute single-threaded; append calls are serialized |
| Determinism | Given identical input sequences, views produce identical state |
| Panics | Views should not panic; panics may cause view restart |
State Management
Section titled “State Management”| Aspect | Specification |
|---|---|
| Location | State resides in memory on the leader node |
| Persistence | Snapshots are persisted to durable storage |
| Recovery | On leader election, state is restored from the latest snapshot |
| Replay | Events since the snapshot are replayed after restore |
Serialization
Section titled “Serialization”Views control the binary format for events, queries, and snapshots. Common patterns:
JSON (Recommended for simplicity)
Section titled “JSON (Recommended for simplicity)”fn append(event: Vec<u8>) { let event: MyEvent = serde_json::from_slice(&event).unwrap(); // process event}Protocol Buffers (Recommended for performance)
Section titled “Protocol Buffers (Recommended for performance)”fn append(event: Vec<u8>) { let event = MyEvent::decode(&event[..]).unwrap(); // process event}Custom Binary (for minimal overhead)
Section titled “Custom Binary (for minimal overhead)”fn append(event: Vec<u8>) { let op = event[0]; let value = i32::from_le_bytes(event[1..5].try_into().unwrap()); // process event}Views should document their serialization format for client implementations.