Skip to content

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.


Terminal window
npm install hyperstack-typescript

No peer dependencies. Works anywhere JavaScript runs.


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 safety
for await (const round of hs.views.OreRound.latest.use()) {
console.log("Round:", round.id.round_id);
}

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 stack
const hs = await HyperStack.connect(ORE_STREAM_STACK);
// Now you have fully typed views
const rounds = await hs.views.OreRound.latest.get();

You can override the stack’s default URL if needed:

const hs = await HyperStack.connect(ORE_STREAM_STACK, {
url: "wss://custom.endpoint.com",
});
const hs = await HyperStack.connect(ORE_STREAM_STACK, {
maxEntriesPerView: 5000, // Limit entries per view (default: 10000)
});
OptionTypeDefaultDescription
urlstringstack.urlOverride the stack’s default URL
maxEntriesPerViewnumber | null10000Max entries per view before LRU eviction
validateFramesbooleanfalseValidate incoming frames against Zod schemas before storing
// Check current state
console.log(hs.connectionState);
// 'connected' | 'connecting' | 'disconnected' | 'reconnecting' | 'error'
// Listen for changes
const unsubscribe = hs.onConnectionStateChange((state) => {
console.log("Connection state:", state);
});
// Later: stop listening
unsubscribe();
await hs.disconnect();

Every view operates in one of two modes, which determines how you access data:

ModeDescriptionKey RequiredReturns
StateLookup individual entities by keyYesSingle entity (T | null)
ListAccess a collection of entitiesNoArray of entities (T[])

By default, each entity in a stack exposes two views:

View NameModeDescription
stateStateLookup any entity by its key (e.g., address)
listListCollection of all entities
// State view — get a specific round by address
const round = await hs.views.OreRound.state.get(roundAddress);
// List view — get all rounds
const rounds = await hs.views.OreRound.list.get();

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 stack
for 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 return AsyncIterable and continuously emit data as entities change. The connection stays open until you break out of the loop or abort.

MethodEmitsUse Case
.use()TSimplest — 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

The simplest streaming method. Emits the full merged entity after each change:

// List view — no key required
for 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 required
const roundAddress = "So11111111111111111111111111111111111111112";
for await (const round of hs.views.OreRound.state.use(roundAddress)) {
console.log("Round updated:", round.state.motherlode);
}

Signatures:

// State view
use(key: string, options?: WatchOptions): AsyncIterable<T>
// List view
use(options?: WatchOptions): AsyncIterable<T>

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 view
watch(key: string, options?: WatchOptions): AsyncIterable<Update<T>>
// List view
watch(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 view
watchRich(key: string, options?: WatchOptions): AsyncIterable<RichUpdate<T>>
// List view
watchRich(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 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.

MethodReturnsBehavior
.get()Promise<T>Async — waits for data if not yet loaded
.getSync()T | undefinedSync — returns immediately, undefined if not loaded

Fetches the current state. Returns a promise that resolves when data is available:

// List view — returns all entities
const rounds = await hs.views.OreRound.latest.get();
console.log(`Found ${rounds.length} rounds`);
// State view — returns single entity or null
const 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 found
get(key: string): Promise<T | null>
// List view — returns array of all entities
get(): Promise<T[]>

Returns immediately with cached data. Returns undefined if data hasn’t been loaded yet:

// List view
const rounds = hs.views.OreRound.latest.getSync();
if (rounds) {
console.log(`Cached: ${rounds.length} rounds`);
} else {
console.log("Data not yet loaded");
}
// State view
const 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[] | undefined

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

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
}
// Only receive the first 10 entities
for await (const round of hs.views.OreRound.latest.use({ take: 10 })) {
console.log("Round:", round.id.round_id);
}
// Skip first 20, take next 10
for await (const round of hs.views.OreRound.latest.use({ skip: 20, take: 10 })) {
console.log("Round:", round.id.round_id);
}

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.


Use standard AsyncIterable patterns to control streams client-side.

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

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

Every stack ships with Zod schemas alongside its TypeScript interfaces. Use them to validate data at two levels:

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

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


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.


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

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>

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 key
hs.views.OreRound.list.get() // List mode — no key

For views in state mode (keyed lookup). All methods require a key parameter:

MethodSignatureReturns
useuse(key, options?)AsyncIterable<T>
watchwatch(key, options?)AsyncIterable<Update<T>>
watchRichwatchRich(key, options?)AsyncIterable<RichUpdate<T>>
getget(key)Promise<T | null>
getSyncgetSync(key)T | null | undefined

For views in list mode (collections). No key parameter:

MethodSignatureReturns
useuse(options?)AsyncIterable<T>
watchwatch(options?)AsyncIterable<Update<T>>
watchRichwatchRich(options?)AsyncIterable<RichUpdate<T>>
getget()Promise<T[]>
getSyncgetSync()T[] | undefined

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.

Current connection state:

  • disconnected — Not connected
  • connecting — Establishing connection
  • connected — Active connection
  • reconnecting — Auto-reconnecting after failure
  • error — Connection failed

Subscribe to connection state changes. Returns unsubscribe function.

Close the WebSocket connection gracefully.