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

Introduction

Welcome to the Torvyn documentation — the ownership-aware reactive streaming runtime built on WebAssembly.

Torvyn is a reactive streaming runtime that combines WebAssembly’s sandboxing guarantees with a novel ownership model for safe, composable stream processing pipelines.

This documentation covers everything from getting started to deep architectural internals. Use the sidebar to navigate, or jump to:

Installation

This guide walks you through installing Torvyn and verifying that your development environment is ready.

Time required: 5–10 minutes.

Prerequisites

Torvyn components are compiled to WebAssembly using the Rust toolchain. You need the following tools installed before proceeding.

Rust Toolchain

Torvyn requires Rust 1.78 or later (the minimum supported Rust version). The wasm32-wasip2 compilation target must be available.

If you do not have Rust installed, use rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

After installation, add the WebAssembly target:

rustup target add wasm32-wasip2

Verify your Rust version:

rustc --version
# Expected: rustc 1.78.0 or later

cargo-component

Torvyn uses cargo-component to compile Rust code into WebAssembly components that conform to the WebAssembly Component Model.

cargo install cargo-component

Verify:

cargo component --version

wasm-tools

The wasm-tools suite provides utilities for inspecting and manipulating WebAssembly binaries. Torvyn uses it for component inspection and validation.

cargo install wasm-tools

Verify:

wasm-tools --version

Installing Torvyn

Install the torvyn CLI from source using Cargo:

cargo install torvyn-cli

This compiles and installs the torvyn binary into your Cargo bin directory (typically ~/.cargo/bin/).

From Prebuilt Binaries

Prebuilt binaries for Linux (x86_64, aarch64), macOS (x86_64, Apple Silicon), and Windows (x86_64) are available on the GitHub Releases page.

Download the archive for your platform, extract it, and place the torvyn binary on your PATH:

# Example for Linux x86_64 — adjust the URL for your platform and version
curl -L https://github.com/torvyn/torvyn/releases/latest/download/torvyn-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv torvyn /usr/local/bin/

Note: Prebuilt binaries are planned for the first stable release. During pre-release development, install from source.

Via Homebrew (macOS and Linux)

brew install torvyn

Note: The Homebrew formula is planned for the first stable release. During pre-release development, install from source.

Via Nix

nix profile install nixpkgs#torvyn

Note: The Nix package is planned for the first stable release. During pre-release development, install from source.

Verification

After installation, verify that the torvyn binary is accessible:

torvyn --version

Expected output:

torvyn 0.1.0

Environment Check

The torvyn doctor command inspects your environment and reports any missing or misconfigured tools:

torvyn doctor

Expected output when everything is correctly installed:

  Torvyn CLI
    ✓ torvyn 0.1.0 (up to date)

  Rust Toolchain
    ✓ rustc 1.78.0 (or later)
    ✓ wasm32-wasip2 target installed
    ✓ cargo-component installed

  WebAssembly Tools
    ✓ wasm-tools installed

  All checks passed!

If any checks fail, torvyn doctor displays the issue and a suggested fix. You can also run torvyn doctor --fix to attempt automatic repair — for example, installing missing Rust targets.

Shell Completions

Torvyn can generate shell completion scripts for Bash, Zsh, Fish, and PowerShell:

# Bash
torvyn completions bash > ~/.bash_completion.d/torvyn

# Zsh
torvyn completions zsh > ~/.zfunc/_torvyn

# Fish
torvyn completions fish > ~/.config/fish/completions/torvyn.fish

# PowerShell
torvyn completions powershell > $PROFILE.CurrentUserAllHosts

Troubleshooting

torvyn command not found

Ensure your Cargo bin directory is on your PATH. Add the following to your shell profile (.bashrc, .zshrc, etc.):

export PATH="$HOME/.cargo/bin:$PATH"

Then restart your shell or run source ~/.bashrc.

wasm32-wasip2 target not available

If rustup target add wasm32-wasip2 fails, ensure you are using a recent enough Rust version:

rustup update stable
rustup target add wasm32-wasip2

The wasm32-wasip2 target requires Rust 1.78 or later.

cargo-component build failures

The cargo-component tool depends on a compatible version of the wit-bindgen crate. If you encounter version conflicts during builds, ensure your project’s Cargo.toml specifies wit-bindgen = "0.36" (the version used by Torvyn’s templates). Check the Torvyn compatibility matrix for the current recommended versions.

Proxy or firewall issues

If cargo install fails due to network issues, ensure that crates.io and github.com are accessible from your machine. If you are behind a corporate proxy, configure Cargo’s HTTP proxy settings in ~/.cargo/config.toml:

[http]
proxy = "http://your-proxy:port"

Next Steps

Your environment is ready. Continue to the Quickstart to create and run your first Torvyn project.

Quickstart

This tutorial walks you through the complete Torvyn developer workflow in about ten minutes. You will scaffold a project, explore its structure, build it, run it, trace its execution, benchmark it, and package it.

Prerequisites: A working Torvyn installation. If you have not installed Torvyn yet, follow the Installation Guide first.

Time required: 10 minutes.

graph LR
    S1["1. Init<br/><small>torvyn init</small>"]
    S2["2. Check<br/><small>torvyn check</small>"]
    S3["3. Build<br/><small>cargo component<br/>build</small>"]
    S4["4. Run<br/><small>torvyn run</small>"]
    S5["5. Trace<br/><small>torvyn trace</small>"]
    S6["6. Bench<br/><small>torvyn bench</small>"]
    S7["7. Pack<br/><small>torvyn pack</small>"]

    S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7

    style S1 fill:#2563EB,stroke:#1D4ED8,color:#fff
    style S2 fill:#7C3AED,stroke:#6D28D9,color:#fff
    style S3 fill:#7C3AED,stroke:#6D28D9,color:#fff
    style S4 fill:#059669,stroke:#047857,color:#fff
    style S5 fill:#D97706,stroke:#B45309,color:#fff
    style S6 fill:#D97706,stroke:#B45309,color:#fff
    style S7 fill:#059669,stroke:#047857,color:#fff

Step 1: Create a New Project

Use torvyn init to scaffold a new project. The --template transform flag creates a single-component stateless transform — the simplest useful Torvyn component.

torvyn init my-transform --template transform

Expected output:

  ✓ Created project "my-transform" with template "transform"

  my-transform
  ├── Torvyn.toml
  ├── Cargo.toml
  ├── wit/world.wit
  ├── src/lib.rs
  ├── .gitignore
  └── README.md

  Next steps:
    cd my-transform
    torvyn check              # Validate contracts and manifest
    torvyn build              # Compile to WebAssembly component
    torvyn run --limit 10     # Run and see output

Move into the project directory:

cd my-transform

Step 2: Explore the Project Structure

The scaffolded project contains five files. Each serves a specific role:

Torvyn.toml — The project manifest. This is the central configuration file that identifies the project, declares its components, and defines pipeline topology.

[torvyn]
name = "my-transform"
version = "0.1.0"
contract_version = "0.1.0"

[[component]]
name = "my-transform"
path = "."
language = "rust"

The [torvyn] table declares the project name and version. The contract_version field indicates which version of the Torvyn streaming contracts this component targets. The [[component]] array declares one component, rooted at the current directory, written in Rust.

Cargo.toml — Standard Rust project manifest. The key detail is crate-type = ["cdylib"], which tells the compiler to produce a WebAssembly module rather than a native binary.

wit/world.wit — The WIT (WebAssembly Interface Type) contract for this component. This file is explored in the next step.

src/lib.rs — The component implementation. This is where your logic lives.

.gitignore — Ignores build artifacts (target/, .torvyn/, *.wasm).

Step 3: Read the WIT Contract

Open wit/world.wit:

package my-transform:component;

world my-transform {
    import torvyn:streaming/types@0.1.0;
    import torvyn:resources/buffer-ops@0.1.0;
    export torvyn:streaming/processor@0.1.0;
}

This contract says three things:

  1. This component imports torvyn:streaming/types — it uses the core Torvyn types: buffers, stream elements, element metadata, and error types. These are host-managed resources, meaning the Torvyn runtime provides them and the component interacts through opaque handles.

  2. This component imports torvyn:resources/buffer-ops — it can allocate new buffers through the host’s buffer allocator. The host manages buffer pooling and tracks every allocation for observability.

  3. This component exports torvyn:streaming/processor — it implements the process function, which receives a stream element and produces either a new output element or a drop signal.

In Torvyn, the WIT contract is the center of the design. It defines what a component can do, what it requires, and how it interacts with the runtime. Contracts are checked statically before any code runs.

Step 4: Look at the Implementation

Open src/lib.rs:

#![allow(unused)]
fn main() {
wit_bindgen::generate!({
    world: "my-transform",
    path: "wit",
});

use exports::torvyn::streaming::processor::{Guest, ProcessResult};
use torvyn::streaming::types::{StreamElement, ProcessError};

struct MyTransform;

impl Guest for MyTransform {
    fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
        // Pass-through: emit the input unchanged
        Ok(ProcessResult::Emit(input))
    }
}

export!(MyTransform);
}

The wit_bindgen::generate! macro reads the WIT contract and generates Rust types and traits that correspond to the imported and exported interfaces. Your component implements the Guest trait, which requires a process function.

The generated template is a pass-through: it receives each stream element and emits it unchanged. In a real component, you would read the input buffer, transform the data, allocate a new output buffer, and return the result.

The StreamElement that arrives in process contains:

  • input.meta — element metadata: a monotonic sequence number, a wall-clock timestamp, and a content type string.
  • input.payload — a borrowed handle to an immutable buffer in host memory. You can read its contents with input.payload.read(offset, len) or input.payload.read_all().
  • input.context — a borrowed handle to the flow context, providing trace IDs and deadline information.

Step 5: Validate the Project

Before compiling, run torvyn check to validate the project structure, manifest, and contracts:

torvyn check

Expected output:

  ✓ Manifest valid (Torvyn.toml)
  ✓ Contracts valid (1 interface file)
  ✓ Component "my-transform" declared correctly

  All checks passed.

torvyn check performs static analysis only — it does not compile or execute anything. It catches manifest errors, malformed WIT files, undeclared dependencies, and version mismatches before you spend time building.

If there are problems, torvyn check provides precise diagnostic messages with file paths, line numbers, and suggested fixes, following the same ergonomic principles as the Rust compiler’s error output.

Step 6: Build the Component

Compile the component to a WebAssembly module:

cargo component build --target wasm32-wasip2

Expected output:

   Compiling my-transform v0.1.0 (/path/to/my-transform)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in X.XXs

The compiled component is at target/wasm32-wasip2/debug/my_transform.wasm.

Note: cargo component build wraps the standard cargo build with additional steps to produce a WebAssembly component (not just a core module). The component embeds its WIT interface metadata, which the Torvyn runtime uses for contract validation at load time.

Step 7: Run the Component

To run a component, you need a pipeline definition. For a single transform component, create a minimal pipeline by adding flow configuration to your Torvyn.toml. Append the following:

[flow.main]
description = "Pass-through transform with built-in test source"

[flow.main.nodes.transform]
component = "file://./target/wasm32-wasip2/debug/my_transform.wasm"
interface = "torvyn:streaming/processor"

Now run the pipeline with a limited number of elements:

torvyn run --limit 10

Expected output:

▶ Running flow "main" (limit: 10 elements)

Hello, Torvyn! (1)
Hello, Torvyn! (2)
Hello, Torvyn! (3)
Hello, Torvyn! (4)
Hello, Torvyn! (5)
Hello, Torvyn! (6)
Hello, Torvyn! (7)
Hello, Torvyn! (8)
Hello, Torvyn! (9)
Hello, Torvyn! (10)

  ── Summary ──────────────
  Flow:        main
  Elements:    10
  Duration:    0.03s
  Throughput:  ~333 elem/s

torvyn run loads the compiled component into the Torvyn host runtime, instantiates it inside a WebAssembly sandbox, connects it to a built-in test source (which generates numbered greeting messages), and drives data through the pipeline. The --limit 10 flag stops the pipeline after 10 elements.

Step 8: Trace the Execution

torvyn trace runs the same pipeline with full diagnostic tracing enabled. It shows you exactly what happens to each element as it flows through the pipeline:

torvyn trace --limit 3

Expected output:

▶ Tracing flow "main" (limit: 3 elements)

  elem-0  ┬─ source         pull     4.2µs
          ├─ transform      process  1.8µs  B001 (19B, borrowed)
          └─ sink           push     0.9µs
          total: 6.9µs

  elem-1  ┬─ source         pull     2.1µs
          ├─ transform      process  1.5µs  B002 (19B, borrowed)
          └─ sink           push     0.8µs
          total: 4.4µs

  elem-2  ┬─ source         pull     2.0µs
          ├─ transform      process  1.4µs  B003 (19B, borrowed)
          └─ sink           push     0.7µs
          total: 4.1µs

  ── Trace Summary ────────
  Elements traced:  3
  Avg latency:      5.1µs (p50: 4.4µs, p99: 6.9µs)
  Copies:           0
  Buffer reuse:     0%
  Backpressure:     0 events

Each trace entry shows the component name, the operation it performed, the wall-clock duration, and (when relevant) which buffer was accessed and how. This is one of Torvyn’s design priorities: making performance and data flow visible rather than opaque.

The “Copies: 0” line confirms that this pass-through transform did not trigger any buffer copies — the element’s buffer handle was borrowed, not copied. The resource manager tracks every copy, and torvyn trace reports the total.

Step 9: Benchmark the Pipeline

torvyn bench runs the pipeline under sustained load and produces a performance report:

torvyn bench --duration 5s

Expected output:

▶ Benchmarking flow "main" (warmup: 2s, duration: 5s)

  ── Throughput ───────────
  Elements/s:     128,450
  Bytes/s:        2.4 MiB/s

  ── Latency (µs) ────────
  p50:            3.2
  p90:            4.8
  p95:            5.9
  p99:            12.1
  p999:           48.3
  max:            210.5

  ── Per-Component Latency (µs, p50) ──
  source:         1.4
  transform:      1.2
  sink:           0.6

  ── Resources ────────────
  Buffer allocs:  642,250
  Pool reuse rate: 94.2%
  Total copies:   0
  Peak memory:    1.2 MiB

  ── Scheduling ───────────
  Total wakeups:  642,250
  Backpressure:   0 events
  Queue peak:     4 / 256

The benchmark report includes latency percentiles (not just averages — tail latency matters), per-component breakdowns, resource usage including buffer allocation behavior and copy counts, and scheduling metrics like backpressure events and queue utilization. The 2-second warmup period is excluded from measurements to avoid cold-start effects.

Note: The numbers shown above are illustrative. Actual performance depends on your hardware, the complexity of your component logic, and the data being processed. Torvyn’s benchmarking methodology is designed to produce reproducible results — run the same benchmark multiple times and compare.

Step 10: Package the Component

torvyn pack packages your compiled component as an OCI-compatible artifact for distribution:

torvyn pack

Expected output:

  ✓ Contracts valid ✓
  ✓ Packed: my-transform-0.1.0.tar (12.4 KiB)

  Artifact: .torvyn/artifacts/my-transform-0.1.0.tar

  Artifact contents:
    component.wasm    11.2 KiB
    manifest.json     0.3 KiB
    contracts/        0.9 KiB

The packaged artifact contains the compiled WebAssembly component, the project manifest metadata, and the WIT contract definitions. This artifact can be pushed to a Torvyn-compatible OCI registry with torvyn publish, shared with other teams, or deployed to production environments.

What You Have Built

In ten minutes you have:

  1. Scaffolded a Torvyn project with a typed WIT contract and a working implementation.
  2. Validated the project structure and contracts statically.
  3. Compiled Rust to a WebAssembly component.
  4. Executed the component inside the sandboxed Torvyn runtime.
  5. Traced element-level flow to see exactly where time was spent and how buffers were used.
  6. Benchmarked the pipeline with latency percentiles, throughput, and resource accounting.
  7. Packaged the component as a distributable artifact.

This is the full Torvyn workflow: contract → build → validate → run → trace → benchmark → package.

Next Steps

Your First Pipeline

This tutorial walks you through building a multi-component streaming pipeline from scratch. You will define WIT contracts for three components — a source, a processor, and a sink — implement each one, wire them together in a pipeline configuration, and observe the result.

Prerequisites: Complete the Quickstart first. You should be comfortable with torvyn init, torvyn check, and cargo component build.

Time required: 20–30 minutes.

What You Will Build

A pipeline that:

  1. Source — generates timestamped log-style messages.
  2. Processor — converts each message to uppercase and prepends a sequence number.
  3. Sink — prints each processed message to stdout.

Along the way, you will see how Torvyn’s typed contracts enforce compatibility between components, how buffers flow through the pipeline, and how tracing reveals per-component behavior.

graph LR
    Src["Source<br/><small>Log Generator<br/>torvyn:streaming/source</small>"]
    Proc["Processor<br/><small>Uppercase + Prefix<br/>torvyn:streaming/processor</small>"]
    Sink["Sink<br/><small>Console Output<br/>torvyn:streaming/sink</small>"]

    Src -->|"Stream Queue<br/><small>bounded, backpressure-aware</small>"| Proc
    Proc -->|"Stream Queue<br/><small>bounded, backpressure-aware</small>"| Sink

    style Src fill:#2563EB,stroke:#1D4ED8,color:#fff
    style Proc fill:#7C3AED,stroke:#6D28D9,color:#fff
    style Sink fill:#DC2626,stroke:#B91C1C,color:#fff

Step 1: Scaffold the Project

Use the full-pipeline template to create a multi-component project:

torvyn init log-pipeline --template full-pipeline
cd log-pipeline

Expected output:

  ✓ Created project "log-pipeline" with template "fullpipeline"

  log-pipeline
  ├── Torvyn.toml
  ├── components/
  │   ├── source/
  │   │   ├── Cargo.toml
  │   │   ├── wit/world.wit
  │   │   └── src/lib.rs
  │   └── transform/
  │       ├── Cargo.toml
  │       ├── wit/world.wit
  │       └── src/lib.rs
  ├── .gitignore
  └── README.md

The template gives us a source and a transform. We will add a custom sink and modify all three components to match our log-processing scenario.

Step 2: Define the WIT Contracts

Each component’s WIT contract declares what it imports and exports. Let us examine each one.

Source Contract

Open components/source/wit/world.wit:

package source:component;

world source {
    import torvyn:streaming/types@0.1.0;
    import torvyn:resources/buffer-ops@0.1.0;
    export torvyn:streaming/source@0.1.0;
}

This contract declares a source component: it imports the Torvyn type system and buffer allocator, and exports the source interface. The source interface requires implementing a pull function that the runtime calls repeatedly to fetch the next element. When there are no more elements, pull returns None to signal end of stream.

Processor Contract

Open components/transform/wit/world.wit:

package transform:component;

world transform {
    import torvyn:streaming/types@0.1.0;
    import torvyn:resources/buffer-ops@0.1.0;
    export torvyn:streaming/processor@0.1.0;
}

A processor imports the same foundations and exports the processor interface. The runtime calls process once per element, passing a borrowed reference to the input. The processor returns either emit(output-element) with a new owned buffer, or drop to filter the element out.

Sink Contract

We need to create the sink component. Create the directory structure:

mkdir -p components/sink/wit
mkdir -p components/sink/src

Create components/sink/wit/world.wit:

package sink:component;

world sink {
    import torvyn:streaming/types@0.1.0;
    export torvyn:streaming/sink@0.1.0;
}

A sink imports the type system (it does not need the buffer allocator because it consumes data rather than producing new buffers) and exports the sink interface. The runtime calls push for each element arriving at the sink.

Notice the key difference: sinks do not import buffer-ops because they do not allocate output buffers. They receive borrowed references to input data, read what they need during the push call, and return. This is part of Torvyn’s ownership-aware design — each component declares only the capabilities it requires.

graph TD
    subgraph Source["Source"]
        direction TB
        SI["Imports:<br/>streaming/types<br/>resources/buffer-ops"]
        SE["Exports:<br/>streaming/source"]
    end

    subgraph Processor["Processor"]
        direction TB
        PI["Imports:<br/>streaming/types<br/>resources/buffer-ops"]
        PE["Exports:<br/>streaming/processor"]
    end

    subgraph Sink["Sink"]
        direction TB
        KI["Imports:<br/>streaming/types"]
        KE["Exports:<br/>streaming/sink"]
    end

    SE -->|"output type<br/>compatible?"| PI
    PE -->|"output type<br/>compatible?"| KI

    style Source fill:#2563EB,stroke:#1D4ED8,color:#fff
    style Processor fill:#7C3AED,stroke:#6D28D9,color:#fff
    style Sink fill:#DC2626,stroke:#B91C1C,color:#fff

Create components/sink/Cargo.toml:

[package]
name = "sink"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "sink:component"

Step 3: Implement Each Component

Source: Timestamped Log Generator

Replace the contents of components/source/src/lib.rs:

#![allow(unused)]
fn main() {
// Source component: generates timestamped log messages.

wit_bindgen::generate!({
    world: "source",
    path: "wit",
});

use exports::torvyn::streaming::source::Guest;
use torvyn::streaming::types::{OutputElement, ElementMeta, ProcessError, BackpressureSignal};
use torvyn::resources::buffer_ops;

struct LogSource;

static mut COUNTER: u64 = 0;

// Simulated log messages.
const MESSAGES: &[&str] = &[
    "connection accepted from 10.0.1.42",
    "request received: GET /api/status",
    "database query completed in 12ms",
    "response sent: 200 OK",
    "connection accepted from 10.0.1.87",
    "request received: POST /api/events",
    "authentication succeeded for user admin",
    "event stored: id=evt_38f71a",
    "response sent: 201 Created",
    "connection closed: 10.0.1.42",
];

impl Guest for LogSource {
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        let count = unsafe {
            COUNTER += 1;
            COUNTER
        };

        // End the stream after 100 elements.
        if count > 100 {
            return Ok(None);
        }

        // Cycle through the simulated log messages.
        let msg_index = ((count - 1) as usize) % MESSAGES.len();
        let message = format!("[ts={}] {}", count * 1_000_000, MESSAGES[msg_index]);

        // Allocate a buffer and write the message into it.
        let buf = buffer_ops::allocate(message.len() as u64)
            .map_err(|_| ProcessError::Internal("buffer allocation failed".into()))?;
        buf.append(message.as_bytes())
            .map_err(|_| ProcessError::Internal("buffer write failed".into()))?;
        buf.set_content_type("text/plain");

        let output_buf = buf.freeze();

        Ok(Some(OutputElement {
            meta: ElementMeta {
                sequence: 0,       // Runtime assigns the actual sequence number.
                timestamp_ns: 0,   // Runtime assigns the actual timestamp.
                content_type: "text/plain".into(),
            },
            payload: output_buf,
        }))
    }

    fn notify_backpressure(
        _signal: BackpressureSignal,
    ) {
        // This simple source ignores backpressure signals.
        // A production source might pause an upstream connection.
    }
}

export!(LogSource);
}

Key details:

  • The source allocates a mutable-buffer via buffer_ops::allocate, writes data into it with append, then calls freeze() to convert it into an immutable buffer. Ownership of the frozen buffer transfers to the runtime when pull returns.
  • The sequence and timestamp_ns fields in ElementMeta are set to 0 because the runtime overwrites them with authoritative values. Component-provided values are advisory only.
  • The source returns Ok(None) to signal end of stream after 100 elements.

Processor: Uppercase with Sequence Prefix

Replace the contents of components/transform/src/lib.rs:

#![allow(unused)]
fn main() {
// Transform component: converts text to uppercase and prepends a line number.

wit_bindgen::generate!({
    world: "transform",
    path: "wit",
});

use exports::torvyn::streaming::processor::{Guest, ProcessResult};
use torvyn::streaming::types::{StreamElement, OutputElement, ElementMeta, ProcessError};
use torvyn::resources::buffer_ops;

struct UppercaseTransform;

impl Guest for UppercaseTransform {
    fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
        // Read the input buffer contents.
        let data = input.payload.read_all();
        let text = String::from_utf8_lossy(&data);

        // Transform: uppercase and prepend the sequence number.
        let transformed = format!("[{:04}] {}", input.meta.sequence, text.to_uppercase());

        // Allocate an output buffer and write the transformed data.
        let out_buf = buffer_ops::allocate(transformed.len() as u64)
            .map_err(|_| ProcessError::Internal("buffer allocation failed".into()))?;
        out_buf.append(transformed.as_bytes())
            .map_err(|_| ProcessError::Internal("buffer write failed".into()))?;
        out_buf.set_content_type("text/plain");

        let frozen = out_buf.freeze();

        Ok(ProcessResult::Emit(OutputElement {
            meta: ElementMeta {
                sequence: input.meta.sequence,
                timestamp_ns: input.meta.timestamp_ns,
                content_type: "text/plain".into(),
            },
            payload: frozen,
        }))
    }
}

export!(UppercaseTransform);
}

Key details:

  • input.payload.read_all() copies the buffer contents from host memory into the component’s linear memory. The resource manager records this as a measured copy. This is one of the copies that torvyn trace and torvyn bench will report.
  • A new output buffer is allocated, written, and frozen. The old input buffer handle is dropped when process returns — the host reclaims it (or returns it to the buffer pool).
  • The processor uses input.meta.sequence to access the runtime-assigned sequence number.

Sink: Console Output

Create components/sink/src/lib.rs:

#![allow(unused)]
fn main() {
// Sink component: prints each element to stdout.

wit_bindgen::generate!({
    world: "sink",
    path: "wit",
});

use exports::torvyn::streaming::sink::Guest;
use torvyn::streaming::types::{StreamElement, BackpressureSignal, ProcessError};

struct ConsoleSink;

impl Guest for ConsoleSink {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        // Read the buffer contents.
        let data = element.payload.read_all();
        let text = String::from_utf8_lossy(&data);

        // Print to stdout.
        println!("{text}");

        // Signal that we are ready for more data.
        Ok(BackpressureSignal::Ready)
    }

    fn complete() -> Result<(), ProcessError> {
        // Flush is a no-op for stdout in this example.
        // A production sink writing to a file or network would flush here.
        Ok(())
    }
}

export!(ConsoleSink);
}

Key details:

  • The sink receives a StreamElement with borrowed handles. It reads the payload during the push call. After push returns, the borrowed handle is no longer valid — the sink must not store it.
  • The push function returns a BackpressureSignal. Returning Ready tells the runtime to continue delivering elements. Returning Pause would cause the runtime to pause upstream delivery until the sink signals readiness again. This is Torvyn’s built-in backpressure mechanism.
  • The complete function is called once when the upstream flow is finished. Sinks should flush any buffered data here.

Step 4: Configure the Pipeline

Replace the contents of Torvyn.toml with the full pipeline configuration:

[torvyn]
name = "log-pipeline"
version = "0.1.0"
description = "A three-stage log processing pipeline"
contract_version = "0.1.0"

[[component]]
name = "source"
path = "components/source"
language = "rust"

[[component]]
name = "transform"
path = "components/transform"
language = "rust"

[[component]]
name = "sink"
path = "components/sink"
language = "rust"

# Pipeline topology: source → transform → sink
[flow.main]
description = "Generate logs, uppercase them, and print to console"

[flow.main.nodes.source]
component = "file://./components/source/target/wasm32-wasip2/debug/source.wasm"
interface = "torvyn:streaming/source"

[flow.main.nodes.transform]
component = "file://./components/transform/target/wasm32-wasip2/debug/transform.wasm"
interface = "torvyn:streaming/processor"

[flow.main.nodes.sink]
component = "file://./components/sink/target/wasm32-wasip2/debug/sink.wasm"
interface = "torvyn:streaming/sink"

[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "transform", port = "input" }

[[flow.main.edges]]
from = { node = "transform", port = "output" }
to = { node = "sink", port = "input" }

The [flow.main] section defines the pipeline topology. Each node names a compiled component and the interface it implements. The [[flow.main.edges]] array wires them together: source → transform → sink.

Step 5: Build and Run

Build all three components:

cd components/source && cargo component build --target wasm32-wasip2 && cd ../..
cd components/transform && cargo component build --target wasm32-wasip2 && cd ../..
cd components/sink && cargo component build --target wasm32-wasip2 && cd ../..

Validate the pipeline:

torvyn check

Expected output:

  ✓ Manifest valid (Torvyn.toml)
  ✓ Contracts valid (3 components)
  ✓ Component "source" declared correctly
  ✓ Component "transform" declared correctly
  ✓ Component "sink" declared correctly

  All checks passed.

Verify that the components can compose correctly:

torvyn link

Expected output:

  ✓ Flow "main": source → transform → sink
  ✓ All interfaces compatible
  ✓ Topology valid (DAG, connected, role-consistent)

  Link check passed.

torvyn link statically verifies that the WIT interfaces of connected components are compatible. It checks that the output type of the source is compatible with the input type of the transform, and so on. It also validates the topology: the graph must be a directed acyclic graph (DAG), fully connected, and each node’s declared interface must match its role in the graph.

Now run the pipeline:

torvyn run --limit 10

Expected output:

▶ Running flow "main" (limit: 10 elements)

[0000] [TS=1000000] CONNECTION ACCEPTED FROM 10.0.1.42
[0001] [TS=2000000] REQUEST RECEIVED: GET /API/STATUS
[0002] [TS=3000000] DATABASE QUERY COMPLETED IN 12MS
[0003] [TS=4000000] RESPONSE SENT: 200 OK
[0004] [TS=5000000] CONNECTION ACCEPTED FROM 10.0.1.87
[0005] [TS=6000000] REQUEST RECEIVED: POST /API/EVENTS
[0006] [TS=7000000] AUTHENTICATION SUCCEEDED FOR USER ADMIN
[0007] [TS=8000000] EVENT STORED: ID=EVT_38F71A
[0008] [TS=9000000] RESPONSE SENT: 201 CREATED
[0009] [TS=10000000] CONNECTION CLOSED: 10.0.1.42

  ── Summary ──────────────
  Flow:        main
  Elements:    10
  Duration:    0.05s
  Throughput:  ~200 elem/s

Each log message was generated by the source, uppercased and sequence-numbered by the transform, and printed by the sink.

Step 6: Observe with Tracing

Run the pipeline with full tracing to see per-element behavior:

torvyn trace --limit 3 --show-backpressure

The trace output shows how each element flows through the three stages, the latency at each stage, buffer allocation and copy events, and whether any backpressure occurred. This level of visibility is a core Torvyn design goal: every stream element, resource handoff, and scheduling event is observable.

What You Have Learned

In this tutorial you:

  • Defined WIT contracts for three different component roles: source, processor, and sink.
  • Implemented each component in Rust, working with Torvyn’s buffer ownership model: allocate → write → freeze → transfer.
  • Configured a multi-component pipeline topology in Torvyn.toml.
  • Used torvyn link to statically verify interface compatibility before running anything.
  • Observed the running pipeline and traced individual element flow.

The key concepts demonstrated:

  • Contract-first design — the WIT contract defines what each component can do before any code is written.
  • Ownership-aware buffers — mutable buffers are frozen before transfer; borrowed handles are valid only during a single call.
  • Backpressure signaling — sinks explicitly signal whether they are ready for more data.
  • Static composition verificationtorvyn link catches interface mismatches before runtime.

Next Steps

What Is Torvyn?

Torvyn is an ownership-aware reactive streaming runtime for building safe, low-latency, polyglot pipelines. It composes sandboxed WebAssembly components into streaming data pipelines on a single node, using typed contracts to define every interaction and host-managed resources to track every byte of data movement.

Torvyn combines several ideas into a single cohesive system: WebAssembly component boundaries for portability and isolation, WIT (WebAssembly Interface Types) for precise interface contracts, a Rust host runtime for performance and memory safety, async stream scheduling for reactive flow control, host-managed resources for explicit ownership transfer, capability-based sandboxing for security, and industrial tooling for adoption and operation.

The central insight behind Torvyn is that most systems do not fail because a single technology is missing. They fail because the stack is too fragmented. Torvyn is opinionated about the full path from contract definition through deployment:

contract → build → compose → validate → run → trace → benchmark → package → deploy

That full-path coherence is one of its core differentiators.

graph LR
    A["Contract"] --> B["Build"]
    B --> C["Compose"]
    C --> D["Validate"]
    D --> E["Run"]
    E --> F["Trace"]
    F --> G["Benchmark"]
    G --> H["Package"]
    H --> I["Deploy"]

    style A fill:#6366F1,stroke:#4F46E5,color:#fff
    style B fill:#6366F1,stroke:#4F46E5,color:#fff
    style C fill:#6366F1,stroke:#4F46E5,color:#fff
    style D fill:#6366F1,stroke:#4F46E5,color:#fff
    style E fill:#6366F1,stroke:#4F46E5,color:#fff
    style F fill:#6366F1,stroke:#4F46E5,color:#fff
    style G fill:#6366F1,stroke:#4F46E5,color:#fff
    style H fill:#6366F1,stroke:#4F46E5,color:#fff
    style I fill:#6366F1,stroke:#4F46E5,color:#fff

The Problem Torvyn Solves

Modern infrastructure increasingly consists of streaming processors, local inference modules, edge functions, event transformers, policy engines, protocol adapters, data enrichers, and autonomous agent tooling. These systems need to be fast enough for real-time workloads, safe enough for multi-tenant execution, modular enough for rapid iteration, observable enough for regulated production environments, and portable enough to run across diverse infrastructure.

Current systems force teams to choose among low latency, isolation, portability, polyglot interoperability, and operational simplicity. No widely adopted execution model simultaneously provides all of these.

Traditional microservices provide isolation and deployment flexibility, but impose heavy boundary overhead. Each service boundary introduces serialization and deserialization, multiple buffer allocations, network stack overhead, retries and backoff complexity, observability stitching, schema drift, and nontrivial operational cost. Even when services run on the same node, teams often still pay the full cost of remote-style communication patterns. Systems become modular in theory but expensive in practice.

Containers are excellent for deployment packaging and isolation, but they introduce too much friction for extremely fine-grained, low-latency pipelines. Startup time, memory footprint, orchestration overhead, and resource granularity become limiting factors when a design calls for dozens or hundreds of small pipeline stages on a single node.

In-process plugin models are fast but often unsafe. They reduce boundary overhead by running everything in a single process, but create risks including memory unsafety in extension code, undefined behavior at FFI boundaries, weak isolation between plugins, poor resource governance, versioning fragility, and language lock-in. These models are attractive for performance but brittle at industrial scale.

Reactive streaming is everywhere, but the tooling landscape is fragmented across message brokers, stream processors, actor systems, async runtimes, RPC layers, service meshes, and ad hoc pipeline libraries. Teams assemble streaming systems using many layers that were not designed as a unified, ownership-aware runtime.

AI-native workloads amplify these problems. Local inference pipelines combine token streams, embeddings, policy filters, retrieval stages, model adapters, tracing layers, content guards, caching logic, and downstream delivery systems. These workloads do not always need full remote-distributed infrastructure between each stage. They need safe local composition, deterministic streaming behavior, and high-throughput modular execution.

Torvyn is designed to fill this gap.

How Torvyn Compares to Existing Approaches

DimensionMicroservicesContainersIn-Process PluginsActor SystemsTorvyn
IsolationProcess-levelOS-levelNone or minimalLogical onlyWasm sandbox per component
Boundary overheadHigh (network)Moderate (IPC)NoneLow (message passing)Low (host-managed handles)
Contract enforcementSchema (often lax)None at runtimeNone or ad hocProtocol-levelTyped WIT, validated at link time
Polyglot supportIndependent binariesIndependent imagesLanguage-locked (FFI)VariesAny language → Wasm component
Ownership trackingNoneNoneManualMessage ownershipHost-enforced, every copy measured
BackpressureApplication-levelApplication-levelNoneMailbox-basedBuilt into stream semantics
ObservabilityPer-service tracesPer-container logsApplication-dependentActor tracesPer-element, per-copy, per-queue
PackagingContainer imagesContainer imagesLibrary artifactsVariesOCI-compatible artifacts

Torvyn does not replace microservices or containers for every workload. It targets the specific and painful category of problems where traditional service boundaries add overhead without proportional benefit: same-node streaming pipelines, edge-local stream processing, ultra-low-latency component graphs, secure plugin ecosystems, local inference and dataflow composition, and high-frequency internal service chaining where network boundaries are unnecessary.

Key Concepts

Torvyn’s developer experience revolves around six core concepts:

Contracts are WIT interface definitions that specify how components exchange data. Contracts make ownership rules explicit (borrow vs. own), define error models, declare capability requirements, and enable static compatibility checking before any code runs. In Torvyn, the contract is the center of the product.

Components are sandboxed WebAssembly modules that implement one or more Torvyn interfaces. A component can be a Source (data producer), Processor (transform), Sink (data consumer), Filter (accept/reject), Router (fan-out), or Aggregator (stateful accumulation). Components can be written in any language that compiles to WebAssembly Components — Rust, Go, Python, Zig, and others.

Streams are typed connections between components through which stream-element records flow. Each stream has a bounded queue and a configurable backpressure policy. Streams carry both the data payload (as a host-managed buffer handle) and flow metadata (trace context, deadlines, sequence numbers).

Resources are host-managed byte buffers that components access through opaque handles. The host allocates buffers from tiered pools, tracks ownership state (Pooled, Owned, Borrowed, Leased), enforces access rules, and instruments every data copy. Components never directly share memory — all data movement is mediated and measured by the host.

Capabilities are declared permissions that control what each component can access. Torvyn follows a deny-all-by-default model: a component with no capability grants can do nothing beyond pure computation on data provided through its stream interface. Filesystem access, network access, clock access, and other system services require explicit grants from the pipeline operator.

Flows are instantiated pipeline topologies: a directed acyclic graph of components connected by streams, executing as a unit. A flow has its own lifecycle, its own resource budget, its own observability context, and its own backpressure domain.

The following diagram illustrates how these concepts compose into a running flow:

graph LR
    subgraph Flow["Flow (DAG topology)"]
        direction LR
        Src["Source<br/><small>produces elements</small>"]
        Proc["Processor<br/><small>transforms data</small>"]
        Filt["Filter<br/><small>accept / reject</small>"]
        Rtr["Router<br/><small>fan-out by key</small>"]
        Sink1["Sink A<br/><small>consumes data</small>"]
        Sink2["Sink B<br/><small>consumes data</small>"]

        Src -->|"Stream<br/>(bounded queue)"| Proc
        Proc -->|"Stream"| Filt
        Filt -->|"Stream"| Rtr
        Rtr -->|"Route A"| Sink1
        Rtr -->|"Route B"| Sink2
    end

    subgraph Host["Torvyn Host Runtime"]
        direction TB
        RM["Resource Manager<br/><small>buffers, pools, ownership</small>"]
        Cap["Capability Guard<br/><small>deny-all-by-default</small>"]
        Obs["Observability<br/><small>traces, metrics, diagnostics</small>"]
    end

    Flow -.->|"buffer handles<br/>ownership transfers"| RM
    Flow -.->|"capability checks"| Cap
    Flow -.->|"events &amp; spans"| Obs

    style Src fill:#2563EB,stroke:#1D4ED8,color:#fff
    style Proc fill:#7C3AED,stroke:#6D28D9,color:#fff
    style Filt fill:#D97706,stroke:#B45309,color:#fff
    style Rtr fill:#059669,stroke:#047857,color:#fff
    style Sink1 fill:#DC2626,stroke:#B91C1C,color:#fff
    style Sink2 fill:#DC2626,stroke:#B91C1C,color:#fff
    style RM fill:#475569,stroke:#334155,color:#fff
    style Cap fill:#475569,stroke:#334155,color:#fff
    style Obs fill:#475569,stroke:#334155,color:#fff

When to Use Torvyn

Torvyn is a strong fit for:

  • Same-node streaming pipelines where multiple processing stages need to operate on data with minimal latency and no network overhead between stages.
  • Edge-local stream processing where compute resources are constrained and portability across different edge hardware is important.
  • Secure plugin ecosystems where third-party or untrusted code must be executed with strong isolation and explicit capability boundaries.
  • AI inference pipelines that chain preprocessing, model invocation, post-processing, policy enforcement, and delivery stages on a single node.
  • Event-driven data transformation where data flows through a sequence of enrichment, filtering, and routing stages.
  • High-frequency internal service chaining where teams have co-located services communicating over the network that could benefit from in-process composition without sacrificing isolation.

When Not to Use Torvyn

Torvyn is not the right tool for every problem. Being explicit about this makes the project stronger and more trustworthy.

Do not use Torvyn when you need a distributed orchestrator. Torvyn operates on a single node. If your pipeline spans multiple machines and needs distributed coordination, consensus, or fault tolerance across nodes, use a distributed streaming system (Kafka Streams, Flink, etc.) or a container orchestrator (Kubernetes). Torvyn may eventually expand to multi-node topologies, but this is not a current capability.

Do not use Torvyn when network service boundaries provide genuine value. If independent deployment, independent scaling, and organizational team autonomy are your primary concerns, microservices remain the right architecture. Torvyn is valuable when the overhead of those boundaries exceeds their organizational benefit.

Do not use Torvyn for simple request-response APIs. Torvyn is designed for streaming dataflow. If your workload is fundamentally request-response with no streaming semantics, a standard HTTP server or RPC framework is simpler and more appropriate.

Do not use Torvyn when you need guaranteed zero-copy transfer. Torvyn minimizes copies and tracks every one that occurs, but WebAssembly’s memory isolation model means that some copies are unavoidable when a component needs to read or transform payload data. If your workload requires absolute zero-copy data movement, you need shared-memory approaches that operate outside the Wasm sandbox model.

Do not use Torvyn when the WebAssembly component ecosystem does not support your language. While WebAssembly is polyglot in principle, component model tooling maturity varies by language. Rust support is production-grade. Support for Go, Python, and other languages is maturing but should be evaluated before committing to production use. Torvyn’s Phase 0 targets Rust-first; polyglot support expands in subsequent phases.

Contracts and WIT Interfaces

What Are WIT Contracts?

WIT (WebAssembly Interface Types) is the interface description language of the WebAssembly Component Model. In Torvyn, WIT contracts define every interaction between components and between components and the host runtime. A contract specifies what data a component expects to receive, what data it produces, what host services it requires, and what ownership rules govern the data exchange.

Contracts are the center of Torvyn’s design. They serve as the single source of truth for component compatibility, security requirements, versioning, and composition rules. If two components have compatible contracts, they can be composed into a pipeline regardless of the language they were written in. If their contracts are incompatible, torvyn link will reject the composition before any code runs.

WIT contracts are:

  • Explicit. Every parameter type, return type, and ownership semantic is visible in the interface definition. There are no hidden assumptions or implicit behaviors.
  • Statically validatable. Compatibility can be checked at link time without running any component code. Contract violations become compile-time or validation-time errors, not runtime surprises.
  • Human-readable. WIT is designed for humans to read and write. Contracts are documentation, not just machine metadata.
  • Language-neutral. A WIT contract defines an interface that can be implemented in Rust, Go, Python, Zig, or any other language with WebAssembly Component Model support.
  • Evolvable. The versioning model supports compatible contract evolution without breaking existing components.

How Torvyn Uses Contracts for Composition

Torvyn defines a set of WIT packages that establish the streaming contract surface:

torvyn:streaming@0.1.0 — The core package, containing types for stream elements, buffers, flow contexts, error models, and the primary processing interfaces (Processor, Source, Sink). Every Torvyn component depends on this package.

torvyn:filtering@0.1.0 — Extension package for filter and router interfaces. Components that make accept/reject or routing decisions use these interfaces.

torvyn:aggregation@0.1.0 — Extension package for stateful accumulation. Components that ingest many elements and emit aggregated results use this interface.

torvyn:capabilities@0.1.0 — Capability declaration types and host-provided service interfaces.

The core package is versioned together and forms the minimum viable contract surface. Extension packages are versioned independently and are optional. This separation means the core can be stabilized faster, components that only need basic streaming do not pay the conceptual cost of learning aggregation or routing, and extension packages can evolve at their own pace.

graph TD
    Core["torvyn:streaming@0.1.0<br/><small>Types, Processor, Source, Sink<br/>Required by all components</small>"]
    Filt["torvyn:filtering@0.1.0<br/><small>Filter, Router interfaces</small>"]
    Agg["torvyn:aggregation@0.1.0<br/><small>Stateful accumulation</small>"]
    Cap["torvyn:capabilities@0.1.0<br/><small>Capability declarations<br/>Host service interfaces</small>"]

    Filt -->|"depends on"| Core
    Agg -->|"depends on"| Core
    Cap -->|"depends on"| Core

    subgraph Required["Required"]
        Core
    end
    subgraph Optional["Optional Extensions"]
        Filt
        Agg
        Cap
    end

    style Core fill:#2563EB,stroke:#1D4ED8,color:#fff
    style Filt fill:#7C3AED,stroke:#6D28D9,color:#fff
    style Agg fill:#7C3AED,stroke:#6D28D9,color:#fff
    style Cap fill:#7C3AED,stroke:#6D28D9,color:#fff

When you create a Torvyn component, you define a WIT world that specifies which interfaces your component imports (host services it requires) and exports (capabilities it provides to the pipeline):

package my-namespace:my-component@0.1.0;

world my-transform {
    // Import the types and buffer allocation interface from the host
    import torvyn:streaming/types@0.1.0;
    import torvyn:streaming/buffer-allocator@0.1.0;

    // Export the processor interface — this is what the component does
    export torvyn:streaming/processor@0.1.0;

    // Optionally export lifecycle hooks for initialization and teardown
    export torvyn:streaming/lifecycle@0.1.0;
}

This world declaration is the component’s contract with the runtime. The host reads it to understand what the component needs and what it provides, the linker uses it to verify compatibility with other components in the pipeline, and the security system uses it to validate capability declarations.

The Contract Lifecycle

Contracts in Torvyn follow a four-stage lifecycle: define, validate, link, evolve.

graph LR
    D["Define<br/><small>Write .wit files<br/>Scaffold with torvyn init</small>"]
    V["Validate<br/><small>torvyn check<br/>Parse, resolve, verify</small>"]
    L["Link<br/><small>torvyn link<br/>Cross-component composition</small>"]
    E["Evolve<br/><small>Semantic versioning<br/>Compatibility checking</small>"]

    D --> V --> L --> E
    E -.->|"new version"| D

    style D fill:#2563EB,stroke:#1D4ED8,color:#fff
    style V fill:#7C3AED,stroke:#6D28D9,color:#fff
    style L fill:#059669,stroke:#047857,color:#fff
    style E fill:#D97706,stroke:#B45309,color:#fff

Define

The developer writes WIT interface definitions in .wit files within the component’s wit/ directory. The torvyn init command scaffolds the correct structure and vendors the appropriate Torvyn WIT packages into wit/deps/. The component’s world.wit file declares which interfaces the component imports and exports.

Validate

torvyn check validates the contract:

  1. All .wit files parse without syntax errors.
  2. All use statements and package references resolve correctly.
  3. The component’s world exports at least one Torvyn processing interface.
  4. If the manifest declares capabilities, they correspond to interfaces the component actually imports.
  5. Version constraints are internally consistent.

Validation requires only the component’s own files. It does not need access to other components in the pipeline.

torvyn link validates composition across multiple components:

  1. For each connection in the pipeline topology, the upstream component’s output type is compatible with the downstream component’s input type.
  2. Every required capability is granted by the pipeline configuration.
  3. The pipeline graph is a valid DAG with correct role assignments (sources have no inputs, sinks have no outputs, etc.).
  4. All components’ contract version ranges have a non-empty intersection.
  5. Router port names match actual downstream component names in the topology.

Linking catches the class of errors that occur when independently developed components are composed for the first time.

Evolve

Contracts evolve through semantic versioning. Torvyn’s versioning rules are strict:

Breaking changes (major version bump): Removing a type, function, or interface. Changing a function’s parameter or return types. Removing a field from a record. Removing a case from a variant or enum. Changing ownership semantics (borrow → own or vice versa). Renaming any public type, function, or interface.

Compatible changes (minor version bump): Adding a new function to an existing interface. Adding a new interface to a package. Adding a new case to a variant (consumers must handle unknown variants).

Patch changes: Documentation updates. Clarifications that do not change behavior.

The torvyn link command performs structural compatibility checking when components target different minor versions of the same contract package, verifying that the specific interfaces and functions used by each component are available in the other’s version.

Versioning and Compatibility Rules

Each compiled Wasm component embeds metadata recording the exact WIT package version it was compiled against and the minimum compatible version it requires from imports. This metadata is immutable once the component is compiled.

The compatibility checking algorithm works as follows:

flowchart TD
    Start["Extract version constraints<br/>from each component"] --> VC{"Same major<br/>version?"}
    VC -->|"No"| Fail1["Incompatible<br/><small>Major version mismatch</small>"]
    VC -->|"Yes"| MinCheck{"Consumer required<br/>version ≤ provider<br/>version?"}
    MinCheck -->|"No"| Fail2["Incompatible<br/><small>Provider too old</small>"]
    MinCheck -->|"Yes"| Structural["Structural compatibility check<br/><small>Verify all interfaces and functions<br/>used by consumer exist in provider</small>"]
    Structural --> StructOk{"All used interfaces<br/>present?"}
    StructOk -->|"No"| Fail3["Incompatible<br/><small>Missing interface or function</small>"]
    StructOk -->|"Yes"| Pass["Compatible"]

    style Fail1 fill:#DC2626,stroke:#B91C1C,color:#fff
    style Fail2 fill:#DC2626,stroke:#B91C1C,color:#fff
    style Fail3 fill:#DC2626,stroke:#B91C1C,color:#fff
    style Pass fill:#16A34A,stroke:#15803D,color:#fff
    style Start fill:#2563EB,stroke:#1D4ED8,color:#fff
  1. Extract version constraints from each component’s embedded metadata.
  2. Compute version intersection for each shared package. Same major version: compatible if the consumer’s required version ≤ the provider’s version. Different major versions: incompatible.
  3. Structural compatibility check. Even within compatible version ranges, verify that specific interfaces used by the consumer are present in the provider. This catches cases where a consumer uses a function added in a minor version that the provider does not yet implement.
  4. Report results with detailed diagnostics if incompatible.

Examples of Well-Designed Contracts

A well-designed Torvyn contract follows these principles:

Use borrowed handles for input, owned handles for output. The Processor interface exemplifies this: input stream elements carry borrow<buffer> (the component reads but does not own), while output elements carry owned buffers (ownership transfers to the runtime).

Separate read-only and mutable access. The split between buffer (immutable, read-only) and mutable-buffer (writable, single-owner) makes mutation boundaries explicit and eliminates the need for copy-on-write complexity.

Use variants for results that have multiple meaningful outcomes. The process-result variant distinguishes between emit(output-element) (produced output) and drop (consumed input without output). This is essential for filters and aggregators.

Use typed error categories. The process-error variant provides invalid-input, unavailable, internal, deadline-exceeded, and fatal — each triggering different runtime behavior (retry, skip, terminate). This is richer than a flat error string and enables programmatic error handling.

Keep metadata as records, resources as handles. Element metadata (element-meta) is a record because it is small and cheap to copy. Buffers and flow contexts are resources because they have identity, host-managed lifecycle, and should not be copied at every boundary.

Common Contract Design Mistakes

Embedding payload bytes in records. If you define a record containing list<u8> for payload data, every cross-component transfer copies the full byte content. Use a buffer resource handle instead, which transfers as an integer (the handle) while the payload bytes remain in host memory.

Using own<T> when borrow<T> suffices. If a component only needs to read data during a function call and does not need to store or modify it, use borrow<T>. Borrows have lower overhead and clearer lifetime semantics.

Creating monolithic interfaces. Splitting functionality across focused interfaces (Processor, Filter, Router, Aggregator) rather than a single large interface enables better observability (the runtime can report filter rates vs. transform rates), better optimization (filters do not allocate output buffers), and easier composition.

Ignoring version evolution. Designing a record that will likely need new fields without using option<T> wrappers means that adding fields later requires a major version bump. Consider which fields might be extended and wrap them accordingly from the start.

Declaring capabilities you do not need. Every declared capability is a trust surface. Request the minimum set. Do not declare wasi:filesystem/write if your component only reads. The security system validates capability declarations against WIT imports, so false declarations are caught, but unnecessary requests add friction for operators reviewing capability grants.

The Ownership Model

Why Ownership Matters in Streaming Systems

In any system where data flows through multiple processing stages, a fundamental question arises: who is responsible for each piece of data at each point in time? Who can read it? Who can modify it? When is it safe to recycle the underlying memory?

Traditional streaming systems often leave these questions implicit. A buffer is allocated, passed through a chain of functions, and eventually freed — with the programmer responsible for ensuring that no stage uses the buffer after it has been freed, that no two stages write to the same buffer concurrently, and that buffers are returned to pools promptly. When these invariants are violated, the result is use-after-free bugs, data corruption, memory leaks, and undefined behavior.

Torvyn makes ownership explicit and enforced. Every buffer in the system has exactly one owner at any given time. Ownership can be transferred, but never duplicated. Borrows are tracked with lifetimes scoped to function calls. The type system and runtime checks together make it impossible for two components to hold conflicting access to the same buffer. Every allocation, every copy, every borrow, every transfer is instrumented.

This is what “ownership-aware” means. It is not a marketing term. It is a specific, verifiable property of the runtime: the resource manager knows, at all times, who owns what.

The Four Resource States

Every host-managed buffer in Torvyn exists in one of four states. These states form a finite state machine with well-defined transitions and invariants.

Pooled

The buffer is not in active use. It resides in a buffer pool, ready for allocation. No component and no active flow references it. The host owns the underlying memory, and the buffer’s content is considered stale. No entity can read or write the buffer’s payload while it is in this state.

Invariants: No outstanding handles exist in any component’s handle space. The buffer slot in the resource table is either vacant or marked as pooled.

Owned

Exactly one entity — either the host or a specific component instance — holds exclusive ownership. The owner has full read and write access. No other entity may read, write, or free the buffer. Ownership can be transferred (moved) to another entity, after which the original owner’s handle becomes invalid.

Invariants: Exactly one valid handle exists. The owner field in the resource table matches the entity holding the handle. No borrows are outstanding (ownership cannot be transferred while borrows exist).

Borrowed

One entity owns the buffer. One or more other entities hold read-only borrow handles. Borrows are scoped to a single host-to-component function call — they are created when the host invokes a component function that takes a borrow<buffer> parameter, and they are automatically released when the function returns. This aligns directly with WIT’s borrow<T> semantics.

Invariants: The owner handle remains valid. A borrow count > 0 is recorded. The borrow count decrements to zero when the component function returns. Ownership transfer and free are blocked while borrows are outstanding.

Leased

A component holds a time-bounded or scope-bounded lease on a buffer. A lease is more flexible than a borrow — it can span multiple function calls within a single processing stage — but is still tracked and reclaimed by the host when the lease expires. Read-only leases allow shared access; mutable leases provide exclusive access.

Invariants: The lease has an explicit scope (stage boundary or time deadline). The host reclaims leases on scope exit, timeout, or component termination. Mutable leases block all other access.

Note: Leased state support is planned for Phase 1. Phase 0 uses only Pooled, Owned, and Borrowed states, which are sufficient for the single-call-per-element processor model.

Ownership State Machine

The following state diagram shows all valid transitions between the four resource states:

stateDiagram-v2
    [*] --> Pooled: Buffer allocated in pool
    Pooled --> Owned: acquire()
    Owned --> Borrowed: borrow() — host invokes component
    Borrowed --> Owned: function returns — borrow released
    Owned --> Pooled: release()

    Owned --> Leased: lease() — Phase 1
    Leased --> Owned: lease expires / scope exit
    Leased --> Pooled: release after lease

    note right of Pooled
        No outstanding handles.
        Content is stale.
        Host owns memory.
    end note

    note right of Owned
        Exactly one valid handle.
        Full read/write access.
        No borrows outstanding.
    end note

    note right of Borrowed
        Owner handle + borrow count > 0.
        Read-only access for borrower.
        Transfer/free blocked.
    end note

    note left of Leased
        Phase 1 — spans multiple calls.
        Time or scope bounded.
        Host reclaims on expiry.
    end note

How Buffers Move Through a Pipeline

Consider a simple three-stage pipeline: Source → Processor → Sink. The following sequence diagram summarizes the buffer ownership transitions and copy points for a single element:

sequenceDiagram
    participant Pool as Buffer Pool
    participant Src as Source
    participant Host as Host Runtime
    participant Proc as Processor
    participant Sink as Sink

    Pool->>Src: acquire() → mutable-buffer B1
    Note over Src: State: OWNED by Source
    Src->>Host: append() — write payload
    Note right of Src: Copy 1: component → host
    Src->>Host: freeze() → immutable buffer B1
    Note over Host: State: OWNED by Host (in transit)

    Host->>Proc: process(borrow B1)
    Note over Proc: State: BORROWED (read-only)
    Proc->>Proc: read_all()
    Note right of Proc: Copy 2: host → component
    Pool->>Proc: acquire() → mutable-buffer B2
    Note over Proc: State: OWNED by Processor
    Proc->>Host: append() — write transformed data
    Note right of Proc: Copy 3: component → host
    Proc->>Host: freeze() → immutable buffer B2
    Proc-->>Host: return B2 + metadata
    Host->>Pool: release B1
    Note over Pool: B1 → POOLED

    Host->>Sink: push(borrow B2)
    Note over Sink: State: BORROWED (read-only)
    Sink->>Sink: read_all()
    Note right of Sink: Copy 4: host → component
    Sink-->>Host: BackpressureSignal::Ready
    Host->>Pool: release B2
    Note over Pool: B2 → POOLED

Here is the same lifecycle shown in detail with memory layouts:

                 HOST MEMORY                    COMPONENT MEMORY
                 ═══════════                    ════════════════

 1. Source produces data
    ┌──────────────┐
    │ Buffer B1    │ ◄── Source calls allocate(), gets mutable-buffer
    │ [empty]      │     State: OWNED by Source
    └──────────────┘
           │
           ▼
    ┌──────────────┐                         ┌─────────────────┐
    │ Buffer B1    │     copy ──────────────► │ Source linear    │
    │ [payload]    │     ◄── Source writes    │ memory: payload │
    └──────────────┘         bytes via        │ data            │
           │                 mutable-buffer   └─────────────────┘
           │                 .append()
           ▼                 (1 copy: component → host)
    ┌──────────────┐
    │ Buffer B1    │ ◄── Source calls freeze(), returns owned buffer
    │ [immutable]  │     State: OWNED by Host (in transit)
    └──────────────┘

 2. Host passes buffer to Processor as borrow
    ┌──────────────┐
    │ Buffer B1    │ ◄── State: BORROWED (Processor has read-only borrow)
    │ [immutable]  │
    └──────────────┘
           │
           │  Processor calls buffer.read()
           │                                 ┌─────────────────┐
           ├── copy ────────────────────────►│ Processor linear│
           │   (1 copy: host → component)    │ memory: input   │
           │                                 └─────────────────┘
           │
           │  Processor allocates new output buffer
    ┌──────────────┐
    │ Buffer B2    │ ◄── State: OWNED by Processor
    │ [mutable]    │
    └──────────────┘
           │
           │  Processor writes transformed data
           │                                 ┌─────────────────┐
           │◄── copy ◄──────────────────────│ Processor linear│
           │    (1 copy: component → host)   │ memory: output  │
           │                                 └─────────────────┘
           ▼
    ┌──────────────┐
    │ Buffer B2    │ ◄── Processor freezes and returns B2
    │ [immutable]  │     State: OWNED by Host (in transit)
    └──────────────┘
    ┌──────────────┐
    │ Buffer B1    │ ◄── Borrow released when process() returns
    │ [immutable]  │     State: OWNED by Host → released to POOL
    └──────────────┘

 3. Host passes buffer to Sink as borrow
    ┌──────────────┐
    │ Buffer B2    │ ◄── State: BORROWED (Sink has read-only borrow)
    │ [immutable]  │
    └──────────────┘
           │
           │  Sink calls buffer.read-all()
           │                                 ┌─────────────────┐
           ├── copy ────────────────────────►│ Sink linear     │
           │   (1 copy: host → component)    │ memory: data    │
           │                                 └─────────────────┘
           ▼
    ┌──────────────┐
    │ Buffer B2    │ ◄── Borrow released → OWNED by Host → POOL
    └──────────────┘

 Total payload copies: 4 (source write, processor read, processor write, sink read)
 All copies instrumented. All ownership transitions tracked.

What “Ownership-Aware” Means vs. “Zero-Copy”

Torvyn is deliberately not marketed as “zero-copy.” The WebAssembly Component Model imposes real memory isolation between components. Each component has its own linear memory. The host has its own address space. When a component needs to read or transform data, the bytes must exist in that component’s linear memory at some point.

“Ownership-aware” means something more precise and more useful:

  • The runtime knows who owns every buffer at all times. There is never ambiguity about which entity is responsible for a buffer.
  • Every copy is intentional, measured, and attributed. You can run torvyn bench and see exactly how many bytes were copied, at which component boundary, and for what reason.
  • Unnecessary copies are eliminated. When a buffer passes through a component that only inspects metadata (content-type, size, sequence number), no payload copy occurs. The component receives an opaque handle, and the payload bytes never leave host memory. This is the handle-pass fast path — Torvyn’s most valuable optimization.
  • Copies that cannot be eliminated are bounded and predictable. The maximum number of copies per pipeline stage is known at design time from the contract: a processor that reads and writes payload data incurs exactly two copies (read + write). This is a fixed, measurable cost.

This is a more industrially credible foundation than promising universal zero-copy behavior. Where true zero-copy transfer is possible (handle passing, metadata-only routing), Torvyn uses it. Where copies are unavoidable (reading payload bytes into component memory), Torvyn ensures they are bounded, measurable, predictable, and operationally visible.

How Copy Accounting Works

Every copy operation in Torvyn produces a TransferRecord that captures when the copy occurred, what resource was copied, the source and destination entities, the number of bytes, and why the copy was performed.

graph TD
    Copy["Copy Operation"] --> TR["TransferRecord<br/><small>timestamp, resource_id,<br/>src, dst, bytes, reason</small>"]

    TR --> R1["MetadataMarshal<br/><small>~30-50 bytes<br/>Canonical ABI overhead</small>"]
    TR --> R2["PayloadRead<br/><small>buffer.read() / read_all()<br/>Host → component memory</small>"]
    TR --> R3["PayloadWrite<br/><small>mutable-buffer.append()<br/>Component → host memory</small>"]
    TR --> R4["HostSerialize<br/><small>Host-initiated copy<br/>Export / serialization</small>"]

    TR --> Agg["Aggregation"]
    Agg --> FlowSum["Per-Flow Summary<br/><small>Total bytes, total ops<br/>Always-on, near-zero overhead</small>"]
    Agg --> CompDet["Per-Component Detail<br/><small>Diagnostic mode or<br/>torvyn bench only</small>"]

    FlowSum --> Amp["Copy Amplification Metric<br/><small>total bytes copied ÷ total bytes produced<br/>0.0 = handle-pass only | ~2.0 = transform</small>"]

    style Copy fill:#2563EB,stroke:#1D4ED8,color:#fff
    style TR fill:#7C3AED,stroke:#6D28D9,color:#fff
    style R1 fill:#6B7280,stroke:#4B5563,color:#fff
    style R2 fill:#D97706,stroke:#B45309,color:#fff
    style R3 fill:#D97706,stroke:#B45309,color:#fff
    style R4 fill:#6B7280,stroke:#4B5563,color:#fff
    style Amp fill:#059669,stroke:#047857,color:#fff

Copy reasons include:

  • MetadataMarshal — The canonical ABI marshaled metadata (small, fixed-size, ~30-50 bytes) into component linear memory during a function call.
  • PayloadRead — A component called buffer.read() or buffer.read-all(), pulling payload bytes from host memory into component memory.
  • PayloadWrite — A component wrote data into a mutable buffer, copying bytes from component memory to host memory.
  • HostSerialize — The host initiated a copy for serialization or export purposes.

These records are aggregated at two levels. Per-flow summaries (total bytes copied, total copy operations, breakdown by reason) are maintained on every element with near-zero overhead. Per-component detailed records are available when running in diagnostic mode or during benchmarking.

The copy amplification metric — the ratio of total payload bytes copied to total payload bytes produced — provides a single number that characterizes pipeline efficiency. A metadata-routing pipeline might achieve 0.0 (no payload copies at all). A transform-heavy pipeline typically shows close to 2.0 (one read + one write per stage). Values significantly above 2.0 per stage indicate unnecessary copying that should be investigated.

Performance Implications and Optimization Strategies

Use metadata-only routing where possible. Filters, routers, and priority classifiers often need only the element’s content-type, size, or sequence number to make a decision. These metadata fields are available without reading the payload. If your component can operate on metadata alone, it operates on the handle-pass fast path with zero payload copies.

Prefer streaming reads over full reads. For large payloads, calling buffer.read(offset, len) with a smaller window (e.g., 4 KiB at a time) limits linear memory usage. For small payloads, a single buffer.read-all() is more efficient (one host call instead of many).

Pre-allocate output buffers at the right size. When calling buffer-allocator.allocate(capacity-hint), providing an accurate capacity hint lets the pool system select the best-fit buffer tier, avoiding waste (oversized buffers) and reallocation (undersized buffers).

Monitor pool reuse rate. The pool_reuse_rate metric (returned buffers / allocated buffers) indicates how effectively buffers are being recycled. A rate below 80% suggests that buffers are being lost to fallback allocation, which typically means the pool is undersized for the workload. Adjust pool sizes in the host configuration.

Monitor copy amplification. Use torvyn bench to measure the copy amplification metric for your pipeline. If a component is producing more copies than expected, review whether it is reading payload bytes unnecessarily or whether a metadata-only routing path is possible.

Backpressure

What Is Backpressure and Why It Matters

Backpressure is the mechanism by which a slow consumer tells a fast producer to slow down. In any streaming system where stages process data at different speeds, backpressure prevents unbounded queue growth, memory exhaustion, and cascading failures.

Without backpressure, a producer that outpaces its consumer fills an ever-growing queue until the system runs out of memory. With backpressure, the queue has a bounded capacity, and when that capacity is reached, the producer is suspended until the consumer catches up. Memory usage becomes deterministic and bounded.

In Torvyn, backpressure is not optional. It is built into the stream semantics and the reactor’s scheduling model. Every stream connection between components has a bounded queue with a configurable backpressure policy. The runtime enforces these policies automatically — components do not need to implement backpressure logic themselves.

How Torvyn Implements Backpressure

Torvyn uses a credit-based demand model, inspired by the Reactive Streams specification’s request(n) pattern and TCP’s sliding window. Each stream maintains a demand counter: the number of elements the consumer is willing to accept.

When a consumer processes an element, it replenishes one demand credit. When a producer enqueues an element, it consumes one demand credit. A producer with zero demand credits must not produce — this is the backpressure trigger.

When a flow starts, the reactor grants each stream an initial demand equal to the stream’s queue capacity. This allows the pipeline to fill up without waiting for explicit demand signals, avoiding a cold-start latency penalty.

Demand propagation follows the pipeline graph from consumer to producer. In a multi-stage pipeline (A → B → C → D), if D (the sink) is slow, the backpressure cascades through the entire pipeline: queue C→D fills, C is suspended, queue B→C fills, B is suspended, queue A→B fills, A (the source) is suspended. The entire pipeline is now backpressured, with bounded queue depths at every stage.

Backpressure State Machine

The following diagram shows the backpressure state transitions for a single stream:

stateDiagram-v2
    [*] --> Normal: Flow starts — initial demand granted
    Normal --> HighWatermark: Queue reaches capacity
    HighWatermark --> Paused: Producer suspended

    Paused --> LowWatermark: Consumer drains below low watermark
    LowWatermark --> Normal: Producer resumed — demand replenished

    Normal --> Normal: Elements flowing — demand available

    note right of Normal
        Producer has demand credits.
        Elements flow freely.
    end note

    note right of HighWatermark
        Queue at capacity.
        BackpressureEvent::Triggered emitted.
    end note

    note left of Paused
        Producer not scheduled.
        Consumer draining queue.
    end note

    note left of LowWatermark
        Queue below low watermark.
        BackpressureEvent::Relieved emitted.
    end note

Demand Propagation in Multi-Stage Pipelines

In a multi-stage pipeline, backpressure cascades upstream through the entire graph:

graph LR
    A["Source"] -->|"Queue A→B"| B["Processor 1"]
    B -->|"Queue B→C"| C["Processor 2"]
    C -->|"Queue C→D"| D["Sink"]

    style D fill:#E04E2D,stroke:#c0412a,color:#fff
    style C fill:#CA8A04,stroke:#a87003,color:#fff
    style B fill:#CA8A04,stroke:#a87003,color:#fff
    style A fill:#CA8A04,stroke:#a87003,color:#fff

When the Sink (D) is slow: Queue C→D fills → C is suspended → Queue B→C fills → B is suspended → Queue A→B fills → Source (A) is suspended. The entire pipeline is backpressured with bounded queue depths at every stage.

The High/Low Watermark Mechanism

Backpressure uses hysteresis to prevent rapid oscillation between pressured and unpressured states:

  • Backpressure activates when the queue reaches capacity (the high watermark, effectively 100%).
  • Backpressure deactivates when the queue drops below the low watermark (default: 50% of capacity).

Without this hysteresis, a system where the consumer is only slightly slower than the producer would oscillate between backpressured and normal on every single element. The watermark gap provides a stability band.

graph LR
    subgraph WatermarkBand["Queue Depth Over Time"]
        direction TB
        HW["High Watermark (100%) ── Backpressure activates"]
        Band["Hysteresis Band<br/><small>Producer remains paused<br/>while queue drains</small>"]
        LW["Low Watermark (50%) ── Backpressure deactivates"]
        Normal["Normal operation zone<br/><small>Producer has demand credits</small>"]
    end

    HW --- Band
    Band --- LW
    LW --- Normal

    style HW fill:#DC2626,stroke:#B91C1C,color:#fff
    style Band fill:#D97706,stroke:#B45309,color:#fff
    style LW fill:#2563EB,stroke:#1D4ED8,color:#fff
    style Normal fill:#16A34A,stroke:#15803D,color:#fff

The sequence of events during a backpressure episode:

  1. The producer component returns a new element.
  2. The flow driver attempts to enqueue the element into the downstream stream’s queue.
  3. The queue is at capacity. Backpressure is triggered.
  4. An observability event is emitted: BackpressureEvent::Triggered.
  5. The producer is no longer eligible for scheduling. The flow driver focuses on executing downstream stages to drain the queue.
  6. When the consumer processes enough elements for the queue to drop below the low watermark, the stream exits backpressure.
  7. An observability event is emitted: BackpressureEvent::Relieved.
  8. The producer is eligible for scheduling again.
sequenceDiagram
    participant Prod as Producer
    participant Q as Stream Queue
    participant FD as Flow Driver
    participant Obs as Observability
    participant Cons as Consumer

    Prod->>Q: enqueue element
    Note over Q: Queue at capacity (high watermark)
    Q->>Obs: BackpressureEvent::Triggered
    Q-->>FD: backpressure active

    FD->>FD: Suspend producer scheduling

    loop Consumer drains queue
        FD->>Cons: invoke process/push
        Cons-->>FD: result
        FD->>Q: dequeue element
    end

    Note over Q: Queue below low watermark
    Q->>Obs: BackpressureEvent::Relieved
    Q-->>FD: backpressure relieved

    FD->>FD: Resume producer scheduling
    Prod->>Q: enqueue element
    Note over Q: Normal flow resumes

Fan-Out and Fan-In Behavior

graph TD
    subgraph FanOut["Fan-Out: One Producer → Multiple Consumers"]
        direction LR
        P1["Producer<br/><small>effective demand =<br/>min(demand A, demand B)</small>"]
        P1 -->|"Stream A"| C1["Consumer A<br/><small>fast</small>"]
        P1 -->|"Stream B"| C2["Consumer B<br/><small>slow</small>"]
    end

    subgraph FanIn["Fan-In: Multiple Producers → One Consumer"]
        direction LR
        PA["Producer A"] -->|"Stream A<br/><small>independent demand</small>"| Merge["Consumer<br/><small>merge policy:<br/>equal or priority</small>"]
        PB["Producer B"] -->|"Stream B<br/><small>independent demand</small>"| Merge
    end

    style P1 fill:#2563EB,stroke:#1D4ED8,color:#fff
    style C1 fill:#16A34A,stroke:#15803D,color:#fff
    style C2 fill:#DC2626,stroke:#B91C1C,color:#fff
    style PA fill:#2563EB,stroke:#1D4ED8,color:#fff
    style PB fill:#2563EB,stroke:#1D4ED8,color:#fff
    style Merge fill:#7C3AED,stroke:#6D28D9,color:#fff

For fan-out topologies (one producer, multiple consumers), the producer’s effective demand is the minimum across all downstream streams by default. This ensures the producer does not outrun the slowest consumer. An alternative IndependentPerBranch mode allows faster consumers to pull ahead.

For fan-in topologies (multiple producers, one consumer), each upstream stream maintains independent demand. The consumer grants demand based on its merge policy (equal allocation across branches or priority-based).

Configuring Backpressure Policies

Each stream in a pipeline can be configured with a BackpressurePolicy that dictates behavior when the queue is full:

PolicyBehaviorData LossUse Case
Block (default)Suspend the producer until the consumer frees capacity.NoneCorrectness-critical pipelines where every element must be processed.
DropOldestRemove the oldest element in the queue to make room.Yes (oldest)Real-time workloads where freshness matters more than completeness (e.g., live sensor data).
DropNewestReject the new element. The producer continues.Yes (newest)Rate-limiting scenarios where the latest burst can be safely discarded.
ErrorReturn an error to the producer, propagated as a ProcessError.None (but stops)Pipelines where backpressure indicates a fundamental problem that should halt processing.
RateLimit { max_elements_per_second }Delay the producer to maintain a maximum throughput.NonePipelines that need predictable throughput without suspension.

Policies are configured per-stream in the pipeline definition within Torvyn.toml:

[runtime.backpressure]
default_queue_depth = 64
default_policy = "block"
low_watermark_ratio = 0.5

# Override for a specific stream
[flow.main.edges.transform-to-sink.backpressure]
queue_depth = 256
policy = "drop-oldest"
low_watermark_ratio = 0.25

Observing Backpressure in Production

Torvyn exposes several metrics and diagnostic tools for understanding backpressure behavior:

Per-stream metrics:

  • stream.backpressure.events — Total number of backpressure episodes on this stream.
  • stream.backpressure.duration_ns — Total time spent in backpressure.
  • stream.queue.current_depth — Current queue depth (a gauge).
  • stream.queue.peak_depth — Maximum queue depth observed.

In torvyn bench reports: The scheduling section shows total backpressure events and queue peak across all streams. A pipeline with zero backpressure events under sustained load typically means the source is slower than the pipeline’s processing capacity. Frequent backpressure events indicate a consumer bottleneck.

In torvyn trace output: With --show-backpressure, trace output highlights backpressure events inline with element processing, showing which stream triggered, how long the producer was suspended, and how many elements were drained before the pressure was relieved.

Via the inspection API: The GET /flows/{flow_id} endpoint returns current queue depths and backpressure state for every stream in the flow.

Common Backpressure Patterns and Anti-Patterns

Pattern: End-to-end bounded memory. With the Block policy, total pipeline memory is deterministic: Σ(queue_capacity × max_element_size) for all streams. This is the recommended default for production pipelines where correctness matters more than drop tolerance.

Pattern: Fresh-data preference. For live data feeds (sensor streams, market data), use DropOldest to ensure the consumer always processes the most recent data when it falls behind.

Pattern: Backpressure-driven autoscaling. Monitor stream.backpressure.duration_ns over time. Sustained backpressure on a specific stream indicates that the downstream component is the bottleneck. This metric can drive operational decisions about resource allocation.

Anti-pattern: Unbounded queues. Setting queue_depth to an extremely large value (e.g., 1,000,000) effectively disables backpressure and returns to the failure mode of unbounded queue growth. If you find yourself setting very large queue depths, reconsider whether the pipeline topology or component performance should be addressed instead.

Anti-pattern: Ignoring backpressure metrics. Backpressure events are not errors — they are a healthy signal that the system is self-regulating. However, persistent backpressure indicates a capacity imbalance. Monitor and investigate pipelines where backpressure events are sustained.

Anti-pattern: Over-aggressive watermarks. Setting the low watermark very high (e.g., 0.95) reduces the hysteresis band and can cause rapid oscillation. The default of 0.5 provides a stable equilibrium for most workloads.

Capabilities and Security

What Are Capabilities?

In Torvyn, a capability is a specific permission to perform an action or access a resource. Capabilities govern what a component is allowed to do beyond pure computation on the data provided through its stream interface.

A Torvyn component that has been granted no capabilities can receive stream elements, read payload data, perform computation, produce output elements, and return results. It cannot read files, open network connections, access the system clock, generate random numbers, or interact with any system service. Every additional permission requires an explicit capability grant.

This model is often called “deny-all by default” or “principle of least privilege.” It is the security foundation of Torvyn.

The Deny-All-by-Default Model

The security design is governed by five principles:

Deny-all by default. A component with no capability grants can do nothing beyond pure computation on data provided through its stream interface. Every capability must be explicitly granted.

Fail closed. If the capability system encounters an ambiguous state — a grant that cannot be resolved, a policy that cannot be evaluated — the default behavior is to deny the operation.

Static over dynamic. Catch capability violations at link time (torvyn link) rather than at runtime wherever possible. A runtime capability violation is a defect in the static validation pipeline.

Least privilege. Operators should grant the minimum capability set needed. Tooling warns when a component requests capabilities that appear excessive for its declared interface type.

Audit everything. Every capability exercise, every denial, every grant resolution is recorded as a structured audit event. The audit log is the security team’s interface to Torvyn.

Declaring Capabilities in Component Manifests

Components declare their capability requirements in Torvyn.toml:

[component]
name = "my-transform"
version = "0.2.0"

[capabilities.required]
# WASI capabilities this component needs to function
wasi-filesystem-read = true
wasi-clocks = true

[capabilities.optional]
# Capabilities that enhance functionality but are not required
wasi-random = true
# Explicitly declare capabilities not needed (aids auditing)
wasi-network-egress = false

[capabilities.torvyn]
# Torvyn-specific resource requirements
max-buffer-size = "16MiB"
max-memory = "64MiB"
buffer-pool-access = "default"

The manifest serves two purposes. First, it informs operators what the component needs, enabling informed trust decisions. Second, it enables bidirectional validation: the host checks that the manifest is consistent with the component’s actual WIT imports. If a component’s WIT world imports wasi:filesystem/preopens but the manifest does not declare wasi-filesystem-read = true, this is a link-time error. If the manifest declares wasi-network-egress = false but the WIT world imports wasi:sockets/tcp, this is also an error. No component can silently gain capabilities it did not declare.

Capability Taxonomy

Capabilities are organized into domains:

WASI-aligned capabilities map to standard WASI interfaces: filesystem read/write (scoped to directory subtrees), TCP connect/listen, UDP, HTTP outgoing, wall clock, monotonic clock, cryptographic random, insecure random, environment variables, stdout, stderr.

Torvyn-specific capabilities govern resource and stream access: buffer pool allocation, named pool access, backpressure signal emission, flow metadata inspection, runtime diagnostics query, and custom metrics emission.

Many capabilities require scoping. A filesystem read permission is meaningless without knowing which paths are accessible. A network connect permission can be scoped to specific host patterns and port ranges. Scoping rules are directional: a grant with a broader scope satisfies a request with a narrower scope, but never the reverse.

Capability Resolution Flow

At link time, torvyn link resolves the effective capability set for each component by intersecting what the component requests with what the operator grants:

graph TD
    A["Component Manifest<br/>(required + optional capabilities)"] --> D["Capability Resolver"]
    B["Pipeline Config<br/>(operator grants)"] --> D
    C["WIT World Imports<br/>(actual interface usage)"] --> E["Manifest Validator"]

    A --> E
    E -->|"Mismatch"| F["Link Error:<br/>manifest inconsistent with WIT"]
    E -->|"Consistent"| D

    D --> G{"All required<br/>capabilities granted?"}
    G -->|"No"| H["Link Error:<br/>unmet required capability"]
    G -->|"Yes"| I["Resolved Capability Set"]

    I --> J["Sandbox Configurator"]
    J --> K["Per-Component<br/>Wasm Sandbox Config"]
    J --> L["Audit Log:<br/>grant resolution event"]

    style F fill:#DC2626,stroke:#b91c1c,color:#fff
    style H fill:#DC2626,stroke:#b91c1c,color:#fff
    style I fill:#16A34A,stroke:#15803d,color:#fff
    style K fill:#1E5EF0,stroke:#1a4fd0,color:#fff
    style L fill:#0EA5A0,stroke:#0c918d,color:#fff

Granting Capabilities in Pipeline Configuration

The pipeline operator specifies capability grants in the flow definition:

[security]
default_capability_policy = "deny-all"

[security.grants.my-transform]
capabilities = [
    "wasi:filesystem/read:/data/input",
    "wasi:clocks/wall-clock",
    "wasi:random/random",
]

[security.grants.my-sink]
capabilities = [
    "wasi:filesystem/write:/data/output",
]

At link time, torvyn link computes the intersection of the component’s required capabilities and the operator’s grants. If any required capability is not granted, linking fails with a clear error message identifying the unmet capability. Optional capabilities that are not granted are silently skipped — the component should handle their absence gracefully.

Auditing Capabilities in Deployed Pipelines

Every security-relevant event produces a structured audit record:

  • Capability exercises: Each time a component uses a granted capability (opens a file, makes a network connection), the exercise is recorded with the component ID, capability identifier, and timestamp.
  • Capability denials: Each time a component attempts an operation it has not been granted, the denial is recorded. In a properly validated pipeline, runtime denials should never occur (they would have been caught at link time). A runtime denial indicates either a validation gap or a dynamic capability request.
  • Grant resolution events: The full capability resolution result for each component is logged at pipeline startup, creating a complete audit trail of what each component was authorized to do.

The torvyn inspect command can display the capability profile of any component or artifact, showing required capabilities, optional capabilities, and the effective grants in a specific pipeline configuration.

Security Implications and Best Practices

Treat capability grants as security policy. Reviewing a pipeline’s [security.grants] section should be part of the deployment review process, just as reviewing IAM policies is part of cloud deployment review.

Use scoped grants. Grant wasi:filesystem/read:/data/input rather than wasi:filesystem/read (which grants access to the entire filesystem). Narrower scopes reduce blast radius.

Audit capability exercises in production. The capability.exercises and capability.denials metrics provide ongoing visibility into what components are actually doing with their granted permissions.

Verify third-party components before granting capabilities. Use torvyn inspect to examine what capabilities a component requests before adding it to your pipeline. A stateless transform that requests network egress deserves scrutiny.

Separate concerns across components. Rather than granting filesystem read and write to a single component, consider splitting into a reader component (read-only filesystem access) and a writer component (write-only access). Each component has a smaller capability surface, and a vulnerability in the reader cannot lead to data modification.

Observability

Torvyn treats observability as a design primitive, not an afterthought. Every stream, resource handoff, queueing point, backpressure event, and failure is measurable. The goal is to make Torvyn the most introspectable streaming runtime available — not by volume of data emitted, but by the precision and actionability of the insights it provides.

Torvyn’s Three-Level Observability Model

The observability system supports three operational levels, configurable at startup and adjustable at runtime without restarting flows:

graph LR
    L0["Level 0: Off<br/><small>No collection<br/>Bare-metal benchmarks only</small>"]
    L1["Level 1: Production<br/><small>Counters, histograms, sampled traces<br/>&lt; 1% overhead</small>"]
    L2["Level 2: Diagnostic<br/><small>Per-element spans, copy records<br/>&lt; 5% overhead</small>"]

    L0 -->|"enable"| L1
    L1 -->|"escalate"| L2
    L2 -->|"de-escalate"| L1
    L1 -->|"disable"| L0

    style L0 fill:#6B7280,stroke:#4B5563,color:#fff
    style L1 fill:#2563EB,stroke:#1D4ED8,color:#fff
    style L2 fill:#D97706,stroke:#B45309,color:#fff

Level transitions are atomic and do not require restarting flows or pipelines.

Level 0: Off

Nothing is collected. This level exists for bare-metal benchmarks that measure raw runtime overhead without any observability instrumentation. It is not recommended for any deployment other than performance characterization.

Level 1: Production (default)

Flow-level counters, latency histograms, error counts, resource pool summaries, and trace context propagation with sampled span export. This level is designed to be always-on in every production deployment with negligible overhead.

Target overhead: < 1% throughput reduction, < 500 nanoseconds per element.

Sampling strategy at Production level:

  • Head-based sampling: A configurable fraction of flows are fully traced (default: 1%). All invocations within a sampled flow produce spans.
  • Error-triggered sampling: If any component returns an error, the entire flow’s trace is promoted to full sampling, including retroactive span export from a ring buffer.
  • Tail-latency sampling: If a flow’s end-to-end latency exceeds a configurable threshold, it is promoted to full sampling.

This adaptive approach ensures that interesting traces (errors, slow flows) are always captured without incurring full tracing overhead on every element.

flowchart TD
    Elem["Incoming Element"] --> Sampled{"Head-based<br/>sample?<br/><small>(default 1%)</small>"}
    Sampled -->|"Yes"| Full["Full Trace"]
    Sampled -->|"No"| Ring["Record to Ring Buffer<br/><small>(lightweight)</small>"]
    Ring --> ErrCheck{"Error<br/>returned?"}
    ErrCheck -->|"Yes"| Promote1["Promote to Full Trace<br/><small>Retroactive span export</small>"]
    ErrCheck -->|"No"| LatCheck{"Tail latency<br/>exceeded?"}
    LatCheck -->|"Yes"| Promote2["Promote to Full Trace<br/><small>Retroactive span export</small>"]
    LatCheck -->|"No"| Discard["Discard Ring Buffer"]

    style Full fill:#16A34A,stroke:#15803D,color:#fff
    style Promote1 fill:#D97706,stroke:#B45309,color:#fff
    style Promote2 fill:#D97706,stroke:#B45309,color:#fff
    style Discard fill:#6B7280,stroke:#4B5563,color:#fff
    style Elem fill:#2563EB,stroke:#1D4ED8,color:#fff

Level 2: Diagnostic

All of Production level, plus per-element span events, per-copy accounting records, per-backpressure event records, and queue depth snapshots. This level provides complete visibility into every data movement in the pipeline.

Target overhead: < 5% throughput reduction, < 2 microseconds per element.

Level transitions are atomic. Switching from Production to Diagnostic does not require restarting flows or pipelines. This means operators can escalate observability on a running system when investigating a problem.

Tracing: How Trace Context Propagates Through Pipelines

Every flow in Torvyn carries a TraceContext containing a W3C Trace ID (128-bit), a current span ID (64-bit), a parent span ID, and trace flags. The host runtime creates a new span for each component invocation and updates the trace context automatically. Components do not need to implement trace propagation — it is invisible to them unless they choose to read the current trace ID for their own purposes.

The span hierarchy Torvyn generates:

flow:{flow_id}                              [Flow-level root span]
├── component:{component_id}:invoke:{seq}   [Per-invocation span]
│   ├── resource:transfer:{id}              [Diagnostic only]
│   └── resource:copy:{id}                  [Diagnostic only]
├── component:{component_id}:invoke:{seq+1}
│   └── ...
├── backpressure:{stream_id}                [Diagnostic only]
└── ...

When a pipeline ingests data from an external source (HTTP request, Kafka message), the source component can extract an incoming W3C traceparent header and pass it to the host. The flow’s root span will then be parented to the external trace, enabling end-to-end distributed tracing across Torvyn and non-Torvyn services. Similarly, sink components can inject the current trace ID into outgoing messages.

sequenceDiagram
    participant Ext as External Service
    participant Src as Source Component
    participant Host as Torvyn Host
    participant Proc as Processor
    participant Sink as Sink Component
    participant Col as Observability Collector
    participant OTLP as OTLP Exporter

    Ext->>Src: Request with traceparent header
    Src->>Host: Pass incoming trace context
    Host->>Host: Create flow root span<br/>(parented to external trace)

    Host->>Src: invoke pull()
    Host->>Col: Span: component:source:invoke:0

    Host->>Proc: invoke process(borrow)
    Host->>Col: Span: component:processor:invoke:0
    Note over Col: Diagnostic: resource:transfer, resource:copy

    Host->>Sink: invoke push(borrow)
    Host->>Col: Span: component:sink:invoke:0
    Sink->>Ext: Outgoing message with traceparent

    Col->>OTLP: Export spans (batched, non-blocking)

Traces are exported via OTLP (OpenTelemetry Protocol), supporting gRPC, HTTP, stdout (JSON), file output, and in-process channels for CLI tool integration. Export runs in a dedicated background task and never blocks the hot path.

Metrics: What Is Available and How to Access Them

Torvyn’s metrics are pre-allocated at flow creation time and use direct struct field access on the hot path (no hash-map lookups, no dynamic registration). The export layer translates internal metric structures into Prometheus exposition format and OTLP metrics for external consumption.

Per-flow metrics: flow.elements.total (elements processed), flow.elements.errors (errors), flow.latency (end-to-end latency histogram), flow.copies.total (copy operations), flow.copies.bytes (bytes copied).

Per-component metrics: component.invocations (call count), component.errors (error count), component.processing_time (latency histogram per invocation), component.fuel_consumed (Wasm fuel if metering is enabled), component.memory_current (linear memory gauge).

Per-stream metrics: stream.elements.transferred, stream.backpressure.events, stream.backpressure.duration_ns, stream.queue.current_depth, stream.queue.wait_time (histogram).

Resource pool metrics: pool.capacity, pool.available, pool.allocated, pool.returned, pool.fallback_count, pool.exhaustion_events.

System-level metrics: system.flows.active, system.components.active, system.memory.total, system.scheduler.wakeups, system.scheduler.idle_ns.

Metrics are accessible through two mechanisms. Pull (Prometheus): The runtime inspection API serves a /metrics endpoint in Prometheus exposition format, scrapable by any Prometheus-compatible monitoring system. Push (OTLP): Metrics are batched and exported via OTLP at configurable intervals (default: 15 seconds).

graph TD
    subgraph HotPath["Hot Path (per-element)"]
        direction LR
        FD["Flow Driver"] -->|"direct struct<br/>field access"| PM["Pre-allocated<br/>Metric Structs"]
    end

    subgraph Export["Export Layer"]
        direction TB
        PM -->|"read"| Prom["/metrics endpoint<br/><small>Prometheus exposition format</small>"]
        PM -->|"batch &amp; serialize"| OTLP["OTLP Exporter<br/><small>gRPC / HTTP / stdout / file</small>"]
    end

    subgraph Consumers["External Systems"]
        direction TB
        Prom --> Grafana["Grafana"]
        OTLP --> Jaeger["Jaeger / Tempo"]
        OTLP --> CI["CI/CD Pipeline"]
    end

    style FD fill:#2563EB,stroke:#1D4ED8,color:#fff
    style PM fill:#7C3AED,stroke:#6D28D9,color:#fff
    style Prom fill:#059669,stroke:#047857,color:#fff
    style OTLP fill:#059669,stroke:#047857,color:#fff
    style Grafana fill:#475569,stroke:#334155,color:#fff
    style Jaeger fill:#475569,stroke:#334155,color:#fff
    style CI fill:#475569,stroke:#334155,color:#fff

Benchmarking: How to Use torvyn bench

torvyn bench is a first-class tool for performance analysis. It runs a pipeline under sustained load and produces a structured report covering throughput, latency distribution, per-component breakdown, resource utilization, and scheduling behavior.

A benchmark report includes:

  • Throughput in elements per second and bytes per second.
  • Latency percentiles (p50, p90, p95, p99, p999, max).
  • Per-component latency breakdown (which stage is slowest).
  • Queue occupancy statistics (mean depth, peak depth, backpressure events).
  • Buffer allocation and reuse statistics (allocations, pool hits, fallback allocations, copy counts and breakdown by reason).
  • Scheduling statistics (wakeup counts per component, idle time).
  • Memory usage (peak and mean across all components).

Benchmark results can be saved as named baselines and compared across runs using --compare, enabling regression detection in CI/CD pipelines.

Integration with External Tools

Grafana: Import Torvyn’s Prometheus metrics and build dashboards showing flow throughput, latency percentiles, queue depths, pool utilization, and backpressure activity over time.

Jaeger / Tempo: Point Torvyn’s OTLP trace export to a Jaeger or Grafana Tempo endpoint. View per-element traces showing the path through each component, timing at each stage, and resource events. Error-promoted and tail-latency-promoted traces provide insight into the worst-performing flows.

CI/CD integration: Use torvyn bench --report-format json to produce machine-readable benchmark results. Compare against a baseline (--compare baseline.json) and fail the pipeline if latency regressions exceed a threshold.

Interpreting Benchmark Reports

A benchmark report is most useful when you know what to look for:

  • If p99 latency is much higher than p50: Look for occasional backpressure events or garbage collection in component code. The per-component breakdown will show which stage introduces the tail.
  • If throughput is lower than expected: Check queue peak depth. If queues are consistently full, the bottleneck is downstream. If queues are rarely full but throughput is low, the bottleneck is the source or the scheduler yield frequency.
  • If copy count is high: Check the copy reason breakdown. MetadataMarshal copies are unavoidable but tiny. If PayloadRead or PayloadWrite counts seem excessive, investigate whether any component is reading payload unnecessarily (a filter that reads the full payload when metadata-only filtering would suffice).
  • If pool fallback allocations are high: The buffer pool is undersized for the workload. Increase pool sizes for the tier(s) showing high fallback rates.

Tutorial: AI Token Streaming Pipeline

This tutorial builds a pipeline that simulates AI token streaming — a pattern common in AI inference systems where tokens are generated incrementally and must be filtered, assembled, and delivered in real time.

What you will build:

  1. Token Source — simulates an AI model emitting tokens one at a time.
  2. Content Policy Filter — evaluates each token against a blocklist and drops prohibited content.
  3. Token Sink — collects tokens and displays the assembled text.

What you will learn: Streaming at the individual-element level, content filtering as a zero-allocation operation, backpressure between a fast producer and a slower consumer, and tracing to understand flow behavior.

Prerequisites: Complete Your First Pipeline.

Time required: 20–30 minutes.

Project Setup

torvyn init token-pipeline --template full-pipeline
cd token-pipeline

We will replace the generated components with our own implementations.

Step 1: Token Source

The source simulates an AI model generating tokens. Each token is a short string (a word or punctuation mark). The source emits one token per pull call.

Contract: components/source/wit/world.wit

package source:component;

world source {
    import torvyn:streaming/types@0.1.0;
    import torvyn:resources/buffer-ops@0.1.0;
    export torvyn:streaming/source@0.1.0;
}

Implementation: components/source/src/lib.rs

#![allow(unused)]
fn main() {
// Token source: simulates an AI model emitting tokens.

wit_bindgen::generate!({
    world: "source",
    path: "wit",
});

use exports::torvyn::streaming::source::Guest;
use torvyn::streaming::types::{OutputElement, ElementMeta, ProcessError, BackpressureSignal};
use torvyn::resources::buffer_ops;

struct TokenSource;

static mut INDEX: usize = 0;
static mut PAUSED: bool = false;

// Simulated token stream: a paragraph of generated text, split into tokens.
const TOKENS: &[&str] = &[
    "The", " quick", " brown", " fox", " jumped",
    " over", " the", " lazy", " dog", ".",
    " Meanwhile", ",", " the", " harmful_content",
    " was", " intercepted", " by", " the",
    " content", " policy", " filter", ".",
    " The", " system", " continued", " to",
    " operate", " normally", " after",
    " filtering", ".",
];

impl Guest for TokenSource {
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        // Respect backpressure.
        if unsafe { PAUSED } {
            return Ok(None);
        }

        let idx = unsafe {
            let current = INDEX;
            INDEX += 1;
            current
        };

        if idx >= TOKENS.len() {
            return Ok(None); // End of stream.
        }

        let token = TOKENS[idx];
        let buf = buffer_ops::allocate(token.len() as u64)
            .map_err(|_| ProcessError::Internal("allocation failed".into()))?;
        buf.append(token.as_bytes())
            .map_err(|_| ProcessError::Internal("write failed".into()))?;
        buf.set_content_type("text/plain");

        Ok(Some(OutputElement {
            meta: ElementMeta {
                sequence: 0,
                timestamp_ns: 0,
                content_type: "text/plain".into(),
            },
            payload: buf.freeze(),
        }))
    }

    fn notify_backpressure(signal: BackpressureSignal) {
        unsafe {
            PAUSED = matches!(signal, BackpressureSignal::Pause);
        }
    }
}

export!(TokenSource);
}

This source respects backpressure signals: when the downstream pipeline signals Pause, the source stops producing tokens until it receives Ready.

Step 2: Content Policy Filter

The filter examines each token and rejects any that match a blocklist. Filters in Torvyn implement the torvyn:filtering/filter interface, which is optimized for this use case: the filter receives a borrowed reference to the element and returns a boolean. No buffer allocation is needed because the filter does not produce new data — it only decides whether to pass or drop the element.

Contract: components/transform/wit/world.wit

Replace the transform’s WIT contract with a filter contract:

package transform:component;

world transform {
    import torvyn:streaming/types@0.1.0;
    export torvyn:filtering/filter@0.1.0;
}

Notice: no buffer-ops import. Filters do not allocate buffers, which makes them very efficient.

Implementation: components/transform/src/lib.rs

#![allow(unused)]
fn main() {
// Content policy filter: drops tokens that match a blocklist.

wit_bindgen::generate!({
    world: "transform",
    path: "wit",
});

use exports::torvyn::filtering::filter::Guest;
use torvyn::streaming::types::{StreamElement, ProcessError};

struct ContentFilter;

// Tokens that should be blocked by the content policy.
const BLOCKED_TOKENS: &[&str] = &[
    "harmful_content",
    "prohibited_term",
    "unsafe_output",
];

impl Guest for ContentFilter {
    fn evaluate(input: &StreamElement) -> Result<bool, ProcessError> {
        let data = input.payload.read_all();
        let token = String::from_utf8_lossy(&data);
        let trimmed = token.trim();

        // Check against blocklist.
        for blocked in BLOCKED_TOKENS {
            if trimmed.eq_ignore_ascii_case(blocked) {
                // Reject this token.
                return Ok(false);
            }
        }

        // Pass the token through.
        Ok(true)
    }
}

export!(ContentFilter);
}

The filter reads the token text and checks it against the blocklist. Returning false tells the runtime to drop the element — no copy, no allocation, no downstream delivery. This is one of the most efficient patterns in Torvyn: a filter that only reads metadata and a small payload can run with minimal overhead.

Step 3: Token Collection Sink

The sink collects tokens and assembles them into complete text. It demonstrates how a sink can maintain internal state.

Contract: components/sink/wit/world.wit

Create the sink directory and contract:

mkdir -p components/sink/wit
mkdir -p components/sink/src
package sink:component;

world sink {
    import torvyn:streaming/types@0.1.0;
    export torvyn:streaming/sink@0.1.0;
}

Cargo.toml: components/sink/Cargo.toml

[package]
name = "sink"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "sink:component"

Implementation: components/sink/src/lib.rs

#![allow(unused)]
fn main() {
// Token collection sink: assembles tokens into text and displays them.

wit_bindgen::generate!({
    world: "sink",
    path: "wit",
});

use exports::torvyn::streaming::sink::Guest;
use torvyn::streaming::types::{StreamElement, BackpressureSignal, ProcessError};

struct TokenCollector;

static mut COLLECTED: Vec<u8> = Vec::new();
static mut TOKEN_COUNT: u64 = 0;

impl Guest for TokenCollector {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let data = element.payload.read_all();

        unsafe {
            COLLECTED.extend_from_slice(&data);
            TOKEN_COUNT += 1;

            // Print a running status every 10 tokens.
            if TOKEN_COUNT % 10 == 0 {
                let text = String::from_utf8_lossy(&COLLECTED);
                eprintln!("[{} tokens] {}", TOKEN_COUNT, text);
            }
        }

        Ok(BackpressureSignal::Ready)
    }

    fn complete() -> Result<(), ProcessError> {
        unsafe {
            let text = String::from_utf8_lossy(&COLLECTED);
            println!("\n── Assembled Text ──");
            println!("{text}");
            println!("\n── Stats ──");
            println!("Total tokens received: {TOKEN_COUNT}");
        }
        Ok(())
    }
}

export!(TokenCollector);
}

The sink collects all tokens into an internal buffer and prints progress every 10 tokens. When the stream completes, it prints the fully assembled text. The token “harmful_content” will be absent — filtered out by the content policy stage.

Step 4: Pipeline Configuration

Update Torvyn.toml:

[torvyn]
name = "token-pipeline"
version = "0.1.0"
description = "AI token streaming with content policy filtering"
contract_version = "0.1.0"

[[component]]
name = "source"
path = "components/source"
language = "rust"

[[component]]
name = "filter"
path = "components/transform"
language = "rust"

[[component]]
name = "sink"
path = "components/sink"
language = "rust"

[flow.main]
description = "Token stream → content filter → collector"

[flow.main.nodes.source]
component = "file://./components/source/target/wasm32-wasip2/debug/source.wasm"
interface = "torvyn:streaming/source"

[flow.main.nodes.filter]
component = "file://./components/transform/target/wasm32-wasip2/debug/transform.wasm"
interface = "torvyn:filtering/filter"

[flow.main.nodes.sink]
component = "file://./components/sink/target/wasm32-wasip2/debug/sink.wasm"
interface = "torvyn:streaming/sink"

[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "filter", port = "input" }

[[flow.main.edges]]
from = { node = "filter", port = "output" }
to = { node = "sink", port = "input" }

Step 5: Build and Run

cd components/source && cargo component build --target wasm32-wasip2 && cd ../..
cd components/transform && cargo component build --target wasm32-wasip2 && cd ../..
cd components/sink && cargo component build --target wasm32-wasip2 && cd ../..
torvyn check
torvyn link
torvyn run

Expected output:

▶ Running flow "main"

[10 tokens] The quick brown fox jumped over the lazy dog.
[20 tokens] The quick brown fox jumped over the lazy dog. Meanwhile, the

── Assembled Text ──
The quick brown fox jumped over the lazy dog. Meanwhile, the was intercepted by the content policy filter. The system continued to operate normally after filtering.

── Stats ──
Total tokens received: 30

Notice that the token “harmful_content” is missing from the output. The filter dropped it, and the remaining tokens assembled into coherent text (minus the blocked word).

Step 6: Trace the Filter Behavior

torvyn trace --limit 15 --show-backpressure

In the trace output, look for elements where the filter stage shows “drop” instead of “pass.” This confirms that the filter is working and that the dropped element never reaches the sink — no buffer was allocated for it, and no downstream processing occurred.

Concepts Demonstrated

  • Streaming at token granularity — each token is a separate stream element, enabling per-token processing.
  • Filtering without allocation — the filter reads the element and returns a boolean. No output buffer is allocated for passed elements; the runtime forwards the original buffer.
  • Backpressure — the source respects pause signals from the downstream pipeline.
  • End-of-stream signaling — the source returns None to signal completion; the runtime calls complete() on the sink.
  • Observable filteringtorvyn trace shows exactly which elements were dropped and why.

Next Steps

Tutorial: Event Enrichment Pipeline

This tutorial builds a pipeline that reads events, enriches them with metadata, filters by criteria, and writes the enriched results. It demonstrates multi-stage processing, the buffer ownership model, and how Torvyn’s resource manager tracks copies and allocations across the pipeline.

What you will build:

  1. Event Source — generates structured event records.
  2. Enrichment Processor — adds metadata (priority, category) to each event.
  3. Priority Filter — passes only high-priority events.
  4. Event Sink — writes enriched events to output.

What you will learn: Multi-stage pipelines with four components, buffer copy accounting across stages, resource lifecycle visibility, and how the resource manager’s ownership tracking works in practice.

Prerequisites: Complete Your First Pipeline.

Time required: 25–35 minutes.

Project Setup

torvyn init enrichment-pipeline --template empty
cd enrichment-pipeline

We start with the empty template and build everything from scratch.

Create the component directories:

mkdir -p components/event-source/wit components/event-source/src
mkdir -p components/enricher/wit components/enricher/src
mkdir -p components/priority-filter/wit components/priority-filter/src
mkdir -p components/event-sink/wit components/event-sink/src

Step 1: Event Source

The source generates JSON-formatted event records.

components/event-source/Cargo.toml

[package]
name = "event-source"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "event-source:component"

components/event-source/wit/world.wit

package event-source:component;

world event-source {
    import torvyn:streaming/types@0.1.0;
    import torvyn:resources/buffer-ops@0.1.0;
    export torvyn:streaming/source@0.1.0;
}

components/event-source/src/lib.rs

#![allow(unused)]
fn main() {
// Event source: generates JSON event records.

wit_bindgen::generate!({
    world: "event-source",
    path: "wit",
});

use exports::torvyn::streaming::source::Guest;
use torvyn::streaming::types::{OutputElement, ElementMeta, ProcessError, BackpressureSignal};
use torvyn::resources::buffer_ops;

struct EventSource;

static mut COUNTER: u64 = 0;

struct EventTemplate {
    event_type: &'static str,
    source: &'static str,
    severity: &'static str,
}

const EVENTS: &[EventTemplate] = &[
    EventTemplate { event_type: "login",         source: "auth-service",  severity: "info" },
    EventTemplate { event_type: "purchase",      source: "order-service", severity: "info" },
    EventTemplate { event_type: "error",         source: "api-gateway",   severity: "high" },
    EventTemplate { event_type: "login_failed",  source: "auth-service",  severity: "high" },
    EventTemplate { event_type: "page_view",     source: "web-frontend",  severity: "low" },
    EventTemplate { event_type: "deployment",    source: "ci-pipeline",   severity: "high" },
    EventTemplate { event_type: "health_check",  source: "monitor",       severity: "low" },
    EventTemplate { event_type: "rate_limited",  source: "api-gateway",   severity: "high" },
];

impl Guest for EventSource {
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        let count = unsafe {
            COUNTER += 1;
            COUNTER
        };

        if count > 50 {
            return Ok(None);
        }

        let template = &EVENTS[((count - 1) as usize) % EVENTS.len()];
        let json = format!(
            r#"{{"id":"evt_{:06}","type":"{}","source":"{}","severity":"{}"}}"#,
            count, template.event_type, template.source, template.severity
        );

        let buf = buffer_ops::allocate(json.len() as u64)
            .map_err(|_| ProcessError::Internal("allocation failed".into()))?;
        buf.append(json.as_bytes())
            .map_err(|_| ProcessError::Internal("write failed".into()))?;
        buf.set_content_type("application/json");

        Ok(Some(OutputElement {
            meta: ElementMeta {
                sequence: 0,
                timestamp_ns: 0,
                content_type: "application/json".into(),
            },
            payload: buf.freeze(),
        }))
    }

    fn notify_backpressure(_signal: BackpressureSignal) {}
}

export!(EventSource);
}

Step 2: Enrichment Processor

The enricher reads each event, parses the JSON, adds enrichment fields (a priority score and a category), and writes an enriched JSON record to a new buffer.

components/enricher/Cargo.toml

[package]
name = "enricher"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "enricher:component"

components/enricher/wit/world.wit

package enricher:component;

world enricher {
    import torvyn:streaming/types@0.1.0;
    import torvyn:resources/buffer-ops@0.1.0;
    export torvyn:streaming/processor@0.1.0;
}

components/enricher/src/lib.rs

#![allow(unused)]
fn main() {
// Enrichment processor: adds priority score and category to events.

wit_bindgen::generate!({
    world: "enricher",
    path: "wit",
});

use exports::torvyn::streaming::processor::{Guest, ProcessResult};
use torvyn::streaming::types::{StreamElement, OutputElement, ElementMeta, ProcessError};
use torvyn::resources::buffer_ops;

struct Enricher;

impl Guest for Enricher {
    fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
        // Read the input event.
        // NOTE: This is a measured copy — the resource manager records it.
        let data = input.payload.read_all();
        let text = String::from_utf8_lossy(&data);

        // Simple enrichment: extract severity and assign a priority score.
        let priority = if text.contains(r#""severity":"high""#) {
            100
        } else if text.contains(r#""severity":"info""#) {
            50
        } else {
            10
        };

        let category = if text.contains(r#""source":"auth-service""#) {
            "security"
        } else if text.contains(r#""source":"order-service""#) {
            "business"
        } else if text.contains(r#""source":"ci-pipeline""#) {
            "operations"
        } else {
            "general"
        };

        // Build enriched JSON by appending fields.
        // In production, you would use a proper JSON library.
        let enriched = if text.ends_with('}') {
            format!(
                r#"{},"priority":{},"category":"{}"}}"#,
                &text[..text.len() - 1],
                priority,
                category
            )
        } else {
            text.to_string()
        };

        // Allocate a new buffer for the enriched output.
        // NOTE: This is a second allocation — the bench report will show it.
        let out_buf = buffer_ops::allocate(enriched.len() as u64)
            .map_err(|_| ProcessError::Internal("allocation failed".into()))?;
        out_buf.append(enriched.as_bytes())
            .map_err(|_| ProcessError::Internal("write failed".into()))?;
        out_buf.set_content_type("application/json");

        Ok(ProcessResult::Emit(OutputElement {
            meta: ElementMeta {
                sequence: input.meta.sequence,
                timestamp_ns: input.meta.timestamp_ns,
                content_type: "application/json".into(),
            },
            payload: out_buf.freeze(),
        }))
    }
}

export!(Enricher);
}

The comments in the code highlight copy and allocation events. The read_all() call is a copy from host memory into the component’s linear memory. The allocate() call creates a new buffer. Both events are recorded by the resource manager and will appear in torvyn trace and torvyn bench output. This is Torvyn’s copy accounting in action: copies are not hidden — they are measured and reported.

Step 3: Priority Filter

The priority filter examines the enriched event and passes only high-priority events (priority >= 100).

components/priority-filter/Cargo.toml

[package]
name = "priority-filter"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "priority-filter:component"

components/priority-filter/wit/world.wit

package priority-filter:component;

world priority-filter {
    import torvyn:streaming/types@0.1.0;
    export torvyn:filtering/filter@0.1.0;
}

components/priority-filter/src/lib.rs

#![allow(unused)]
fn main() {
// Priority filter: passes only high-priority events.

wit_bindgen::generate!({
    world: "priority-filter",
    path: "wit",
});

use exports::torvyn::filtering::filter::Guest;
use torvyn::streaming::types::{StreamElement, ProcessError};

struct PriorityFilter;

impl Guest for PriorityFilter {
    fn evaluate(input: &StreamElement) -> Result<bool, ProcessError> {
        let data = input.payload.read_all();
        let text = String::from_utf8_lossy(&data);

        // Pass only events with priority >= 100.
        Ok(text.contains(r#""priority":100"#))
    }
}

export!(PriorityFilter);
}

Step 4: Event Sink

components/event-sink/Cargo.toml

[package]
name = "event-sink"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "event-sink:component"

components/event-sink/wit/world.wit

package event-sink:component;

world event-sink {
    import torvyn:streaming/types@0.1.0;
    export torvyn:streaming/sink@0.1.0;
}

components/event-sink/src/lib.rs

#![allow(unused)]
fn main() {
// Event sink: prints enriched, filtered events.

wit_bindgen::generate!({
    world: "event-sink",
    path: "wit",
});

use exports::torvyn::streaming::sink::Guest;
use torvyn::streaming::types::{StreamElement, BackpressureSignal, ProcessError};

struct EventSink;

static mut EVENT_COUNT: u64 = 0;

impl Guest for EventSink {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let data = element.payload.read_all();
        let text = String::from_utf8_lossy(&data);

        unsafe { EVENT_COUNT += 1; }
        let count = unsafe { EVENT_COUNT };

        println!("[event {count}] {text}");

        Ok(BackpressureSignal::Ready)
    }

    fn complete() -> Result<(), ProcessError> {
        let count = unsafe { EVENT_COUNT };
        println!("\n── Pipeline Complete ──");
        println!("High-priority events delivered: {count}");
        Ok(())
    }
}

export!(EventSink);
}

Step 5: Pipeline Configuration

Create Torvyn.toml:

[torvyn]
name = "enrichment-pipeline"
version = "0.1.0"
description = "Event enrichment with priority filtering"
contract_version = "0.1.0"

[[component]]
name = "event-source"
path = "components/event-source"
language = "rust"

[[component]]
name = "enricher"
path = "components/enricher"
language = "rust"

[[component]]
name = "priority-filter"
path = "components/priority-filter"
language = "rust"

[[component]]
name = "event-sink"
path = "components/event-sink"
language = "rust"

[flow.main]
description = "Generate events → enrich → filter by priority → deliver"

[flow.main.nodes.source]
component = "file://./components/event-source/target/wasm32-wasip2/debug/event_source.wasm"
interface = "torvyn:streaming/source"

[flow.main.nodes.enricher]
component = "file://./components/enricher/target/wasm32-wasip2/debug/enricher.wasm"
interface = "torvyn:streaming/processor"

[flow.main.nodes.filter]
component = "file://./components/priority-filter/target/wasm32-wasip2/debug/priority_filter.wasm"
interface = "torvyn:filtering/filter"

[flow.main.nodes.sink]
component = "file://./components/event-sink/target/wasm32-wasip2/debug/event_sink.wasm"
interface = "torvyn:streaming/sink"

[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "enricher", port = "input" }

[[flow.main.edges]]
from = { node = "enricher", port = "output" }
to = { node = "filter", port = "input" }

[[flow.main.edges]]
from = { node = "filter", port = "output" }
to = { node = "sink", port = "input" }

Step 6: Build and Run

Build all components:

for component in event-source enricher priority-filter event-sink; do
    (cd "components/$component" && cargo component build --target wasm32-wasip2)
done

Validate and run:

torvyn check
torvyn link
torvyn run

Expected output shows only the high-priority events:

▶ Running flow "main"

[event 1] {"id":"evt_000003","type":"error","source":"api-gateway","severity":"high","priority":100,"category":"general"}
[event 2] {"id":"evt_000004","type":"login_failed","source":"auth-service","severity":"high","priority":100,"category":"security"}
[event 3] {"id":"evt_000006","type":"deployment","source":"ci-pipeline","severity":"high","priority":100,"category":"operations"}
[event 4] {"id":"evt_000008","type":"rate_limited","source":"api-gateway","severity":"high","priority":100,"category":"general"}
...

── Pipeline Complete ──
High-priority events delivered: 25

Of the 50 source events, only those with severity “high” pass the priority filter. Each has been enriched with a priority score and a category.

Step 7: Observe Copy Accounting

Run torvyn bench --duration 5s and examine the Resources section of the report. You will see:

  • Buffer allocations from the source (one per event) and the enricher (one per event — it creates a new buffer for enriched output).
  • Total copies from the enricher’s read_all() call and the filter’s read_all() call.
  • Pool reuse rate showing how effectively the runtime’s buffer pool recycles allocations.

This is the resource ownership accounting described in the Torvyn design: every allocation, copy, and deallocation is tracked and reported. You can use this data to identify optimization opportunities — for example, if a processor could use buffer metadata instead of reading the full payload, it would eliminate a copy.

Concepts Demonstrated

  • Four-stage pipeline — source → processor → filter → sink.
  • Copy accounting — every read_all() is a measured copy visible in benchmarks and traces.
  • Filter efficiency — the priority filter drops events without allocating output buffers.
  • Enrichment pattern — read input, compute new fields, write to a new buffer, transfer ownership.
  • Resource lifecycle visibilitytorvyn bench reports exactly how many buffers were allocated, reused, and copied.

Guide: Writing a Custom Torvyn Component

This guide explains how to write a Torvyn component from scratch — without templates. It covers WIT contract design, implementing the component in Rust, testing locally, and common patterns for error handling, configuration, and state management.

Use this guide when you need a component that does not fit any of the standard templates, or when you want to understand the full mechanics behind what the templates generate.

Prerequisites: Familiarity with Rust, basic understanding of WebAssembly concepts, and completion of at least the Quickstart.

Time required: 30–40 minutes (reading and building).

Part 1: Designing the WIT Contract

Every Torvyn component begins with a WIT contract. The contract declares what the component exports (its role in the pipeline) and what it imports (the host capabilities it requires).

Choosing a Component Role

Torvyn defines several standard interfaces. Choose the one that matches your component’s role:

RoleInterfaceBehavior
Sourcetorvyn:streaming/source@0.1.0Produces elements. The runtime calls pull() repeatedly.
Processortorvyn:streaming/processor@0.1.0Transforms elements. Receives one input, emits one output or drops.
Filtertorvyn:filtering/filter@0.1.0Accept/reject decisions. No buffer allocation needed.
Sinktorvyn:streaming/sink@0.1.0Consumes elements. The runtime calls push() for each element.
Routertorvyn:streaming/router@0.1.0Routes elements to named output ports.
AggregatorUses torvyn:streaming/processor@0.1.0Accumulates state and emits periodically. Same interface as processor, different pattern.

Declaring Imports

Every component that reads data needs torvyn:streaming/types@0.1.0. Components that produce new buffers also need torvyn:resources/buffer-ops@0.1.0 (which provides the buffer-allocator interface). Declare only what you need:

  • Sources and processors typically import both types and buffer-ops.
  • Filters typically import only types (they do not produce new buffers).
  • Sinks typically import only types.

Adding Lifecycle Hooks

If your component needs initialization (for example, to parse a configuration string or open a connection), you can also export torvyn:streaming/lifecycle@0.1.0:

package my-component:component;

world my-component {
    import torvyn:streaming/types@0.1.0;
    import torvyn:resources/buffer-ops@0.1.0;
    export torvyn:streaming/processor@0.1.0;
    export torvyn:streaming/lifecycle@0.1.0;
}

The lifecycle interface provides two functions:

  • init(config: string) -> result<_, process-error> — called once after instantiation, before any stream processing begins. The config string is provided by the pipeline configuration and is typically JSON.
  • teardown() — called once during shutdown. Release external resources here. The runtime calls this on a best-effort basis; it may not be called if the host shuts down forcefully or a timeout expires.

Example: Rate Limiter Contract

Let us build a rate limiter component as our example. It tracks the rate of incoming elements and drops elements that exceed a configured threshold.

package rate-limiter:component;

world rate-limiter {
    import torvyn:streaming/types@0.1.0;
    export torvyn:filtering/filter@0.1.0;
    export torvyn:streaming/lifecycle@0.1.0;
}

This component is a filter (accept/reject, no buffer allocation) with lifecycle hooks for configuration.

Part 2: Project Structure

Create the project directory:

mkdir -p rate-limiter/wit rate-limiter/src

rate-limiter/Cargo.toml

[package]
name = "rate-limiter"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "rate-limiter:component"

rate-limiter/wit/world.wit

package rate-limiter:component;

world rate-limiter {
    import torvyn:streaming/types@0.1.0;
    export torvyn:filtering/filter@0.1.0;
    export torvyn:streaming/lifecycle@0.1.0;
}

rate-limiter/Torvyn.toml

[torvyn]
name = "rate-limiter"
version = "0.1.0"
contract_version = "0.1.0"

[[component]]
name = "rate-limiter"
path = "."
language = "rust"

Part 3: Implementation

rate-limiter/src/lib.rs

#![allow(unused)]
fn main() {
//! Rate limiter component.
//!
//! Tracks the rate of incoming elements using a sliding window
//! and drops elements that exceed the configured maximum rate.
//!
//! Configuration (JSON):
//! {
//!     "max_per_second": 100,
//!     "window_ms": 1000
//! }

wit_bindgen::generate!({
    world: "rate-limiter",
    path: "wit",
});

use exports::torvyn::filtering::filter::Guest as FilterGuest;
use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use torvyn::streaming::types::{StreamElement, ProcessError};

struct RateLimiter;

// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------

/// Configuration parsed from the init config string.
struct Config {
    max_per_second: u64,
    window_ms: u64,
}

static mut CONFIG: Option<Config> = None;
static mut WINDOW_COUNT: u64 = 0;
static mut WINDOW_START_NS: u64 = 0;
static mut TOTAL_PASSED: u64 = 0;
static mut TOTAL_DROPPED: u64 = 0;

// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------

impl LifecycleGuest for RateLimiter {
    fn init(config: String) -> Result<(), ProcessError> {
        // Parse the configuration string.
        // In a production component, use a proper JSON parser.
        let max_per_second = extract_u64(&config, "max_per_second").unwrap_or(100);
        let window_ms = extract_u64(&config, "window_ms").unwrap_or(1000);

        if max_per_second == 0 {
            return Err(ProcessError::InvalidInput(
                "max_per_second must be greater than 0".into(),
            ));
        }

        unsafe {
            CONFIG = Some(Config {
                max_per_second,
                window_ms,
            });
        }

        Ok(())
    }

    fn teardown() {
        unsafe {
            let passed = TOTAL_PASSED;
            let dropped = TOTAL_DROPPED;
            // In a production component, you might flush metrics here.
            // For now, we just reset state.
            CONFIG = None;
            WINDOW_COUNT = 0;
            WINDOW_START_NS = 0;
            let _ = (passed, dropped); // suppress unused warnings
        }
    }
}

// ---------------------------------------------------------------------------
// Filter
// ---------------------------------------------------------------------------

impl FilterGuest for RateLimiter {
    fn evaluate(input: &StreamElement) -> Result<bool, ProcessError> {
        let config = unsafe {
            CONFIG.as_ref().ok_or_else(|| {
                ProcessError::Internal("rate limiter not initialized".into())
            })?
        };

        let now_ns = input.meta.timestamp_ns;
        let window_ns = config.window_ms * 1_000_000;

        unsafe {
            // Check if we are still within the current window.
            if now_ns.saturating_sub(WINDOW_START_NS) > window_ns {
                // Start a new window.
                WINDOW_START_NS = now_ns;
                WINDOW_COUNT = 0;
            }

            WINDOW_COUNT += 1;

            if WINDOW_COUNT <= config.max_per_second {
                TOTAL_PASSED += 1;
                Ok(true)
            } else {
                TOTAL_DROPPED += 1;
                Ok(false)
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Extract a u64 value from a simple JSON string.
/// This is a minimal parser for tutorial purposes.
/// Production components should use a proper JSON library.
fn extract_u64(json: &str, key: &str) -> Option<u64> {
    let pattern = format!(r#""{key}":"#);
    let start = json.find(&pattern)?;
    let value_start = start + pattern.len();
    let rest = &json[value_start..];

    let end = rest.find(|c: char| !c.is_ascii_digit())?;
    rest[..end].parse().ok()
}

// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------

export!(RateLimiter);
}

What This Demonstrates

Configuration via lifecycle hooks. The init function receives a JSON configuration string from the pipeline definition. This allows the same component binary to be configured differently in different pipelines — for example, one instance limiting to 100 elements/second and another to 1,000.

Stateful filtering. The rate limiter maintains a sliding window counter. State is stored in static mut variables because WebAssembly components are single-threaded (each instance runs in its own isolated sandbox). This is safe within the component’s execution model.

Error handling patterns. The component uses the ProcessError variant types defined in the Torvyn contract:

  • ProcessError::InvalidInput — the configuration string was malformed.
  • ProcessError::Internal — an unexpected condition (component not initialized).

The runtime’s error policy determines what happens when a component returns an error: it may retry, skip the element, or shut down the component, depending on the configured ErrorPolicy.

Clean teardown. The teardown function is called by the runtime during orderly shutdown. Components should release external resources (close connections, flush buffers) here. The runtime enforces a configurable timeout on teardown; if the component does not return in time, the runtime proceeds with forced termination.

Part 4: Testing Locally

Validate the Contract

cd rate-limiter
torvyn check

This validates the manifest and WIT contract without compiling.

Build

cargo component build --target wasm32-wasip2

Inspect the Component

After building, inspect the compiled component to verify its interface:

torvyn inspect target/wasm32-wasip2/debug/rate_limiter.wasm

This shows the component’s exports, imports, and metadata — useful for verifying that the compiled binary matches your expected contract.

Integration Test in a Pipeline

To test the rate limiter, wire it into a pipeline between a source and a sink. Add a flow configuration to Torvyn.toml that includes the rate limiter node with a configuration string:

[flow.test]
description = "Test rate limiter with 10 elements per second"

[flow.test.nodes.source]
component = "file://./path/to/source.wasm"
interface = "torvyn:streaming/source"

[flow.test.nodes.limiter]
component = "file://./target/wasm32-wasip2/debug/rate_limiter.wasm"
interface = "torvyn:filtering/filter"
config = '{"max_per_second": 10, "window_ms": 1000}'

[flow.test.nodes.sink]
component = "file://./path/to/sink.wasm"
interface = "torvyn:streaming/sink"

[[flow.test.edges]]
from = { node = "source", port = "output" }
to = { node = "limiter", port = "input" }

[[flow.test.edges]]
from = { node = "limiter", port = "output" }
to = { node = "sink", port = "input" }

Run and observe the rate limiting:

torvyn run --flow test --limit 50

Trace to see which elements were dropped:

torvyn trace --flow test --limit 20

Benchmark to measure the overhead of the rate limiter:

torvyn bench --flow test --duration 5s

Part 5: Common Patterns

Pattern: Error Recovery

When a component encounters a non-fatal error, return the appropriate ProcessError variant. The runtime’s error policy determines the response:

#![allow(unused)]
fn main() {
fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
    let data = input.payload.read_all();

    if data.is_empty() {
        // Non-fatal: input was empty. Signal invalid input.
        return Err(ProcessError::InvalidInput("empty payload".into()));
    }

    // ... process normally ...
}
}

The error categories have different runtime behaviors:

  • InvalidInput — the element was malformed. The runtime may skip it or retry, depending on the configured error policy.
  • Unavailable — a dependency is temporarily unreachable. The runtime may apply circuit-breaker logic.
  • Internal — an unexpected error. The runtime may retry or skip.
  • DeadlineExceeded — processing took too long. The runtime records a timeout.
  • Fatal — the component cannot continue. The runtime tears down the component and will not send more elements.

Pattern: Using Configuration

Pass structured configuration through the lifecycle.init function:

#![allow(unused)]
fn main() {
impl LifecycleGuest for MyComponent {
    fn init(config: String) -> Result<(), ProcessError> {
        if config.is_empty() {
            // Use defaults.
            return Ok(());
        }

        // Parse configuration (JSON recommended).
        // Store in static state for use during processing.
        Ok(())
    }
}
}

The configuration string is provided by the pipeline’s Torvyn.toml via the config field on the node definition. JSON is the recommended format, but the string is opaque to the runtime — your component can parse it however it prefers.

Pattern: Stateful Accumulation (Aggregator)

Aggregators use the processor interface but accumulate state across multiple elements:

#![allow(unused)]
fn main() {
static mut WINDOW: Vec<Vec<u8>> = Vec::new();
const WINDOW_SIZE: usize = 10;

fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
    let data = input.payload.read_all();

    unsafe {
        WINDOW.push(data);

        if WINDOW.len() >= WINDOW_SIZE {
            // Aggregate the window and emit a result.
            let aggregated = aggregate(&WINDOW);
            WINDOW.clear();

            // ... allocate buffer, write aggregated data, emit ...
        } else {
            // Accumulating — drop this element from output.
            Ok(ProcessResult::Drop)
        }
    }
}
}

Returning ProcessResult::Drop tells the runtime that the input was consumed without producing output. This is not an error — it means the element was absorbed into the component’s internal state.

Pattern: Minimal Buffer Reads

If your component only needs metadata to make a decision, avoid reading the buffer:

#![allow(unused)]
fn main() {
fn evaluate(input: &StreamElement) -> Result<bool, ProcessError> {
    // Decision based on metadata only — no buffer copy.
    Ok(input.meta.content_type == "application/json")
}
}

Each read() or read_all() call copies data from host memory into the component’s linear memory. The resource manager records every copy. By checking metadata first and only reading the buffer when necessary, you minimize copies and improve throughput.

Pattern: Content Type Routing

Use the router interface to send elements to different output ports based on content:

#![allow(unused)]
fn main() {
fn route(input: &StreamElement) -> String {
    match input.meta.content_type.as_str() {
        "application/json" => "json-sink".into(),
        "text/plain" => "text-sink".into(),
        _ => "default".into(),
    }
}
}

The returned string must match a port name defined in the pipeline topology.

Summary

Writing a Torvyn component from scratch involves:

  1. Design the WIT contract — choose the right role interface and declare only the imports you need.
  2. Create the project structureCargo.toml with crate-type = ["cdylib"], a wit/ directory with the contract, and src/lib.rs with the implementation.
  3. Implement the interface — use wit_bindgen::generate! to create bindings, then implement the required traits.
  4. Handle configuration — use the lifecycle interface if you need initialization from a config string.
  5. Handle errors — use the appropriate ProcessError variant for each failure mode.
  6. Test locallytorvyn check for contract validation, cargo component build to compile, torvyn run and torvyn trace to verify behavior.

The key principle: declare only what you need. A filter that does not allocate buffers should not import buffer-ops. A sink that does not need initialization should not export lifecycle. This minimizes the component’s capability surface and makes the contract self-documenting.

Architecture Overview

This document provides a detailed technical overview of Torvyn’s internal architecture. It is intended for experienced engineers evaluating Torvyn for adoption, potential contributors, and operators who need to understand the runtime’s internal structure for capacity planning and troubleshooting.

System Context

Torvyn is a single-process runtime that hosts multiple concurrent streaming flows. Each flow is a directed acyclic graph of WebAssembly components connected by bounded, backpressure-aware stream queues. The runtime is written in Rust, uses Tokio for async I/O and task scheduling, and uses Wasmtime as the WebAssembly execution engine.

┌─────────────────────────────────────────────────────────────────┐
│                     Torvyn Host Process                          │
│                                                                 │
│  ┌────────────┐  ┌────────────┐  ┌──────────────────────────┐  │
│  │  Reactor    │  │ Resource   │  │ Host Lifecycle Manager   │  │
│  │ (scheduler, │  │ Manager    │  │ (load, link, instantiate │  │
│  │  demand,    │  │ (buffers,  │  │  Wasm components)        │  │
│  │  backpress) │  │  pools,    │  │                          │  │
│  └──────┬──────┘  │  ownership)│  └────────────┬─────────────┘  │
│         │         └──────┬─────┘               │                │
│         │  EventSink     │  EventSink          │ EventSink      │
│         ▼                ▼                     ▼                │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Observability Collector                      │   │
│  │  ┌────────┐  ┌───────────────┐  ┌───────────────────┐   │   │
│  │  │ Tracer │  │ Metrics Agg.  │  │ Event Recorder    │   │   │
│  │  └───┬────┘  └───────┬───────┘  └──────┬────────────┘   │   │
│  └──────┼───────────────┼──────────────────┼────────────────┘   │
│         │               │                  │                    │
│         ▼               ▼                  ▼                    │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              Export / Inspection Layer                     │   │
│  │  OTLP gRPC  │ Prometheus /metrics │ Inspection API │ File│   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Architecture Diagram

graph TD
    A["Developer Defines WIT Contracts"] --> B["Generate Bindings / Scaffolds"]
    B --> C["Implement Components"]
    C --> D["Compile to Wasm Components"]
    D --> E["Torvyn Host Runtime"]
    E --> F["Reactor / Scheduler"]
    E --> G["Resource Manager"]
    E --> H["Observability Layer"]
    F --> I["Component A"]
    F --> J["Component B"]
    F --> K["Component C"]
    G --> I
    G --> J
    G --> K
    H --> L["Traces / Metrics / Diagnostics"]
    E --> M["OCI Packaging / Registry"]

Data Flow Diagram

The following diagram shows the lifecycle of a single stream element as it passes through a Source → Processor → Sink pipeline:

sequenceDiagram
    participant FD as Flow Driver
    participant Src as Source Component
    participant SQ1 as Stream Queue 1
    participant Proc as Processor Component
    participant SQ2 as Stream Queue 2
    participant Sink as Sink Component
    participant RM as Resource Manager

    FD->>Src: invoke pull()
    Src->>RM: allocate buffer B1
    RM-->>Src: mutable buffer handle
    Note over Src: Write payload (1 copy: component→host)
    Src->>Src: freeze() → immutable
    Src-->>FD: owned B1 + metadata

    FD->>SQ1: enqueue element
    Note over SQ1: Backpressure check

    FD->>SQ1: dequeue element
    FD->>Proc: invoke process(borrow B1)
    Note over Proc: Read input (1 copy: host→component)
    Proc->>RM: allocate buffer B2
    Note over Proc: Write output (1 copy: component→host)
    Proc-->>FD: owned B2 + metadata
    FD->>RM: release B1 → pool

    FD->>SQ2: enqueue element
    FD->>SQ2: dequeue element
    FD->>Sink: invoke push(borrow B2)
    Note over Sink: Read data (1 copy: host→component)
    Sink-->>FD: backpressure signal
    FD->>RM: release B2 → pool

Crate Dependency Graph

graph BT
    types["torvyn-types"]
    contracts["torvyn-contracts"]
    config["torvyn-config"]
    obs["torvyn-observability"]
    engine["torvyn-engine"]
    resources["torvyn-resources"]
    security["torvyn-security"]
    reactor["torvyn-reactor"]
    linker["torvyn-linker"]
    pipeline["torvyn-pipeline"]
    packaging["torvyn-packaging"]
    host["torvyn-host"]
    cli["torvyn-cli"]

    contracts --> types
    config --> types
    obs --> types
    engine --> types
    resources --> types
    resources --> engine
    security --> types
    security --> config
    reactor --> types
    reactor --> engine
    reactor --> resources
    reactor --> obs
    linker --> types
    linker --> contracts
    linker --> engine
    linker --> security
    pipeline --> types
    pipeline --> linker
    pipeline --> reactor
    pipeline --> engine
    pipeline --> resources
    pipeline --> config
    packaging --> types
    packaging --> contracts
    packaging --> config
    host --> reactor
    host --> pipeline
    host --> linker
    host --> packaging
    host --> obs
    host --> security
    cli --> host

Crate-by-Crate Description

Torvyn is organized as a Cargo workspace of focused crates. Each crate has a clear responsibility boundary and explicit dependencies.

torvyn-types — Shared Identity and Error Types

The leaf crate with zero internal dependencies. Defines all shared identifiers (ComponentId, FlowId, ResourceId, StreamId, BufferHandle), the ProcessError enum (Rust mapping of the WIT process-error variant), the ComponentRole enum (Source, Processor, Sink, Filter, Router), and basic configuration types. Every other crate depends on this one.

torvyn-config — Configuration Parsing and Validation

Parses and validates Torvyn.toml manifests and pipeline definitions. Handles the configuration layering model (CLI flags > environment variables > project manifest > global user config > built-in defaults). Depends on torvyn-types.

torvyn-contracts — WIT Loading and Validation

Loads WIT package files, validates syntax and semantics using the wit-parser crate from the Bytecode Alliance toolchain, and implements the compatibility checking algorithm for version resolution. Provides the validation pipeline that torvyn check executes. Depends on torvyn-types.

torvyn-engine — Wasm Engine Abstraction

Provides the WasmEngine trait (compile, instantiate, call) and the WasmtimeEngine implementation. Also defines the ComponentInvoker trait with typed invocation methods (invoke_pull, invoke_process, invoke_push) that the reactor uses to call component code. This crate is on the hot path for every component invocation. Depends on torvyn-types.

torvyn-resources — Resource Manager and Buffer Pools

Implements the resource table (dense slab with generation counters), the four-state ownership model (Pooled → Owned → Borrowed/Leased → Pooled), tiered buffer pools (Small 256B, Medium 4 KiB, Large 64 KiB, Huge 1 MiB), copy accounting, and per-component memory budget enforcement. Hot-path modules include the resource table, handle validation, state transitions, and transfer logic. Depends on torvyn-types and torvyn-engine.

torvyn-security — Capability Model and Sandboxing

Implements the capability taxonomy, the SandboxConfigurator that produces per-component Wasm sandbox configurations, the CapabilityGuard for runtime enforcement, and the audit event subsystem. Depends on torvyn-types and torvyn-contracts.

torvyn-observability — Tracing, Metrics, and Diagnostics

Implements the EventSink trait for non-blocking event recording, the trace context model (W3C-compatible), pre-allocated metric structures, the three-level observability system, and OTLP export. Provides the inspection API (HTTP server on Unix domain socket or localhost TCP). Depends on torvyn-types.

torvyn-linker — Component Linking and Composition

Implements contract compatibility checking, capability resolution (intersecting component requirements with operator grants), and topology validation (DAG structure, role consistency, port name matching). Used by torvyn link and implicitly by torvyn run. Depends on torvyn-types, torvyn-engine, torvyn-contracts, and torvyn-security.

torvyn-reactor — Stream Scheduling and Backpressure

Implements the flow driver (one Tokio task per flow), the demand propagation model, the scheduling policy interface, the backpressure state machine, cancellation and timeout handling, and cooperative yield logic. This is the execution heartbeat of the runtime. Depends on torvyn-types, torvyn-engine, torvyn-resources, and torvyn-observability.

torvyn-pipeline — Pipeline Topology and Instantiation

Constructs pipeline topologies from configuration, instantiates component graphs, and registers flows with the reactor. Depends on most other crates.

torvyn-host — Runtime Binary Entry Point

The main host process binary. A thin orchestration shell that wires together all subsystems, manages startup and shutdown sequences, and exposes the runtime’s programmatic API. Depends on everything above.

torvyn-cli — Developer CLI Binary

The torvyn command-line tool. Implements all 11 subcommands (init, check, link, build, run, trace, bench, pack, publish, inspect, doctor). Built with clap for argument parsing, miette for diagnostic error rendering, and indicatif for progress display. Depends on library crates from the workspace.

Data Flow: Processing a Single Stream Element

The following describes the complete data flow for a single element through a Source → Processor → Sink pipeline. This is the hot path that determines Torvyn’s per-element performance.

Step 1: Source produces an element. The flow driver (reactor) invokes source.pull() via the ComponentInvoker. The source component allocates a mutable buffer (host call to buffer-allocator.allocate), writes payload data into it (one copy: component → host), calls freeze() to convert it to an immutable buffer, and returns the owned buffer handle along with element metadata.

Step 2: Element enters the stream queue. The flow driver receives the output element, assigns the element-meta.sequence and element-meta.timestamp-ns fields, and enqueues it into the Source→Processor stream queue. If the queue is full, the backpressure policy is applied.

Step 3: Processor transforms the element. The flow driver dequeues the element and invokes processor.process() with a borrowed buffer handle. The processor reads the input (one copy: host → component), allocates a new output buffer, writes transformed data (one copy: component → host), and returns the new owned buffer handle with updated metadata.

Step 4: Input buffer is released. The borrow on the input buffer is automatically released when process() returns. The host releases the input buffer back to the pool.

Step 5: Output element enters the next queue. The flow driver enqueues the processor’s output into the Processor→Sink stream queue.

Step 6: Sink consumes the element. The flow driver dequeues the element and invokes sink.push() with a borrowed buffer handle. The sink reads the data (one copy: host → component), performs its output operation (write to file, send to network, etc.), and returns a backpressure signal.

Step 7: Output buffer is released. The borrow on the output buffer is released and the buffer returns to the pool.

Total per-element: 4 payload copies, all instrumented. Steps 2, 4, 5, and 7 are handle-only operations with zero payload copying.

Concurrency Model

graph TD
    subgraph Tokio["Tokio Multi-Threaded Runtime"]
        direction TB
        Coord["Reactor Coordinator Task<br/><small>flow lifecycle, admin control</small>"]

        subgraph Flows["Flow Driver Tasks (1 per flow)"]
            direction LR
            F1["Flow A Driver<br/><small>sequential intra-flow<br/>scheduling</small>"]
            F2["Flow B Driver<br/><small>sequential intra-flow<br/>scheduling</small>"]
            F3["Flow C Driver<br/><small>...</small>"]
        end

        Coord --> Flows

        BG["Background Tasks<br/><small>observability export,<br/>OTLP serialization</small>"]

        SB["spawn_blocking<br/><small>component loading,<br/>config parsing</small>"]
    end

    F1 -->|"yield after 32 elements<br/>or 100µs"| Tokio
    F2 -->|"yield"| Tokio

    style Coord fill:#2563EB,stroke:#1D4ED8,color:#fff
    style F1 fill:#7C3AED,stroke:#6D28D9,color:#fff
    style F2 fill:#7C3AED,stroke:#6D28D9,color:#fff
    style F3 fill:#7C3AED,stroke:#6D28D9,color:#fff
    style BG fill:#059669,stroke:#047857,color:#fff
    style SB fill:#6B7280,stroke:#4B5563,color:#fff

Torvyn uses Tokio’s multi-threaded work-stealing runtime with no custom OS threads.

  • One Tokio task per flow (the flow driver). Intra-flow scheduling is sequential within the flow driver — pipeline stages within a flow execute in dependency order, not in parallel. This keeps related work cache-local and gives the reactor control over intra-flow scheduling policy.
  • One reactor coordinator task for flow lifecycle management, flow creation and teardown, and administrative control (operator commands like pause, cancel, inspect).
  • Export background tasks for observability data serialization and delivery.
  • tokio::spawn_blocking for filesystem I/O (component loading, configuration parsing).

Inter-flow fairness is provided by Tokio’s work-stealing scheduler. Intra-flow fairness is provided by the reactor’s cooperative yield mechanism: the flow driver yields to Tokio after processing a configurable batch of elements (default: 32) or after a configurable time quantum (default: 100 microseconds), whichever comes first.

Wasmtime’s fuel mechanism provides cooperative preemption within Wasm execution, preventing a single component invocation from monopolizing a thread.

Trade-off: The task-per-flow model means a single flow cannot utilize multiple OS threads simultaneously for different stages. For Torvyn’s initial target of same-node pipelines, this is acceptable — the bottleneck is typically Wasm execution speed per stage, not parallelism within a single flow. Parallel stage execution within a flow is deferred to a future phase.

Hot Path vs. Cold Path

graph TD
    subgraph HotPath["Hot Path (per-element)"]
        direction LR
        R["Reactor<br/><small>scheduling, demand,<br/>wakeup</small>"]
        Res["Resources<br/><small>transfer, borrow,<br/>handle validation</small>"]
        Eng["Engine<br/><small>call dispatch</small>"]
        ObsH["Observability<br/><small>counter increment,<br/>timestamp</small>"]
    end

    subgraph WarmPath["Warm Path (per-invocation, sampled)"]
        ObsW["Observability<br/><small>span creation</small>"]
    end

    subgraph ColdPath["Cold Path (startup / link time)"]
        direction LR
        Link["Linker<br/><small>contract compat,<br/>capability resolution</small>"]
        Cfg["Config<br/><small>TOML parsing</small>"]
        Con["Contracts<br/><small>WIT validation</small>"]
        Sec["Security<br/><small>grant resolution</small>"]
        Pipe["Pipeline<br/><small>topology construction</small>"]
    end

    style HotPath fill:#FEF3C7,stroke:#D97706
    style WarmPath fill:#DBEAFE,stroke:#2563EB
    style ColdPath fill:#F3F4F6,stroke:#6B7280

    style R fill:#DC2626,stroke:#B91C1C,color:#fff
    style Res fill:#DC2626,stroke:#B91C1C,color:#fff
    style Eng fill:#DC2626,stroke:#B91C1C,color:#fff
    style ObsH fill:#DC2626,stroke:#B91C1C,color:#fff
    style ObsW fill:#2563EB,stroke:#1D4ED8,color:#fff
    style Link fill:#6B7280,stroke:#4B5563,color:#fff
    style Cfg fill:#6B7280,stroke:#4B5563,color:#fff
    style Con fill:#6B7280,stroke:#4B5563,color:#fff
    style Sec fill:#6B7280,stroke:#4B5563,color:#fff
    style Pipe fill:#6B7280,stroke:#4B5563,color:#fff
ModulePathReason
Reactor (scheduling, demand, wakeup)HotExecutes per element
Resources (transfer, borrow, handle validation)HotBuffer handoff per element crossing
Engine (call dispatch)HotEvery component invocation
Observability (counter increment, timestamp)HotPer-element metric updates
Observability (span creation)WarmPer-invocation when tracing is sampled
LinkerColdOnce at pipeline startup
ConfigColdOnce at host startup
Contracts (validation)ColdAt check/link time only
Security (grant resolution)ColdAt link time; enforcement checks are hot but simple
Pipeline (topology construction)ColdOnce per flow

Hot-path code must be lock-free, allocation-free, and branchless where possible. Cold-path code may allocate, may block, and may acquire locks.

Performance Characteristics and Design Trade-Offs

Wasm boundary cost. Every component invocation crosses the Wasm boundary via the Component Model’s canonical ABI. This adds overhead compared to a direct function call (approximately 100-500 nanoseconds per invocation depending on parameter complexity). This is the cost of isolation. For workloads where sub-microsecond per-element latency is critical, fewer pipeline stages means fewer boundary crossings.

Copy cost vs. isolation. The split between host memory (where buffers live) and component linear memory (where computation happens) means that reading or writing payload data always involves a copy. The design prioritizes safety and measurability over eliminating every copy. Components that need to inspect only metadata operate at zero payload copy cost.

Task-per-flow vs. task-per-component. Task-per-flow keeps intra-flow scheduling simple and cache-friendly but limits intra-flow parallelism. Task-per-component would enable parallel stage execution within a flow but at the cost of higher Tokio task overhead and more complex scheduling. The current design is the right trade-off for same-node pipelines with moderate stage counts.

Pre-allocated metrics vs. dynamic metrics. Pre-allocating all metric storage at flow creation time means the hot path never allocates and never looks up metric handles in a hash map. The trade-off is that metric storage is fixed-size and cannot accommodate arbitrary custom metrics from components. Custom component metrics are supported through the torvyn:runtime/metrics capability, which uses a separate (slower) path.

Design Decisions

This document indexes the most significant architectural decisions in Torvyn, their rationale, alternatives that were considered, and the trade-offs accepted. Each decision is traceable to a specific section of the HLI design documents.

ADR Index

IDDecisionHLI SourceStatus
ADR-001Buffer as WIT resource, not list<u8>Doc 01 §3.2.1Accepted
ADR-002Split buffer and mutable-buffer typesDoc 01 §3.2.2Accepted
ADR-003Task-per-flow, not task-per-componentDoc 04 §1.3Accepted
ADR-004Layer reactor on Tokio, not custom executorDoc 04 §1.2Accepted
ADR-005Deny-all-by-default capability modelDoc 06 §1.5Accepted
ADR-006Capabilities in manifest, not in WITDoc 01 §5.1Accepted
ADR-007Pre-allocated metrics, not dynamic registryDoc 05 §3.1Accepted
ADR-008Embedded observability, not sidecarDoc 05 §1.5Accepted
ADR-009OCI-native artifact formatDoc 08 §1.2Accepted
ADR-010WASI 0.2 now, migration path to 0.3Doc 01 §4Accepted
ADR-011Credit-based demand model for backpressureDoc 04 §4.1Accepted
ADR-012Global tiered buffer pools, not per-flowDoc 03 §5.1Accepted
ADR-013Monolithic CLI binary, not plugin-basedDoc 07 §1.2Accepted
ADR-014TOML for configuration, not YAMLDoc 07 §4.1Accepted
ADR-015High/low watermark hysteresis for backpressureDoc 04 §5.3Accepted

Key Decisions in Detail

ADR-001: Buffer as WIT Resource

Decision: Buffers are WIT resource types with opaque handles, not list<u8> value types.

Rationale: If buffers were list<u8>, every cross-component transfer would require copying the full byte content into and out of component linear memory. With resource handles, the host can transfer ownership by moving a handle (an integer) while the payload bytes remain in host memory. This is the foundation of Torvyn’s ownership-aware transfer model.

Alternative rejected: Embedding payload bytes as list<u8> in stream-element. Rejected because it would force a full copy at every component boundary.

Alternative rejected: Making buffer a record with inline bytes. Rejected because records are value types in WIT and are always copied in full across component boundaries.

ADR-002: Split Buffer and Mutable-Buffer

Decision: Separate buffer (immutable, read-only) and mutable-buffer (writable, single-owner) resource types.

Rationale: The split enforces a clear write-then-freeze lifecycle. A component obtains a mutable buffer from the host, writes data, calls freeze() to convert it to immutable, and returns it. This avoids copy-on-write complexity and makes mutation boundaries explicit. A buffer is either being written (mutable, single owner) or being read (immutable, can be borrowed by multiple readers).

Alternative rejected: A single buffer with both read and write methods and runtime mutability tracking. Rejected because it complicates the resource manager’s invariant checking.

ADR-003: Task-Per-Flow

Decision: Each active flow gets one Tokio task (the flow driver). Stages within a flow execute sequentially within that task.

Rationale: Task-per-component would create excessive task overhead for large pipelines (20+ components × hundreds of flows = thousands of Tokio tasks). Task-per-flow gives the reactor control over intra-flow scheduling, reduces task switching overhead, and keeps related work cache-local. Tokio handles inter-flow scheduling; the reactor handles intra-flow scheduling.

Trade-off accepted: A single flow cannot utilize multiple OS threads simultaneously for different stages.

ADR-004: Layer on Tokio

Decision: The reactor is a domain-specific scheduling layer on top of Tokio, not a replacement for it.

Rationale: Building a custom async runtime would be enormous engineering effort with no corresponding benefit. Tokio provides mature I/O polling, timers, work-stealing, and task infrastructure. What Tokio does not provide is stream-level scheduling policy, demand propagation, backpressure enforcement, or fairness across Wasm components. That is the reactor’s domain.

ADR-010: WASI 0.2 Now, Migration Path to 0.3

Decision: Torvyn’s Phase 0 and Phase 1 target WASI 0.2. WASI 0.3 migration is planned for Phase 2 or later.

Rationale: WASI 0.3 introduces native stream<T>, future<T>, and async func — directly aligned with Torvyn’s streaming model. However, as of early 2026, WASI 0.3 is in preview and not yet stable. The contract design is structured so that migration is straightforward: source.pull() maps to async func, sink.push() maps to async func, and the explicit backpressure signal enum may become unnecessary with native stream pressure. The semantic meaning of interfaces does not change during migration — only the mechanism changes.

Proposing New Architectural Changes

Significant architectural changes to Torvyn follow an RFC (Request for Comments) process:

  1. Open a GitHub issue tagged rfc describing the proposed change, motivation, and alternatives considered.
  2. Write a design document in the docs/rfcs/ directory following the ADR template: Decision, Context, Rationale, Alternatives Rejected, Trade-offs Accepted.
  3. The RFC is discussed publicly for a minimum of 14 days.
  4. Maintainers approve, request changes, or reject the RFC based on alignment with Torvyn’s design principles and technical merit.
  5. Approved RFCs are added to this index with a link to the implementation tracking issue.

CLI Reference

The torvyn CLI is a single statically-linked binary with a subcommand dispatch model. All commands support --format json for machine-readable output.

Global Options

torvyn [OPTIONS] <COMMAND>

Options:
  --format <FORMAT>    Output format for all commands: human (default), json
  --color <WHEN>       Color output: auto (default), always, never
  --quiet              Suppress non-essential output
  --verbose            Increase output verbosity
  --help               Print help information
  --version            Print version information

Environment variables:

  • NO_COLOR — If set (to any value), disables color output. Follows the no-color.org convention.
  • TORVYN_LOG — Controls log verbosity (overridden by --verbose / --quiet).

Commands

torvyn init

Create a new Torvyn project with correct structure, valid manifest, WIT contracts, and a working starting point.

torvyn init [PROJECT_NAME] [OPTIONS]

Arguments:
  [PROJECT_NAME]         Directory name and project name
                         (default: current directory name)

Options:
  --template <TEMPLATE>  Project template
                         Values: source, sink, transform, filter, router,
                         aggregator, full-pipeline, empty
                         Default: transform
  --language <LANG>      Implementation language
                         Values: rust, go, python, zig
                         Default: rust
  --no-git               Skip git repository initialization
  --no-example           Generate contract stubs only, skip example implementation
  --contract-version <V> Torvyn contract version to target (default: 0.1.0)
  --interactive          Launch interactive wizard for guided setup
  --force                Overwrite existing directory contents

Example:

$ torvyn init my-transform --template transform --language rust
✓ Created project "my-transform" with template "transform"

  Next steps:
    cd my-transform
    $EDITOR wit/world.wit     # Review your component's contract
    $EDITOR src/lib.rs        # Implement your component
    torvyn check              # Validate contracts and manifest
    torvyn build              # Compile to WebAssembly component

Exit codes: 0 (success), 1 (error — directory exists, invalid template, etc.)

torvyn check

Validate WIT contracts, manifest, and project structure. Does not compile or execute anything.

torvyn check [OPTIONS]

Options:
  --manifest <PATH>    Path to Torvyn.toml (default: ./Torvyn.toml)
  --strict             Treat warnings as errors

Runs a seven-step validation pipeline: manifest parse, manifest schema validation, WIT syntax validation, WIT resolution, world consistency, capability cross-check, and deprecation warnings.

Exit codes: 0 (all checks passed), 1 (errors found), 2 (warnings found, only with --strict)

Verify that a pipeline’s components are compatible and can be composed.

torvyn link [OPTIONS]

Options:
  --manifest <PATH>       Path to Torvyn.toml with flow definition
  --flow <NAME>           Specific flow to check (default: all flows)
  --components <DIR>      Directory containing compiled .wasm components
  --verbose               Show full interface compatibility details

Validates interface compatibility for every edge in the flow graph, DAG structure, role consistency, capability satisfaction, and contract version range intersection.

Exit codes: 0 (links successfully), 1 (incompatible), 2 (missing components)

torvyn build

Compile source code into a WebAssembly component.

torvyn build [OPTIONS]

Options:
  --manifest <PATH>       Path to Torvyn.toml
  --release               Build with optimizations
  --target <COMPONENT>    Specific component to build (multi-component projects)
  --all                   Build all components

Runs torvyn check before compilation. For Rust, invokes cargo component build (if available) or falls back to cargo build --target wasm32-wasip2 + wasm-tools component new.

Exit codes: 0 (build succeeded), 1 (check failed), 2 (compilation failed)

torvyn run

Execute a pipeline locally for development and testing.

torvyn run [OPTIONS]

Options:
  --manifest <PATH>       Path to Torvyn.toml
  --flow <NAME>           Flow to execute (default: first defined flow)
  --input <SOURCE>        Override source input (file path, stdin, or generator)
  --output <SINK>         Override sink output (file path, stdout)
  --limit <N>             Process at most N elements then exit
  --timeout <DURATION>    Maximum execution time (e.g., 30s, 5m)
  --config <KEY=VALUE>    Override component configuration values
  --log-level <LEVEL>     Log verbosity: error, warn, info, debug, trace

Runs torvyn check and torvyn link implicitly before execution. Displays real-time throughput and error counters. Prints summary statistics on completion or Ctrl+C.

Exit codes: 0 (completed successfully), 1 (pipeline error), 2 (validation failed), 130 (interrupted by Ctrl+C)

torvyn trace

Execute a pipeline with full tracing enabled, producing per-element diagnostic output.

torvyn trace [OPTIONS]

Options:
  --manifest <PATH>       Path to Torvyn.toml
  --flow <NAME>           Flow to trace
  --input <SOURCE>        Override source input
  --limit <N>             Trace at most N elements
  --output-trace <PATH>   Write trace data to file (default: stdout)
  --trace-format <FMT>    Trace output: pretty (default), json, otlp
  --show-buffers          Include buffer content snapshots
  --show-backpressure     Highlight backpressure events

Same as run but with Diagnostic-level observability enabled. Every element’s path through the pipeline is traced with timing, buffer operations, and copy events.

Exit codes: Same as torvyn run.

torvyn bench

Run a pipeline under sustained load and produce a performance report.

torvyn bench [OPTIONS]

Options:
  --manifest <PATH>       Path to Torvyn.toml
  --flow <NAME>           Flow to benchmark
  --duration <DURATION>   Benchmark duration (default: 10s)
  --warmup <DURATION>     Warmup period excluded from results (default: 2s)
  --input <SOURCE>        Override source input for reproducible benchmarks
  --report <PATH>         Write report to file (default: stdout)
  --report-format <FMT>   Report format: pretty (default), json, csv, markdown
  --compare <PATH>        Compare against a previous benchmark result
  --baseline <NAME>       Save result as a named baseline

Reports throughput, latency percentiles, per-component breakdown, queue statistics, buffer reuse rate, copy accounting, and scheduling metrics.

Exit codes: 0 (benchmark completed), 1 (pipeline error), 3 (regression detected when comparing)

torvyn pack

Package a compiled component as an OCI-compatible artifact.

torvyn pack [OPTIONS]

Options:
  --manifest <PATH>       Path to Torvyn.toml
  --component <NAME>      Specific component to pack (default: all)
  --output <PATH>         Output artifact path (default: .torvyn/artifacts/)
  --tag <TAG>             OCI tag (default: derived from manifest version)
  --include-source        Include source WIT contracts in artifact metadata
  --sign                  Sign artifact (requires signing key configuration)

Runs torvyn check, collects the compiled .wasm binary, contract metadata, and benchmark metadata (if available), and assembles an OCI artifact.

Exit codes: 0 (packed), 1 (check failed), 2 (packaging error)

torvyn publish

Publish a packaged artifact to an OCI registry.

torvyn publish [OPTIONS]

Options:
  --artifact <PATH>       Path to packed artifact
  --registry <URL>        Target registry URL
  --tag <TAG>             Override tag
  --dry-run               Validate without pushing
  --force                 Overwrite existing tag

Exit codes: 0 (published), 1 (authentication failed), 2 (push failed), 3 (artifact invalid)

torvyn inspect

Display metadata about a compiled component or packaged artifact.

torvyn inspect <TARGET> [OPTIONS]

Arguments:
  <TARGET>                Path to .wasm file, OCI artifact, or registry reference

Options:
  --show <SECTION>        What to show: all (default), interfaces, capabilities,
                          metadata, size, contracts, benchmarks

Exit codes: 0 (success), 1 (target not found or invalid)

torvyn doctor

Check the developer’s environment for required tools and common misconfigurations.

torvyn doctor [OPTIONS]

Options:
  --fix                   Attempt to fix common issues automatically

Checks: Torvyn CLI version, Rust toolchain and wasm32-wasip2 target, cargo-component, wasm-tools, wasmtime (optional), project structure, WIT dependencies, registry connectivity.

Exit codes: 0 (all checks passed), 1 (issues found)

Environment Variables

VariableDescriptionDefault
TORVYN_LOGLog filter (e.g., info, torvyn_reactor=debug)info
TORVYN_HOMETorvyn global config and cache directory~/.config/torvyn/
TORVYN_RUNTIME_WORKER_THREADSNumber of Tokio worker threadsCPU count
TORVYN_RUNTIME_MAX_MEMORY_PER_COMPONENTMemory limit per component64MiB
TORVYN_OBSERVABILITY_LEVELObservability level: off, production, diagnosticproduction
TORVYN_STATE_DIRRuntime state directory (inspection socket)$XDG_RUNTIME_DIR/torvyn/
NO_COLORDisable terminal color outputunset

Environment variables follow the pattern TORVYN_ + uppercase section + _ + uppercase key. Example: runtime.worker_threadsTORVYN_RUNTIME_WORKER_THREADS.

Exit Codes

CodeMeaning
0Success
1General error (validation, compilation, runtime failure)
2Missing input or prerequisite
3Regression detected (bench comparison) or publish conflict
130Interrupted (Ctrl+C / SIGINT)

All commands produce structured JSON output with --format json, including an exit_code field, an errors array, and command-specific result fields.

Configuration Reference

Torvyn uses TOML for all configuration, consistent with the Rust ecosystem convention established by Cargo.toml. Configuration lives in Torvyn.toml at the project root.

Configuration Merging Rules

Configuration values are resolved through a layered precedence model (highest precedence first):

  1. CLI flags--config key=value on torvyn run and other commands.
  2. Environment variablesTORVYN_ prefix + uppercase path (e.g., TORVYN_RUNTIME_WORKER_THREADS).
  3. Project manifestTorvyn.toml in the project root.
  4. Global user config~/.config/torvyn/config.toml.
  5. Built-in defaults — Compiled into the binary.

Component Manifest (Torvyn.toml — per component)

[torvyn] — Project Metadata

FieldTypeRequiredDefaultDescription
namestringYesProject name. Must be a valid identifier (lowercase, hyphens allowed).
versionstringYesProject version (semantic versioning, e.g., "0.1.0").
contract_versionstringYesTorvyn contract version this project targets (e.g., "0.1.0").
descriptionstringNo""Human-readable project description.
authorslist of stringsNo[]Author names and emails (e.g., ["Alice <alice@example.com>"]).
licensestringNo""SPDX license identifier (e.g., "Apache-2.0").
repositorystringNo""Source repository URL.

[capabilities.required] — Required Capabilities

Key-value pairs where keys are capability identifiers and values are booleans. Components will not link if required capabilities are not granted.

[capabilities.required]
wasi-filesystem-read = true
wasi-clocks = true

[capabilities.optional] — Optional Capabilities

Capabilities that enhance functionality but are not required. The component must handle their absence gracefully.

[capabilities.torvyn] — Torvyn-Specific Resource Requirements

FieldTypeDefaultDescription
max-buffer-sizestring"16MiB"Maximum single buffer size this component needs.
max-memorystring"64MiB"Maximum Wasm linear memory this component needs.
buffer-pool-accessstring"default"Named buffer pool to use.

Pipeline Manifest (Torvyn.toml — pipeline project)

[[component]] — Component Declarations

FieldTypeRequiredDefaultDescription
namestringYesComponent name within this project.
pathstringYesPath to component source root (relative to project root).
languagestringNo"rust"Implementation language. Values: rust, go, python, zig.
build_commandstringNoauto-detectedCustom build command override.

[flow.<NAME>] — Flow Definition

FieldTypeRequiredDefaultDescription
descriptionstringNo""Human-readable flow description.

[flow.<NAME>.nodes.<NODE>] — Component Nodes

FieldTypeRequiredDefaultDescription
componentstringYesPath to compiled .wasm file or registry reference.
interfacestringYesTorvyn interface this component implements (e.g., torvyn:streaming/processor).
configstringNo""Configuration string passed to lifecycle.init(). JSON recommended.

[[flow.<NAME>.edges]] — Stream Connections

FieldTypeRequiredDescription
from.nodestringYesUpstream node name.
from.portstringYesOutput port name (usually "output").
to.nodestringYesDownstream node name.
to.portstringYesInput port name (usually "input").

[runtime] — Runtime Configuration

FieldTypeDefaultDescription
worker_threadsintegerCPU countTokio worker thread count.
max_memory_per_componentstring"64MiB"Wasm linear memory limit per component instance.
fuel_per_invocationinteger1_000_000Wasmtime fuel budget per component call. 0 = unlimited.
component_init_timeout_msinteger5000Timeout for lifecycle.init() calls.
component_teardown_timeout_msinteger2000Timeout for lifecycle.teardown() calls.

[runtime.backpressure] — Global Backpressure Defaults

FieldTypeDefaultDescription
default_queue_depthinteger64Default bounded queue capacity per stream.
backpressure_policystring"block"Default policy. Values: block, drop-oldest, drop-newest, error.
low_watermark_ratiofloat0.5Queue depth ratio at which backpressure deactivates.

[runtime.pools] — Buffer Pool Configuration

FieldTypeDefaultDescription
small_pool_sizeinteger4096Number of 256-byte buffers pre-allocated.
medium_pool_sizeinteger1024Number of 4 KiB buffers pre-allocated.
large_pool_sizeinteger256Number of 64 KiB buffers pre-allocated.
huge_pool_sizeinteger32Maximum cached 1 MiB buffers (on-demand).
exhaustion_policystring"fallback-alloc"Policy when pool is empty. Values: fallback-alloc, error.

[observability] — Observability Configuration

FieldTypeDefaultDescription
levelstring"production"Observability level. Values: off, production, diagnostic.
tracing_enabledbooleantrueEnable trace collection.
tracing_exporterstring"stdout"Trace export target. Values: otlp-grpc, otlp-http, stdout, file, none.
tracing_endpointstring""OTLP endpoint URL (for otlp-grpc and otlp-http).
sample_ratefloat0.01Head-based trace sampling rate (0.0–1.0).
error_promotebooleantruePromote errored flows to full tracing.
latency_promote_threshold_msinteger10Promote flows exceeding this latency.
metrics_enabledbooleantrueEnable metrics collection.
prometheus_enabledbooleantrueServe /metrics on inspection API.
otlp_metrics_enabledbooleanfalsePush metrics via OTLP.
otlp_metrics_interval_sinteger15OTLP metrics push interval.
ring_buffer_capacityinteger64Per-flow span ring buffer size for retroactive sampling.

[security] — Security Configuration

FieldTypeDefaultDescription
default_capability_policystring"deny-all"Default policy for ungranted capabilities.
audit_enabledbooleantrueEnable security audit logging.
audit_targetstring"file"Audit log target. Values: file, stdout, event-sink.

[security.grants.<COMPONENT>] — Per-Component Capability Grants

[security.grants.my-transform]
capabilities = [
    "wasi:filesystem/read:/data/input",
    "wasi:clocks/wall-clock",
]

[registry] — Registry Configuration

FieldTypeDefaultDescription
defaultstring""Default OCI registry URL for torvyn publish.

WIT Interface Reference

This is the complete reference for all Torvyn WIT interfaces. All interfaces are defined in the torvyn:streaming@0.1.0 package unless otherwise noted.

Core Types (torvyn:streaming/types)

resource buffer

A host-managed immutable byte buffer. Buffers exist in host memory, not in component linear memory. Components interact with buffers through opaque handles.

MethodSignatureOwnershipDescription
sizefunc() -> u64Read-only (borrow)Returns the byte length of the buffer contents.
content-typefunc() -> stringRead-only (borrow)Returns a content-type hint (e.g., "application/json"). Empty string if unset.
readfunc(offset: u64, len: u64) -> list<u8>Read-only (borrow). Triggers a measured copy from host to component memory.Read up to len bytes starting at offset. Returns fewer bytes if buffer is shorter than offset+len.
read-allfunc() -> list<u8>Read-only (borrow). Triggers a measured copy.Read the entire buffer contents. Equivalent to read(0, self.size()).

Performance note: read and read-all copy data from host memory into component linear memory. The resource manager records this as a PayloadRead copy event. Components that only need metadata should use size() and content-type() instead.

resource mutable-buffer

A writable buffer obtained from the host. Single-owner. Must be frozen into an immutable buffer before returning to the host.

MethodSignatureOwnershipDescription
writefunc(offset: u64, bytes: list<u8>) -> result<_, buffer-error>Write (own). Triggers a measured copy from component to host memory.Write bytes at offset. Extends buffer if necessary up to capacity.
appendfunc(bytes: list<u8>) -> result<_, buffer-error>Write (own). Triggers a measured copy.Append bytes to the end of current content.
sizefunc() -> u64Read-only (own)Current byte length of written content.
capacityfunc() -> u64Read-only (own)Maximum capacity of this buffer.
set-content-typefunc(content-type: string)Write (own)Set the content-type hint.
freezefunc() -> bufferConsumes the mutable-buffer handle. Returns an owned immutable buffer.Finalize into an immutable buffer. After this call, the mutable-buffer handle is invalid.

resource flow-context

Carries trace correlation, deadline, and pipeline-scoped metadata. Created by the runtime, passed to components with each stream element.

MethodSignatureDescription
trace-idfunc() -> stringW3C Trace ID (hex-encoded, 32 chars). Empty if tracing disabled.
span-idfunc() -> stringCurrent span ID (hex-encoded, 16 chars). Empty if tracing disabled.
deadline-nsfunc() -> u64Remaining deadline in nanoseconds. 0 means no deadline set.
flow-idfunc() -> stringUnique flow identifier (opaque string).

record element-meta

FieldTypeDescription
sequenceu64Monotonic sequence number within the flow. Assigned by the runtime.
timestamp-nsu64Wall-clock timestamp (ns since Unix epoch). Assigned by the runtime.
content-typestringContent type of the payload.

record stream-element

The fundamental unit of data flow. Passed to processor.process() and sink.push().

FieldTypeOwnershipDescription
metaelement-metaCopied (small record)Element metadata.
payloadborrow<buffer>Borrowed. Must not be stored beyond the function call.Reference to the payload buffer.
contextborrow<flow-context>Borrowed.Reference to the flow context.

record output-element

Produced by components that create new data. Returned from processor.process() and source.pull().

FieldTypeOwnershipDescription
metaelement-metaCopied. sequence and timestamp-ns are advisory — the runtime may overwrite them.Output metadata.
payloadbufferOwned. Ownership transfers from the component to the runtime.Output payload buffer.

variant process-result

CasePayloadDescription
emitoutput-elementThe component produced output. The buffer in output-element is owned by the runtime after the call returns.
drop(none)The component consumed the input but produced no output. Not an error — used for filtering, deduplication, aggregation.

variant process-error

CasePayloadRuntime Behavior
invalid-inputstringThe input element was malformed. Error policy applies (skip, retry, terminate).
unavailablestringA required resource or service was unavailable. May trigger circuit-breaker logic.
internalstringUnexpected internal error. Use sparingly.
deadline-exceeded(none)The processing deadline has passed. Feeds into timeout accounting.
fatalstringThe component is permanently unable to process further elements. Triggers teardown.

variant buffer-error

CaseDescription
capacity-exceededWrite would exceed the buffer’s capacity limit.
out-of-boundsOffset is beyond current bounds.
allocation-failedstring — Host-side allocation failure.

enum backpressure-signal

CaseDescription
readyConsumer is ready to accept more data.
pauseConsumer requests the producer to pause.

Core Interfaces

interface buffer-allocator

Imported by components that produce output (processors, sources).

FunctionSignatureDescription
allocatefunc(capacity-hint: u64) -> result<mutable-buffer, buffer-error>Request a new mutable buffer. The host may allocate larger than requested but never smaller. Returns error if memory budget is exceeded.
clone-into-mutablefunc(source: borrow<buffer>) -> result<mutable-buffer, buffer-error>Request a mutable buffer initialized with a copy of an existing buffer’s contents. The source buffer is borrowed, not consumed.

interface processor

Exported by transform components.

FunctionSignatureDescription
processfunc(input: stream-element) -> result<process-result, process-error>Process a single stream element. Input is borrowed; output (if emit) is owned by the runtime. Called once per element, not concurrently.

interface source

Exported by data-producing components.

FunctionSignatureDescription
pullfunc() -> result<option<output-element>, process-error>Pull the next element. ok(some(element)): data available. ok(none): source exhausted. err(error): production error. Output buffer is owned by the runtime.
notify-backpressurefunc(signal: backpressure-signal)Receive a backpressure signal from the downstream pipeline. Called by the runtime between pull() invocations.

interface sink

Exported by data-consuming components.

FunctionSignatureDescription
pushfunc(element: stream-element) -> result<backpressure-signal, process-error>Push an element into the sink. Returns ready (accept more) or pause (slow down). Input is borrowed — sink must copy payload bytes during this call if it needs to buffer them.
completefunc() -> result<_, process-error>Signal that no more elements will arrive. Sink should flush any buffered data.

interface lifecycle

Optional. Exported by components that need initialization or cleanup.

FunctionSignatureDescription
initfunc(config: string) -> result<_, process-error>Called once after instantiation, before stream processing. Configuration string is component-specific (JSON recommended). Error prevents pipeline startup.
teardownfunc()Called once during shutdown. Best-effort — the runtime may skip this on forced termination.

Extension Interfaces

interface filter (torvyn:filtering@0.1.0)

FunctionSignatureDescription
evaluatefunc(element: stream-element) -> result<bool, process-error>Accept (true) or reject (false) an element. Input is borrowed. No output buffer allocation — filters are extremely cheap.

interface router (torvyn:filtering@0.1.0)

FunctionSignatureDescription
routefunc(element: stream-element) -> result<list<string>, process-error>Return output port names for this element. Empty list = drop. Multiple names = fan-out. Port names must match topology configuration.

interface aggregator (torvyn:aggregation@0.1.0)

FunctionSignatureDescription
ingestfunc(element: stream-element) -> result<option<output-element>, process-error>Absorb an element into internal state. Optionally emit an aggregated result.
flushfunc() -> result<list<output-element>, process-error>Emit any remaining buffered results. Called when the upstream flow completes.

Standard Worlds

WorldImportsExportsUse Case
transformtypes, buffer-allocatorprocessorStateless stream processor
managed-transformtypes, buffer-allocatorprocessor, lifecycleProcessor with init/teardown
data-sourcetypes, buffer-allocatorsourceData producer
managed-sourcetypes, buffer-allocatorsource, lifecycleSource with init/teardown
data-sinktypessinkData consumer
managed-sinktypessink, lifecycleSink with init/teardown
content-filtertypesfilterAccept/reject filter
content-routertypesrouterMulti-port router
stream-aggregatortypes, buffer-allocatoraggregator, lifecycleStateful aggregator

Metrics Catalog

All Torvyn metrics are pre-allocated at flow creation time. Counters are AtomicU64 with Relaxed ordering on the hot path. Histograms use fixed-bucket boundaries with logarithmic distribution from 100 ns to 10 s.

Per-Flow Metrics

Metric NameTypeUnitLabelsDescription
flow.elements.totalCountercountflow_idTotal stream elements processed.
flow.elements.errorsCountercountflow_idTotal elements that produced errors.
flow.latencyHistogramnsflow_idEnd-to-end latency per element (source entry to sink exit).
flow.throughputDerivedelements/sflow_idComputed from element count and wall time during export.
flow.copies.totalCountercountflow_idTotal buffer copy operations.
flow.copies.bytesCounterbytesflow_idTotal bytes copied across all copy operations.
flow.active_durationGaugensflow_idWall time since flow started.
flow.stateGauge (enum)flow_idCurrent flow lifecycle state.

Per-Component Metrics

Metric NameTypeUnitLabelsDescription
component.invocationsCountercountflow_id, component_idTotal invocations of this component.
component.errorsCountercountflow_id, component_idTotal error returns.
component.processing_timeHistogramnsflow_id, component_idWall time per invocation (excludes queue wait).
component.fuel_consumedCounterunitsflow_id, component_idWasm fuel consumed (if fuel metering enabled).
component.memory_currentGaugebytesflow_id, component_idCurrent Wasm linear memory size.

Per-Stream Metrics

Metric NameTypeUnitLabelsDescription
stream.elements.transferredCountercountflow_id, stream_idTotal elements transferred through this stream.
stream.backpressure.eventsCountercountflow_id, stream_idTotal backpressure activation events.
stream.backpressure.duration_nsCounternsflow_id, stream_idTotal time spent in backpressure.
stream.queue.current_depthGaugecountflow_id, stream_idCurrent queue depth.
stream.queue.peak_depthGaugecountflow_id, stream_idMaximum queue depth observed.
stream.queue.wait_timeHistogramnsflow_id, stream_idTime each element spent waiting in the queue.

Resource Pool Metrics

Metric NameTypeUnitLabelsDescription
pool.capacityGaugecounttierTotal slots in this pool tier.
pool.availableGaugecounttierFree buffers currently available.
pool.allocatedCountercounttierTotal buffers allocated since startup.
pool.returnedCountercounttierTotal buffers returned since startup.
pool.fallback_countCountercounttierAllocations that fell back to system allocator.
pool.exhaustion_eventsCountercounttierTimes the free list was empty when allocation was requested.
pool.reuse_rateDerivedratiotierreturned / allocated (computed during export).

Per-Capability Metrics

Metric NameTypeUnitLabelsDescription
capability.exercisesCountercountcomponent_id, capabilityTimes a capability was exercised.
capability.denialsCountercountcomponent_id, capabilityTimes a capability was denied.

System-Level Metrics

Metric NameTypeUnitDescription
system.flows.activeGaugecountCurrently active flows.
system.components.activeGaugecountCurrently instantiated components.
system.memory.totalGaugebytesTotal memory (host + all linear memories).
system.memory.hostGaugebytesHost-side memory (tables, queues, metrics).
system.scheduler.wakeupsCountercountTotal scheduler wakeup events.
system.scheduler.idle_nsCounternsTime spent idle (no work available).
system.spans_droppedCountercountTrace spans dropped due to export backpressure.

Querying Metrics

Prometheus: Scrape http://localhost:<port>/metrics (or the Unix domain socket at $TORVYN_STATE_DIR/torvyn.sock). All metrics are exported in Prometheus text exposition format.

OTLP: Configure [observability] otlp_metrics_enabled = true and otlp_export_interval_s to push metrics to an OpenTelemetry Collector, Grafana Cloud, or any OTLP-compatible backend.

torvyn bench: Benchmark reports include all metrics as computed deltas over the benchmark window.

Alerting Recommendations

ConditionMetricThresholdMeaning
Error rate spikeflow.elements.errors rate> 1% of totalComponents are failing. Investigate error logs.
Sustained backpressurestream.backpressure.duration_ns rate> 50% of wall timeConsumer cannot keep up. Scale or optimize downstream.
Pool exhaustionpool.exhaustion_events rate> 0 sustainedBuffer pool is undersized. Increase pool configuration.
High copy amplificationflow.copies.bytes / flow.throughput * avg_element_size> 3.0 per stageMore copies than expected. Investigate component data access patterns.
Memory growthcomponent.memory_currentSustained increasePossible memory leak in component. Investigate component logic.
Tail latencyflow.latency p99> 10× p50Occasional slow processing. Check for backpressure, GC, or I/O pauses.

Error Code Reference

Every error produced by the Torvyn CLI and runtime includes a unique error code, a description of what went wrong, the location where it occurred, why it is an error, and how to fix it. Error codes are stable across versions and searchable in documentation.

Error Code Ranges

RangeCategory
E0001–E0099General errors (I/O, configuration, environment)
E0100–E0199Contract and WIT errors
E0200–E0299Linking and composition errors
E0300–E0399Resource manager errors
E0400–E0499Reactor and scheduling errors
E0500–E0599Security and capability errors
E0600–E0699Packaging and distribution errors
E0700–E0799Configuration errors

General Errors (E0001–E0099)

CodeNameCauseFix
E0001ManifestNotFoundTorvyn.toml not found at the specified or default path.Run torvyn init to create a project, or specify --manifest <PATH>.
E0002ManifestParseErrorTorvyn.toml contains invalid TOML syntax.Check the indicated line and column for syntax errors.
E0003DirectoryExistsTarget directory for torvyn init already exists and is not empty.Use --force to overwrite, or choose a different name.
E0010ToolchainMissingA required external tool (cargo-component, wasm-tools, etc.) was not found.Run torvyn doctor for installation instructions.

Contract Errors (E0100–E0199)

CodeNameCauseFix
E0100WitSyntaxErrorA .wit file contains a syntax error.Check the indicated file, line, and column.
E0101WitResolutionErrorA use statement or package reference could not be resolved.Ensure the referenced package exists in wit/deps/. Run torvyn init or re-vendor dependencies.
E0102WorldIncompleteThe component’s world does not export any Torvyn processing interface.Add an export for at least one of: processor, source, sink, filter, router, aggregator.
E0103CapabilityMismatchWIT imports require a capability that is not declared in the manifest.Add the required capability to [capabilities.required] in Torvyn.toml.
E0110DeprecatedContractVersionThe targeted contract version is deprecated.Upgrade to a supported contract version.

Linking Errors (E0200–E0299)

CodeNameCauseFix
E0200InterfaceIncompatibleAn upstream component’s output type is incompatible with the downstream component’s input type.Ensure both components target compatible contract versions.
E0201VersionMismatchComponents target different major versions of the same contract package.Recompile one or both components against a compatible contract version.
E0202CapabilityNotGrantedA required capability is not granted in the pipeline configuration.Add the capability to [security.grants.<component>].
E0203TopologyInvalidThe pipeline graph has structural errors (cycles, disconnected nodes, role violations).Review the flow definition. Sources must have no inputs. Sinks must have no outputs. The graph must be a DAG.
E0204RouterPortUnknownA router returned a port name that does not match any downstream connection.Verify router port names match the edge definitions in the flow topology.
E0210ComponentNotFoundA component referenced in the flow definition could not be located.Check the component path in the flow node definition. Run torvyn build first.

Resource Errors (E0300–E0399)

CodeNameCauseFix
E0300AllocationFailedBuffer allocation failed (pool exhausted and system allocator failed).Increase pool sizes or reduce component memory budgets.
E0301PoolExhaustedBuffer pool for the requested tier is empty.Increase pool size for the affected tier, or switch exhaustion policy to fallback-alloc.
E0302NotOwnerA component attempted to access a buffer it does not own.This indicates a contract or runtime bug. Report the issue.
E0303StaleHandleA component used a buffer handle that has been invalidated (wrong generation).This indicates a use-after-free pattern in component code. Review component logic.
E0304BorrowsOutstandingAttempted to transfer or free a buffer while borrows are still active.Ensure all borrows are released before transferring ownership.
E0305CapacityExceededA write to a mutable buffer would exceed its capacity.Allocate a larger buffer or write data in smaller chunks.
E0310BudgetExceededA component exceeded its per-component memory budget.Increase max_memory_per_component or optimize component memory usage.

Reactor Errors (E0400–E0499)

CodeNameCauseFix
E0400FlowDeadlineExceededThe flow exceeded its configured maximum execution time.Increase the timeout or investigate which component is slow.
E0401ComponentTimeoutA single component invocation exceeded its per-call timeout.Increase fuel_per_invocation or optimize the component.
E0402FlowCancelledThe flow was cancelled by operator command or fatal error.Check the cancellation reason in the flow trace.
E0410FatalComponentErrorA component returned process-error::fatal.The component cannot process further elements. Review component logs for the cause.

Security Errors (E0500–E0599)

CodeNameCauseFix
E0500CapabilityDeniedA component attempted to use a capability it was not granted.Grant the capability in [security.grants.<component>], or remove the capability use from the component.
E0501SandboxViolationA component attempted an operation outside its sandbox.Review component code for disallowed operations.
E0510AuditLogFailedThe audit log could not be written.Check audit log target configuration and disk space.

Packaging Errors (E0600–E0699)

CodeNameCauseFix
E0600ArtifactInvalidThe packaged artifact failed validation.Run torvyn check before torvyn pack.
E0601RegistryAuthFailedAuthentication with the OCI registry failed.Check registry credentials.
E0602PushFailedThe artifact could not be pushed to the registry.Check network connectivity and registry availability.
E0603SigningFailedArtifact signing failed.Check signing key configuration.

Configuration Errors (E0700–E0799)

CodeNameCauseFix
E0700InvalidFieldTypeA configuration field has the wrong type.Check the field type in the configuration reference.
E0701UnknownFieldAn unrecognized field name in Torvyn.toml.Check for typos. Refer to the configuration reference for valid field names.
E0702ConstraintViolationA configuration value is outside its valid range.Check the valid range in the configuration reference.

Production Deployment Guide

This guide covers deploying Torvyn pipelines in production environments. It assumes familiarity with Torvyn’s concepts (contracts, components, flows, capabilities) and prior experience running pipelines locally with torvyn run.

Resource Sizing and Capacity Planning

Memory Budget

Torvyn’s memory consumption has three components:

  1. Host overhead: Resource tables, stream queues, metric structures, trace buffers, and runtime bookkeeping. Typically 10–50 MiB depending on the number of active flows and components.

  2. Buffer pool reservation: Pre-allocated buffer pools. With default settings, the pool reservation is approximately 53 MiB (Small: ~1 MiB, Medium: ~4 MiB, Large: ~16 MiB, Huge: ~32 MiB on-demand). Adjust pool sizes based on workload: if your pipeline processes mostly small messages, reduce Large and Huge pools; if it processes large binary payloads, increase Large pool allocation.

  3. Component linear memory: Each Wasm component instance has its own linear memory, starting small and growing up to the configured limit (max_memory_per_component, default 64 MiB). Total component memory = number of active component instances × their actual memory usage. For a pipeline with 5 components at 20 MiB each, budget 100 MiB.

Formula: total_memory ≈ host_overhead + pool_reservation + (active_components × avg_component_memory)

CPU Budget

Torvyn uses Tokio’s multi-threaded runtime. Set worker_threads to the number of CPU cores available to the process. Each flow driver consumes one Tokio task; Tokio distributes tasks across worker threads. For workloads with many concurrent flows, ensure worker_threads ≥ number of flows that must make progress simultaneously.

Wasmtime’s fuel mechanism provides CPU time limiting per component invocation. The default fuel_per_invocation of 1,000,000 fuel units is approximately 1–10 ms of CPU time depending on instruction mix. Adjust based on component complexity.

Queue Depth Sizing

The default queue depth of 64 elements per stream is suitable for most workloads. Increase it for bursty sources (to absorb bursts without triggering backpressure) or decrease it for latency-sensitive pipelines (smaller queues reduce maximum queuing delay).

Total queue memory = Σ(queue_depth × max_element_handle_size) per stream. Since queues hold handle references (not payload bytes), queue memory is small relative to buffer pool memory.

Monitoring and Alerting Setup

Prometheus Integration

Enable the Prometheus metrics endpoint:

[observability]
metrics_enabled = true
prometheus_enabled = true

The /metrics endpoint is served on the inspection API (default: Unix domain socket at $TORVYN_STATE_DIR/torvyn.sock, or localhost TCP if configured). Configure your Prometheus scrape target accordingly.

Trace Export

For production trace export, configure OTLP gRPC:

[observability.tracing]
level = "production"
tracing_exporter = "otlp-grpc"
tracing_endpoint = "http://your-otel-collector:4317"
sample_rate = 0.01
error_promote = true
latency_promote_threshold_ms = 10

Build dashboards around these key metrics: flow.elements.total rate (throughput), flow.latency percentiles, stream.backpressure.duration_ns (health), pool.available per tier (capacity), component.processing_time per component (bottleneck identification), and system.memory.total (resource utilization).

Security Hardening Checklist

  • Set default_capability_policy = "deny-all" in [security].
  • Grant each component the minimum required capabilities. Review grants against component manifests.
  • Enable audit logging: audit_enabled = true.
  • Bind the inspection API to localhost only (the default). Do not expose it to the network without authentication.
  • If exposing the inspection API over TCP, configure token-based authentication.
  • Verify component artifacts are signed before deploying (torvyn inspect --show capabilities on every artifact).
  • Set appropriate max_memory_per_component limits to prevent memory exhaustion by any single component.
  • Set appropriate fuel_per_invocation limits to prevent CPU monopolization.
  • Review and pin component versions in pipeline definitions. Do not use mutable tags (like latest) for production components.

Operational Runbook

Pipeline is not processing elements

  1. Check flow state: GET /flows on the inspection API. Look for flows in Failed or Paused state.
  2. Check for component errors: review component.errors metrics. A fatal error stops the flow.
  3. Check for backpressure: if stream.backpressure.duration_ns is high, the pipeline may be stalled on a slow sink.
  4. Check resource availability: if pool.exhaustion_events is increasing, buffer pools may be exhausted.

Latency is increasing

  1. Check per-component latency: component.processing_time histograms identify the slow stage.
  2. Check queue depths: stream.queue.current_depth shows where data is accumulating.
  3. Check backpressure: sustained backpressure on a specific stream indicates the downstream component is the bottleneck.
  4. Check system resources: CPU saturation or memory pressure can increase latency across all components.

Memory usage is growing

  1. Check component.memory_current for each component. A steadily increasing gauge suggests a memory leak in component code.
  2. Check pool.available per tier. Decreasing availability without corresponding increase in throughput suggests buffers are not being returned to the pool.
  3. Check for stale flows: flows that have stopped processing but have not been cleaned up may hold resource references.

Upgrade Procedures

  1. Test the upgrade locally. Run the new version with your pipeline using torvyn run and torvyn bench. Compare benchmark results against your baseline.
  2. Verify contract compatibility. If the new version includes contract changes, run torvyn link against all your component artifacts.
  3. Rolling upgrade (multi-instance). If running multiple Torvyn instances behind a load balancer, upgrade instances one at a time. Drain active flows on an instance before upgrading.
  4. In-place upgrade (single instance). Stop the running pipeline (torvyn run responds to SIGTERM with graceful shutdown — flows drain, components teardown, resources are reclaimed). Replace the binary. Restart.

Performance Tuning Guide

This guide explains how to optimize Torvyn pipeline performance. It assumes you have a working pipeline and want to improve throughput, reduce latency, or lower resource consumption.

Start with torvyn bench

Before tuning anything, establish a baseline. Run:

torvyn bench --duration 30s --warmup 5s --baseline my-baseline

This produces a comprehensive report covering throughput, latency percentiles, per-component breakdown, resource utilization, and scheduling statistics. Save the result as a named baseline for comparison after each change.

After each tuning adjustment, re-run the benchmark and compare:

torvyn bench --duration 30s --warmup 5s --compare .torvyn/bench/my-baseline.json

Reading and Interpreting Benchmark Reports

Throughput section: Elements per second and bytes per second. If throughput is lower than expected, the bottleneck is either a slow component, queue contention, or insufficient CPU threads.

Latency section: p50, p90, p95, p99, p999, and max. A large gap between p50 and p99 indicates tail latency variance — occasional slow processing caused by backpressure episodes, fuel exhaustion, or component-internal causes.

Per-component latency: Identifies the slowest stage. The component with the highest p50 latency is usually the throughput bottleneck.

Resources section: Buffer allocations, pool reuse rate, total copies, and peak memory. A low pool reuse rate (< 80%) suggests the pool is undersized. High copy counts with a high copy-amplification ratio suggest unnecessary data access patterns.

Scheduling section: Total wakeups, backpressure events, and queue peak. Frequent backpressure events indicate a consumer bottleneck. A queue peak near capacity suggests the queue is appropriately sized (the pipeline is backpressure-aware). A queue peak far below capacity suggests the producer is slower than the consumer.

Identifying Bottlenecks

CPU-bound

Symptom: High throughput but high per-component latency. component.processing_time is large relative to total end-to-end latency. The component is spending most of its time in computation.

Remedy: Optimize component code. Reduce the computational cost per element. Consider whether the component can operate on metadata alone (avoiding payload read/write copies). If the component must be CPU-intensive, ensure fuel_per_invocation is set high enough to avoid premature preemption.

Memory-bound

Symptom: High pool.fallback_count (buffers allocated outside the pool) or frequent pool.exhaustion_events. Component memory_current is near the configured limit.

Remedy: Increase buffer pool sizes for the relevant tier. Increase max_memory_per_component if component linear memory is constrained. Check whether components are holding buffers longer than necessary.

Backpressure-bound

Symptom: stream.backpressure.events is high on a specific stream. stream.backpressure.duration_ns is a significant fraction of total flow time.

Remedy: The downstream component on the backpressured stream is the bottleneck. Optimize that component’s processing time. Alternatively, increase the queue depth on the affected stream to absorb bursts (this trades memory for latency stability, not for throughput).

Copy-bound

Symptom: High flow.copies.bytes relative to the data volume. Per-component copy counts are higher than expected (more than 2 copies per stage for a transform).

Remedy: Review component data access patterns. If a filter or router is calling buffer.read-all() when it only needs metadata, change it to use buffer.size() and buffer.content-type() instead. If a processor reads the entire payload but only modifies a small portion, the current “new-buffer” pattern is correct (there is no copy-on-write in Phase 0), but the read copy is still necessary.

Tuning Configuration

Queue Depths

Increase default_queue_depth (or per-stream queue_depth) to absorb bursts from bursty sources. Decrease it for latency-sensitive pipelines where you prefer backpressure over queuing delay. The default of 64 is a balanced starting point.

Pool Sizes

If pool.fallback_count is non-zero for a tier, increase that tier’s pool size. The goal is for the pool to handle steady-state allocation without falling back to the system allocator. For bursty workloads, size the pool for peak, not average.

Fuel Budgets

If component.fuel_consumed is frequently hitting the fuel_per_invocation limit (observable as E0401 ComponentTimeout errors or as elevated tail latency), increase the fuel budget. Setting fuel to 0 disables fuel metering, which removes the per-instruction overhead but also removes CPU time protection.

Yield Frequency

The reactor yields to Tokio after processing a batch of elements (default: 32) or after a time quantum (default: 100 microseconds). Increasing the batch size improves throughput (fewer context switches) but can increase latency for other flows. Decreasing it improves inter-flow fairness at the cost of throughput.

Worker Threads

Set worker_threads to match available CPU cores. More threads than cores provides no benefit and adds context-switch overhead. Fewer threads than concurrent flows may cause scheduling delays.

Pipeline Topology Optimization

Minimize pipeline depth. Each component boundary adds Wasm invocation overhead (~100-500 ns) and potentially two payload copies. If two adjacent stages can be combined into a single component without sacrificing modularity or reusability, the combined version will be faster.

Put filters early. Filters that reject elements are extremely cheap (no buffer allocation, no payload copy). Placing filters early in the pipeline reduces the amount of data processed by downstream stages.

Use metadata-only routing. If routing decisions can be made from element metadata alone (content-type, size, sequence number), the router operates on the handle-pass fast path with zero payload copies.

Consider batch processing for high-throughput workloads. Processing elements one at a time incurs per-call overhead for each Wasm invocation. A future batch-processor interface that processes list<stream-element> could amortize this cost. This is planned for Phase 1+ based on benchmark data.

Advanced: Component-Level Performance Profiling

For deep investigation of a specific component’s performance:

  1. Use torvyn trace --limit 100 --show-buffers to see exactly what buffer operations the component performs per element. Look for unnecessary read-all calls, oversized buffer allocations, or multiple small writes that could be consolidated into a single write.

  2. Check Wasm fuel consumption. High fuel per invocation relative to processing time suggests the component has an inefficient inner loop. Consider optimizing the source code or the compilation settings (optimization level, LTO).

  3. Profile the component outside Torvyn. Compile the component as a native binary (not Wasm) and profile it with standard tools (perf, Instruments, etc.) to identify hot functions. Optimize those functions, then recompile to Wasm and re-benchmark in Torvyn.

  4. Measure copy overhead separately. Run the pipeline with a pass-through component (one that returns the input unchanged, using the drop variant) to measure the baseline cost of pipeline orchestration without component computation. The difference between this baseline and the actual pipeline is the component computation cost.

Hello World

What It Demonstrates

The simplest possible Torvyn pipeline: a source that produces “Hello, World!” messages and a sink that prints them. This example teaches the foundational concepts — WIT contracts, component implementation, pipeline configuration, and the torvyn run command.

Concepts Covered

  • Defining WIT contracts for source and sink components
  • Implementing the torvyn:streaming/source interface
  • Implementing the torvyn:streaming/sink interface
  • Implementing the torvyn:streaming/lifecycle interface for initialization
  • Configuring a two-node pipeline in Torvyn.toml
  • Building and running with torvyn run

File Listing

examples/hello-world/
├── Torvyn.toml
├── Makefile
├── README.md
├── wit/
│   └── torvyn-streaming/
│       ├── types.wit
│       ├── source.wit
│       ├── sink.wit
│       ├── lifecycle.wit
│       ├── buffer-allocator.wit
│       └── world.wit
├── components/
│   ├── hello-source/
│   │   ├── Cargo.toml
│   │   └── src/
│   │       └── lib.rs
│   └── hello-sink/
│       ├── Cargo.toml
│       └── src/
│           └── lib.rs
└── expected-output.txt

WIT Contracts

The example uses the canonical torvyn:streaming@0.1.0 contracts. The relevant interfaces are included in the wit/ directory so the example is self-contained.

wit/torvyn-streaming/types.wit

/// Core types for Torvyn streaming — subset for this example.
///
/// See the full types.wit in the Torvyn repository for complete documentation.

package torvyn:streaming@0.1.0;

interface types {
    resource buffer {
        size: func() -> u64;
        content-type: func() -> string;
        read: func(offset: u64, len: u64) -> list<u8>;
        read-all: func() -> list<u8>;
    }

    resource mutable-buffer {
        write: func(offset: u64, bytes: list<u8>) -> result<_, buffer-error>;
        append: func(bytes: list<u8>) -> result<_, buffer-error>;
        size: func() -> u64;
        capacity: func() -> u64;
        set-content-type: func(content-type: string);
        freeze: func() -> buffer;
    }

    variant buffer-error {
        capacity-exceeded,
        out-of-bounds,
        allocation-failed(string),
    }

    resource flow-context {
        trace-id: func() -> string;
        span-id: func() -> string;
        deadline-ns: func() -> u64;
        flow-id: func() -> string;
    }

    record element-meta {
        sequence: u64,
        timestamp-ns: u64,
        content-type: string,
    }

    record stream-element {
        meta: element-meta,
        payload: borrow<buffer>,
        context: borrow<flow-context>,
    }

    record output-element {
        meta: element-meta,
        payload: buffer,
    }

    variant process-error {
        invalid-input(string),
        unavailable(string),
        internal(string),
        deadline-exceeded,
        fatal(string),
    }

    enum backpressure-signal {
        ready,
        pause,
    }
}

wit/torvyn-streaming/source.wit

package torvyn:streaming@0.1.0;

interface source {
    use types.{output-element, process-error, backpressure-signal};

    pull: func() -> result<option<output-element>, process-error>;
    notify-backpressure: func(signal: backpressure-signal);
}

wit/torvyn-streaming/sink.wit

package torvyn:streaming@0.1.0;

interface sink {
    use types.{stream-element, process-error, backpressure-signal};

    push: func(element: stream-element) -> result<backpressure-signal, process-error>;
    complete: func() -> result<_, process-error>;
}

wit/torvyn-streaming/lifecycle.wit

package torvyn:streaming@0.1.0;

interface lifecycle {
    use types.{process-error};

    init: func(config: string) -> result<_, process-error>;
    teardown: func();
}

wit/torvyn-streaming/buffer-allocator.wit

package torvyn:streaming@0.1.0;

interface buffer-allocator {
    use types.{mutable-buffer, buffer-error, buffer};

    allocate: func(capacity-hint: u64) -> result<mutable-buffer, buffer-error>;
    clone-into-mutable: func(source: borrow<buffer>) -> result<mutable-buffer, buffer-error>;
}

wit/torvyn-streaming/world.wit

package torvyn:streaming@0.1.0;

world data-source {
    import types;
    import buffer-allocator;

    export source;
    export lifecycle;
}

world data-sink {
    import types;

    export sink;
    export lifecycle;
}

Source Component: hello-source

components/hello-source/Cargo.toml

[package]
name = "hello-source"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "torvyn:streaming"
world = "data-source"

components/hello-source/src/lib.rs

#![allow(unused)]
fn main() {
//! Hello World source component.
//!
//! Produces a configurable number of "Hello, World!" messages,
//! then signals stream exhaustion.

// Generate bindings from the WIT contract.
// This creates Rust types and traits matching the WIT interfaces.
wit_bindgen::generate!({
    world: "data-source",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::source::Guest as SourceGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::{BackpressureSignal, OutputElement, ElementMeta, ProcessError};

/// Component state. Holds configuration and tracks how many
/// messages have been produced.
struct HelloSource {
    /// Total messages to produce before signaling exhaustion.
    total_messages: u64,
    /// Messages produced so far.
    produced: u64,
}

// Global mutable state. In a Wasm component, there is exactly one
// instance of this state per component instantiation. The host
// guarantees no concurrent access (no reentrancy).
static mut STATE: Option<HelloSource> = None;

fn state() -> &'static mut HelloSource {
    unsafe { STATE.as_mut().expect("component not initialized") }
}

impl LifecycleGuest for HelloSource {
    /// Initialize the source. Accepts an optional JSON config string
    /// specifying `{"count": N}`. Defaults to 5 messages.
    fn init(config: String) -> Result<(), ProcessError> {
        let total = if config.is_empty() {
            5
        } else {
            // Simple manual parsing to avoid pulling in serde for a demo.
            // In production, use serde_json.
            config
                .trim()
                .strip_prefix("{\"count\":")
                .and_then(|s| s.strip_suffix('}'))
                .and_then(|s| s.trim().parse::<u64>().ok())
                .unwrap_or(5)
        };

        unsafe {
            STATE = Some(HelloSource {
                total_messages: total,
                produced: 0,
            });
        }
        Ok(())
    }

    fn teardown() {
        unsafe { STATE = None; }
    }
}

impl SourceGuest for HelloSource {
    /// Pull the next element. Returns None when all messages are produced.
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        let s = state();

        if s.produced >= s.total_messages {
            // Stream exhausted. The runtime will call complete() on
            // downstream sinks and transition the flow to Draining.
            return Ok(None);
        }

        // Format the message payload.
        let message = format!("Hello, World! (message {})", s.produced + 1);
        let payload_bytes = message.as_bytes();

        // Allocate a buffer from the host's buffer pool.
        // The host manages the memory — the component never directly
        // allocates host-side buffers.
        let mut_buf = buffer_allocator::allocate(payload_bytes.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("buffer allocation failed: {e:?}")))?;

        // Write the payload into the mutable buffer.
        mut_buf.append(payload_bytes)
            .map_err(|e| ProcessError::Internal(format!("buffer write failed: {e:?}")))?;

        // Set content type for downstream consumers.
        mut_buf.set_content_type("text/plain");

        // Freeze the mutable buffer into an immutable buffer.
        // Ownership of the buffer transfers to the runtime when
        // we return it inside the OutputElement.
        let frozen = mut_buf.freeze();

        s.produced += 1;

        Ok(Some(OutputElement {
            meta: ElementMeta {
                // The runtime overwrites sequence and timestamp-ns (per C01-4).
                // These values are advisory.
                sequence: s.produced - 1,
                timestamp_ns: 0,
                content_type: "text/plain".to_string(),
            },
            payload: frozen,
        }))
    }

    fn notify_backpressure(_signal: BackpressureSignal) {
        // This simple source ignores backpressure signals.
        // A production source would pause or slow its data generation.
    }
}

// Register the component with the Wasm component model.
export!(HelloSource);
}

Sink Component: hello-sink

components/hello-sink/Cargo.toml

[package]
name = "hello-sink"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.36"

[package.metadata.component]
package = "torvyn:streaming"
world = "data-sink"

components/hello-sink/src/lib.rs

#![allow(unused)]
fn main() {
//! Hello World sink component.
//!
//! Receives stream elements and prints their payload contents
//! to the component's standard output (captured by the host).

wit_bindgen::generate!({
    world: "data-sink",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::sink::Guest as SinkGuest;
use torvyn::streaming::types::{BackpressureSignal, ProcessError, StreamElement};

struct HelloSink {
    received: u64,
}

static mut STATE: Option<HelloSink> = None;

fn state() -> &'static mut HelloSink {
    unsafe { STATE.as_mut().expect("component not initialized") }
}

impl LifecycleGuest for HelloSink {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe {
            STATE = Some(HelloSink { received: 0 });
        }
        Ok(())
    }

    fn teardown() {
        let s = state();
        // Print summary on teardown.
        println!("[hello-sink] Received {} messages total.", s.received);
        unsafe { STATE = None; }
    }
}

impl SinkGuest for HelloSink {
    /// Receive a stream element and print its payload.
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let s = state();

        // Read the payload bytes from the borrowed buffer handle.
        // This copies data from host memory into component linear memory.
        // The resource manager records this copy for observability.
        let payload_bytes = element.payload.read_all();

        // Convert bytes to a UTF-8 string.
        let text = String::from_utf8(payload_bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("payload is not UTF-8: {e}")))?;

        println!("[hello-sink] seq={}: {}", element.meta.sequence, text);

        s.received += 1;

        // Signal that we are ready for the next element.
        // Returning BackpressureSignal::Pause would tell the runtime
        // to stop delivering elements until we are ready.
        Ok(BackpressureSignal::Ready)
    }

    /// Called when the upstream source is exhausted.
    fn complete() -> Result<(), ProcessError> {
        println!("[hello-sink] Stream complete.");
        Ok(())
    }
}

export!(HelloSink);
}

Pipeline Configuration

Torvyn.toml

[torvyn]
name = "hello-world"
version = "0.1.0"
contract_version = "0.1.0"
description = "The simplest possible Torvyn pipeline"

# Declare the two components in this workspace.
[[component]]
name = "hello-source"
path = "components/hello-source"

[[component]]
name = "hello-sink"
path = "components/hello-sink"

# Define the pipeline flow.
[flow.main]
description = "Source produces greetings, sink prints them"

[flow.main.nodes.source]
component = "hello-source"
interface = "torvyn:streaming/source"
config = '{"count": 5}'

[flow.main.nodes.sink]
component = "hello-sink"
interface = "torvyn:streaming/sink"

[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "sink", port = "input" }

Makefile

.PHONY: build run clean check

# Build both components to WebAssembly.
build:
	torvyn check
	cd components/hello-source && cargo component build --release
	cd components/hello-sink && cargo component build --release

# Run the pipeline.
run: build
	torvyn run flow.main

# Run with tracing enabled to see flow lifecycle events.
trace: build
	torvyn trace flow.main

# Validate contracts and topology without running.
check:
	torvyn check
	torvyn link flow.main

# Benchmark this pipeline (latency, throughput, copies).
bench: build
	torvyn bench flow.main --iterations 1000

clean:
	cd components/hello-source && cargo clean
	cd components/hello-sink && cargo clean

Expected Output

expected-output.txt

$ torvyn run flow.main
[torvyn] Loading flow 'main' from Torvyn.toml
[torvyn] Validating contracts...  ok
[torvyn] Linking components...    ok (2 components, 1 edge)
[torvyn] Instantiating...         ok
[torvyn] Running flow 'main'

[hello-sink] seq=0: Hello, World! (message 1)
[hello-sink] seq=1: Hello, World! (message 2)
[hello-sink] seq=2: Hello, World! (message 3)
[hello-sink] seq=3: Hello, World! (message 4)
[hello-sink] seq=4: Hello, World! (message 5)
[hello-sink] Stream complete.
[hello-sink] Received 5 messages total.

[torvyn] Flow 'main' completed. 5 elements processed.
[torvyn] Duration: 12ms | Copies: 5 | Peak queue depth: 1

Performance Characteristics (Design Targets)

MetricTarget
End-to-end latency per element< 50 us (includes Wasm boundary crossing)
Host-side overhead per element< 5 us (per MAX_HOT_PATH_NS constant)
Copies per element1 (sink reads payload from host buffer)
Component instantiation< 10 ms per component
Memory per component< 2 MB linear memory

Commentary

This example demonstrates the fundamental Torvyn data flow model:

  1. Contract-first: Before writing any code, the WIT interfaces define the exact shape of source and sink interactions. The source.pull() function returns an output-element with an owned buffer. The sink.push() function receives a stream-element with a borrowed buffer. This ownership distinction is not incidental — it is central to Torvyn’s design.

  2. Host-managed buffers: The source does not allocate memory directly. It requests a mutable-buffer from the host via buffer-allocator.allocate(), writes into it, then freezes it. The host controls the buffer pool, tracks ownership, and records every copy.

  3. Lifecycle management: Both components implement the lifecycle interface. The runtime calls init() before any stream processing and teardown() after the flow completes. Component state lives in the component’s linear memory and is isolated from all other components.

  4. Flow state machine: The flow transitions through Created -> Validated -> Instantiated -> Running -> Draining -> Completed. You can observe these transitions by running with torvyn trace.

Learn More

Stream Transform

What It Demonstrates

A three-stage pipeline: a source produces JSON events, a processor transforms them (adds a timestamp field, renames a field), and a sink writes the transformed output. This example teaches the processor interface, buffer allocation for output, and the ownership model across a transform boundary.

Concepts Covered

  • Implementing the torvyn:streaming/processor interface
  • Reading borrowed input, allocating new output
  • Buffer ownership transfer (borrow input → own output)
  • JSON payload manipulation inside a Wasm component
  • Three-node pipeline topology

File Listing

examples/stream-transform/
├── Torvyn.toml
├── Makefile
├── README.md
├── wit/
│   └── torvyn-streaming/
│       └── ... (same canonical WIT files)
├── components/
│   ├── json-source/
│   │   ├── Cargo.toml
│   │   └── src/lib.rs
│   ├── json-transform/
│   │   ├── Cargo.toml
│   │   └── src/lib.rs
│   └── json-sink/
│       ├── Cargo.toml
│       └── src/lib.rs
└── expected-output.txt

Source Component: json-source

components/json-source/src/lib.rs

#![allow(unused)]
fn main() {
//! JSON event source.
//!
//! Produces a sequence of JSON objects representing user events.
//! Each event has a "user" field and an "action" field.

wit_bindgen::generate!({
    world: "data-source",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::source::Guest as SourceGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

struct JsonSource {
    events: Vec<String>,
    index: usize,
}

static mut STATE: Option<JsonSource> = None;
fn state() -> &'static mut JsonSource {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for JsonSource {
    fn init(_config: String) -> Result<(), ProcessError> {
        let events = vec![
            r#"{"user":"alice","action":"login"}"#.to_string(),
            r#"{"user":"bob","action":"purchase"}"#.to_string(),
            r#"{"user":"carol","action":"logout"}"#.to_string(),
        ];
        unsafe { STATE = Some(JsonSource { events, index: 0 }); }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl SourceGuest for JsonSource {
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        let s = state();
        if s.index >= s.events.len() {
            return Ok(None);
        }
        let payload = s.events[s.index].as_bytes();
        let buf = buffer_allocator::allocate(payload.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(payload)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("application/json");
        let frozen = buf.freeze();
        s.index += 1;
        Ok(Some(OutputElement {
            meta: ElementMeta {
                sequence: (s.index - 1) as u64,
                timestamp_ns: 0,
                content_type: "application/json".to_string(),
            },
            payload: frozen,
        }))
    }
    fn notify_backpressure(_signal: BackpressureSignal) {}
}

export!(JsonSource);
}

Processor Component: json-transform

components/json-transform/src/lib.rs

#![allow(unused)]
fn main() {
//! JSON transform processor.
//!
//! Reads each JSON event, adds a "processed_at" timestamp field,
//! and renames "user" to "username". Demonstrates the processor
//! interface's ownership model: input is borrowed, output is owned.

wit_bindgen::generate!({
    world: "managed-transform",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::processor::Guest as ProcessorGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

struct JsonTransform;

static mut INITIALIZED: bool = false;

impl LifecycleGuest for JsonTransform {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { INITIALIZED = true; }
        Ok(())
    }
    fn teardown() {
        unsafe { INITIALIZED = false; }
    }
}

impl ProcessorGuest for JsonTransform {
    fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
        // Step 1: Read the input payload (borrowed buffer → copy into
        // component linear memory). This is a measured copy.
        let input_bytes = input.payload.read_all();

        // Step 2: Parse and transform the JSON.
        // Using simple string manipulation to avoid pulling in serde.
        // In production code, use serde_json.
        let input_str = String::from_utf8(input_bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("not UTF-8: {e}")))?;

        // Rename "user" → "username" and add "processed_at".
        let transformed = input_str
            .replace("\"user\":", "\"username\":")
            .trim_end_matches('}')
            .to_string()
            + ",\"processed_at\":\"2025-01-15T10:30:00Z\"}";

        let out_bytes = transformed.as_bytes();

        // Step 3: Allocate a new output buffer from the host pool.
        let out_buf = buffer_allocator::allocate(out_bytes.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        out_buf.append(out_bytes)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        out_buf.set_content_type("application/json");

        // Step 4: Freeze and return. Ownership of the output buffer
        // transfers to the runtime via the OutputElement.
        let frozen = out_buf.freeze();

        Ok(ProcessResult::Emit(OutputElement {
            meta: ElementMeta {
                sequence: input.meta.sequence,
                timestamp_ns: 0,
                content_type: "application/json".to_string(),
            },
            payload: frozen,
        }))
    }
}

export!(JsonTransform);
}

Sink Component: json-sink

components/json-sink/src/lib.rs

#![allow(unused)]
fn main() {
//! JSON sink — prints each transformed JSON event.

wit_bindgen::generate!({
    world: "data-sink",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::sink::Guest as SinkGuest;
use torvyn::streaming::types::*;

struct JsonSink;
static mut INITIALIZED: bool = false;

impl LifecycleGuest for JsonSink {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { INITIALIZED = true; }
        Ok(())
    }
    fn teardown() { unsafe { INITIALIZED = false; } }
}

impl SinkGuest for JsonSink {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let bytes = element.payload.read_all();
        let text = String::from_utf8(bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("{e}")))?;
        println!("[json-sink] seq={}: {}", element.meta.sequence, text);
        Ok(BackpressureSignal::Ready)
    }
    fn complete() -> Result<(), ProcessError> {
        println!("[json-sink] Stream complete.");
        Ok(())
    }
}

export!(JsonSink);
}

Pipeline Configuration

Torvyn.toml

[torvyn]
name = "stream-transform"
version = "0.1.0"
contract_version = "0.1.0"
description = "JSON event transformation pipeline"

[[component]]
name = "json-source"
path = "components/json-source"

[[component]]
name = "json-transform"
path = "components/json-transform"

[[component]]
name = "json-sink"
path = "components/json-sink"

[flow.main]
description = "Source → Transform → Sink"

[flow.main.nodes.source]
component = "json-source"
interface = "torvyn:streaming/source"

[flow.main.nodes.transform]
component = "json-transform"
interface = "torvyn:streaming/processor"

[flow.main.nodes.sink]
component = "json-sink"
interface = "torvyn:streaming/sink"

[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "transform", port = "input" }

[[flow.main.edges]]
from = { node = "transform", port = "output" }
to = { node = "sink", port = "input" }

Expected Output

$ torvyn run flow.main
[torvyn] Loading flow 'main' from Torvyn.toml
[torvyn] Validating contracts...  ok
[torvyn] Linking components...    ok (3 components, 2 edges)
[torvyn] Instantiating...         ok
[torvyn] Running flow 'main'

[json-sink] seq=0: {"username":"alice","action":"login","processed_at":"2025-01-15T10:30:00Z"}
[json-sink] seq=1: {"username":"bob","action":"purchase","processed_at":"2025-01-15T10:30:00Z"}
[json-sink] seq=2: {"username":"carol","action":"logout","processed_at":"2025-01-15T10:30:00Z"}
[json-sink] Stream complete.

[torvyn] Flow 'main' completed. 3 elements processed.
[torvyn] Duration: 8ms | Copies: 6 (3 reads + 3 writes) | Peak queue depth: 1

Performance Characteristics (Design Targets)

MetricTarget
End-to-end latency per element< 80 us (two Wasm boundary crossings)
Copies per element2 (processor reads input, writes output)
Processor overhead< 5 us host-side per invocation

Commentary

The key teaching point is ownership transfer. The processor receives a stream-element with a borrowed buffer handle. It can read from this buffer, but the handle is only valid for the duration of the process() call. To produce output, the processor allocates a new mutable buffer, writes transformed data, freezes it, and returns it as an owned output-element. The runtime then takes ownership of the output buffer and delivers it downstream.

This two-copy pattern (read input, write output) is the normal case for processors that modify data. For pass-through processors that do not modify the payload, Torvyn’s planned Phase 2 buffer view mechanism will allow forwarding without copying.

Learn More

Backpressure Demo

What It Demonstrates

A fast source producing 1,000 elements/sec connected to a deliberately slow sink processing 100 elements/sec. Demonstrates Torvyn’s built-in backpressure mechanism: queue depth monitoring, watermark-based flow control, and the BackpressureSignal enum.

Concepts Covered

  • Backpressure activation and deactivation
  • Queue depth and watermark behavior (high watermark = queue capacity, low watermark = 50% per DEFAULT_LOW_WATERMARK_RATIO)
  • Default queue depth of 64 elements (per DEFAULT_QUEUE_DEPTH)
  • BackpressureSignal::Pause and BackpressureSignal::Ready
  • Observing backpressure via torvyn trace

Source Component: fast-source

components/fast-source/src/lib.rs

#![allow(unused)]
fn main() {
//! Fast source — produces elements as quickly as possible.
//! Emits 1,000 numbered elements, respecting backpressure signals.

wit_bindgen::generate!({
    world: "data-source",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::source::Guest as SourceGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

struct FastSource {
    total: u64,
    produced: u64,
    paused: bool,
}

static mut STATE: Option<FastSource> = None;
fn state() -> &'static mut FastSource {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for FastSource {
    fn init(config: String) -> Result<(), ProcessError> {
        let total = config.trim().parse::<u64>().unwrap_or(1000);
        unsafe { STATE = Some(FastSource { total, produced: 0, paused: false }); }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl SourceGuest for FastSource {
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        let s = state();

        // Respect backpressure: if paused, return no element.
        // The runtime will poll again after backpressure clears.
        if s.paused {
            return Ok(None);
        }

        if s.produced >= s.total {
            return Ok(None);
        }

        let msg = format!("element-{}", s.produced);
        let bytes = msg.as_bytes();
        let buf = buffer_allocator::allocate(bytes.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(bytes).map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("text/plain");
        let frozen = buf.freeze();
        s.produced += 1;

        Ok(Some(OutputElement {
            meta: ElementMeta {
                sequence: s.produced - 1,
                timestamp_ns: 0,
                content_type: "text/plain".to_string(),
            },
            payload: frozen,
        }))
    }

    fn notify_backpressure(signal: BackpressureSignal) {
        let s = state();
        match signal {
            BackpressureSignal::Pause => {
                s.paused = true;
                // In a real source, you would also stop reading from
                // the external data source (network, file, etc.).
            }
            BackpressureSignal::Ready => {
                s.paused = false;
            }
        }
    }
}

export!(FastSource);
}

Sink Component: slow-sink

components/slow-sink/src/lib.rs

#![allow(unused)]
fn main() {
//! Slow sink — simulates a slow consumer by introducing a delay
//! per element via busy-waiting (since WASI sleep is not available
//! in all environments).

wit_bindgen::generate!({
    world: "data-sink",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::sink::Guest as SinkGuest;
use torvyn::streaming::types::*;

struct SlowSink {
    received: u64,
    delay_iterations: u64,
}

static mut STATE: Option<SlowSink> = None;
fn state() -> &'static mut SlowSink {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for SlowSink {
    fn init(config: String) -> Result<(), ProcessError> {
        // delay_iterations controls how slow the sink is.
        // Higher = slower. Calibrate for your hardware.
        let delay = config.trim().parse::<u64>().unwrap_or(100_000);
        unsafe { STATE = Some(SlowSink { received: 0, delay_iterations: delay }); }
        Ok(())
    }
    fn teardown() {
        let s = state();
        println!("[slow-sink] Processed {} elements total.", s.received);
        unsafe { STATE = None; }
    }
}

impl SinkGuest for SlowSink {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let s = state();

        // Simulate slow processing with a busy loop.
        // In a real sink, this delay would come from I/O (database writes,
        // network calls, disk writes, etc.).
        let mut acc: u64 = 0;
        for i in 0..s.delay_iterations {
            acc = acc.wrapping_add(i);
        }
        // Prevent the optimizer from removing the loop.
        if acc == u64::MAX { println!("{acc}"); }

        let bytes = element.payload.read_all();
        let text = String::from_utf8_lossy(&bytes);
        if s.received % 100 == 0 {
            println!("[slow-sink] seq={}: {} (every 100th logged)", element.meta.sequence, text);
        }

        s.received += 1;
        Ok(BackpressureSignal::Ready)
    }

    fn complete() -> Result<(), ProcessError> {
        println!("[slow-sink] Stream complete.");
        Ok(())
    }
}

export!(SlowSink);
}

Pipeline Configuration

Torvyn.toml

[torvyn]
name = "backpressure-demo"
version = "0.1.0"
contract_version = "0.1.0"
description = "Demonstrates backpressure between a fast source and slow sink"

[[component]]
name = "fast-source"
path = "components/fast-source"

[[component]]
name = "slow-sink"
path = "components/slow-sink"

[flow.main]
description = "Fast source → Slow sink (backpressure active)"

# Override queue depth to a small value so backpressure activates quickly.
default_queue_depth = 16

[flow.main.nodes.source]
component = "fast-source"
interface = "torvyn:streaming/source"
config = "1000"

[flow.main.nodes.sink]
component = "slow-sink"
interface = "torvyn:streaming/sink"
config = "100000"

[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "sink", port = "input" }

How to Observe Backpressure

Run with tracing to see backpressure events in real time:

$ torvyn trace flow.main

The trace output will include entries like:

[trace] flow=main stream=source→sink queue_depth=16/16 backpressure=ACTIVATED
[trace] flow=main stream=source→sink source notified: PAUSE
[trace] flow=main stream=source→sink queue_depth=8/16  backpressure=DEACTIVATED (low watermark)
[trace] flow=main stream=source→sink source notified: READY

The default behavior: when the queue between source and sink fills to capacity (16 in this configuration), the runtime activates backpressure and sends BackpressureSignal::Pause to the source. When the queue drains to the low watermark (50% of capacity = 8 elements, per DEFAULT_LOW_WATERMARK_RATIO), the runtime deactivates backpressure and sends BackpressureSignal::Ready.

Expected Output

$ torvyn run flow.main
[torvyn] Running flow 'main'

[slow-sink] seq=0: element-0 (every 100th logged)
[slow-sink] seq=100: element-100 (every 100th logged)
[slow-sink] seq=200: element-200 (every 100th logged)
...
[slow-sink] seq=900: element-900 (every 100th logged)
[slow-sink] Stream complete.
[slow-sink] Processed 1000 elements total.

[torvyn] Flow 'main' completed. 1000 elements processed.
[torvyn] Duration: ~10s | Backpressure activations: ~62 | Peak queue depth: 16

Performance Characteristics (Design Targets)

MetricTarget
Queue depth (configured)16 elements
Low watermark8 elements (50% of 16)
Backpressure overhead< 500 ns per signal
No unbounded queue growthQueue never exceeds configured depth

Commentary

Backpressure is not optional in Torvyn — it is fundamental to the reactive streaming model (per the vision document, Section 5.3). Without backpressure, a fast producer paired with a slow consumer would cause unbounded memory growth. Torvyn’s reactor enforces queue bounds and propagates demand signals automatically.

The notify-backpressure callback on the source interface gives components the opportunity to respond to flow control signals. A well-behaved source pauses its external data intake when it receives Pause and resumes when it receives Ready. The runtime handles the mechanics; the component decides the policy.

Learn More

Token Streaming

What It Demonstrates

An AI-oriented pipeline: a simulated LLM token source emits tokens one at a time, a content policy filter rejects tokens matching a block list, a token aggregator collects tokens into complete sentences, and an output sink writes the assembled text. Demonstrates Torvyn’s fit for AI inference pipelines.

Concepts Covered

  • The torvyn:filtering/filter interface for accept/reject decisions
  • Token-by-token streaming (granular elements)
  • Aggregation with sentence-boundary detection
  • AI pipeline composition pattern
  • Filter components that inspect data without allocating buffers

Key Components

components/token-source/src/lib.rs

#![allow(unused)]
fn main() {
//! Simulated LLM token source.
//!
//! Emits tokens from a pre-defined sequence simulating model output.
//! Each token is a separate stream element, mimicking the granularity
//! of real language model decoding.

wit_bindgen::generate!({
    world: "data-source",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::source::Guest as SourceGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

const TOKENS: &[&str] = &[
    "The", " quick", " brown", " fox", " jumped", " over",
    " the", " lazy", " dog", ".",
    " The", " blocked_word", " was", " filtered", ".",
    " Torvyn", " handles", " streaming", " tokens", ".",
];

struct TokenSource {
    index: usize,
}

static mut STATE: Option<TokenSource> = None;
fn state() -> &'static mut TokenSource {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for TokenSource {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { STATE = Some(TokenSource { index: 0 }); }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl SourceGuest for TokenSource {
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        let s = state();
        if s.index >= TOKENS.len() {
            return Ok(None);
        }
        let token = TOKENS[s.index];
        let buf = buffer_allocator::allocate(token.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(token.as_bytes())
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("text/plain; charset=utf-8");
        let frozen = buf.freeze();
        s.index += 1;
        Ok(Some(OutputElement {
            meta: ElementMeta {
                sequence: (s.index - 1) as u64,
                timestamp_ns: 0,
                content_type: "text/plain; charset=utf-8".to_string(),
            },
            payload: frozen,
        }))
    }
    fn notify_backpressure(_signal: BackpressureSignal) {}
}

export!(TokenSource);
}

components/content-filter/src/lib.rs

#![allow(unused)]
fn main() {
//! Content policy filter.
//!
//! Uses the torvyn:filtering/filter interface to accept or reject tokens.
//! This component type is extremely efficient: it does not allocate output
//! buffers. It reads the token to inspect it, then returns a boolean.
//! The runtime forwards or drops the element based on the result.

wit_bindgen::generate!({
    world: "content-filter",
    path: "../../wit",
});

use exports::torvyn::filtering::filter::Guest as FilterGuest;
use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use torvyn::streaming::types::*;

struct ContentFilter {
    blocked_words: Vec<String>,
    filtered_count: u64,
}

static mut STATE: Option<ContentFilter> = None;
fn state() -> &'static mut ContentFilter {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for ContentFilter {
    fn init(config: String) -> Result<(), ProcessError> {
        // Config: comma-separated list of blocked words.
        let blocked: Vec<String> = if config.is_empty() {
            vec!["blocked_word".to_string()]
        } else {
            config.split(',').map(|s| s.trim().to_string()).collect()
        };
        unsafe {
            STATE = Some(ContentFilter {
                blocked_words: blocked,
                filtered_count: 0,
            });
        }
        Ok(())
    }
    fn teardown() {
        let s = state();
        if s.filtered_count > 0 {
            println!(
                "[content-filter] Filtered {} token(s) during this flow.",
                s.filtered_count
            );
        }
        unsafe { STATE = None; }
    }
}

impl FilterGuest for ContentFilter {
    /// Evaluate whether a token passes the content policy.
    ///
    /// - true: token passes through to downstream.
    /// - false: token is dropped by the runtime (no output buffer allocated).
    ///
    /// The filter reads the borrowed buffer to inspect the token contents.
    /// This is a single measured copy. No output buffer is allocated.
    fn evaluate(element: StreamElement) -> Result<bool, ProcessError> {
        let s = state();
        let bytes = element.payload.read_all();
        let token = String::from_utf8_lossy(&bytes);
        let trimmed = token.trim();

        for blocked in &s.blocked_words {
            if trimmed.eq_ignore_ascii_case(blocked) {
                s.filtered_count += 1;
                return Ok(false);
            }
        }
        Ok(true)
    }
}

export!(ContentFilter);
}

components/sentence-aggregator/src/lib.rs

#![allow(unused)]
fn main() {
//! Token-to-sentence aggregator.
//!
//! Collects streaming tokens into complete sentences. A sentence boundary
//! is detected when a token ends with '.', '!', or '?'. When a boundary
//! is reached, the accumulated text is emitted as a single output element.
//! Any remaining text is emitted on flush().

wit_bindgen::generate!({
    world: "stream-aggregator",
    path: "../../wit",
});

use exports::torvyn::aggregation::aggregator::Guest as AggregatorGuest;
use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

struct SentenceAggregator {
    buffer: String,
    sentence_count: u64,
}

static mut STATE: Option<SentenceAggregator> = None;
fn state() -> &'static mut SentenceAggregator {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for SentenceAggregator {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe {
            STATE = Some(SentenceAggregator {
                buffer: String::new(),
                sentence_count: 0,
            });
        }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

/// Helper: allocate a buffer, write sentence text, freeze, return as OutputElement.
fn emit_sentence(sentence: &str, seq: u64) -> Result<OutputElement, ProcessError> {
    let bytes = sentence.as_bytes();
    let buf = buffer_allocator::allocate(bytes.len() as u64)
        .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
    buf.append(bytes)
        .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
    buf.set_content_type("text/plain");
    let frozen = buf.freeze();
    Ok(OutputElement {
        meta: ElementMeta {
            sequence: seq,
            timestamp_ns: 0,
            content_type: "text/plain".to_string(),
        },
        payload: frozen,
    })
}

impl AggregatorGuest for SentenceAggregator {
    fn ingest(element: StreamElement) -> Result<Option<OutputElement>, ProcessError> {
        let s = state();
        let bytes = element.payload.read_all();
        let token = String::from_utf8(bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("{e}")))?;

        s.buffer.push_str(&token);

        // Detect sentence boundary.
        let trimmed = token.trim_end();
        if trimmed.ends_with('.') || trimmed.ends_with('!') || trimmed.ends_with('?') {
            let sentence = s.buffer.trim().to_string();
            s.buffer.clear();
            let seq = s.sentence_count;
            s.sentence_count += 1;
            return Ok(Some(emit_sentence(&sentence, seq)?));
        }

        Ok(None) // Absorb — sentence not yet complete.
    }

    fn flush() -> Result<Vec<OutputElement>, ProcessError> {
        let s = state();
        if s.buffer.trim().is_empty() {
            return Ok(vec![]);
        }
        // Emit any remaining partial sentence.
        let sentence = s.buffer.trim().to_string();
        s.buffer.clear();
        let seq = s.sentence_count;
        Ok(vec![emit_sentence(&sentence, seq)?])
    }
}

export!(SentenceAggregator);
}

components/output-sink/src/lib.rs

#![allow(unused)]
fn main() {
//! Output sink for the token-streaming pipeline.
//! Prints each assembled sentence.

wit_bindgen::generate!({
    world: "data-sink",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::sink::Guest as SinkGuest;
use torvyn::streaming::types::*;

struct OutputSink {
    received: u64,
}

static mut STATE: Option<OutputSink> = None;
fn state() -> &'static mut OutputSink {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for OutputSink {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { STATE = Some(OutputSink { received: 0 }); }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl SinkGuest for OutputSink {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let s = state();
        let bytes = element.payload.read_all();
        let text = String::from_utf8(bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("{e}")))?;
        println!("[output-sink] sentence {}: {}", s.received, text);
        s.received += 1;
        Ok(BackpressureSignal::Ready)
    }
    fn complete() -> Result<(), ProcessError> {
        println!("[output-sink] Stream complete.");
        Ok(())
    }
}

export!(OutputSink);
}

Pipeline Configuration

Torvyn.toml

[torvyn]
name = "token-streaming"
version = "0.1.0"
contract_version = "0.1.0"
description = "AI token streaming pipeline with content filtering"

[[component]]
name = "token-source"
path = "components/token-source"

[[component]]
name = "content-filter"
path = "components/content-filter"

[[component]]
name = "sentence-aggregator"
path = "components/sentence-aggregator"

[[component]]
name = "output-sink"
path = "components/output-sink"

[flow.main]
description = "Tokens → Filter → Aggregate → Output"

[flow.main.nodes.source]
component = "token-source"
interface = "torvyn:streaming/source"

[flow.main.nodes.filter]
component = "content-filter"
interface = "torvyn:filtering/filter"
config = "blocked_word"

[flow.main.nodes.aggregator]
component = "sentence-aggregator"
interface = "torvyn:aggregation/aggregator"

[flow.main.nodes.sink]
component = "output-sink"
interface = "torvyn:streaming/sink"

[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "filter", port = "input" }

[[flow.main.edges]]
from = { node = "filter", port = "output" }
to = { node = "aggregator", port = "input" }

[[flow.main.edges]]
from = { node = "aggregator", port = "output" }
to = { node = "sink", port = "input" }

Expected Output

$ torvyn run flow.main
[torvyn] Running flow 'main'

[output-sink] sentence 0: The quick brown fox jumped over the lazy dog.
[output-sink] sentence 1: The was filtered.
[output-sink] sentence 2: Torvyn handles streaming tokens.
[output-sink] Stream complete.

[torvyn] Flow 'main' completed.
[torvyn] 20 tokens produced | 1 filtered | 3 sentences emitted

Note that “blocked_word” was filtered out, so the second sentence reads “The was filtered.” instead of “The blocked_word was filtered.”

Performance Characteristics (Design Targets)

MetricTarget
Filter overhead per token< 10 us (no buffer allocation in filter)
Token-to-sentence latency< 200 us (accumulation + emit)
Memory per aggregatorProportional to longest sentence

Learn More

Event Enrichment

What It Demonstrates

A realistic event processing pipeline: structured log events are enriched with geo-IP data (simulated), classified by severity, and routed to an alerting sink for high-severity events. Demonstrates multi-stage enrichment, a common pattern in observability and security systems.

Pipeline Topology

event-source → geo-enricher → severity-classifier → alert-sink

Key Components

The geo-enricher processor reads an event’s IP address field, looks it up in a simulated geo-IP table (loaded at init()), and adds country and city fields to the JSON.

The severity-classifier processor examines the event’s level field and a set of keyword rules to assign a severity score. Events with severity above a threshold are annotated with "alert": true.

The alert-sink prints only events where "alert": true, ignoring routine events.

Source Component: event-source

components/event-source/src/lib.rs

#![allow(unused)]
fn main() {
//! Structured log event source.
//!
//! Produces a sequence of JSON log events with varying severity levels
//! and IP addresses for downstream enrichment and classification.

wit_bindgen::generate!({
    world: "data-source",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::source::Guest as SourceGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

const EVENTS: &[&str] = &[
    r#"{"ip":"192.0.2.10","level":"info","message":"user login successful"}"#,
    r#"{"ip":"198.51.100.42","level":"warn","message":"high memory usage detected"}"#,
    r#"{"ip":"203.0.113.5","level":"error","message":"disk full on /data"}"#,
    r#"{"ip":"192.0.2.10","level":"info","message":"config reload completed"}"#,
    r#"{"ip":"198.51.100.1","level":"critical","message":"auth service down"}"#,
];

struct EventSource {
    index: usize,
}

static mut STATE: Option<EventSource> = None;
fn state() -> &'static mut EventSource {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for EventSource {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { STATE = Some(EventSource { index: 0 }); }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl SourceGuest for EventSource {
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        let s = state();
        if s.index >= EVENTS.len() {
            return Ok(None);
        }
        let payload = EVENTS[s.index].as_bytes();
        let buf = buffer_allocator::allocate(payload.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(payload)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("application/json");
        let frozen = buf.freeze();
        s.index += 1;
        Ok(Some(OutputElement {
            meta: ElementMeta {
                sequence: (s.index - 1) as u64,
                timestamp_ns: 0,
                content_type: "application/json".to_string(),
            },
            payload: frozen,
        }))
    }
    fn notify_backpressure(_signal: BackpressureSignal) {}
}

export!(EventSource);
}

Processor Component: geo-enricher

components/geo-enricher/src/lib.rs

#![allow(unused)]
fn main() {
//! Geo-IP enrichment processor.
//!
//! Reads the "ip" field from each JSON event and adds "country" and "city"
//! fields based on a simulated lookup table. In a production deployment,
//! this would use a real geo-IP database loaded at init().
//!
//! Demonstrates: stateful processor with lookup table loaded during
//! lifecycle initialization.

wit_bindgen::generate!({
    world: "managed-transform",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::processor::Guest as ProcessorGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

struct GeoEnricher {
    /// Simulated geo-IP database: IP → (country, city).
    db: Vec<(&'static str, &'static str, &'static str)>,
}

static mut STATE: Option<GeoEnricher> = None;
fn state() -> &'static mut GeoEnricher {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for GeoEnricher {
    fn init(_config: String) -> Result<(), ProcessError> {
        // In production, load a real geo-IP database from config path.
        // This simulated table maps example IPs to locations.
        let db = vec![
            ("192.0.2.10",    "DE", "Berlin"),
            ("198.51.100.42", "JP", "Tokyo"),
            ("203.0.113.5",   "AU", "Sydney"),
            ("198.51.100.1",  "US", "Chicago"),
        ];
        unsafe { STATE = Some(GeoEnricher { db }); }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl ProcessorGuest for GeoEnricher {
    fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
        let s = state();
        let bytes = input.payload.read_all();
        let text = String::from_utf8(bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("{e}")))?;

        // Extract IP address using simple string search.
        // Production code should use a proper JSON parser.
        let ip = extract_json_string(&text, "ip").unwrap_or_default();

        // Look up geo data.
        let (country, city) = s.db.iter()
            .find(|(db_ip, _, _)| *db_ip == ip.as_str())
            .map(|(_, country, city)| (*country, *city))
            .unwrap_or(("unknown", "unknown"));

        // Append geo fields to the JSON.
        let enriched = text.trim_end_matches('}').to_string()
            + &format!(",\"country\":\"{}\",\"city\":\"{}\"}}", country, city);

        let out_bytes = enriched.as_bytes();
        let buf = buffer_allocator::allocate(out_bytes.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(out_bytes)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("application/json");
        let frozen = buf.freeze();

        Ok(ProcessResult::Emit(OutputElement {
            meta: ElementMeta {
                sequence: input.meta.sequence,
                timestamp_ns: 0,
                content_type: "application/json".to_string(),
            },
            payload: frozen,
        }))
    }
}

/// Simple JSON string field extractor (no serde dependency).
/// Finds `"key":"value"` and returns value.
fn extract_json_string(json: &str, key: &str) -> Option<String> {
    let pattern = format!("\"{}\":\"", key);
    let start = json.find(&pattern)? + pattern.len();
    let rest = &json[start..];
    let end = rest.find('"')?;
    Some(rest[..end].to_string())
}

export!(GeoEnricher);
}

Processor Component: severity-classifier

components/severity-classifier/src/lib.rs

#![allow(unused)]
fn main() {
//! Severity classifier processor.
//!
//! Examines each event's "level" field, assigns a numeric severity
//! score, and adds "severity" and "alert" fields. Events with
//! severity above the configured threshold are marked with
//! "alert": true.

wit_bindgen::generate!({
    world: "managed-transform",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::processor::Guest as ProcessorGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

struct SeverityClassifier {
    alert_threshold: u32,
}

static mut STATE: Option<SeverityClassifier> = None;
fn state() -> &'static mut SeverityClassifier {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for SeverityClassifier {
    fn init(config: String) -> Result<(), ProcessError> {
        // Parse threshold from JSON config: {"alert_threshold": 7}
        let threshold = config
            .trim()
            .strip_prefix("{\"alert_threshold\":")
            .and_then(|s| s.strip_suffix('}'))
            .and_then(|s| s.trim().parse::<u32>().ok())
            .unwrap_or(7);
        unsafe { STATE = Some(SeverityClassifier { alert_threshold: threshold }); }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl ProcessorGuest for SeverityClassifier {
    fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
        let s = state();
        let bytes = input.payload.read_all();
        let text = String::from_utf8(bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("{e}")))?;

        // Extract level field.
        let level = extract_json_string(&text, "level").unwrap_or_default();

        // Map level to numeric severity.
        let severity: u32 = match level.as_str() {
            "debug"    => 1,
            "info"     => 3,
            "warn"     => 5,
            "error"    => 9,
            "critical" => 10,
            _          => 0,
        };

        let alert = severity >= s.alert_threshold;

        // Append severity and alert fields.
        let classified = text.trim_end_matches('}').to_string()
            + &format!(",\"severity\":{},\"alert\":{}}}", severity, alert);

        let out_bytes = classified.as_bytes();
        let buf = buffer_allocator::allocate(out_bytes.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(out_bytes)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("application/json");
        let frozen = buf.freeze();

        Ok(ProcessResult::Emit(OutputElement {
            meta: ElementMeta {
                sequence: input.meta.sequence,
                timestamp_ns: 0,
                content_type: "application/json".to_string(),
            },
            payload: frozen,
        }))
    }
}

fn extract_json_string(json: &str, key: &str) -> Option<String> {
    let pattern = format!("\"{}\":\"", key);
    let start = json.find(&pattern)? + pattern.len();
    let rest = &json[start..];
    let end = rest.find('"')?;
    Some(rest[..end].to_string())
}

export!(SeverityClassifier);
}

Sink Component: alert-sink

components/alert-sink/src/lib.rs

#![allow(unused)]
fn main() {
//! Alert sink.
//!
//! Prints events where "alert":true. Suppresses routine events
//! and reports a count at the end.

wit_bindgen::generate!({
    world: "data-sink",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::sink::Guest as SinkGuest;
use torvyn::streaming::types::*;

struct AlertSink {
    alert_count: u64,
    suppressed_count: u64,
}

static mut STATE: Option<AlertSink> = None;
fn state() -> &'static mut AlertSink {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for AlertSink {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe {
            STATE = Some(AlertSink {
                alert_count: 0,
                suppressed_count: 0,
            });
        }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl SinkGuest for AlertSink {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let s = state();
        let bytes = element.payload.read_all();
        let text = String::from_utf8(bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("{e}")))?;

        // Check if this event is an alert.
        if text.contains("\"alert\":true") {
            println!(
                "[alert-sink] ALERT seq={}: {}",
                element.meta.sequence, text
            );
            s.alert_count += 1;
        } else {
            s.suppressed_count += 1;
        }

        Ok(BackpressureSignal::Ready)
    }

    fn complete() -> Result<(), ProcessError> {
        let s = state();
        println!(
            "[alert-sink] Non-alert events: {} (suppressed)",
            s.suppressed_count
        );
        println!("[alert-sink] Stream complete.");
        Ok(())
    }
}

export!(AlertSink);
}

Pipeline Configuration

Torvyn.toml

[torvyn]
name = "event-enrichment"
version = "0.1.0"
contract_version = "0.1.0"
description = "Multi-stage event enrichment pipeline"

[[component]]
name = "event-source"
path = "components/event-source"

[[component]]
name = "geo-enricher"
path = "components/geo-enricher"

[[component]]
name = "severity-classifier"
path = "components/severity-classifier"

[[component]]
name = "alert-sink"
path = "components/alert-sink"

[flow.main]
description = "Events → Geo-IP → Severity → Alert"

[flow.main.nodes.source]
component = "event-source"
interface = "torvyn:streaming/source"

[flow.main.nodes.geo]
component = "geo-enricher"
interface = "torvyn:streaming/processor"
config = '{"db": "simulated"}'

[flow.main.nodes.severity]
component = "severity-classifier"
interface = "torvyn:streaming/processor"
config = '{"alert_threshold": 7}'

[flow.main.nodes.sink]
component = "alert-sink"
interface = "torvyn:streaming/sink"

[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "geo", port = "input" }

[[flow.main.edges]]
from = { node = "geo", port = "output" }
to = { node = "severity", port = "input" }

[[flow.main.edges]]
from = { node = "severity", port = "output" }
to = { node = "sink", port = "input" }

Expected Output

$ torvyn run flow.main
[torvyn] Running flow 'main'

[alert-sink] ALERT seq=2: {"ip":"203.0.113.5","level":"error","message":"disk full on /data","country":"AU","city":"Sydney","severity":9,"alert":true}
[alert-sink] ALERT seq=4: {"ip":"198.51.100.1","level":"critical","message":"auth service down","country":"US","city":"Chicago","severity":10,"alert":true}
[alert-sink] Non-alert events: 3 (suppressed)
[alert-sink] Stream complete.

[torvyn] Flow 'main' completed. 5 events processed, 2 alerts raised.

Performance Characteristics (Design Targets)

MetricTarget
Enrichment latency per event< 100 us (simulated lookup)
Copies per event3 (source write, geo-enricher transform, classifier transform)
Memory for geo-IP tableProportional to table size (loaded at init)

Learn More

Fan-Out

What It Demonstrates

A pipeline with fan-out topology: a single source produces events, a router component inspects each event and directs it to one of two branches based on a routing criterion, and each branch has its own processor and sink.

Concepts Covered

  • The torvyn:filtering/router interface for multi-port dispatch
  • Named output ports in pipeline configuration
  • Fan-out topology (one-to-many routing)
  • Branch-specific processing

Router Component

components/event-router/src/lib.rs

#![allow(unused)]
fn main() {
//! Event router.
//!
//! Routes events to named output ports based on the "type" field.
//! Events with type "metric" go to port "metrics".
//! Events with type "log" go to port "logs".
//! Events with any other type are broadcast to both ports.
//!
//! The router interface returns a list of port name strings.
//! The runtime uses this list to forward the element's borrowed
//! buffer handle to each named downstream edge. No additional
//! buffer allocation occurs for fan-out — the same host buffer
//! is borrowed by each receiving component in sequence.

wit_bindgen::generate!({
    world: "content-router",
    path: "../../wit",
});

use exports::torvyn::filtering::router::Guest as RouterGuest;
use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use torvyn::streaming::types::*;

struct EventRouter;
static mut INITIALIZED: bool = false;

impl LifecycleGuest for EventRouter {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { INITIALIZED = true; }
        Ok(())
    }
    fn teardown() { unsafe { INITIALIZED = false; } }
}

impl RouterGuest for EventRouter {
    fn route(element: StreamElement) -> Result<Vec<String>, ProcessError> {
        let bytes = element.payload.read_all();
        let text = String::from_utf8_lossy(&bytes);

        if text.contains("\"type\":\"metric\"") {
            Ok(vec!["metrics".to_string()])
        } else if text.contains("\"type\":\"log\"") {
            Ok(vec!["logs".to_string()])
        } else {
            // Unknown type: broadcast to both branches.
            Ok(vec!["metrics".to_string(), "logs".to_string()])
        }
    }
}

export!(EventRouter);
}

Source Component: event-source

components/event-source/src/lib.rs

#![allow(unused)]
fn main() {
//! Event source for the fan-out example.
//! Produces a mix of metric and log events.

wit_bindgen::generate!({
    world: "data-source",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::source::Guest as SourceGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

const EVENTS: &[&str] = &[
    r#"{"type":"metric","name":"cpu_usage","value":72.5}"#,
    r#"{"type":"log","level":"info","message":"user login successful"}"#,
    r#"{"type":"metric","name":"mem_used","value":4096}"#,
    r#"{"type":"log","level":"warn","message":"disk usage above 80%"}"#,
];

struct EventSource { index: usize }
static mut STATE: Option<EventSource> = None;
fn state() -> &'static mut EventSource {
    unsafe { STATE.as_mut().expect("not initialized") }
}

impl LifecycleGuest for EventSource {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { STATE = Some(EventSource { index: 0 }); }
        Ok(())
    }
    fn teardown() { unsafe { STATE = None; } }
}

impl SourceGuest for EventSource {
    fn pull() -> Result<Option<OutputElement>, ProcessError> {
        let s = state();
        if s.index >= EVENTS.len() { return Ok(None); }
        let payload = EVENTS[s.index].as_bytes();
        let buf = buffer_allocator::allocate(payload.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(payload).map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("application/json");
        let frozen = buf.freeze();
        s.index += 1;
        Ok(Some(OutputElement {
            meta: ElementMeta {
                sequence: (s.index - 1) as u64,
                timestamp_ns: 0,
                content_type: "application/json".to_string(),
            },
            payload: frozen,
        }))
    }
    fn notify_backpressure(_signal: BackpressureSignal) {}
}

export!(EventSource);
}

Branch Processors

components/metric-processor/src/lib.rs

#![allow(unused)]
fn main() {
//! Metric processor — adds a "processed_by":"metrics-pipeline" tag.

wit_bindgen::generate!({
    world: "managed-transform",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::processor::Guest as ProcessorGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

struct MetricProcessor;
static mut INITIALIZED: bool = false;

impl LifecycleGuest for MetricProcessor {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { INITIALIZED = true; }
        Ok(())
    }
    fn teardown() { unsafe { INITIALIZED = false; } }
}

impl ProcessorGuest for MetricProcessor {
    fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
        let bytes = input.payload.read_all();
        let text = String::from_utf8(bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("{e}")))?;

        let tagged = text.trim_end_matches('}').to_string()
            + ",\"processed_by\":\"metrics-pipeline\"}";

        let out_bytes = tagged.as_bytes();
        let buf = buffer_allocator::allocate(out_bytes.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(out_bytes).map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("application/json");
        let frozen = buf.freeze();

        Ok(ProcessResult::Emit(OutputElement {
            meta: ElementMeta {
                sequence: input.meta.sequence,
                timestamp_ns: 0,
                content_type: "application/json".to_string(),
            },
            payload: frozen,
        }))
    }
}

export!(MetricProcessor);
}

components/log-processor/src/lib.rs

#![allow(unused)]
fn main() {
//! Log processor — adds a "processed_by":"log-pipeline" tag.

wit_bindgen::generate!({
    world: "managed-transform",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::processor::Guest as ProcessorGuest;
use torvyn::streaming::buffer_allocator;
use torvyn::streaming::types::*;

struct LogProcessor;
static mut INITIALIZED: bool = false;

impl LifecycleGuest for LogProcessor {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { INITIALIZED = true; }
        Ok(())
    }
    fn teardown() { unsafe { INITIALIZED = false; } }
}

impl ProcessorGuest for LogProcessor {
    fn process(input: StreamElement) -> Result<ProcessResult, ProcessError> {
        let bytes = input.payload.read_all();
        let text = String::from_utf8(bytes)
            .map_err(|e| ProcessError::InvalidInput(format!("{e}")))?;

        let tagged = text.trim_end_matches('}').to_string()
            + ",\"processed_by\":\"log-pipeline\"}";

        let out_bytes = tagged.as_bytes();
        let buf = buffer_allocator::allocate(out_bytes.len() as u64)
            .map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.append(out_bytes).map_err(|e| ProcessError::Internal(format!("{e:?}")))?;
        buf.set_content_type("application/json");
        let frozen = buf.freeze();

        Ok(ProcessResult::Emit(OutputElement {
            meta: ElementMeta {
                sequence: input.meta.sequence,
                timestamp_ns: 0,
                content_type: "application/json".to_string(),
            },
            payload: frozen,
        }))
    }
}

export!(LogProcessor);
}

Branch Sinks

components/metric-sink/src/lib.rs

#![allow(unused)]
fn main() {
//! Metric sink — prints received metric events.

wit_bindgen::generate!({
    world: "data-sink",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::sink::Guest as SinkGuest;
use torvyn::streaming::types::*;

struct MetricSink;
static mut INITIALIZED: bool = false;

impl LifecycleGuest for MetricSink {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { INITIALIZED = true; }
        Ok(())
    }
    fn teardown() { unsafe { INITIALIZED = false; } }
}

impl SinkGuest for MetricSink {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let bytes = element.payload.read_all();
        let text = String::from_utf8_lossy(&bytes);
        println!("[metric-sink] Received metric: {}", text);
        Ok(BackpressureSignal::Ready)
    }
    fn complete() -> Result<(), ProcessError> {
        println!("[metric-sink] Stream complete.");
        Ok(())
    }
}

export!(MetricSink);
}

components/log-sink/src/lib.rs

#![allow(unused)]
fn main() {
//! Log sink — prints received log events.

wit_bindgen::generate!({
    world: "data-sink",
    path: "../../wit/torvyn-streaming",
});

use exports::torvyn::streaming::lifecycle::Guest as LifecycleGuest;
use exports::torvyn::streaming::sink::Guest as SinkGuest;
use torvyn::streaming::types::*;

struct LogSink;
static mut INITIALIZED: bool = false;

impl LifecycleGuest for LogSink {
    fn init(_config: String) -> Result<(), ProcessError> {
        unsafe { INITIALIZED = true; }
        Ok(())
    }
    fn teardown() { unsafe { INITIALIZED = false; } }
}

impl SinkGuest for LogSink {
    fn push(element: StreamElement) -> Result<BackpressureSignal, ProcessError> {
        let bytes = element.payload.read_all();
        let text = String::from_utf8_lossy(&bytes);
        println!("[log-sink] Received log: {}", text);
        Ok(BackpressureSignal::Ready)
    }
    fn complete() -> Result<(), ProcessError> {
        println!("[log-sink] Stream complete.");
        Ok(())
    }
}

export!(LogSink);
}

Pipeline Configuration

Torvyn.toml

[torvyn]
name = "fan-out"
version = "0.1.0"
contract_version = "0.1.0"
description = "Fan-out pipeline with router"

[[component]]
name = "event-source"
path = "components/event-source"

[[component]]
name = "event-router"
path = "components/event-router"

[[component]]
name = "metric-processor"
path = "components/metric-processor"

[[component]]
name = "log-processor"
path = "components/log-processor"

[[component]]
name = "metric-sink"
path = "components/metric-sink"

[[component]]
name = "log-sink"
path = "components/log-sink"

[flow.main]
description = "Source → Router → (Metrics branch, Logs branch)"

[flow.main.nodes.source]
component = "event-source"
interface = "torvyn:streaming/source"

[flow.main.nodes.router]
component = "event-router"
interface = "torvyn:filtering/router"

[flow.main.nodes.metric-proc]
component = "metric-processor"
interface = "torvyn:streaming/processor"

[flow.main.nodes.log-proc]
component = "log-processor"
interface = "torvyn:streaming/processor"

[flow.main.nodes.metric-sink]
component = "metric-sink"
interface = "torvyn:streaming/sink"

[flow.main.nodes.log-sink]
component = "log-sink"
interface = "torvyn:streaming/sink"

# Source feeds the router.
[[flow.main.edges]]
from = { node = "source", port = "output" }
to = { node = "router", port = "input" }

# Router "metrics" port feeds the metrics branch.
[[flow.main.edges]]
from = { node = "router", port = "metrics" }
to = { node = "metric-proc", port = "input" }

# Router "logs" port feeds the logs branch.
[[flow.main.edges]]
from = { node = "router", port = "logs" }
to = { node = "log-proc", port = "input" }

# Each branch flows to its sink.
[[flow.main.edges]]
from = { node = "metric-proc", port = "output" }
to = { node = "metric-sink", port = "input" }

[[flow.main.edges]]
from = { node = "log-proc", port = "output" }
to = { node = "log-sink", port = "input" }

Expected Output

$ torvyn run flow.main
[torvyn] Running flow 'main'

[metric-sink] Received metric: {"type":"metric","name":"cpu_usage","value":72.5}
[log-sink] Received log: {"type":"log","level":"info","message":"user login successful"}
[metric-sink] Received metric: {"type":"metric","name":"mem_used","value":4096}
[log-sink] Received log: {"type":"log","level":"warn","message":"disk usage above 80%"}

[torvyn] Flow 'main' completed. 4 events routed: 2 to metrics, 2 to logs.

Performance Characteristics (Design Targets)

MetricTarget
Router overhead per element< 15 us (read + string match)
Fan-out to N portsNo additional buffer allocation (borrow forwarding)
Independent backpressureEach branch has its own queue and backpressure

Learn More

Benchmark Comparison

What It Demonstrates

A controlled performance comparison of the same logical pipeline implemented three ways: in Torvyn (Wasm component boundary), over gRPC localhost, and over Unix domain sockets. Includes a benchmark harness that measures latency percentiles, throughput, memory usage, and copy counts.

Concepts Covered

  • Torvyn’s performance relative to conventional IPC mechanisms
  • Benchmark methodology and reproducibility
  • Using torvyn bench for profiling
  • Statistical rigor (percentiles, not averages)

Methodology

All three implementations perform the same work: a source produces 100,000 64-byte messages, a processor appends an 8-byte timestamp, and a sink consumes the result. The benchmark measures end-to-end latency (time from source.pull() to sink.push() completion) and throughput (elements per second).

The comparison is designed to be fair. The gRPC and Unix socket implementations serialize the same payload format and perform the same transformation logic. The Torvyn implementation uses the standard WIT interfaces.

File Listing

examples/benchmark-comparison/
├── Torvyn.toml
├── Makefile
├── README.md
├── bench.sh                     # Runs all three and produces report
├── torvyn/                      # Torvyn implementation
│   └── ... (standard Torvyn components)
├── grpc-baseline/               # gRPC localhost implementation
│   ├── Cargo.toml
│   ├── proto/pipeline.proto
│   └── src/main.rs
├── unix-socket-baseline/        # Unix domain socket implementation
│   ├── Cargo.toml
│   └── src/main.rs
└── results/
    └── .gitkeep

Benchmark Script

bench.sh

#!/usr/bin/env bash
set -euo pipefail

ITERATIONS=${1:-100000}
PAYLOAD_SIZE=${2:-64}
RESULTS_DIR="results"

mkdir -p "$RESULTS_DIR"

echo "=== Torvyn Benchmark ==="
echo "Iterations: $ITERATIONS | Payload: ${PAYLOAD_SIZE}B"
echo ""

# 1. Torvyn pipeline benchmark
echo "--- Torvyn (Wasm component boundary) ---"
cd torvyn
torvyn bench flow.main \
    --iterations "$ITERATIONS" \
    --payload-size "$PAYLOAD_SIZE" \
    --warmup 1000 \
    --output json > "../$RESULTS_DIR/torvyn.json"
cd ..

# 2. gRPC localhost benchmark
echo "--- gRPC localhost ---"
cd grpc-baseline
cargo run --release -- \
    --iterations "$ITERATIONS" \
    --payload-size "$PAYLOAD_SIZE" \
    --warmup 1000 \
    --output json > "../$RESULTS_DIR/grpc.json"
cd ..

# 3. Unix domain socket benchmark
echo "--- Unix domain socket ---"
cd unix-socket-baseline
cargo run --release -- \
    --iterations "$ITERATIONS" \
    --payload-size "$PAYLOAD_SIZE" \
    --warmup 1000 \
    --output json > "../$RESULTS_DIR/unix-socket.json"
cd ..

# Generate comparison report
echo ""
echo "=== Comparison Report ==="
echo ""
printf "%-25s %12s %12s %12s %12s\n" "Method" "p50 (us)" "p99 (us)" "Throughput" "Copies"
printf "%-25s %12s %12s %12s %12s\n" "-------" "--------" "--------" "----------" "------"

for method in torvyn grpc unix-socket; do
    file="$RESULTS_DIR/${method}.json"
    if [ -f "$file" ]; then
        # Parse JSON results (requires jq)
        p50=$(jq -r '.latency_p50_us' "$file")
        p99=$(jq -r '.latency_p99_us' "$file")
        throughput=$(jq -r '.throughput_eps' "$file")
        copies=$(jq -r '.total_copies // "N/A"' "$file")
        printf "%-25s %12s %12s %12s %12s\n" "$method" "$p50" "$p99" "$throughput" "$copies"
    fi
done

echo ""
echo "Full results in $RESULTS_DIR/"
echo "Hardware: $(uname -m) | OS: $(uname -s) | Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"

Expected Results (Design Targets)

These are design targets based on the architecture’s performance goals. Actual measurements will vary by hardware.

Methodp50 Latencyp99 LatencyThroughputCopies/Element
Torvyn (Wasm boundary)~5-15 us~25-50 us~200K-500K elem/s2 (measured)
gRPC localhost~50-200 us~500-2000 us~20K-100K elem/s4+ (serialization)
Unix domain socket~10-30 us~50-150 us~100K-300K elem/s3 (socket + serialization)

Torvyn’s design target for per-element host overhead is < 5 us (MAX_HOT_PATH_NS). The additional latency comes from Wasm boundary crossing and buffer operations. The key advantage over gRPC is the elimination of network stack overhead, protobuf serialization, and HTTP/2 framing for same-node communication.

Commentary

This benchmark is not intended to prove that Torvyn is universally faster than gRPC. gRPC is designed for distributed communication; comparing it to a same-node runtime on localhost is inherently unfair to gRPC. The purpose is to quantify the overhead difference for the specific use case Torvyn targets: same-node, low-latency component composition.

The benchmark methodology follows the vision document’s requirement for “rigorous methodology and reproducible benchmarks” (Section 14.3). All results include hardware specifications, OS version, iteration counts, warmup periods, and percentile distributions — not just averages.

Learn More

AI Inference Pipelines

The Problem

AI-native applications are no longer simple request-response systems. A modern inference pipeline may chain together token stream ingestion, embedding generation, retrieval-augmented generation (RAG) lookups, model scoring, policy filtering, content guard evaluation, caching, and downstream delivery — all on the same node, all with latency requirements measured in milliseconds.

Teams building these pipelines face a difficult choice. Running all stages in a single process provides low latency but sacrifices isolation: a bug in a policy filter can corrupt model output, and updating one stage risks destabilizing the entire system. Splitting stages into microservices provides isolation but introduces serialization overhead, network latency, and operational complexity that real-time workloads cannot absorb.

The result is that most teams build ad hoc in-process glue code: custom thread pools, manual buffer management, bespoke back-pressure logic, and fragile error handling that is different for every pipeline.

How Torvyn Solves It

Torvyn provides a structured alternative. Each inference stage — tokenizer, embedding encoder, retrieval stage, model adapter, policy filter, content guard, cache layer, delivery endpoint — is implemented as an isolated WebAssembly component with a typed contract.

Isolation without serialization overhead. Components run in sandboxed Wasm environments with their own linear memory. Data transfers between stages use host-managed buffers with explicit ownership. Where a stage only inspects metadata (routing, content-type checks, trace context), the payload buffer passes through without a copy.

Built-in back-pressure. When a model scoring stage is slower than the tokenizer upstream, Torvyn’s reactor automatically propagates demand signals upstream, pausing producers until the bottleneck clears. Queue depths are bounded and configurable. No stage can overwhelm another.

End-to-end tracing. Every element carries a trace context through the entire pipeline. Torvyn’s observability layer records per-stage latency, queue wait time, back-pressure events, copy counts, and resource ownership transfers — providing the visibility that production inference systems require.

Modular updates. Updating a policy filter or swapping a model adapter means recompiling a single Wasm component. Contracts guarantee compatibility. torvyn link validates that the updated component still satisfies the pipeline’s interface requirements before it reaches production.

Example Pipeline

Token Source → Embedding Encoder → RAG Retrieval → Model Scorer → Policy Filter → Content Guard → Response Sink

Each arrow is a contract-defined, back-pressure-aware stream with bounded queuing and host-managed resource transfer. The entire pipeline runs on a single node with single-digit microsecond reactor overhead per stream element.

Performance Characteristics

Torvyn targets < 5us of reactor overhead per stream element and < 10us wakeup latency from data availability to consumer invocation. For inference pipelines where end-to-end latency budgets are measured in tens of milliseconds, Torvyn’s scheduling overhead is a negligible fraction of the total.

Copy behavior is measurable: torvyn bench reports exactly how many bytes cross each component boundary and why, enabling data-informed optimization of the pipeline topology.

Get Started

Edge Stream Processing

The Problem

Edge computing pushes processing closer to data sources — IoT gateways, retail locations, factory floors, mobile network access points, connected vehicles. These environments impose constraints that traditional cloud architectures do not face: limited memory, limited CPU, unreliable connectivity, strict latency requirements, and the need to run diverse processing stages on constrained hardware.

Teams building edge stream processors often assemble custom stacks from message brokers, lightweight container runtimes, and hand-written pipeline code. The result is operationally fragile, hard to update remotely, and difficult to observe. Each edge deployment becomes a unique snowflake.

How Torvyn Solves It

Torvyn is designed for exactly these constraints.

Small footprint. Torvyn components are compiled WebAssembly modules. The host runtime is a single Rust binary. There is no JVM, no container orchestrator, and no heavyweight middleware layer required. Memory overhead per flow is targeted at < 4KB of reactor-specific state, plus configurable stream queue buffers.

Portability. The same component binary runs identically on cloud servers, edge gateways, developer laptops, and CI environments. Components are packaged as OCI-compatible artifacts that deploy using standard container tooling. No platform-specific recompilation is needed.

Deterministic resource behavior. Stream queues are bounded. Back-pressure is enforced. Memory budgets are configurable per component. The host runtime tracks every buffer allocation and reclamation. On resource-constrained edge hardware, predictable memory behavior is essential — Torvyn guarantees bounded memory usage for every flow.

Offline-capable. Torvyn pipelines process data locally. They do not require constant connectivity to a cloud control plane. Pipeline definitions, component artifacts, and configuration travel with the deployment. Remote updates can be applied when connectivity is available.

Observable from anywhere. Torvyn emits OpenTelemetry-compatible traces and metrics. When the edge device has connectivity, diagnostics flow to your central monitoring system. When it does not, the runtime continues processing and buffers diagnostic data for later export.

Example Pipeline

Sensor Source → Anomaly Detector → Data Enricher → Local Aggregator → Uplink Sink

This pipeline runs on an edge gateway with 512MB of RAM. Each component is a sandboxed Wasm module with a memory budget. The anomaly detector processes sensor readings in real time; the aggregator reduces data volume before uplinking to the cloud.

Performance Characteristics

Torvyn’s reactor supports 1,000+ concurrent flows. On edge hardware, the typical deployment runs tens of flows with strict latency requirements. The reactor’s cooperative scheduling with configurable yield frequency ensures that high-priority flows receive CPU time proportional to their priority level: Critical flows process 4x as many elements per yield cycle as Normal flows.

Get Started

Event-Driven Architectures

The Problem

Event-driven systems are everywhere — order processing, fraud detection, notification delivery, audit logging, inventory synchronization, real-time analytics. These systems typically chain together multiple processing stages: validation, enrichment, routing, transformation, aggregation, and delivery.

The standard approach is to connect these stages through message brokers (Kafka, RabbitMQ, NATS) or service meshes, with each stage running as an independent microservice. This provides isolation and deployment flexibility, but for many event-driven workloads, the full cost of inter-service communication is unnecessary. When all stages run on the same node, teams still pay for serialization, network stack traversal, retry logic, and schema drift management — even though the data never leaves the machine.

For high-frequency event processing (thousands to hundreds of thousands of events per second), this overhead becomes a significant fraction of the total processing budget.

How Torvyn Solves It

Torvyn replaces heavyweight inter-service boundaries with lightweight, contract-defined component boundaries. Each event processing stage is a Wasm component with a typed interface. Stages communicate through host-managed streams with bounded queues and built-in back-pressure.

Typed contracts eliminate schema drift. WIT contracts define the exact types, fields, and ownership semantics of every event crossing a component boundary. The torvyn link command validates compatibility between stages before deployment. Interface changes that break downstream consumers are caught at link time, not in production.

Back-pressure prevents cascade failures. When a downstream stage slows down (a delivery endpoint is temporarily unavailable, an enrichment service is under load), back-pressure propagates upstream through the demand model. Queues are bounded. Configurable overflow policies (block, drop-oldest, drop-newest, error, rate-limit) let you define the right behavior for each stage.

Routing and filtering are first-class. Torvyn’s contract packages include dedicated filter and router interfaces. Filters make accept/reject decisions without allocating new buffers — they inspect metadata and return a boolean. Routers direct events to named output ports for fan-out topologies. Both are observable and type-safe.

Aggregation with explicit flush semantics. The aggregator interface provides ingest and flush functions, supporting windowed aggregation, accumulation, and stateful event processing with well-defined completion semantics.

Example Pipeline

Event Source → Validator → Enricher → Router → [Branch A: Aggregator → Analytics Sink]
                                              → [Branch B: Transformer → Delivery Sink]

The router fans events to multiple downstream branches based on event type. Each branch applies its own processing logic. All branches share the same back-pressure model and observability framework.

Performance Characteristics

For event-driven pipelines processing high-frequency event streams, Torvyn’s per-element reactor overhead (target < 5us) and bounded queue depths ensure predictable performance. Back-pressure response time — from queue-full to producer suspension — is measured and reported. Copy accounting shows exactly where serialization overhead occurs, enabling targeted optimization.

Get Started

Secure Plugin Systems

The Problem

Many platforms need extensibility. Users want to add custom processing logic, integrations, and business rules without modifying the core platform. The traditional approach is to offer a plugin API: load user code as a shared library, a scripted extension, or an embedded interpreter.

Each approach carries risks. Shared libraries (C/C++ plugins, Rust dylibs) execute in the same address space as the host — a bug in a plugin can corrupt host memory, and a malicious plugin can access any host resource. Scripted extensions (Lua, JavaScript) provide better isolation but often with limited performance and limited type safety. Docker-based plugin execution provides strong isolation but introduces container orchestration overhead, cold-start latency, and complex lifecycle management.

The fundamental tension is between performance (in-process speed), safety (isolation from the host), and governance (knowing exactly what a plugin can do).

How Torvyn Solves It

Torvyn resolves this tension through the WebAssembly Component Model’s sandboxing combined with an explicit capability system.

In-process speed, out-of-process safety. Plugin components are compiled to WebAssembly and execute within the Torvyn host process, avoiding the overhead of inter-process communication or container startup. But each component runs in its own linear memory space, enforced by the Wasm sandbox. A plugin cannot read host memory, another plugin’s memory, or any resource it has not been explicitly granted access to.

Deny-all default. Every capability — filesystem access, network access, system clocks, random number generation — is denied unless explicitly granted in the pipeline configuration. The component manifest declares what the plugin needs. The operator decides what to grant. The host enforces the grant at runtime.

Auditable permissions. Operators can inspect the capability manifest of every deployed component. torvyn link validates that capability grants satisfy each component’s requirements. Audit events record every capability exercise at runtime. Security teams can review exactly what each plugin is authorized to do and verify that it has not exceeded its grants.

Safe multi-tenant execution. Multiple plugins from different tenants can run in the same Torvyn host process with resource isolation. Memory budgets, CPU fuel limits, and queue capacity constraints prevent any single plugin from monopolizing shared resources.

Contract-governed interfaces. Plugins interact with the host platform through typed WIT contracts. The interface a plugin can use is defined and versioned. Breaking changes to the plugin API are caught at link time, not at runtime. Plugin authors get stable, documented, type-safe interfaces.

Example Architecture

Platform Event Bus → [Plugin A: Custom Validator] → [Plugin B: Enrichment from External API] → Platform Core

Plugin A and Plugin B are third-party Wasm components. Each runs in its own sandbox with its own capability grants. Plugin A is granted no capabilities (it operates only on the event data it receives). Plugin B is granted network egress to a specific API endpoint. Neither plugin can access the host filesystem, the platform’s internal state, or each other’s data.

Performance Characteristics

Torvyn’s component-to-component boundary overhead adds single-digit microsecond latency per invocation, as measured by the reactor’s per-element overhead target (< 5us). For plugin systems where plugins perform meaningful computation (parsing, validation, transformation, API calls), the boundary overhead is negligible compared to the plugin’s own execution time.

The security enforcement (capability checking, resource budget tracking, audit event emission) is designed to operate within the reactor’s per-element overhead budget. Security is not an opt-in feature with a performance cost — it is always active.

Get Started

Torvyn vs. gRPC for Local Pipelines

An honest comparison of two different approaches to composing processing stages on the same machine.


What gRPC Provides

gRPC is a high-performance RPC framework with strong typing (via Protocol Buffers), bidirectional streaming, broad language support, and a large ecosystem. It is the standard for service-to-service communication in many organizations.

Where Torvyn Is a Better Fit

Same-node composition without network overhead. gRPC is fundamentally a network protocol. Even when two services run on the same machine, gRPC communication traverses the network stack: Protocol Buffers serialization, HTTP/2 framing, TCP socket I/O (or Unix domain sockets, which are better but still involve kernel context switches). Torvyn components communicate through host-managed memory transfers without leaving the process. For high-frequency local pipelines, this eliminates a significant latency and CPU cost at each boundary.

Ownership-aware resource transfer. gRPC transfers data by serializing it into a wire format. Every boundary involves a full serialization/deserialization cycle. Torvyn tracks buffer ownership at the host level and only copies payload data when a component actually needs to read or transform it. Routing and metadata-only stages skip payload copies entirely.

Built-in reactive back-pressure. gRPC’s flow control operates at the HTTP/2 stream level and is designed for network conditions. Torvyn’s back-pressure operates at the stream element level with demand-driven scheduling, configurable overflow policies, and observable watermarks. It is designed specifically for fine-grained streaming pipeline stages.

Contract validation before runtime. Both gRPC and Torvyn use typed contracts (Protocol Buffers and WIT, respectively). Torvyn adds composition-level validation through torvyn link, which checks interface compatibility across an entire pipeline graph — including version compatibility, capability satisfaction, and topology correctness — before any code runs.

Where gRPC Is a Better Fit

Cross-network communication. gRPC is designed for network-first communication. If your pipeline stages run on different machines, Torvyn is not a replacement for gRPC. Torvyn’s current focus is same-node and edge-local composition.

Ecosystem maturity and tooling. gRPC has years of production use, extensive documentation, wide language support, and deep integration with service mesh infrastructure (Envoy, Istio). Torvyn is a new project in active development.

Request-response patterns. gRPC excels at request-response communication with well-defined service endpoints. If your architecture is not a streaming pipeline — if it is a set of services handling discrete requests — gRPC’s model is more natural.

Organizational scaling. gRPC’s service-per-team deployment model is well-suited for large organizations with independent team ownership. Torvyn’s same-node composition model works best when pipeline stages are co-deployed and co-versioned.

When to Use Which

Use Torvyn when your processing stages run on the same node, latency budgets are tight (single-digit milliseconds or less for the full pipeline), you need fine-grained back-pressure with observable queue behavior, and you value static contract validation across the full pipeline graph.

Use gRPC when your services run on different machines, you need mature ecosystem integration, you are building request-response APIs rather than streaming pipelines, or your organization requires independently deployable services with separate team ownership.

Use both when your architecture has a high-frequency local processing pipeline (Torvyn) that communicates with external services via network APIs (gRPC). The two approaches are complementary.

Torvyn vs. Traditional Microservices

An honest comparison for teams evaluating whether to decompose a local pipeline into microservices or compose it as Torvyn components.


What Microservices Provide

The microservice architecture decomposes a system into independently deployable services, each with its own process, data store, and team ownership. It enables organizational scaling, independent release cycles, technology diversity, and fault isolation through process boundaries.

Where Torvyn Is a Better Fit

Lower boundary overhead for co-located stages. Microservice boundaries impose serialization, network transport (even if local), load balancing, retries, and observability stitching at every boundary. For pipeline stages that run on the same node and process high-frequency streams, this overhead is a significant fraction of the total processing budget. Torvyn replaces these heavyweight boundaries with contract-defined component boundaries that transfer data through host-managed memory.

Deterministic resource behavior. Microservice pipelines are subject to network variability, container scheduling latency, and cross-service retry storms. Torvyn pipelines run within a single process with bounded queues, explicit back-pressure, and configurable memory budgets. Resource behavior is deterministic and observable.

Typed composition validation. Microservice interfaces are typically defined by API specifications (OpenAPI, gRPC proto files), but there is no standard mechanism to validate compatibility across an entire service graph before deployment. Torvyn’s torvyn link validates the full pipeline graph statically.

Lighter operational footprint. A Torvyn pipeline runs as a single process. There is no need for container orchestration, service discovery, load balancing, or circuit breaking between pipeline stages. For workloads that do not require independent deployment of individual stages, this reduces operational complexity.

Where Microservices Are a Better Fit

Independent deployment and scaling. Microservices allow each service to be deployed, scaled, and updated independently. Torvyn components within a pipeline are co-deployed and updated together. If different stages have different scaling requirements or different release cadences, microservices provide more flexibility.

Organizational boundaries. Microservices align well with team-per-service ownership. Torvyn pipelines work best when the team that owns the pipeline owns all its component stages.

Failure isolation through process boundaries. A crash in one microservice does not bring down another. In Torvyn, a Wasm trap in one component is isolated from other components within the same flow, but all flows share the same host process. Torvyn’s sandboxing provides strong memory isolation, but the failure domain is the host process, not an independent OS process.

Distributed workloads. Microservices run anywhere on the network. Torvyn’s current scope is same-node and edge-local composition. If your pipeline stages need to run on different machines, microservices are the correct architecture.

When to Use Which

Use Torvyn when your pipeline stages run on the same machine, your latency budget does not accommodate inter-process or inter-service overhead, your stages share a deployment lifecycle, and you value static contract validation and fine-grained resource observability.

Use microservices when your services need independent deployment, independent scaling, or independent team ownership, or when the services run on different machines.

Combine them by building a high-performance local pipeline in Torvyn that exposes a single service interface to the rest of your microservice architecture. The pipeline handles the internal, latency-sensitive composition; the microservice handles the external, network-facing interface.

Torvyn vs. Container-Based Composition

An honest comparison for teams evaluating containers versus WebAssembly components for fine-grained pipeline composition.


What Containers Provide

Containers (Docker, OCI) are the standard unit of deployment in modern infrastructure. They provide strong process-level isolation, a portable packaging format, mature orchestration tooling (Kubernetes), broad ecosystem support, and well-understood operational practices.

Where Torvyn Is a Better Fit

Startup time. Container startup involves image pulling, filesystem layering, namespace setup, and process creation. Cold starts are measured in hundreds of milliseconds to seconds. Wasm component instantiation is measured in microseconds to low milliseconds. For pipelines that create and tear down processing stages frequently, or for edge deployments that need rapid recovery, Torvyn’s startup time is orders of magnitude faster.

Memory footprint. Each container brings a filesystem layer, a process, and runtime dependencies. A minimal container is megabytes; a typical one is tens to hundreds of megabytes. A Torvyn Wasm component is kilobytes to low megabytes. On resource-constrained edge hardware, or when running hundreds of fine-grained stages, the memory savings are significant.

Inter-stage communication. Containers communicate via IPC or network (even on the same host). This requires serialization, kernel context switches, and potentially TCP overhead. Torvyn components communicate through host-managed memory transfers within a single process. For high-frequency streaming (thousands to hundreds of thousands of elements per second), the per-element overhead difference is substantial.

Granularity. Containers are designed for service-level granularity: each container runs an application or service. Torvyn components are designed for stage-level granularity: each component implements a single processing step. A pipeline that would require 15 containers requires 15 Torvyn components, but with drastically lower overhead per stage.

Where Containers Are a Better Fit

Ecosystem maturity. Container tooling — building, testing, deploying, monitoring — is mature, well-documented, and widely understood. Torvyn is a new project with a growing ecosystem.

Arbitrary runtimes. Containers can run any Linux-compatible binary: Node.js services, Python applications, legacy C++ code, databases. Torvyn components must compile to WebAssembly. The Wasm ecosystem is growing rapidly, but not all languages and libraries are Wasm-ready today.

Orchestration and scaling. Kubernetes, ECS, and other orchestrators provide declarative scaling, health checking, rolling updates, and service discovery for containerized workloads. Torvyn does not include orchestration — it is a runtime, not a platform. For workloads that need horizontal scaling across machines, containers with orchestration are the right choice.

Network services. Containers hosting long-running network services (web servers, databases, API gateways) benefit from process isolation, independent scaling, and network-level routing. Torvyn is designed for stream processing stages, not for hosting network services.

When to Use Which

Use Torvyn when you need fine-grained, high-frequency pipeline composition on a single node or edge device, where container startup time and per-stage overhead are prohibitive.

Use containers when you need to deploy arbitrary applications, require orchestrated scaling across machines, or need to run software that does not target WebAssembly.

Use both by packaging a Torvyn pipeline as a single container for deployment. The container provides the familiar operational model; Torvyn provides efficient internal composition.

Torvyn vs. Actor Frameworks

An honest comparison for teams evaluating actor-based concurrency versus Torvyn’s reactive streaming model.


What Actor Frameworks Provide

Actor frameworks (Akka, Erlang/OTP, Microsoft Orleans, Ractor in Rust) structure concurrent systems as isolated actors that communicate through asynchronous message passing. Each actor encapsulates state and processes messages sequentially. Actor systems provide location transparency, fault tolerance through supervision hierarchies, and a natural model for stateful, event-driven workloads.

Where Torvyn Is a Better Fit

Stream-oriented scheduling. Actor systems schedule at the message level — each message to an actor triggers a processing cycle. Torvyn schedules at the stream level — the reactor understands the full pipeline graph, propagates demand from consumer to producer, and applies back-pressure across the entire flow. For streaming pipelines where the relationship between stages is a directed graph, Torvyn’s flow-aware scheduling provides better queue management and more predictable latency.

Typed contracts for composition. Actor message types are typically language-level types, not cross-component contracts. Torvyn’s WIT contracts are language-neutral, version-controlled, and machine-checkable. torvyn link validates that an entire pipeline graph is type-compatible before runtime. Actors typically discover type mismatches through runtime errors.

Observable back-pressure. Most actor systems handle overload through mailbox overflow policies (drop, backoff, fail) but do not provide structured demand propagation or observable watermark-based flow control. Torvyn’s credit-based demand model with hysteresis watermarks provides predictable, measurable back-pressure with clear observability events.

Explicit ownership semantics. In actor systems, message passing typically involves serialization (for remote actors) or reference passing (for local actors). Ownership of the message data is implicit. Torvyn’s resource manager makes ownership explicit: every buffer has exactly one owner, transfers are tracked, and copy behavior is measurable.

Sandboxed polyglot execution. Actors in most frameworks must be written in the framework’s language (Scala/Java for Akka, Erlang for OTP, Rust for Ractor). Torvyn components can be written in any language that targets the WebAssembly Component Model, with each component running in a sandboxed environment.

Where Actor Frameworks Are a Better Fit

Stateful, entity-centric workloads. Actor systems excel at modeling stateful entities (user sessions, device twins, game entities) where each entity has its own isolated state and handles messages independently. Torvyn’s streaming model is optimized for stateless or lightly-stateful transform pipelines, not for entity-per-actor patterns.

Supervision and fault tolerance. OTP and Akka provide sophisticated supervision hierarchies that automatically restart failed actors, isolate failure domains, and implement escalation policies. Torvyn provides flow-level failure isolation and tiered cancellation, but does not include a supervision tree model.

Location transparency and distribution. Actor frameworks can distribute actors across machines transparently. Torvyn’s current scope is same-node composition.

Ecosystem and community. Akka and Erlang/OTP have decades of production use and large communities. Torvyn is a new project.

When to Use Which

Use Torvyn when your workload is a streaming pipeline with defined topology, where flow-aware scheduling and observable back-pressure are more important than per-entity isolation and supervision.

Use an actor framework when your workload consists of many independent stateful entities, you need supervision hierarchies for fault recovery, or you need to distribute processing across machines transparently.

Consider both for architectures where stateful entities (modeled as actors) produce event streams that feed into processing pipelines (modeled in Torvyn).

Development Setup

This guide takes you from a clean machine to a fully working Torvyn development environment. Every command is copy-paste ready. If you hit a problem not covered here, check the Troubleshooting section at the bottom, or run torvyn doctor once the CLI is built.

Prerequisites

You need four things installed before you start.

1. Rust Toolchain

Torvyn requires Rust 1.78 or later and targets the wasm32-wasip2 compilation target.

# Install rustup if you do not have it
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Ensure you are on at least Rust 1.78
rustup update stable
rustc --version  # Should print 1.78.0 or later

# Add the WebAssembly compilation target
rustup target add wasm32-wasip2

2. wasm-tools

The wasm-tools binary is used for component model operations (composing, inspecting, and validating Wasm components).

cargo install wasm-tools
wasm-tools --version

3. Wasmtime CLI

Wasmtime is the reference WebAssembly runtime that Torvyn builds on. You need the CLI for running and testing components outside of the Torvyn host.

curl https://wasmtime.dev/install.sh -sSf | bash
wasmtime --version  # Should print 25.0.0 or later

If your platform does not support the install script, see wasmtime.dev for alternative installation methods.

cargo-component simplifies building WebAssembly components from Rust. It is the recommended build tool for Torvyn guest components, though it is not required for building the host runtime itself.

cargo install cargo-component
cargo component --version

Fallback: If cargo-component is unavailable or unstable on your system, you can compile guest components manually with cargo build --target wasm32-wasip2 followed by wasm-tools component new. The CLI supports both paths.

Clone and Build

# Clone the repository
git clone https://github.com/torvyn/torvyn.git
cd torvyn

# Build the entire workspace in debug mode
cargo build --workspace

# Build in release mode (slower to compile, faster to run)
cargo build --workspace --release

A successful build produces two binaries of interest: the torvyn CLI (target/debug/torvyn or target/release/torvyn) and the torvyn-host runtime binary.

If the build fails, run torvyn doctor (once built) or check Troubleshooting.

Run the Test Suite

# Run all unit and integration tests across the workspace
cargo test --workspace

# Run tests for a specific crate
cargo test -p torvyn-types
cargo test -p torvyn-reactor

# Run a specific test by name
cargo test -p torvyn-types test_flow_state_full_happy_path

# Run tests with output visible (useful for debugging)
cargo test -p torvyn-types -- --nocapture

IDE Setup

rust-analyzer

Torvyn is a standard Cargo workspace. Any editor with rust-analyzer support works. No special configuration is required beyond pointing rust-analyzer at the workspace root.

If you use VS Code, add the following to .vscode/settings.json in the repository root:

{
  "rust-analyzer.cargo.features": "all",
  "rust-analyzer.check.command": "clippy",
  "rust-analyzer.check.extraArgs": ["--workspace", "--", "-D", "warnings"],
  "rust-analyzer.inlayHints.parameterHints.enable": true,
  "rust-analyzer.inlayHints.typeHints.enable": true,
  "rust-analyzer.lens.run.enable": true,
  "rust-analyzer.lens.debug.enable": true,
  "[rust]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "rust-lang.rust-analyzer"
  }
}

These are not required, but they improve the development experience:

  • rust-analyzer — Rust language server (essential)
  • Even Better TOML — TOML syntax support (for Torvyn.toml and Cargo.toml files)
  • Error Lens — Inline diagnostic display
  • CodeLLDB — Debugger integration for Rust
  • WIT IDL — Syntax highlighting for .wit files (if available)

Other Editors

For Neovim, Helix, Zed, or other editors with LSP support: point your LSP client at rust-analyzer with the workspace root as the project directory. No additional configuration is required.

Development Workflow

The standard edit-build-test cycle for Torvyn:

# 1. Edit source files in your editor

# 2. Build the affected crate(s)
cargo build -p torvyn-reactor

# 3. Run the affected tests
cargo test -p torvyn-reactor

# 4. Format your code
cargo fmt --all

# 5. Run the linter
cargo clippy --workspace -- -D warnings

# 6. Check documentation compiles cleanly
cargo doc --workspace --no-deps

# 7. Commit using Conventional Commits format
git add -A
git commit -m "feat(reactor): add weighted fair queuing policy"

Running the Full CI Pipeline Locally

Before pushing a pull request, run the complete CI check locally. This is the same sequence that runs in CI:

# Format check (CI will reject unformatted code)
cargo fmt --all -- --check

# Clippy with warnings-as-errors
cargo clippy --workspace --all-targets -- -D warnings

# Full test suite
cargo test --workspace

# Documentation build (no warnings allowed)
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps

# Build the Wasm test components (if you modified contracts or guest code)
# cargo component build --manifest-path examples/test-components/Cargo.toml --release

If all four commands succeed with no errors and no warnings, your change is ready for review.

Troubleshooting

error[E0658]: use of unstable library feature

You are on a Rust version older than 1.78. Run rustup update stable and try again.

error: target 'wasm32-wasip2' not found

The WASI preview 2 target is not installed. Run rustup target add wasm32-wasip2.

cargo-component build fails with version mismatch

The cargo-component tool evolves quickly and may have compatibility issues with specific wasm-tools versions. If you encounter errors, try updating both tools:

cargo install cargo-component --force
cargo install wasm-tools --force

If problems persist, use the fallback build path described in the Prerequisites section. File an issue if you believe the incompatibility should be documented.

Linker errors on macOS with Wasmtime

If you see linker errors related to wasmtime-runtime on macOS, ensure you have the Xcode command-line tools installed:

xcode-select --install

Tests fail with “component not found” or “artifact missing”

Some integration tests depend on pre-compiled Wasm test components. Build them first:

cd examples/test-components
cargo component build --release
cd ../..
cargo test --workspace

Build is very slow

First builds compile the entire dependency tree including Wasmtime, which is large. Subsequent builds are incremental and much faster. If you are developing a single crate, build and test only that crate:

cargo test -p torvyn-types  # Much faster than --workspace

Consider using cargo-watch for automatic rebuilds:

cargo install cargo-watch
cargo watch -x "test -p torvyn-reactor"

Something else is broken

Run torvyn doctor if the CLI is built. It checks for common environment issues including toolchain versions, missing targets, and configuration problems. If that does not resolve your issue, open a discussion on GitHub with the output of:

rustc --version
cargo --version
wasm-tools --version
wasmtime --version
uname -a

Architecture Guide

This document explains how the Torvyn codebase is organized, what each crate does, how data flows through the system, and where to look when you want to change something. It is written for experienced Rust developers who are evaluating the codebase for the first time.

Crate Dependency Graph

Torvyn is structured as a Cargo workspace with 14 crates. Dependencies flow strictly downward — there are no circular dependencies. torvyn-types is the universal leaf: every other crate depends on it.

torvyn-cli ─────────────────────────────────────────────────────────┐
torvyn-host ────────────────────────────────────────────────────────┤
  ↑                                                                 │
torvyn-pipeline ── torvyn-packaging                                 │
  ↑                    ↑                                            │
torvyn-linker ──── torvyn-reactor                                   │
  ↑                    ↑                                            │
torvyn-resources ─ torvyn-security                                  │
  ↑                    ↑                                            │
torvyn-engine ─── torvyn-observability ── torvyn-config ── torvyn-contracts
  ↑                    ↑                      ↑               ↑     │
  └────────────────────┴──────────────────────┴───────────────┘     │
                            torvyn-types ◄──────────────────────────┘

Reading from bottom to top:

  1. torvyn-types sits at the base. It defines all shared identity types, error enums, state machines, and constants. No internal dependencies, no unsafe code.
  2. torvyn-contracts, torvyn-config, torvyn-engine, and torvyn-observability form the second tier. They depend only on torvyn-types (and, in some cases, on each other within this tier).
  3. torvyn-resources and torvyn-security form the third tier.
  4. torvyn-linker and torvyn-reactor form the fourth tier. These are the core composition and execution subsystems.
  5. torvyn-pipeline and torvyn-packaging form the fifth tier.
  6. torvyn-host and torvyn-cli sit at the top. They are thin orchestration shells that wire together the lower crates.

What Each Crate Does

torvyn-types — Shared Foundation Types

Path: crates/torvyn-types/ Unsafe code: Forbidden (#![forbid(unsafe_code)])

The universal leaf dependency. Defines all identity types (ComponentId, FlowId, StreamId, ResourceId, BufferHandle, TraceId, SpanId), all shared error enums (ProcessError, TorvynError, ContractError, LinkError, ResourceError, ReactorError, ConfigError, SecurityError, PackagingError), domain enumerations (ComponentRole, BackpressureSignal, BackpressurePolicy, ObservabilityLevel), state machines (FlowState, ResourceState), shared records (ElementMeta, TransferRecord, TraceContext), the EventSink trait for observability, and project-wide constants. Contains 48 public items total. Zero external dependencies beyond serde.

torvyn-contracts — WIT Contracts & Validation

Path: crates/torvyn-contracts/

Owns the canonical WIT package files that define Torvyn’s component interfaces (torvyn:streaming@0.1.0, torvyn:lifecycle@0.1.0, torvyn:capabilities@0.1.0). Provides WIT parsing (wrapping wit-parser behind a trait for API isolation), contract validation, semantic compatibility checking between contract versions, and static linking verification. This is the foundation of Torvyn’s contract-first architecture — every component interaction is defined through the WIT definitions owned by this crate.

torvyn-config — Configuration Parsing & Schemas

Path: crates/torvyn-config/ Unsafe code: Forbidden

Implements the two-configuration-context model: component manifests (Torvyn.toml per component project) and pipeline definitions (topology, per-component overrides, scheduling, runtime settings). Handles TOML/JSON parsing, environment variable interpolation, configuration merging and layering, and cross-field semantic validation. All configuration in the Torvyn project flows through this crate.

torvyn-engine — Wasm Engine Abstraction

Path: crates/torvyn-engine/

Abstracts over the WebAssembly runtime. Defines the WasmEngine trait (compile, instantiate, configure) and provides a WasmtimeEngine implementation that wraps Wasmtime. Also defines the ComponentInvoker trait with typed methods (invoke_pull, invoke_process, invoke_push) that the reactor uses to call into components on the hot path. Manages a CompiledComponentCache keyed by ComponentTypeId (SHA-256 of the component binary) for compilation deduplication.

torvyn-resources — Buffer Pools & Ownership

Path: crates/torvyn-resources/ Unsafe code: Isolated to the buffer module (allocation/deallocation), with // SAFETY: comments on every unsafe block.

The central resource registry. Manages tiered buffer pools (Small, Medium, Large, Huge), the resource ownership state machine (Pooled → Owned → Borrowed → Transit → Pooled), copy accounting (the CopyLedger that tracks every payload copy with its reason), per-component memory budgets (BudgetRegistry), and the ResourceTable that maps generational ResourceId handles to actual buffer entries. This crate is where Torvyn’s ownership-aware design is enforced at runtime.

torvyn-reactor — Stream Scheduling & Backpressure

Path: crates/torvyn-reactor/

The execution subsystem for stream-driven flow. Implements the task-per-flow model (one Tokio task per pipeline execution), bounded queues between pipeline stages, demand-driven scheduling with high/low watermark backpressure, cooperative yield (yield after N elements or M microseconds), cancellation propagation, and timeout enforcement. Defines the SchedulingPolicy trait with implementations for FIFO and weighted fair queuing. This is the beating heart of Torvyn’s reactive execution.

torvyn-observability — Tracing, Metrics & Diagnostics

Path: crates/torvyn-observability/

Implements the three-level observability system: Off (zero overhead), Production (counters, histograms, flow-level traces with a budget of 500ns per element), and Diagnostic (per-element spans, full resource lifecycle tracing). Provides the EventSink implementation that the hot path uses for non-blocking event recording, metrics collection handles, and OTLP trace export. All observability in Torvyn routes through this crate.

torvyn-security — Capability Model & Sandboxing

Path: crates/torvyn-security/ Unsafe code: Forbidden

Defines the capability taxonomy (what permissions a component can request), capability declaration and resolution (matching component requirements against operator grants), sandbox configuration (WASI permissions, fuel limits, memory limits), runtime enforcement guards, and audit logging. Implements the deny-all-by-default security model: components receive only the capabilities explicitly granted to them.

torvyn-linker — Component Linking & Compatibility

Path: crates/torvyn-linker/

Performs static linking verification: checks that components in a pipeline have compatible WIT interfaces, resolves capability grants, detects cyclic dependencies, and produces a LinkedPipeline that the pipeline crate uses for instantiation. Generates rich diagnostic reports (LinkReport with LinkDiagnostic entries) when linking fails, so developers get actionable error messages.

torvyn-pipeline — Pipeline Topology & Instantiation

Path: crates/torvyn-pipeline/

Constructs pipeline topologies from configuration, validates topology constraints (source must be first, sink must be last, all edges are connected), and orchestrates component instantiation through an InstantiationContext that coordinates the engine, resources, security, and observability subsystems. Produces a PipelineHandle that the host uses to manage the running pipeline.

torvyn-packaging — OCI Artifacts & Distribution

Path: crates/torvyn-packaging/

Assembles components into OCI-compatible artifacts (.torvyn archives containing the Wasm binary, manifest, contract metadata, and provenance). Handles artifact signing (via a SigningProvider trait, with a Sigstore implementation planned for Phase 2), content-addressed storage, local caching, and registry push/pull operations (via a RegistryClient trait that abstracts over direct OCI API calls and CLI fallbacks).

torvyn-host — Runtime Entry Point

Path: crates/torvyn-host/

A thin orchestration shell. Wires together the engine, resources, reactor, observability, and security subsystems into a running TorvynHost. Manages the startup sequence (parse config → validate contracts → link → compile → instantiate → start flow), graceful shutdown with configurable drain timeouts, and runtime inspection. This crate contains minimal logic of its own — it delegates to the subsystem crates.

torvyn-cli — Developer CLI

Path: crates/torvyn-cli/ Unsafe code: Forbidden

Produces the torvyn binary. Implements all developer-facing commands: init (project scaffolding), check (contract and config validation), link (static compatibility verification), run (pipeline execution with diagnostics), trace (execution with full tracing), bench (latency, throughput, queue pressure, copy behavior measurement), pack (OCI artifact assembly), publish (registry upload), inspect (artifact metadata inspection), and doctor (environment diagnostics). Built with clap for argument parsing and miette for rich error display.

Key Types Glossary

These are the 20 types you will encounter most often when working in the codebase. Understanding them is essential for navigating the code.

TypeCrateWhat It Is
ComponentIdtorvyn-typesAlias for ComponentInstanceId. Runtime identity for a component instance, assigned at instantiation. A u64, monotonically increasing, never reused.
ComponentTypeIdtorvyn-typesContent-addressed identity for a compiled component artifact. A [u8; 32] SHA-256 digest. Used for compilation caching.
FlowIdtorvyn-typesUnique identifier for a flow (a running pipeline instance). A u64.
StreamIdtorvyn-typesIdentifies a specific stream (queue) between two stages in a flow. A u64.
ResourceIdtorvyn-typesGenerational index into the resource table: { index: u32, generation: u32 }. The generation field prevents use-after-free on recycled slots.
BufferHandletorvyn-typesOpaque wrapper around ResourceId. This is what components receive as a handle to host-managed buffers.
ProcessErrortorvyn-typesRust mapping of the WIT process-error variant. Five variants: InvalidInput, Unavailable, Internal, DeadlineExceeded, Fatal.
FlowStatetorvyn-typesState machine for flow lifecycle. Eight states: Created → Validated → Instantiated → Running → Draining → Completed, with Suspended and Failed as alternative terminal/intermediate states.
ResourceStatetorvyn-typesState machine for buffer ownership. States: Pooled, Owned, Borrowed, Transit, Dropped.
ElementMetatorvyn-typesMetadata attached to every stream element: sequence number, timestamp, content type.
ComponentRoletorvyn-typesEnum: Source, Processor, Sink, Filter, Router.
BackpressureSignaltorvyn-typesSignal returned by sink components: Accept, Throttle, Pause.
TraceContexttorvyn-typesW3C-compatible trace and span IDs for distributed tracing.
WasmtimeEnginetorvyn-engineConcrete implementation of the WasmEngine trait wrapping Wasmtime.
CompiledComponenttorvyn-engineA compiled Wasm component ready for instantiation. Cached by ComponentTypeId.
ComponentInstancetorvyn-engineA live, instantiated component with its Wasmtime store and import bindings.
ResourceTabletorvyn-resourcesThe generational-arena data structure that maps ResourceId to ResourceEntry.
BufferPoolSettorvyn-resourcesManages the tiered buffer pools (Small, Medium, Large, Huge).
BoundedQueue<T>torvyn-reactorThe inter-stage queue with capacity limits, used for backpressure enforcement.
TorvynHosttorvyn-hostThe top-level runtime struct that coordinates all subsystems.

Key Traits Glossary

These are the 10 traits that define the major abstraction boundaries in the codebase.

TraitCratePurposeKey Implementors
WasmEnginetorvyn-engineAbstraction over the Wasm runtime. Methods: compile, instantiate, configure engine.WasmtimeEngine
ComponentInvokertorvyn-engineTyped hot-path invocation: invoke_pull, invoke_process, invoke_push. The reactor calls these to execute component logic.WasmtimeInvoker
EventSinktorvyn-typesNon-blocking trait for recording observability events on the hot path. Must not allocate.ObservabilityCollector, NoopEventSink
SchedulingPolicytorvyn-reactorDetermines intra-flow stage execution order.DemandDrivenPolicy, FifoPolicy, WeightedFairPolicy
ResourceManagertorvyn-resourcesBuffer lifecycle: allocate, transfer ownership, create borrows, release, reclaim.DefaultResourceManager
SandboxConfiguratortorvyn-securityProduces SandboxConfig from a component manifest and operator capability grants.DefaultSandboxConfigurator
AuditSinktorvyn-securityRecords security-relevant events (capability checks, sandbox violations).FileAuditSink, EventSinkAdapter
WitParsertorvyn-contractsAbstracts over WIT file parsing. Isolates wit-parser API churn behind a stable internal interface.WitParserImpl
RegistryClienttorvyn-packagingAbstracts OCI registry operations (push, pull, tag, list).Direct OCI client impl, CLI fallback impl
SigningProvidertorvyn-packagingAbstracts artifact signing (Sigstore planned, stub for Phase 0).StubSigningProvider

Hot Path Walkthrough: Following a Stream Element

This walkthrough traces a single stream element through a Source → Processor → Sink pipeline. This is the hot path — the code that runs for every element and where performance matters most.

Step 1: Source produces an element. The reactor’s flow driver task calls ComponentInvoker::invoke_pull() on the source component. The source writes data into a mutable-buffer resource and returns an output-element containing the BufferHandle and ElementMeta.

Step 2: Ownership transfer. The resource manager transitions the buffer from Owned (by the source component) to Transit. The copy ledger records this transfer.

Step 3: Enqueue. The reactor enqueues the element into the BoundedQueue between the source stage and the processor stage. The reactor assigns the canonical sequence number and timestamp-ns to the ElementMeta at this point.

Step 4: Schedule processor. The scheduling policy checks whether the processor stage has input available AND downstream capacity (the queue to the sink is not full). If both conditions hold, the processor is scheduled to run.

Step 5: Processor invocation. The host constructs a borrow<buffer> and borrow<flow-context> for the input element. The reactor calls ComponentInvoker::invoke_process(), passing the borrowed references. The processor reads the input buffer, writes output into a new mutable-buffer, and returns a process-result.

Step 6: Post-processing ownership. The resource manager ends the borrows, transfers the output buffer to Transit, and releases the input buffer back to the pool (Owned → Pooled). The copy ledger records both the read and the write.

Step 7: Enqueue to sink. The output element is enqueued into the processor→sink BoundedQueue.

Step 8: Sink invocation. The host constructs borrows for the sink. The reactor calls ComponentInvoker::invoke_push(). The sink reads the buffer and returns a BackpressureSignal (Accept, Throttle, or Pause).

Step 9: Cleanup and accounting. The resource manager releases the buffer back to the pool. The observability layer records spans and updates counters/histograms. Per element, this three-stage pipeline produces exactly 4 payload copies: source writes (1), processor reads (1) + writes (1), sink reads (1). All copies are instrumented.

Step 10: Backpressure propagation. If the sink returns Throttle or Pause, the reactor propagates demand signals upstream. If the source→processor queue reaches its high watermark, the source is paused until the queue drains to the low watermark.

Cold Path Walkthrough: Pipeline Startup

This walkthrough follows the startup sequence from torvyn run to a running pipeline.

Step 1: CLI parses arguments. The torvyn-cli crate parses the run command and its flags using clap.

Step 2: Load configuration. torvyn-config loads the pipeline definition (either from Torvyn.toml or a standalone pipeline.toml), performs environment variable interpolation, merges layered configs, and validates the result.

Step 3: Validate contracts. torvyn-contracts loads the WIT package files referenced by each component in the pipeline. The validator checks that all WIT definitions parse correctly and that interface versions are compatible.

Step 4: Link components. torvyn-linker performs static linking: it verifies that every component’s imported interfaces are satisfied by another component’s exports (or by the host), resolves capability grants from torvyn-security, and checks for cyclic dependencies. The output is a LinkedPipeline or a LinkReport with diagnostics.

Step 5: Compile Wasm. torvyn-engine compiles each component’s Wasm binary using Wasmtime. Compilation results are cached by ComponentTypeId (SHA-256 of the binary), so recompilation is skipped for unchanged components.

Step 6: Configure sandboxes. torvyn-security produces a SandboxConfig for each component based on its capability manifest and the operator’s capability grants. This includes WASI permissions, fuel limits, and memory limits.

Step 7: Instantiate components. torvyn-pipeline orchestrates instantiation through InstantiationContext. For each component: create a Wasmtime store with the sandbox configuration, instantiate the compiled component, bind host-provided imports (resource manager, observability hooks), and call lifecycle.init(config) if the component exports the lifecycle interface.

Step 8: Construct topology. torvyn-pipeline creates the PipelineTopology — the graph of stages connected by BoundedQueue instances — and registers it with the reactor.

Step 9: Start flow. torvyn-reactor spawns a Tokio task for the flow driver. The flow transitions through Created → Validated → Instantiated → Running. The pipeline is now processing elements.

“If You Want to Change X, Look in Y”

This table maps common modification intents to the relevant crate and files.

If you want to…Look in…
Add a new identity typetorvyn-types/src/identity.rs
Add a new error varianttorvyn-types/src/error.rs, then add a From impl for TorvynError
Modify a WIT interfacetorvyn-contracts/wit/ for the .wit files, then torvyn-contracts/src/validator.rs for validation logic
Change configuration schematorvyn-config/src/manifest.rs (component manifest) or torvyn-config/src/pipeline.rs (pipeline definition)
Change how components are compiled or instantiatedtorvyn-engine/src/WasmtimeEngine for compilation, WasmtimeInvoker for invocation
Modify buffer allocation or poolingtorvyn-resources/src/pool.rs for pool logic, torvyn-resources/src/table.rs for the resource table
Change ownership state transitionstorvyn-types/src/state.rs for the ResourceState machine
Modify scheduling or backpressure behaviortorvyn-reactor/BoundedQueue for queue logic, DemandDrivenPolicy for scheduling
Add a new metric or trace spantorvyn-observability/ for the collectors; update EventSink in torvyn-types if adding a new event kind
Add or change a CLI commandtorvyn-cli/src/ — one module per command
Modify capability checkingtorvyn-security/ for the capability taxonomy and enforcement guards
Change the linking/compatibility algorithmtorvyn-linker/PipelineLinker for the main algorithm, LinkDiagnostic for error reporting
Change the OCI artifact formattorvyn-packaging/src/artifact.rs for format, torvyn-packaging/src/manifest.rs for metadata
Add a new example pipelineexamples/ directory at the repository root

Concurrency Model

Torvyn uses Tokio’s multi-threaded work-stealing runtime. There are no custom OS threads.

The key concurrency primitives are: one Tokio task per flow (the “flow driver”), one reactor coordinator task for flow lifecycle management, background tasks for observability export, and spawn_blocking for filesystem I/O (component loading, config parsing).

Inter-flow fairness comes from Tokio’s work-stealing scheduler. Intra-flow fairness comes from the reactor’s cooperative yield mechanism: a flow driver yields back to Tokio after processing N elements or M microseconds, whichever comes first. Wasmtime’s fuel mechanism provides cooperative preemption within Wasm execution.

Coding Standards

These standards apply to all code contributed to the Torvyn project. They supplement rustfmt and clippy — those tools enforce mechanical formatting and common lints. This document covers the conventions, patterns, and policies that tools cannot enforce automatically.

Rust Style

Naming Conventions

Torvyn follows standard Rust naming conventions with the following project-specific additions:

  • Identity types are suffixed with Id: ComponentId, FlowId, StreamId, ResourceId.
  • Configuration structs are suffixed with Config: WasmtimeEngineConfig, FlowConfig, TierConfig.
  • Builder types are suffixed with Builder: HostBuilder, PipelineTopologyBuilder.
  • Error types are suffixed with Error: EngineError, FlowCreationError, LinkerError.
  • Trait implementations for the primary production implementation use the Default prefix: DefaultResourceManager, DefaultSandboxConfigurator. This leaves the trait name clean for the abstraction.

Module Organization

Each crate follows a consistent internal structure:

  1. src/lib.rs — Crate root. Contains #![forbid(unsafe_code)] (unless the crate has justified unsafe), #![deny(missing_docs)], and re-exports of the public API. No logic beyond re-exports.
  2. src/error.rs or src/errors.rs — All crate-specific error types, grouped in one place.
  3. Domain modules — one file per major concept. Keep files under 500 lines when possible. If a module grows beyond 800 lines, consider splitting it.
  4. tests/ — Integration tests in the standard Cargo test directory.

Error Handling Patterns

Torvyn uses a layered error model:

  • Crate-level errors (e.g., EngineError, LinkerError) — defined per crate using thiserror derives. Every variant has a Display implementation that produces a complete, human-readable message.
  • Cross-crate error typeTorvynError in torvyn-types acts as the top-level error. Every crate-level error implements From<CrateError> for TorvynError.
  • Error codes — Error types that correspond to user-facing diagnostics include structured error codes (e.g., E0100E0199 for contract errors). These codes appear in CLI output and documentation.
  • Never use unwrap() or expect() in library code. These are acceptable only in tests and in CLI main() paths where the error is immediately displayed to the user.
  • Use ? for propagation. Every fallible function returns Result<T, E> where E is either the crate-level error or TorvynError.

Documentation Standards

Every public item must have a documentation comment. The #![deny(missing_docs)] lint is enabled on all crates and enforced in CI.

Documentation comments follow this structure:

#![allow(unused)]
fn main() {
/// One-sentence summary of what this type/function does.
///
/// Longer explanation if the summary is not sufficient. Include
/// context about when and why this is used.
///
/// # Invariants (for types)
/// - List any invariants that must hold.
///
/// # Errors (for fallible functions)
/// Returns `SomeError::Variant` if the specific condition occurs.
///
/// # Panics (only if the function can panic)
/// Panics if the specific condition occurs.
///
/// # Examples
/// ```
/// // At least one runnable example for types and public functions.
/// ```
}

Performance Annotations

Torvyn uses inline comments to mark code by its performance sensitivity. This helps reviewers and future contributors understand which code is latency-critical.

  • // HOT PATH — Executes for every stream element. Must not allocate, must not lock, must not block. Observability overhead budget: 500ns per element at Production level.
  • // WARM PATH — Executes per scheduling cycle or per backpressure event. Allocation is acceptable if amortized. Locks are acceptable if uncontended.
  • // COLD PATH — Executes during startup, shutdown, or configuration changes. No performance constraints.

When adding code to a function marked HOT PATH, you must verify that your change does not introduce allocations or blocking. When in doubt, benchmark.

Test Writing Standards

Test Naming

Test names use the pattern test_<unit>_<condition>_<expected_outcome>:

#![allow(unused)]
fn main() {
#[test]
fn test_flow_state_transition_from_running_to_draining_succeeds() { ... }

#[test]
fn test_resource_id_stale_generation_returns_error() { ... }
}

Test Structure

Tests follow the Arrange-Act-Assert pattern:

#![allow(unused)]
fn main() {
#[test]
fn test_buffer_pool_allocates_from_correct_tier() {
    // Arrange
    let pool = BufferPoolSet::new(default_tier_config());

    // Act
    let handle = pool.allocate(256).unwrap();

    // Assert
    assert_eq!(handle.tier(), PoolTier::Small);
}
}

Coverage Expectations

There is no hard coverage percentage target, but every public function should have at least one test that exercises its happy path and one that exercises its primary error path. State machines (FlowState, ResourceState) must have tests for every valid transition and at least one test for each invalid transition.

Commit Message Format

Torvyn uses Conventional Commits. Every commit message follows this format:

<type>(<scope>): <short description>

<optional body>

<optional footer>

Types:

  • feat — A new feature or capability
  • fix — A bug fix
  • perf — A performance improvement
  • refactor — Code restructuring without behavior change
  • test — Adding or updating tests
  • docs — Documentation changes
  • chore — Build system, CI, dependencies, or tooling changes
  • ci — CI configuration changes

Scope is the crate name without the torvyn- prefix: types, contracts, config, engine, resources, reactor, observability, security, linker, pipeline, packaging, host, cli.

Examples:

feat(reactor): add weighted fair queuing policy
fix(resources): prevent double-free on stale buffer handle
perf(engine): cache compiled components by content hash
docs(contracts): document WIT interface evolution rules
test(types): add property tests for state machine transitions
chore(deps): update wasmtime to 26.0.0

Pull Request Description Standards

Every pull request description must include:

  1. Summary — one or two sentences describing the change.
  2. Motivation — why this change is needed. Link to the issue if one exists.
  3. Design decisions — if the change involves a non-obvious design choice, explain it. If you considered alternatives, note them briefly.
  4. Testing — what tests were added or updated. How to verify the change manually if applicable.
  5. Performance impact — if the change touches hot-path code, describe the expected impact and any benchmark results.

Unsafe Code Policy

Unsafe code is forbidden by default. The #![forbid(unsafe_code)] attribute is set on all crates except torvyn-resources, which requires unsafe for buffer allocation and deallocation.

If you need to add unsafe code:

  1. Justify it. Explain in the PR description why safe alternatives are insufficient.
  2. Isolate it. Unsafe code must live in a dedicated module, never mixed with safe logic.
  3. Document every block. Every unsafe block must have a // SAFETY: comment that explains why the specific safety invariants are upheld.
  4. Test the invariants. Every unsafe block must have corresponding tests that exercise the boundary conditions.
  5. Minimize scope. The unsafe block should contain the absolute minimum number of statements necessary.

Example of acceptable unsafe documentation:

#![allow(unused)]
fn main() {
// SAFETY: `ptr` is valid because it was allocated by `alloc::alloc` in
// `BufferPool::allocate` with a layout of `self.capacity` bytes, and
// `self.capacity` is guaranteed > 0 by the TierConfig constructor.
// The allocation has not been freed because this function holds the
// only `BufferEntry` that references it, and `BufferEntry::drop`
// is the only code path that frees it.
unsafe { std::ptr::write_bytes(ptr, 0, self.capacity) };
}

Dependency Addition Policy

Adding a new external dependency requires justification in the PR description. The following must be addressed:

  1. Purpose — what does this dependency provide that cannot be done with existing dependencies or standard library code?
  2. License — the dependency must be compatible with Apache-2.0. Acceptable licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Zlib. Any other license requires explicit approval from a maintainer.
  3. Maintenance status — is the crate actively maintained? When was the last release? Are there open security advisories?
  4. Size impact — does this dependency pull in a large transitive dependency tree?
  5. Feature flags — enable only the features you need. Do not enable default features unless all of them are required.

Run cargo deny check (if configured) to verify license compliance before submitting.

Testing Guide

This guide covers how to write and run tests for the Torvyn project. Torvyn uses a multi-layered testing strategy: unit tests for individual functions and types, integration tests for cross-module behavior, benchmark tests for performance verification, property tests for invariant validation, and fuzz tests for input boundary exploration.

Test Infrastructure Overview

Tests live in two places:

  • Inline unit tests in #[cfg(test)] modules at the bottom of source files. Use these for testing private functions and implementation details.
  • Integration tests in crates/<crate-name>/tests/. Use these for testing the crate’s public API as an external consumer would use it.

Some integration tests depend on pre-compiled Wasm test components (fixtures) located in examples/test-components/. Build these before running the full test suite:

cd examples/test-components
cargo component build --release
cd ../..

How to Write Unit Tests

Unit tests for Torvyn crates follow the Arrange-Act-Assert pattern and the naming convention described in the Coding Standards.

Testing Identity Types

Identity types in torvyn-types should be tested for: construction, equality, hashing, Copy semantics, display formatting, and debug formatting.

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_flow_id_equality() {
        let a = FlowId::new(42);
        let b = FlowId::new(42);
        let c = FlowId::new(43);
        assert_eq!(a, b);
        assert_ne!(a, c);
    }

    #[test]
    fn test_flow_id_display() {
        let id = FlowId::new(7);
        assert_eq!(format!("{}", id), "flow-7");
    }
}
}

Testing State Machines

The FlowState and ResourceState machines must be tested for every valid transition and every invalid transition. The existing tests in torvyn-types/tests/state_machine_tests.rs demonstrate the pattern:

#![allow(unused)]
fn main() {
#[test]
fn test_flow_state_full_happy_path() {
    let state = FlowState::Created;
    let state = state.transition_to(FlowState::Validated).unwrap();
    let state = state.transition_to(FlowState::Instantiated).unwrap();
    let state = state.transition_to(FlowState::Running).unwrap();
    let state = state.transition_to(FlowState::Draining).unwrap();
    let state = state.transition_to(FlowState::Completed).unwrap();
    assert!(state.is_terminal());
}

#[test]
fn test_flow_state_invalid_transition_returns_error() {
    let state = FlowState::Created;
    let result = state.transition_to(FlowState::Running);
    assert!(result.is_err());
}
}

Testing Error Types

Every error variant should have a test verifying: construction, display output, and conversion to TorvynError.

#![allow(unused)]
fn main() {
#[test]
fn test_process_error_converts_to_torvyn_error() {
    let process_err = ProcessError::Fatal("disk full".into());
    let torvyn_err: TorvynError = process_err.into();
    let display = format!("{torvyn_err}");
    assert!(display.contains("FATAL"));
    assert!(display.contains("disk full"));
}
}

How to Write Integration Tests

Integration tests verify cross-module behavior by using the crate’s public API as an external consumer would. Place these in crates/<crate-name>/tests/.

For crates that depend on the Wasm engine (e.g., torvyn-engine, torvyn-host), integration tests may need compiled Wasm test components. These test fixtures live in examples/test-components/ and are compiled as part of the test setup.

#![allow(unused)]
fn main() {
// crates/torvyn-engine/tests/compilation_test.rs
use torvyn_engine::{WasmtimeEngine, WasmtimeEngineConfig, WasmEngine};

#[test]
fn test_compile_valid_component_succeeds() {
    let engine = WasmtimeEngine::new(WasmtimeEngineConfig::default()).unwrap();
    let wasm_bytes = std::fs::read("../../examples/test-components/target/wasm32-wasip2/release/passthrough.wasm").unwrap();
    let compiled = engine.compile(&wasm_bytes);
    assert!(compiled.is_ok());
}
}

How to Write Benchmark Tests

Torvyn uses Criterion for benchmark tests. Benchmarks live in benches/ directories within each crate.

#![allow(unused)]
fn main() {
// crates/torvyn-resources/benches/pool_benchmark.rs
use criterion::{criterion_group, criterion_main, Criterion};
use torvyn_resources::{BufferPoolSet, TierConfig};

fn bench_allocate_and_release(c: &mut Criterion) {
    let pool = BufferPoolSet::new(TierConfig::default());
    c.bench_function("allocate_small_buffer", |b| {
        b.iter(|| {
            let handle = pool.allocate(256).unwrap();
            pool.release(handle);
        })
    });
}

criterion_group!(benches, bench_allocate_and_release);
criterion_main!(benches);
}

Run benchmarks:

cargo bench -p torvyn-resources

When submitting a PR that modifies hot-path code, include benchmark results showing the before and after. Use cargo bench -- --save-baseline before and cargo bench -- --baseline before to generate comparison reports.

How to Use Test Components

The examples/test-components/ directory contains minimal Wasm components designed for testing. These implement Torvyn’s WIT interfaces with simple, deterministic behavior:

  • passthrough — A processor that copies input to output without modification. Useful for measuring baseline overhead.
  • counter-source — A source that produces elements containing a monotonically increasing counter. Useful for testing flow lifecycle and ordering.
  • slow-sink — A sink that introduces configurable artificial delay. Useful for testing backpressure behavior.
  • failing-processor — A processor that returns ProcessError::Fatal after a configurable number of elements. Useful for testing error handling.

Build all test components:

cd examples/test-components
cargo component build --release

Running Specific Test Subsets

# All tests in a specific crate
cargo test -p torvyn-reactor

# Tests matching a name pattern
cargo test -p torvyn-types flow_state

# Only integration tests (not unit tests)
cargo test -p torvyn-engine --test '*'

# Only unit tests (not integration tests)
cargo test -p torvyn-types --lib

# Only doc tests
cargo test -p torvyn-types --doc

Measuring Code Coverage

Use cargo-llvm-cov for coverage measurement:

cargo install cargo-llvm-cov

# Generate coverage for the workspace
cargo llvm-cov --workspace --html

# Open the report
open target/llvm-cov/html/index.html

# Generate coverage for a single crate
cargo llvm-cov -p torvyn-types --html

There is no hard coverage target. Focus coverage on: all public API functions, all state machine transitions, all error paths, and all hot-path code.

Property Testing with proptest

Torvyn uses proptest for property-based testing, particularly for identity types, state machines, and serialization round-trips.

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

proptest! {
    #[test]
    fn test_resource_id_roundtrip(index in 0u32..u32::MAX, gen in 0u32..u32::MAX) {
        let id = ResourceId::new(index, gen);
        assert_eq!(id.index(), index);
        assert_eq!(id.generation(), gen);
    }

    #[test]
    fn test_flow_state_terminal_states_are_idempotent(
        terminal in prop_oneof![
            Just(FlowState::Completed),
            Just(FlowState::Failed),
        ]
    ) {
        // No transition from a terminal state should succeed
        for target in FlowState::all_variants() {
            if target != terminal {
                assert!(terminal.transition_to(target).is_err());
            }
        }
    }
}
}

Add proptest as a dev dependency in the crate’s Cargo.toml:

[dev-dependencies]
proptest = "1"

Fuzz Testing Strategy

Fuzz testing is planned for security-sensitive input parsing boundaries: WIT file parsing (torvyn-contracts), TOML configuration parsing (torvyn-config), and OCI artifact deserialization (torvyn-packaging).

Fuzz targets use cargo-fuzz with libFuzzer:

cargo install cargo-fuzz

# List available fuzz targets
cargo fuzz list

# Run a specific fuzz target for 60 seconds
cargo fuzz run wit_parser_fuzz -- -max_total_time=60

Fuzz targets live in fuzz/ directories within the relevant crates. When writing a new fuzz target, focus on functions that accept arbitrary byte slices or string input from untrusted sources.

Release Process

This document describes how Torvyn releases are made, versioned, and maintained.

Version Number Conventions

Torvyn follows strict Semantic Versioning 2.0.0 (semver.org) for all crate versions.

MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
  • MAJOR — Incremented for breaking changes to public APIs, WIT interface changes that alter ownership semantics or remove existing types, or configuration format changes that are not backward-compatible.
  • MINOR — Incremented for additive changes: new functions, new interfaces, new CLI commands, new configuration options with defaults that preserve existing behavior.
  • PATCH — Incremented for bug fixes, documentation corrections, and performance improvements that do not change public API behavior.
  • Pre-release — Versions with identifiers like -alpha.1, -beta.2, -rc.1 are not considered stable and are excluded from dependency resolution by default.

All crates in the workspace share a single version number. When any crate has a change that warrants a version bump, all crates are bumped together. This simplifies dependency management and avoids version matrix confusion.

During Phase 0, all releases carry the 0.x.y version range. A 0.x.y version signals that the API is not yet stable and breaking changes may occur in minor versions.

What Triggers a Release

Releases are triggered by the maintainer(s) when one of the following conditions is met:

  • A milestone is completed (e.g., Phase 0 Source→Sink pipeline working).
  • A security vulnerability is patched.
  • A sufficient number of improvements have accumulated since the last release.
  • A critical bug fix that affects production users.

Releases are not made on a fixed calendar schedule. Quality and completeness take priority over cadence.

How Releases Are Made

1. Pre-release Verification

Before tagging a release, verify:

# All tests pass
cargo test --workspace

# No clippy warnings
cargo clippy --workspace --all-targets -- -D warnings

# Documentation builds cleanly
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps

# Format is correct
cargo fmt --all -- --check

# Benchmarks run without regression (compare against previous release)
cargo bench --workspace

2. Update Version Numbers

Update the version in the workspace Cargo.toml and all crate Cargo.toml files. Update the CHANGELOG.md with a summary of changes since the last release, organized by category (Added, Changed, Fixed, Removed, Security).

3. Create Release Commit and Tag

git add -A
git commit -m "chore(release): prepare v0.2.0"
git tag -s v0.2.0 -m "Release v0.2.0"
git push origin main --tags

4. Publish to crates.io

Crates are published in dependency order:

cargo publish -p torvyn-types
cargo publish -p torvyn-contracts
cargo publish -p torvyn-config
cargo publish -p torvyn-engine
cargo publish -p torvyn-observability
cargo publish -p torvyn-resources
cargo publish -p torvyn-security
cargo publish -p torvyn-linker
cargo publish -p torvyn-reactor
cargo publish -p torvyn-pipeline
cargo publish -p torvyn-packaging
cargo publish -p torvyn-host
cargo publish -p torvyn-cli

5. Create GitHub Release

Create a GitHub release from the tag with the CHANGELOG entry as the release notes.

How to Backport Fixes

For critical bug fixes or security patches that need to apply to an older release:

  1. Create a branch from the release tag: git checkout -b release/0.1.x v0.1.0
  2. Cherry-pick the fix commit: git cherry-pick <commit-hash>
  3. Bump the patch version in all Cargo.toml files.
  4. Run the full verification suite.
  5. Tag and publish: git tag -s v0.1.1 -m "Release v0.1.1"

Backport branches are maintained only for the most recent minor release. Older releases receive backports only for security fixes.

Release Checklist

  • All CI checks pass on main
  • CHANGELOG.md updated with categorized changes
  • Version numbers updated in all Cargo.toml files
  • Benchmarks show no unexpected regressions
  • Release commit created and signed
  • Git tag created and signed
  • Tag pushed to origin
  • Crates published to crates.io in dependency order
  • GitHub release created with changelog
  • Announcement posted (if applicable)

Introducing Torvyn

An ownership-aware reactive streaming runtime for safe, low-latency pipelines.


We are releasing the first public version of Torvyn — a reactive streaming runtime built in Rust on the WebAssembly Component Model.

Torvyn is not a general-purpose framework. It is a focused runtime for a specific and painful category of problems: building low-latency streaming pipelines on the same machine or edge node, where teams need isolation without the overhead of microservice boundaries, and composability without the risks of in-process plugins.

Why We Built This

The modern infrastructure landscape has a gap. Teams building streaming systems — AI inference pipelines, event processors, edge analytics, plugin platforms — are forced to choose between latency and safety, between composability and isolation, between polyglot flexibility and operational simplicity.

Traditional microservices solve the isolation problem but impose serialization, network stack traversal, and operational complexity at every boundary. In-process plugins solve the latency problem but sacrifice memory safety, language neutrality, and governance. Containers solve the packaging problem but add too much overhead for fine-grained, high-frequency composition.

These trade-offs are well-known. Most teams work around them with custom glue code, ad hoc buffer management, and application-specific back-pressure logic. Every team reinvents this infrastructure slightly differently.

Torvyn is our attempt at a common solution: a runtime that provides low-latency component composition with real isolation, explicit contracts, tracked resource ownership, built-in back-pressure, and production-grade observability.

How It Works

A Torvyn pipeline is a directed graph of components connected by typed streams.

Contracts come first. Every component interaction is defined by a WIT interface. The contract specifies what data a component accepts, what it produces, what resources it uses, and what capabilities it requires. Contracts are versioned and machine-checkable.

Components are sandboxed. Each component is compiled to WebAssembly and runs in its own isolated environment. Components can be written in any language that targets the WebAssembly Component Model — Rust has first-class support today. The runtime manages component lifecycle, from instantiation to teardown.

The host manages resources. Data buffers are host-managed resources with explicit ownership. The resource manager tracks every allocation, borrow, transfer, and copy. Where components pass data without reading the payload, the transfer is zero-copy in the payload path. Where copies are necessary, they are recorded and reported.

The reactor schedules reactively. The reactor is a flow-aware scheduler that runs on top of Tokio. It uses a task-per-flow architecture with consumer-first, demand-driven scheduling. Back-pressure is built into the stream model. Each stream has a bounded queue with configurable overflow policies and hysteresis-based watermarks that prevent oscillation.

Everything is observable. Flow lifecycle transitions, back-pressure events, resource transfers, scheduling decisions, and copy operations all emit structured events. OpenTelemetry support is native.

What Is Available Now

This initial release (Phase 0) includes:

  • The core contract layer with WIT package definitions for streaming types, processor, source, and sink interfaces.
  • The host runtime with component loading, linking, and lifecycle management via Wasmtime.
  • The reactor with task-per-flow scheduling, demand-driven back-pressure, cooperative yielding, and fairness enforcement.
  • The resource manager with buffer pooling, ownership tracking, and copy accounting.
  • The observability layer with structured event emission and OpenTelemetry integration.
  • The CLI with torvyn init, torvyn check, torvyn link, torvyn run, torvyn trace, and torvyn bench commands.
  • A benchmark suite measuring passthrough latency, throughput saturation, back-pressure response time, and multi-flow fairness.

Rust is the supported component language in Phase 0. Additional language support is planned.

What Is Coming Next

Phase 1 will expand the contract surface (filter, router, and aggregator interfaces), introduce supply chain security foundations (artifact signing design hooks), improve the packaging workflow, and add multi-flow orchestration features.

The roadmap is published and updated in the project repository.

What This Is Not

Torvyn is not a distributed orchestrator, not a Kubernetes replacement, not a general-purpose service mesh, and not a serverless platform. It does not claim to make all data transfers zero-copy across all boundaries. It does not claim to replace all microservice architectures.

Torvyn is a focused runtime for safe, observable, low-latency streaming composition on the same node and at the edge. That is a specific and valuable space. We are building for that space deliberately.

Current Status

Torvyn is in early development. The architecture is well-defined, the core subsystems are implemented, and the benchmark suite is running. It is not yet production-hardened. We are publishing it now because we believe the design is sound, the use cases are real, and the project benefits from community review and participation from this point forward.

We welcome feedback on the design, contributions to the codebase, and conversations with teams whose workloads might be a good fit.

Get Involved

Why “Ownership-Aware,” Not “Zero-Copy”

On being precise about what happens at component boundaries.


When describing a streaming runtime’s data transfer model, the tempting marketing claim is “zero-copy.” It is short, impressive, and immediately suggests performance. Torvyn deliberately avoids this claim. Here is why.

The Problem with “Zero-Copy”

The WebAssembly Component Model provides real memory isolation between components. Each component has its own linear memory. The host has its own address space. When Component A produces data and Component B needs to read or transform that data, the bytes must, at some point, exist in Component B’s linear memory.

This is not a limitation of Torvyn’s design. It is a consequence of the memory isolation that makes sandboxing possible. The same isolation that prevents Component A from corrupting Component B’s memory is the isolation that requires data copies when components need to operate on shared data.

Claiming “zero-copy” would mean either: (a) claiming something that is physically impossible across Wasm component boundaries for all cases, or (b) redefining “zero-copy” in a way that is technically defensible but misleading to practitioners who understand the term in its conventional sense.

Neither option builds trust.

What “Ownership-Aware” Means in Practice

Torvyn’s resource manager tracks every buffer’s lifecycle from allocation to reclamation. At any given moment, every buffer in the system has exactly one owner, and the host knows who that owner is.

This ownership tracking enables three distinct transfer scenarios:

Handle pass-through (zero-copy in the payload path). When a buffer moves from Component A to Component B and neither component needs to read or modify the payload, the transfer updates the ownership record in the resource table. The payload bytes do not move. This path is available for routing stages, metadata-only inspection, fan-out distribution, and any stage where the component operates on the element’s metadata without touching the payload.

Payload read (one copy into consumer memory). When Component B needs to read the payload, the resource manager copies the bytes from the host-managed buffer into Component B’s linear memory. This is one copy, performed on demand, only when the component calls buffer.read().

Payload write (one copy from producer memory). When a component allocates a new buffer and writes output data, the resource manager copies the bytes from the component’s linear memory into a host-managed buffer. Again, one copy, on demand.

The key insight is that many pipeline stages do not need to read every payload. Routing decisions, content-type checking, trace context propagation, policy evaluation based on metadata, priority assignment, and fan-out distribution can all operate on metadata alone. For these stages, the payload transfer is genuinely zero-copy.

Copy Accounting Makes This Measurable

Torvyn does not ask you to trust that copies are minimal. It proves it.

Every copy operation produces a TransferRecord with the timestamp, source and destination entities, byte count, copy reason (metadata marshaling, payload read, payload write, or host serialization), and the flow the copy belongs to.

These records aggregate into per-flow copy statistics: total payload bytes copied, total metadata bytes copied, copy count per component boundary, and a copy amplification ratio — the ratio of payload bytes copied to payload bytes produced. A metadata-routing pipeline should show a copy amplification ratio near 0.0. A transform-heavy pipeline should show a ratio near 1.0.

The torvyn bench command reports these metrics. They are part of the standard output for every benchmark run.

Why This Matters for Performance Engineering

A runtime that claims “zero-copy” gives you nothing to measure. Either everything is zero-copy (which is impossible), or some transfers involve copies and you have no visibility into which ones or how many.

A runtime that provides copy accounting gives you a performance engineering tool. You can identify which component boundaries produce the most copies, evaluate whether a particular stage could be restructured to operate on metadata alone, measure the impact of changing a pipeline topology, and verify that optimization efforts actually reduce data movement.

This is the difference between a marketing claim and an engineering tool.

The Honest Position

Torvyn minimizes copies where the architecture permits, makes every copy visible, and provides the instrumentation to understand and improve data transfer behavior. It does not pretend that sandboxed composition is free of data movement costs.

This is what “ownership-aware” means: the runtime knows who owns what, knows when and why data moves, and makes that information available to you.

Designing the Reactor: Scheduling Challenges of Reactive Streaming

A technical deep-dive into Torvyn’s stream scheduling engine.


The reactor is the execution heartbeat of the Torvyn runtime. It determines which components run, in what order, and how back-pressure propagates through the system. This post describes the key design decisions and the trade-offs behind them.

The Scheduling Problem

A Torvyn pipeline is a directed graph of components connected by bounded-queue streams. At any moment, multiple components within a flow may be ready to execute: a source has data, a processor has both input available and output capacity, a sink has elements to consume. The scheduler must decide which component runs next.

The wrong scheduling order creates problems. Running producers before consumers fills queues, increases memory pressure, and triggers unnecessary back-pressure. Running consumers before producers drains queues faster and keeps the pipeline responsive. But a naive consumer-first policy can starve sources and reduce throughput.

Task-Per-Flow, Not Task-Per-Component

The first major decision was the reactor’s task architecture. We chose task-per-flow: each active pipeline execution gets its own Tokio async task. Within that task, the flow driver executes pipeline stages sequentially in dependency order.

The alternative — task-per-component — would assign each component its own Tokio task. For pipelines with many stages (20+ components per flow across hundreds of flows), this creates thousands of Tokio tasks, each with its own waker, stack, and scheduling overhead. More importantly, it gives up intra-flow scheduling control to Tokio’s work-stealing scheduler, which knows nothing about stream dependencies or demand propagation.

Task-per-flow gives the reactor control over intra-flow scheduling policy. The flow driver can execute stages in dependency order, yield to Tokio between stages, and adjust scheduling based on back-pressure state — all within a single async task. Tokio handles inter-flow scheduling (distributing flow driver tasks across OS threads), while the reactor handles intra-flow scheduling (deciding which component runs next within a flow).

The trade-off is that a single flow cannot utilize multiple OS threads simultaneously for different stages. For Torvyn’s v1 target of same-node pipelines, this is acceptable — the bottleneck is typically Wasm execution speed per stage, not parallelism within a single flow. The design is similar to how Apache Flink’s task slots chain operators sequentially within a slot.

Consumer-First, Demand-Driven Scheduling

Within a flow, the default scheduling policy is demand-driven, consumer-first. The scheduler starts from the sink (the terminal consumer), walks upstream looking for ready stages, and executes the first stage it finds with both input available and output capacity.

This pull-based traversal naturally prevents queue buildup. Work only happens when there is downstream capacity to consume the result. It follows the same principle as the Reactive Streams specification’s request(n) pattern: consumers drive the pipeline by expressing demand.

Credit-Based Demand Model

Each stream maintains a demand counter: the number of elements the consumer is willing to accept. Consumers replenish demand by processing elements. Producers consume demand by enqueuing elements. When demand reaches zero, the producer pauses — this is the back-pressure trigger.

Demand propagation follows the pipeline graph from consumer to producer. When a sink processes an element, the reactor increments demand on the sink’s input stream, checks if the upstream processor can now produce, and if so, propagates demand further upstream until it reaches the source.

For fan-out topologies (one producer, multiple consumers), the producer’s effective demand is the minimum across all downstream branches. This prevents the producer from outrunning the slowest consumer.

Back-Pressure with Hysteresis

When a stream’s queue reaches capacity under the default Block policy, the producer is suspended. But when should the producer resume? Resuming immediately when one element is consumed creates rapid oscillation between backpressured and normal states when the consumer is only slightly slower than the producer.

Torvyn uses high/low watermarks with hysteresis. Back-pressure activates when the queue reaches 100% capacity. It deactivates when the queue drops below 50% capacity (the low watermark, configurable per stream). This provides stability: the producer stays paused until the queue has drained substantially, then resumes and can produce a burst of elements before hitting capacity again.

Every state transition (back-pressure triggered, back-pressure relieved) emits a structured observability event with timestamp, stream identifier, queue depth, and duration.

Cooperative Yielding and Fairness

A flow driver that never yields to Tokio starves other flows of CPU time. The reactor enforces cooperative yielding: after processing a configurable batch of elements (default: 32) or after a configurable time quantum (default: 100 microseconds), the flow driver yields to Tokio’s scheduler.

Flow priority adjusts the yield frequency. Critical flows process up to 128 elements per yield cycle — approximately 4x the CPU share of Normal flows. Background flows process only 8 elements per yield. A hard ceiling (256 elements) prevents any flow from monopolizing a thread regardless of priority.

A watchdog in the reactor coordinator monitors yield timestamps. If any flow driver has not yielded within 10 milliseconds, it logs a warning. This is the safety net against pathological component behavior.

Cancellation and Cleanup

Cancellation uses a tiered protocol. When a flow is cancelled — by operator command, downstream error, timeout, or resource exhaustion — the reactor first allows the current component invocation to complete (cooperative cancellation). If the invocation does not return within 1 second, it exhausts the component’s Wasmtime fuel budget, causing a deterministic Wasm trap. If fuel exhaustion fails within 500 milliseconds (a pathological case), the host drops the component instance entirely.

After cancellation, all stream queues drain, resource handles are released, observability events are flushed, and the flow is removed from the reactor’s active table. The maximum time from cancellation initiation to full cleanup is bounded and configurable (default: approximately 6.5 seconds).

Performance Targets

The reactor’s per-element overhead target is < 5 microseconds, covering scheduler decision, queue operations, demand accounting, back-pressure check, and observability event recording. The wakeup latency target is < 10 microseconds from data availability to consumer invocation. The reactor supports 1,000+ concurrent flows, with each flow driver task contributing approximately 256-512 bytes of Tokio task overhead.

These are engineering targets for Phase 0 benchmarks. Results will be published with full methodology.

What We Are Still Deciding

Several design questions remain open: optimal default values for yield quantum, the best bounded queue implementation (VecDeque vs. custom ring buffer), the fan-in merge policy (first-available vs. round-robin), and whether intra-flow parallelism (executing independent stages concurrently within a flow) provides meaningful throughput improvements for realistic topologies. These decisions will be informed by benchmark data from Phase 0.

Contracts-First Composition: Why Typed Interfaces Are the Center of Torvyn

How WIT contracts prevent production failures and enable safe evolution.


Most system failures at component boundaries are not caused by a missing technology. They are caused by unstated assumptions — about data format, about field presence, about ownership semantics, about error handling, about versioning. Two systems that “work together” in development fail in production because the contract between them was implicit, untested, or out of date.

Torvyn makes the contract explicit, typed, versioned, and machine-checkable. This is not an afterthought. It is the center of the product.

WIT as the Contract Language

Torvyn uses WIT (WebAssembly Interface Types) as its contract definition language. WIT is the standard interface language of the WebAssembly Component Model, maintained by the Bytecode Alliance.

A WIT contract in Torvyn defines:

  • Types: Records, variants, and enums that describe the structure of stream elements, metadata, and error cases.
  • Resources: Host-managed entities with explicit ownership semantics. The buffer resource in Torvyn represents a data payload managed by the host. WIT’s own<T> and borrow<T> handle types make ownership visible in the contract.
  • Interfaces: The functions a component exports (processor, source, sink) or imports (buffer allocation, capability access).
  • Worlds: The complete requirement and capability surface of a component — what it exports, what it imports, and what it depends on.

WIT contracts are human-readable, language-neutral, and statically validatable. They serve as both documentation and enforcement mechanism.

Static Validation Before Runtime

Torvyn’s validation pipeline catches errors at three stages:

Parse-time validation (torvyn check) verifies WIT syntax, manifest format, capability declarations, and world completeness for a single component.

Semantic validation (torvyn check) checks type consistency, resource usage patterns, and version constraints.

Composition validation (torvyn link) validates interface compatibility between connected components, verifies capability satisfaction for the full pipeline, checks topology correctness (valid DAG structure, sources have no inputs, sinks have no outputs, router port names match actual destinations), and ensures version ranges across all components have a non-empty intersection.

Errors produce structured messages with error codes, file locations, explanations, and actionable fix suggestions. Interface mismatches, missing capabilities, and version conflicts are caught before any component code executes.

Version Evolution Rules

Torvyn WIT packages follow semantic versioning with clearly defined rules for what constitutes a breaking change. Removing a type, changing a function signature, removing a record field, or changing ownership semantics (borrow to own or vice versa) requires a major version bump. Adding a new function to an existing interface or adding a new interface to a package is a compatible minor change.

The torvyn link command performs structural compatibility checking. Even within compatible version ranges, it verifies that the specific interfaces and functions used by a consumer are present in the provider. A consumer compiled against version 0.2.0 that uses a function added in 0.2.0 will fail to link with a provider that only implements 0.1.0 — and the error message will identify exactly which function is missing and what version would resolve the issue.

Preventing Production Failures

Consider a concrete scenario. A team updates a policy filter component. The new version expects a priority field in the stream element metadata — a field that was added in contract version 0.2.0. The upstream data enricher was compiled against contract version 0.1.0 and does not produce this field.

Without Torvyn, this pipeline deploys successfully and fails at runtime when the filter attempts to access a field that does not exist. The failure may be intermittent (if the field is optional in the filter’s implementation), hard to diagnose (the error may surface as unexpected behavior rather than a clean failure), and may only occur in production (if the test environment uses different contract versions).

With Torvyn, torvyn link catches this incompatibility before deployment. The error message identifies the conflicting versions, the specific field that causes the mismatch, and suggests upgrading the enricher to a version compiled against contract 0.2.0 or later.

This is not a theoretical benefit. Schema drift, interface mismatch, and version conflict are among the most common causes of distributed system failures. Catching them statically eliminates an entire category of production incidents.

Why Contracts Must Be the Center

A runtime that treats contracts as optional — where components can interact without formal interfaces, or where the interface is defined by convention rather than type — will always be fragile at scale. As the number of components grows, as teams evolve their components independently, and as third-party components enter the ecosystem, the only reliable coordination mechanism is a machine-checked contract.

Torvyn’s design reflects this belief. Contracts are not metadata attached to components. They are the foundation upon which everything else is built: linking, scheduling, resource transfer, capability validation, and version compatibility all flow from the contract layer.