Skip to main content

Component Development Overview

TAHO components are self-contained units of functionality that:
  • Run in secure WebAssembly sandboxes
  • Communicate through well-defined interfaces (WIT)
  • Deploy instantly across the federated mesh
  • Scale automatically based on demand
Best Practice: Start simple with a single-purpose component, then compose multiple components for complex applications.

Development Workflow

1

Define Interface

Create a WIT file defining your component’s API
2

Implement Logic

Write business logic in your preferred language
3

Build Component

Compile to WebAssembly component format
4

Test Locally

Use TAHO dev tools for rapid iteration
5

Deploy to Mesh

Push to production with zero downtime

Creating Your First Component

Let’s build a real-world example: an image processing service.

1. Define the Interface

Create image-processor.wit:
package taho:image-processor@0.1.0;

interface types {
  // Image formats we support
  enum image-format {
    jpeg,
    png,
    webp,
    avif,
  }
  
  // Processing operations
  enum operation {
    resize,
    rotate,
    blur,
    sharpen,
    grayscale,
    thumbnail,
  }
  
  // Processing parameters
  record resize-params {
    width: u32,
    height: u32,
    maintain-aspect: bool,
  }
  
  record rotate-params {
    degrees: float32,
  }
  
  record blur-params {
    radius: u32,
  }
  
  variant processing-params {
    resize(resize-params),
    rotate(rotate-params),
    blur(blur-params),
    sharpen,
    grayscale,
    thumbnail(u32), // max dimension
  }
  
  // Results
  record processed-image {
    data: list<u8>,
    format: image-format,
    width: u32,
    height: u32,
  }
  
  // Errors
  variant processing-error {
    invalid-format,
    corrupt-image,
    unsupported-operation,
    out-of-memory,
    processing-failed(string),
  }
}

interface processor {
  use types.{image-format, processing-params, processed-image, processing-error};
  
  // Process a single image
  process-image: func(
    image-data: list<u8>,
    format: image-format,
    operations: list<processing-params>
  ) -> result<processed-image, processing-error>;
  
  // Batch process multiple images
  process-batch: func(
    images: list<tuple<list<u8>, image-format>>,
    operations: list<processing-params>
  ) -> list<result<processed-image, processing-error>>;
  
  // Get supported formats
  supported-formats: func() -> list<image-format>;
  
  // Validate image without processing
  validate-image: func(
    image-data: list<u8>,
    format: image-format
  ) -> result<tuple<u32, u32>, processing-error>; // returns width, height
}

// Component world
world image-processor-component {
  export processor;
  
  // Import capabilities
  import wasi:logging/logging;
  import wasi:filesystem/types;
  import wasi:io/streams;
}

2. Set Up Your Project

Create Cargo.toml:
[package]
name = "image-processor"
version = "0.1.0"
edition = "2021"

[dependencies]
wit-bindgen = "0.16.0"
image = { version = "0.24", default-features = false, features = ["jpeg", "png", "webp"] }
bytes = "1.5"
tracing = "0.1"

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

[profile.release]
opt-level = "z"     # Optimize for size
lto = true          # Enable Link Time Optimization
strip = true        # Strip symbols

[package.metadata.component]
package = "taho:image-processor"

[package.metadata.component.dependencies]

3. Implement the Component

use image::{DynamicImage, ImageFormat, imageops::FilterType};
use wit_bindgen::generate;

generate!({
    world: "image-processor-component",
    exports: {
        "taho:image-processor/processor": ImageProcessor,
    },
});

struct ImageProcessor;

impl exports::taho::image_processor::processor::Guest for ImageProcessor {
    fn process_image(
        image_data: Vec<u8>,
        format: ImageFormat,
        operations: Vec<ProcessingParams>,
    ) -> Result<ProcessedImage, ProcessingError> {
        // Load image
        let mut img = load_image(&image_data, format)?;
        
        // Apply operations in sequence
        for op in operations {
            img = apply_operation(img, op)?;
        }
        
        // Encode result
        let (data, width, height) = encode_image(img, format)?;
        
        Ok(ProcessedImage {
            data,
            format,
            width,
            height,
        })
    }
    
