Skip to content

WASM Interface

Views are WebAssembly components that implement the primatomic:machine interface. This document specifies the interface contract and requirements for view implementations.

Fetch the WIT interface using wkg:

Terminal window
wkg get --format wit primatomic:[email protected] --output ./wit

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;
}

Initializes the state machine.

AspectSpecification
InvocationCalled exactly once before any append or query calls
Side EffectsShould initialize internal state to a known starting point
ReturnNone

Applies a single log event to the state machine.

AspectSpecification
InvocationCalled for each event in sequence order
OrderingEvents are delivered in strictly ascending sequence order
DeliveryEach event is delivered exactly once during normal operation
InputRaw event bytes as appended to the log
Side EffectsMay update internal state based on the event (handlers may ignore certain events)
ReturnNone

Queries the current state.

AspectSpecification
ConcurrencyMay be called concurrently with append
InputQuery parameters; format is defined by the view implementation
OutputQuery result; format is defined by the view implementation
Side EffectsDoes not modify state
DeterminismGiven the same state and input, returns the same output

Serializes the current state for persistence.

AspectSpecification
InvocationMay be called periodically by the host
TimingCalled only after event processing, never during
OutputSerialized state that restore can deserialize
ConsistencyOutput represents the complete state after processing all prior events

Restores state from a previous snapshot.

AspectSpecification
InvocationCalled before any append calls (but after init)
InputBytes previously returned by snapshot
Side EffectsCompletely replaces any existing state
RecoveryAfter restore, subsequent append calls continue from the snapshot state

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 -> ...

A JSON-based counter that accepts {"inc": N} and {"dec": N} commands and returns {"count": N}:

Cargo.toml
[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 = true
src/lib.rs
wit_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));
}
}
}
RequirementSpecification
TargetCompile to wasm32-unknown-unknown (no WASI imports)
ToolUse cargo-component for Rust
WIT pathFetch with wkg get --format wit primatomic:[email protected] --output ./wit
BindingsUse wit-bindgen for Rust
Terminal window
# Install cargo-component (one-time)
cargo install cargo-component
# Add the wasm32-unknown-unknown target
rustup target add wasm32-unknown-unknown
# Build
cargo component build --release --target wasm32-unknown-unknown
Terminal window
# Verify no WASI imports (should produce no output)
wasm-tools print target/wasm32-unknown-unknown/release/counter.wasm | grep -i wasi
Terminal window
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.wasm
ConstraintSpecification
I/O AccessViews cannot perform I/O (filesystem, network, environment)
MemoryViews are limited to 64 MB of memory by default; exceeding this limit terminates the view
Binary SizeWASM binaries must be under 100 MB
Query InputQuery request bodies are limited to 1 MB
ThreadingViews execute single-threaded; append calls are serialized
DeterminismGiven identical input sequences, views produce identical state
PanicsViews should not panic; panics may cause view restart
AspectSpecification
LocationState resides in memory on the leader node
PersistenceSnapshots are persisted to durable storage
RecoveryOn leader election, state is restored from the latest snapshot
ReplayEvents since the snapshot are replayed after restore

Views control the binary format for events, queries, and snapshots. Common patterns:

fn append(event: Vec<u8>) {
let event: MyEvent = serde_json::from_slice(&event).unwrap();
// process event
}
Section titled “Protocol Buffers (Recommended for performance)”
fn append(event: Vec<u8>) {
let event = MyEvent::decode(&event[..]).unwrap();
// process event
}
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.