Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/hypertekorg/hyperstack/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The SubscriptionRegistry manages WebSocket subscriptions with automatic reference counting and deduplication. Multiple consumers can subscribe to the same view, and the registry ensures only one WebSocket subscription is created.

Automatic Subscription Management

When you use view methods like use(), watch(), or watchRich(), subscriptions are automatically created and cleaned up.
const client = await HyperStack.connect(stack);

// Subscription created automatically
for await (const user of client.views.users.state.use('user-123')) {
  console.log(user);
}
// Subscription cleaned up when loop exits

SubscriptionRegistry

The registry is accessible via client.getSubscriptionRegistry() for advanced use cases.

subscribe()

Manually create a subscription.
subscribe(subscription: Subscription): UnsubscribeFn
subscription
Subscription
required
Subscription parameters
Returns: Function to unsubscribe
const registry = client.getSubscriptionRegistry();

const unsubscribe = registry.subscribe({
  view: 'users.state',
  key: 'user-123',
});

// Later...
unsubscribe();

unsubscribe()

Manually unsubscribe from a subscription.
unsubscribe(subscription: Subscription): void

getRefCount()

Get the current reference count for a subscription.
getRefCount(subscription: Subscription): number
const count = registry.getRefCount({
  view: 'users.state',
  key: 'user-123',
});
console.log(`${count} active consumers`);

getActiveSubscriptions()

Get all active subscriptions.
getActiveSubscriptions(): Subscription[]
const active = registry.getActiveSubscriptions();
console.log(`${active.length} active subscriptions`);
active.forEach(sub => {
  console.log(`- ${sub.view}:${sub.key}`);
});

clear()

Clear all subscriptions.
clear(): void
registry.clear();

Subscription Type

interface Subscription {
  view: string;
  key?: string;
  partition?: string;
  filters?: Record<string, string>;
  take?: number;
  skip?: number;
}
view
string
required
View path to subscribe to (e.g., 'users.state')
key
string
Entity key for state views. Omit for list views
partition
string
Partition identifier for multi-tenant systems
filters
Record<string, string>
Filter criteria for the subscription
take
number
Maximum number of entities to receive
skip
number
Number of entities to skip (pagination)

Reference Counting

The registry uses reference counting to deduplicate subscriptions.
// First subscription - creates WebSocket subscription
const unsub1 = registry.subscribe({ view: 'users.state', key: 'user-123' });
console.log(registry.getRefCount({ view: 'users.state', key: 'user-123' }));
// Output: 1

// Second subscription to same entity - reuses existing subscription
const unsub2 = registry.subscribe({ view: 'users.state', key: 'user-123' });
console.log(registry.getRefCount({ view: 'users.state', key: 'user-123' }));
// Output: 2

// First unsubscribe - keeps subscription alive
unsub1();
console.log(registry.getRefCount({ view: 'users.state', key: 'user-123' }));
// Output: 1

// Second unsubscribe - removes WebSocket subscription
unsub2();
console.log(registry.getRefCount({ view: 'users.state', key: 'user-123' }));
// Output: 0

Connection Management

The ConnectionManager handles WebSocket connection lifecycle and is accessible via client.getConnection().

Connection States

type ConnectionState =
  | 'disconnected'
  | 'connecting'
  | 'connected'
  | 'reconnecting'
  | 'error';

Monitoring Connection State

const unsubscribe = client.onConnectionStateChange((state, error) => {
  switch (state) {
    case 'connecting':
      console.log('Establishing connection...');
      break;
    case 'connected':
      console.log('Connected successfully');
      break;
    case 'reconnecting':
      console.log('Connection lost, reconnecting...');
      break;
    case 'error':
      console.error('Connection error:', error);
      break;
    case 'disconnected':
      console.log('Disconnected');
      break;
  }
});

Manual Connection Control

// Connect without auto-connect
const client = await HyperStack.connect(stack, {
  autoReconnect: false,
});

// Manually connect
await client.connect();

// Check connection status
if (client.isConnected()) {
  console.log('Ready to use');
}

// Manually disconnect
client.disconnect();

Subscription Lifecycle

View Subscriptions

When using typed views, subscriptions follow this lifecycle:
// 1. Create subscription
const iterator = client.views.users.state.use('user-123');