    fn process_batch(
        images: Vec<(Vec<u8>, ImageFormat)>,
        operations: Vec<ProcessingParams>,
    ) -> Vec<Result<ProcessedImage, ProcessingError>> {
        images
            .into_iter()
            .map(|(data, format)| Self::process_image(data, format, operations.clone()))
            .collect()
    }
    
    fn supported_formats() -> Vec<ImageFormat> {
        vec![
            ImageFormat::Jpeg,
            ImageFormat::Png,
            ImageFormat::WebP,
        ]
    }
    
    fn validate_image(
        image_data: Vec<u8>,
        format: ImageFormat,
    ) -> Result<(u32, u32), ProcessingError> {
        let img = load_image(&image_data, format)?;
        Ok((img.width(), img.height()))
    }
}

// Helper functions
fn load_image(data: &[u8], format: ImageFormat) -> Result<DynamicImage, ProcessingError> {
    let format = match format {
        ImageFormat::Jpeg => image::ImageFormat::Jpeg,
        ImageFormat::Png => image::ImageFormat::Png,
        ImageFormat::WebP => image::ImageFormat::WebP,
        _ => return Err(ProcessingError::InvalidFormat),
    };
    
    image::load_from_memory_with_format(data, format)
        .map_err(|_| ProcessingError::CorruptImage)
}

fn apply_operation(
    img: DynamicImage,
    operation: ProcessingParams,
) -> Result<DynamicImage, ProcessingError> {
    Ok(match operation {
        ProcessingParams::Resize(params) => {
            if params.maintain_aspect {
                img.resize(
                    params.width,
                    params.height,
                    FilterType::Lanczos3,
                )
            } else {
                img.resize_exact(
                    params.width,
                    params.height,
                    FilterType::Lanczos3,
                )
            }
        }
        ProcessingParams::Rotate(params) => {
            img.rotate90() // Simplified - real implementation would handle arbitrary angles
        }
        ProcessingParams::Blur(params) => {
            img.blur(params.radius as f32)
        }
        ProcessingParams::Sharpen => {
            img.unsharpen(1.5, 1)
        }
        ProcessingParams::Grayscale => {
            img.grayscale()
        }
        ProcessingParams::Thumbnail(max_dim) => {
            img.thumbnail(max_dim, max_dim)
        }
    })
}

fn encode_image(
    img: DynamicImage,
    format: ImageFormat,
) -> Result<(Vec<u8>, u32, u32), ProcessingError> {
    let mut buffer = Vec::new();
    let format = match format {
        ImageFormat::Jpeg => image::ImageFormat::Jpeg,
        ImageFormat::Png => image::ImageFormat::Png,
        ImageFormat::WebP => image::ImageFormat::WebP,
        _ => return Err(ProcessingError::UnsupportedOperation),
    };
    
    img.write_to(&mut buffer, format)
        .map_err(|e| ProcessingError::ProcessingFailed(e.to_string()))?;
        
    Ok((buffer, img.width(), img.height()))
}

4. Build the Component

# Add wasm32-wasi target
rustup target add wasm32-wasi

# Install cargo-component
cargo install cargo-component

# Build the component
cargo component build --release

# Output: target/wasm32-wasi/release/image_processor.wasm

5. Test Your Component

Create a test harness:
// tests/integration_test.rs
#[test]
fn test_image_resize() {
    // Load test image
    let test_image = include_bytes!("test-image.jpg");
    
    // Create component instance
    let component = ImageProcessor::new();
    
    // Test resize operation
    let result = component.process_image(
        test_image.to_vec(),
        ImageFormat::Jpeg,
        vec![ProcessingParams::Resize(ResizeParams {
            width: 100,
            height: 100,
            maintain_aspect: true,
        })],
    );
    
    assert!(result.is_ok());
    let processed = result.unwrap();
    assert_eq!(processed.width, 100);
}

