TypeScript SDK
The hyperstack-typescript SDK is a framework-agnostic client for consuming streaming data from Hyperstack. It uses AsyncIterable-based streaming and works in any JavaScript environment — Node.js, browsers, Deno, or Bun.
Installation
Section titled “Installation”npm install hyperstack-typescriptNo peer dependencies. Works anywhere JavaScript runs.
Quick Start
Section titled “Quick Start”import { HyperStack } from "hyperstack-typescript";import { ORE_STREAM_STACK, type OreRound } from "hyperstack-stacks/ore";
// Connect using the stack (URL is embedded in the stack definition)const hs = await HyperStack.connect(ORE_STREAM_STACK);
// Stream entities with full type safetyfor await (const round of hs.views.OreRound.latest.use()) { console.log("Round:", round.id.round_id);}Connection
Section titled “Connection”Connect with Stack Definition
Section titled “Connect with Stack Definition”Each stack definition includes its own URL, so connecting is simple:
import { HyperStack } from "hyperstack-typescript";import { ORE_STREAM_STACK } from "hyperstack-stacks/ore";
// Stack includes its URL - just pass the stackconst hs = await HyperStack.connect(ORE_STREAM_STACK);
// Now you have fully typed viewsconst rounds = await hs.views.OreRound.latest.get();Override Stack URL
Section titled “Override Stack URL”You can override the stack’s default URL if needed:
const hs = await HyperStack.connect(ORE_STREAM_STACK, { url: "wss://custom.endpoint.com",});Connection Options
Section titled “Connection Options”const hs = await HyperStack.connect(ORE_STREAM_STACK, { maxEntriesPerView: 5000, // Limit entries per view (default: 10000)});| Option | Type | Default | Description |
|---|---|---|---|
url | string | stack.url | Override the stack’s default URL |
maxEntriesPerView | number | null | 10000 | Max entries per view before LRU eviction |
validateFrames | boolean | false | Validate incoming frames against Zod schemas before storing |
Connection State
Section titled “Connection State”// Check current stateconsole.log(hs.connectionState);// 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'error'
// Listen for changesconst unsubscribe = hs.onConnectionStateChange((state) => { console.log("Connection state:", state);});
// Later: stop listeningunsubscribe();Disconnect
Section titled “Disconnect”await hs.disconnect();View Modes
Section titled “View Modes”Every view operates in one of two modes, which determines how you access data:
| Mode | Description | Key Required | Returns |
|---|---|---|---|
| State | Lookup individual entities by key | Yes | Single entity (T | null) |
| List | Access a collection of entities | No | Array of entities (T[]) |
Default Views
Section titled “Default Views”By default, each entity in a stack exposes two views:
| View Name | Mode | Description |
|---|---|---|
state | State | Lookup any entity by its key (e.g., address) |
list | List | Collection of all entities |
// State view — get a specific round by addressconst round = await hs.views.OreRound.state.get(roundAddress);
// List view — get all roundsconst rounds = await hs.views.OreRound.list.get();Custom Views
Section titled “Custom Views”Stacks can define additional views beyond the defaults. For example, the ORE stack includes a custom latest view that streams only recent rounds. Custom views are defined using the Rust DSL when building your stack — see Stack Definitions for details.
Custom views are accessed the same way as default views:
// Custom list view defined in the ORE stackfor await (const round of hs.views.OreRound.latest.use()) { console.log("Recent round:", round.id.round_id);}Each view type supports both streaming (real-time updates) and one-shot (point-in-time snapshot) access patterns.
Streaming Methods
Section titled “Streaming Methods”Streaming methods return AsyncIterable and continuously emit data as entities change. The connection stays open until you break out of the loop or abort.
| Method | Emits | Use Case |
|---|---|---|
.use() | T | Simplest — just the current entity state after each change |
.watch() | Update<T> | When you need to know the operation type (upsert/patch/delete) |
.watchRich() | RichUpdate<T> | When you need before/after comparison |
.use() — Stream Merged Entities
Section titled “.use() — Stream Merged Entities”The simplest streaming method. Emits the full merged entity after each change:
// List view — no key requiredfor await (const round of hs.views.OreRound.latest.use()) { console.log("Round:", round.id.round_id); console.log("Motherlode:", round.state.motherlode);}
// State view — key requiredconst roundAddress = "So11111111111111111111111111111111111111112";for await (const round of hs.views.OreRound.state.use(roundAddress)) { console.log("Round updated:", round.state.motherlode);}Signatures:
// State viewuse(key: string, options?: WatchOptions): AsyncIterable<T>
// List viewuse(options?: WatchOptions): AsyncIterable<T>.watch() — Stream Raw Updates
Section titled “.watch() — Stream Raw Updates”Use when you need to know what operation occurred:
for await (const update of hs.views.OreRound.latest.watch()) { switch (update.type) { case "upsert": console.log("Created or replaced:", update.data); break; case "patch": console.log("Partial update:", update.data); break; case "delete": console.log("Deleted:", update.key); break; }}Signatures:
// State viewwatch(key: string, options?: WatchOptions): AsyncIterable<Update<T>>
// List viewwatch(options?: WatchOptions): AsyncIterable<Update<T>>Update types:
type Update<T> = | { type: "upsert"; key: string; data: T } // Full entity create/replace | { type: "patch"; key: string; data: Partial<T> } // Partial update | { type: "delete"; key: string }; // Entity removed.watchRich() — Stream with Before/After Diffs
Section titled “.watchRich() — Stream with Before/After Diffs”Use when you need to compare the previous and new state:
for await (const update of hs.views.OreRound.latest.watchRich()) { switch (update.type) { case "created": console.log("New entity:", update.data); break; case "updated": console.log(`Changed: ${update.before.state.motherlode} → ${update.after.state.motherlode}`); break; case "deleted": console.log("Removed:", update.lastKnown); break; }}Signatures:
// State viewwatchRich(key: string, options?: WatchOptions): AsyncIterable<RichUpdate<T>>
// List viewwatchRich(options?: WatchOptions): AsyncIterable<RichUpdate<T>>RichUpdate types:
type RichUpdate<T> = | { type: "created"; key: string; data: T } | { type: "updated"; key: string; before: T; after: T; patch?: unknown } | { type: "deleted"; key: string; lastKnown?: T };One-Shot Methods
Section titled “One-Shot Methods”One-shot methods return a point-in-time snapshot without subscribing to updates. Use these when you need the current state but don’t need real-time streaming.
| Method | Returns | Behavior |
|---|---|---|
.get() | Promise<T> | Async — waits for data if not yet loaded |
.getSync() | T | undefined | Sync — returns immediately, undefined if not loaded |
.get() — Async Snapshot
Section titled “.get() — Async Snapshot”Fetches the current state. Returns a promise that resolves when data is available:
// List view — returns all entitiesconst rounds = await hs.views.OreRound.latest.get();console.log(`Found ${rounds.length} rounds`);
// State view — returns single entity or nullconst round = await hs.views.OreRound.state.get(roundAddress);if (round) { console.log("Round:", round.id.round_id);}Signatures:
// State view — returns single entity or null if not foundget(key: string): Promise<T | null>
// List view — returns array of all entitiesget(): Promise<T[]>.getSync() — Synchronous Snapshot
Section titled “.getSync() — Synchronous Snapshot”Returns immediately with cached data. Returns undefined if data hasn’t been loaded yet:
// List viewconst rounds = hs.views.OreRound.latest.getSync();if (rounds) { console.log(`Cached: ${rounds.length} rounds`);} else { console.log("Data not yet loaded");}
// State viewconst round = hs.views.OreRound.state.getSync(roundAddress);Signatures:
// State view — returns entity, null (not found), or undefined (not loaded)getSync(key: string): T | null | undefined
// List view — returns array or undefined (not loaded)getSync(): T[] | undefinedStore Size Limits
Section titled “Store Size Limits”Each view maintains an in-memory store of entities. By default, stores are limited to 10,000 entries to prevent memory issues on long-running clients. When the limit is reached, oldest entries are evicted (LRU).
const hs = await HyperStack.connect(ORE_STREAM_STACK, { maxEntriesPerView: 5000, // Custom limit});To disable limiting (not recommended for production):
const hs = await HyperStack.connect(ORE_STREAM_STACK, { maxEntriesPerView: null, // Unlimited});Subscription Options
Section titled “Subscription Options”The streaming methods (.use(), .watch(), .watchRich()) accept options for pagination and validation:
interface WatchOptions { take?: number; // Limit number of entities skip?: number; // Skip first N entities schema?: Schema<T>; // Validate and filter entities with a Zod schema}Limit Results
Section titled “Limit Results”// Only receive the first 10 entitiesfor await (const round of hs.views.OreRound.latest.use({ take: 10 })) { console.log("Round:", round.id.round_id);}Pagination
Section titled “Pagination”// Skip first 20, take next 10for await (const round of hs.views.OreRound.latest.use({ skip: 20, take: 10 })) { console.log("Round:", round.id.round_id);}Server-Side Filtering
Section titled “Server-Side Filtering”For server-side filtering beyond pagination, use custom views defined in your stack. Custom views apply filters, sorting, and limits at the server level, reducing bandwidth before data reaches the client.
See Filtering Feeds for details on all filtering options, or Stack Definitions for how to define custom views using the Rust DSL.
Stream Control
Section titled “Stream Control”Use standard AsyncIterable patterns to control streams client-side.
Stop on Condition
Section titled “Stop on Condition”for await (const update of hs.views.OreRound.latest.watch()) { if (update.type === "upsert") { const round = update.data; if (round.state.motherlode && round.state.motherlode > 1_000_000_000) { console.log("Found high-value round:", round.id.round_id); break; } }}Cancellable Streams
Section titled “Cancellable Streams”Use an AbortController to cancel from outside the loop:
const controller = new AbortController();setTimeout(() => controller.abort(), 30_000); // Cancel after 30s
try { for await (const update of hs.views.OreRound.latest.watch()) { if (controller.signal.aborted) break; console.log("Update:", update.data); }} catch (e) { if (!controller.signal.aborted) throw e;}Client-Side Filtering
Section titled “Client-Side Filtering”for await (const update of hs.views.OreRound.latest.watch()) { if (update.type !== "upsert") continue; if ((update.data.metrics.deploy_count ?? 0) < 100) continue;
console.log("Active round:", update.data.id.round_id);}Schema Validation
Section titled “Schema Validation”Every stack ships with Zod schemas alongside its TypeScript interfaces. Use them to validate data at two levels:
Frame Validation
Section titled “Frame Validation”Enable validateFrames on connect to automatically drop malformed data before it enters your local store:
const hs = await HyperStack.connect(ORE_STREAM_STACK, { validateFrames: true,});Query-Level Validation
Section titled “Query-Level Validation”Pass a schema to .use() to filter out entities that don’t match:
import { OreRoundCompletedSchema } from "hyperstack-stacks/ore";
// Only emit rounds where every field is presentfor await (const round of hs.views.OreRound.latest.use({ schema: OreRoundCompletedSchema,})) { console.log(round.id.round_id, round.state.motherlode);}See Schema Validation for the full guide on generated schemas, custom schemas, and the “Completed” schema pattern.
Resolved Data
Section titled “Resolved Data”Hyperstack can automatically enrich your entities with data that doesn’t live on-chain. For example, the ORE stack includes token metadata (name, symbol, decimals, logo) resolved server-side from the DAS API:
for await (const round of hs.views.OreRound.latest.use()) { // Token metadata is resolved automatically — no extra API calls console.log(round.ore_metadata?.name); // "Ore" console.log(round.ore_metadata?.symbol); // "ORE" console.log(round.ore_metadata?.decimals); // 11}Resolved data arrives as part of the entity alongside on-chain fields. Your client code reads it the same way — no distinction between on-chain and resolved data.
See Resolvers for how to add resolved data when building your own stack.
Error Handling
Section titled “Error Handling”import { HyperStack, HyperStackError } from "hyperstack-typescript";import { ORE_STREAM_STACK } from "hyperstack-stacks/ore";
try { const hs = await HyperStack.connect(ORE_STREAM_STACK);} catch (error) { if (error instanceof HyperStackError) { console.error("Hyperstack error:", error.message); console.error("Code:", error.code); } else { throw error; }}API Reference
Section titled “API Reference”HyperStack.connect(stack, options?)
Section titled “HyperStack.connect(stack, options?)”Establishes a WebSocket connection to a Hyperstack stack.
Parameters:
stack— Stack definition (includes URL and typed views)options.url— Override the stack’s default URL (optional)options.maxEntriesPerView— Max entries per view before LRU eviction (optional)options.validateFrames— Validate incoming frames against Zod schemas (optional)
Returns: Promise<HyperStack>
hs.views
Section titled “hs.views”Typed views interface based on your stack definition. Access pattern:
hs.views.<entity>.<viewName>.<method>()
// Examples with default views:hs.views.OreRound.state.get(key) // State mode — requires keyhs.views.OreRound.list.get() // List mode — no keyState Mode Methods
Section titled “State Mode Methods”For views in state mode (keyed lookup). All methods require a key parameter:
| Method | Signature | Returns |
|---|---|---|
use | use(key, options?) | AsyncIterable<T> |
watch | watch(key, options?) | AsyncIterable<Update<T>> |
watchRich | watchRich(key, options?) | AsyncIterable<RichUpdate<T>> |
get | get(key) | Promise<T | null> |
getSync | getSync(key) | T | null | undefined |
List Mode Methods
Section titled “List Mode Methods”For views in list mode (collections). No key parameter:
| Method | Signature | Returns |
|---|---|---|
use | use(options?) | AsyncIterable<T> |
watch | watch(options?) | AsyncIterable<Update<T>> |
watchRich | watchRich(options?) | AsyncIterable<RichUpdate<T>> |
get | get() | Promise<T[]> |
getSync | getSync() | T[] | undefined |
WatchOptions
Section titled “WatchOptions”Options for streaming methods:
interface WatchOptions { take?: number; // Limit number of entities skip?: number; // Skip first N entities schema?: Schema<T>; // Validate and filter entities with a Zod schema}See Schema Validation for details on using schemas to filter and validate streamed data.
hs.connectionState
Section titled “hs.connectionState”Current connection state:
disconnected— Not connectedconnecting— Establishing connectionconnected— Active connectionreconnecting— Auto-reconnecting after failureerror— Connection failed
hs.onConnectionStateChange(callback)
Section titled “hs.onConnectionStateChange(callback)”Subscribe to connection state changes. Returns unsubscribe function.
hs.disconnect()
Section titled “hs.disconnect()”Close the WebSocket connection gracefully.
Next Steps
Section titled “Next Steps”- Schema Validation — Zod schemas for runtime validation and filtering
- React SDK — Hooks and providers for React apps
- Rust SDK — Native Rust client
- Resolvers — Enrich entities with token metadata and computed fields
- Build Your Own Stack — Create custom data streams