// 2. Registry increments ref count and subscribes to WebSocket
for await (const user of iterator) {
  console.log(user);
  
  // 3. Break to unsubscribe
  if (someCondition) break;
}

// 4. Registry decrements ref count and unsubscribes if count reaches 0

Manual Cleanup

const iterator = client.views.users.state.use('user-123');

// Force cleanup
await iterator.return?.();

Stream Creation

Low-level stream creation functions for advanced use cases.

createUpdateStream()

function createUpdateStream<T>(
  storage: StorageAdapter,
  subscriptionRegistry: SubscriptionRegistry,
  subscription: Subscription,
  key?: string
): AsyncIterable<Update<T>>
Creates an async iterable that yields Update<T> objects.

createRichUpdateStream()

function createRichUpdateStream<T>(
  storage: StorageAdapter,
  subscriptionRegistry: SubscriptionRegistry,
  subscription: Subscription,
  key?: string
): AsyncIterable<RichUpdate<T>>
Creates an async iterable that yields RichUpdate<T> objects with before/after snapshots.

createEntityStream()

function createEntityStream<T>(
  storage: StorageAdapter,
  subscriptionRegistry: SubscriptionRegistry,
  subscription: Subscription,
  options?: WatchOptions,
  key?: string
): AsyncIterable<T>
Creates an async iterable that yields entity objects directly.

Reconnection Behavior

HyperStack automatically reconnects with exponential backoff when the connection is lost.

Default Configuration

const DEFAULT_CONFIG = {
  reconnectIntervals: [1000, 2000, 4000, 8000, 16000], // ms
  maxReconnectAttempts: 5,
};

Custom Reconnection

const client = await HyperStack.connect(stack, {
  autoReconnect: true,
  reconnectIntervals: [500, 1000, 2000, 5000],
  maxReconnectAttempts: 10,
});

Disable Reconnection

const client = await HyperStack.connect(stack, {
  autoReconnect: false,
});

// Handle reconnection manually
client.onConnectionStateChange(async (state) => {
  if (state === 'error') {
    await new Promise(resolve => setTimeout(resolve, 3000));
    await client.connect();
  }
});

Subscription Reactivation

When reconnecting, HyperStack automatically resubscribes to all active subscriptions.
const client = await HyperStack.connect(stack);

// Create subscriptions
const stream1 = client.views.users.state.use('user-123');
const stream2 = client.views.posts.list.use();

// Connection drops and reconnects...
// Both subscriptions are automatically reactivated

Advanced Patterns

Shared Subscriptions

Share a single subscription across multiple components.
class SubscriptionManager {
  private subscriptions = new Map<string, AsyncIterable<any>>();

  subscribe<T>(view: string, key: string): AsyncIterable<T> {
    const id = `${view}:${key}`;
    
    if (!this.subscriptions.has(id)) {
      const stream = client.views[view].state.use(key);
      this.subscriptions.set(id, stream);
    }
    
    return this.subscriptions.get(id)!;
  }
}

Conditional Subscriptions

function useConditionalSubscription(userId: string | null) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!userId) return;

    const controller = new AbortController();

    (async () => {
      try {
        for await (const u of client.views.users.state.use(userId)) {
          if (controller.signal.aborted) break;
          setUser(u);
        }
      } catch (err) {
        if (!controller.signal.aborted) throw err;
      }
    })();

    return () => controller.abort();
  }, [userId]);

  return user;
}

Type Exports

import type {
  Subscription,
  UnsubscribeFn,
  ConnectionState,
  ConnectionStateCallback,
} from '@hyperstack/core';

import {
  SubscriptionRegistry,
  ConnectionManager,
} from '@hyperstack/core';

Best Practices

  1. Let the framework manage subscriptions - Use view methods instead of manual registry calls
  2. Clean up properly - Always unsubscribe when components unmount
  3. Monitor connection state - Show loading/offline UI based on connection state
  4. Handle reconnection gracefully - Don’t assume subscriptions remain active forever
  5. Use reference counting - Let multiple components subscribe to the same data safely

Next Steps

Working with Views

Learn about view subscription methods

Storage Adapters

Understand how data is stored and updated

Build docs developers (and LLMs) love