Advanced Component Patterns

State Management

Components can maintain state between calls:
struct StatefulComponent {
    cache: HashMap<String, ProcessedImage>,
    metrics: ProcessingMetrics,
}

impl StatefulComponent {
    fn new() -> Self {
        Self {
            cache: HashMap::new(),
            metrics: ProcessingMetrics::default(),
        }
    }
    
    fn process_with_cache(
        &mut self,
        key: String,
        image_data: Vec<u8>,
        operations: Vec<ProcessingParams>,
    ) -> Result<ProcessedImage, ProcessingError> {
        // Check cache
        if let Some(cached) = self.cache.get(&key) {
            self.metrics.cache_hits += 1;
            return Ok(cached.clone());
        }
        
        // Process and cache
        let result = self.process_image(image_data, operations)?;
        self.cache.insert(key, result.clone());
        self.metrics.processed += 1;
        
        Ok(result)
    }
}

Resource Handles

Manage external resources with handles:
resource gpu-context {
  constructor(device-id: u32);
  process-with-gpu: func(image: list<u8>) -> result<list<u8>, gpu-error>;
  get-memory-usage: func() -> u64;
}

interface gpu-accelerated {
  create-context: func(device-id: u32) -> gpu-context;
}

Streaming Large Data

Handle large files with streaming:
resource image-stream {
  write-chunk: func(data: list<u8>) -> result<_, stream-error>;
  finish: func() -> result<processed-image, processing-error>;
  abort: func();
}

interface streaming-processor {
  create-stream: func(
    format: image-format,
    operations: list<processing-params>
  ) -> image-stream;
}

Component Configuration

Runtime Configuration

record config {
  max-image-size: u32,
  enable-gpu: bool,
  cache-size: u32,
  timeout-seconds: u32,
}

interface configurable {
  configure: func(config: config) -> result<_, config-error>;
  get-config: func() -> config;
}

Environment Variables

use std::env;

impl Component {
    fn new() -> Self {
        let max_size = env::var("MAX_IMAGE_SIZE")
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or(10_000_000); // 10MB default
            
        Self { max_size }
    }
}

Performance Optimization

// Pre-allocate buffers
struct ImageProcessor {
    buffer: Vec<u8>,
}

impl ImageProcessor {
    fn new() -> Self {
        Self {
            buffer: Vec::with_capacity(1024 * 1024), // 1MB
        }
    }
    
    fn process(&mut self, data: &[u8]) -> Result<Vec<u8>, Error> {
        self.buffer.clear();
        self.buffer.extend_from_slice(data);
        // Process in-place
        Ok(self.buffer.clone())
    }
}

Deployment

Local Testing

# Test with TAHO dev server
taho dev image-processor.wasm --watch

# Test endpoint
curl -X POST http://localhost:8080/process \
  -F "image=@test.jpg" \
  -F 'operations=[{"resize": {"width": 200, "height": 200}}]'

Production Deployment

# Deploy to TAHO mesh
taho deploy image-processor.wasm \
  --name image-service \
  --replicas 10 \
  --memory 256MB \
  --cpu 0.5

# Configure auto-scaling
taho autoscale image-service \
  --min 5 \
  --max 100 \
  --target-cpu 70

Best Practices

  • Keep components focused on a single responsibility
  • Design for statelessness when possible
  • Use clear, descriptive interface names
  • Version your interfaces properly
  • Document expected behavior
  • Use Result types for fallible operations
  • Provide meaningful error messages
  • Don’t panic - return errors instead
  • Log errors for debugging
  • Consider retry strategies
  • Minimize allocations
  • Use streaming for large data
  • Profile your components
  • Consider caching strategies
  • Optimize for common cases
  • Validate all inputs
  • Set resource limits
  • Use capability-based security
  • Avoid storing sensitive data
  • Follow principle of least privilege

Next Steps