hyperstack-sdk 0.3.0

Rust SDK client for connecting to HyperStack streaming servers
Documentation

hyperstack-sdk

crates.io docs.rs License

Rust client SDK for connecting to HyperStack streaming servers.

Installation

[dependencies]
hyperstack-sdk = "0.2"

TLS Options

By default, the SDK uses rustls for TLS. You can switch to native TLS:

[dependencies]
hyperstack-sdk = { version = "0.1", default-features = false, features = ["native-tls"] }

Quick Start

use hyperstack_sdk::prelude::*;
use my_stack::{PumpfunToken, PumpfunTokenEntity};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let hs = HyperStack::connect("wss://mainnet.hyperstack.xyz").await?;
    
    // List all entities
    let tokens = hs.list::<PumpfunTokenEntity>().await;
    println!("Found {} tokens", tokens.len());
    
    // Watch for real-time updates (lazy - no .await needed)
    let mut stream = hs.watch::<PumpfunTokenEntity>();
    while let Some(update) = stream.next().await {
        match update {
            Update::Upsert { key, data } => println!("Updated {}", key),
            Update::Patch { key, data } => println!("Patched {}", key),
            Update::Delete { key } => println!("Deleted {}", key),
        }
    }
    
    Ok(())
}

The prelude module re-exports all commonly needed types including StreamExt, so you don't need separate imports from futures_util.

Lazy Streams with Chainable Operators

Streams are lazy - calling watch() returns immediately without subscribing. The subscription happens automatically on first poll. This enables ergonomic method chaining:

use std::collections::HashSet;

let watchlist: HashSet<String> = /* tokens to watch */;

let mut price_alerts = hs
    .watch_rich::<PumpfunTokenEntity>()
    .filter(move |u| watchlist.contains(u.key()))
    .filter_map(|update| match update {
        RichUpdate::Updated { before, after, .. } => {
            let prev = before.trading.last_trade_price.flatten().unwrap_or(0.0);
            let curr = after.trading.last_trade_price.flatten().unwrap_or(0.0);
            if prev > 0.0 {
                let pct = (curr - prev) / prev * 100.0;
                if pct.abs() > 0.1 {
                    return Some((after.info.name.clone(), pct));
                }
            }
            None
        }
        _ => None,
    });

while let Some((name, pct)) = price_alerts.next().await {
    println!("[PRICE] {:?} changed by {:.2}%", name, pct);
}

Available Stream Operators

Operator Description
.filter(predicate) Keep only updates matching the predicate
.filter_map(f) Filter and transform in one step
.map(f) Transform each update

All operators are chainable and return streams that support the same operators.

API Reference

HyperStack Client

// Simple connection
let hs = HyperStack::connect("wss://example.com").await?;

// With configuration
let hs = HyperStack::builder()
    .url("wss://example.com")
    .auto_reconnect(true)
    .max_reconnect_attempts(10)
    .ping_interval(Duration::from_secs(30))
    .initial_data_timeout(Duration::from_secs(5))
    .connect()
    .await?;

Core Methods

Method Returns Description
get::<E>(key).await Option<T> Get a single entity by key
list::<E>().await Vec<T> Get all entities of type E
watch::<E>() EntityStream<T> Stream all updates (lazy)
watch_key::<E>(key) EntityStream<T> Stream updates for a specific key (lazy)
watch_keys::<E>(&[keys]) EntityStream<T> Stream updates for multiple keys (lazy)
watch_rich::<E>() RichEntityStream<T> Stream with before/after values (lazy)
watch_key_rich::<E>(key) RichEntityStream<T> Rich stream for specific key (lazy)
connection_state().await ConnectionState Get current connection state
disconnect().await () Close the connection

Update Types

When streaming with watch(), you receive Update<T> variants:

pub enum Update<T> {
    Upsert { key: String, data: T },  // Full entity update
    Patch { key: String, data: T },   // Partial update (merged)
    Delete { key: String },           // Entity removed
}

Helper methods: key(), data(), is_delete(), has_data(), into_data(), into_key(), map(f)

Rich Updates (Before/After Diffs)

For tracking changes over time, use watch_rich():

pub enum RichUpdate<T> {
    Created { key: String, data: T },
    Updated { key: String, before: T, after: T, patch: Option<Value> },
    Deleted { key: String, last_known: Option<T> },
}

The Updated variant includes patch - the raw JSON of changed fields, useful for checking what specifically changed:

if update.has_patch_field("trading") {
    // The trading field was modified
}

Understanding Option<Option<T>> Fields

Generated entity types often have fields typed as Option<Option<T>>. This represents the patch semantics of HyperStack updates:

Value Meaning
None Field was not included in this update (no change)
Some(None) Field was explicitly set to null
Some(Some(value)) Field has a concrete value

This distinction matters for partial updates (patches). When the server sends a patch, only changed fields are included. An absent field means "keep the previous value", while an explicit null means "clear this field".

Working with Option<Option<T>>

// Access a nested optional field
let price = token.trading.last_trade_price.flatten().unwrap_or(0.0);

// Check if field was explicitly set (vs absent from patch)
match &token.reserves.current_price_sol {
    None => println!("Price not in this update"),
    Some(None) => println!("Price explicitly cleared"),
    Some(Some(price)) => println!("Price: {}", price),
}

// Compare values in before/after
if before.trading.last_trade_price != after.trading.last_trade_price {
    println!("Price changed!");
}

Generating a Rust SDK

Use the HyperStack CLI to generate a typed Rust SDK from your spec:

# Generate SDK crate
hs sdk create rust settlement-game

# With custom output directory
hs sdk create rust settlement-game --output ./crates/game-sdk

# With custom crate name
hs sdk create rust settlement-game --crate-name game-sdk

This generates a crate with:

generated/settlement-game-stack/
├── Cargo.toml
└── src/
    ├── lib.rs      # Re-exports
    ├── types.rs    # Data structs (with Option<Option<T>> for patchable fields)
    └── entity.rs   # Entity trait implementations

Add the generated crate to your Cargo.toml:

[dependencies]
hyperstack-sdk = "0.2"
settlement-game-stack = { path = "./generated/settlement-game-stack" }

Connection Management

Auto-Reconnection

The SDK automatically reconnects on connection loss with configurable backoff:

let hs = HyperStack::builder()
    .url("wss://example.com")
    .auto_reconnect(true)
    .reconnect_intervals(vec![
        Duration::from_secs(1),
        Duration::from_secs(2),
        Duration::from_secs(5),
        Duration::from_secs(10),
    ])
    .max_reconnect_attempts(20)
    .connect()
    .await?;

Connection State

let state = hs.connection_state().await;
match state {
    ConnectionState::Connected => println!("Connected"),
    ConnectionState::Connecting => println!("Connecting..."),
    ConnectionState::Reconnecting { attempt } => println!("Reconnecting (attempt {})", attempt),
    ConnectionState::Disconnected => println!("Disconnected"),
    ConnectionState::Error => println!("Error"),
}

Streaming Modes

Mode View Description
State Entity/state Single shared state object
List Entity/list All entities, key-value lookups
Append Entity/append Append-only event log

License

MIT