# Streaming

Subscribe to realtime market data and private account updates with typed events and a uniform unsubscribe contract.

Every realtime feature uses the same pattern: a `subscribe*` method takes handlers and returns an unsubscribe function. One shared WebSocket multiplexes all channels, so twenty streams still cost one connection (two when public and private are both active).

## The subscription contract

```ts
const unsubscribe = client.orders.subscribe({
	accountId,
	onEvent: (order) => console.log(order), // required: each published event
	onOpen: () => console.log("subscribed"), // optional: (re)connected
	onClose: () => console.log("disconnected"), // optional: connection dropped
	onError: (ctx) => console.error(ctx.channel, ctx.type, ctx.error), // optional
});

unsubscribe(); // idempotent; releases the channel when the last consumer leaves
```

Handlers get fully parsed, typed objects. Same shapes as the request methods, decimal strings included.

The returned unsubscribe function does not mean the channel is ready. The first `onOpen` confirms the subscription. Wait for it before a write that depends on observing the stream; it can run again after reconnect, so keep one-time writes outside that callback.

`onError` receives `SdkSubscriptionErrorContext`: `channel`, error `type` (subscription, connection token, publication handler, and so on), and the underlying `error`. A terminal server disconnect or unsubscribe reports `type` as `"disconnected"` or `"unsubscribed"`, with `error: { code, message }`, then calls `onClose`. Errors in your handlers are isolated and routed here too. A throwing `onEvent` never kills the transport.

Subscriptions are shared. Two `subscribe` calls on the same channel attach to one underlying subscription. Each gets every event.

## What you can stream

| Stream                            | Method                                                                                                      | Auth              |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------- |
| Candles                           | `client.candles.subscribe` / `subscribeInts`                                                                | public            |
| Public trades                     | `client.marketData.subscribeTrades`                                                                         | public            |
| Market overview                   | `client.marketOverview.subscribe`                                                                           | public            |
| Order book                        | `client.orderbook.createSubscription` / `subscribe`                                                         | public            |
| Heatmap buckets                   | `client.heatmap.subscribeLive`                                                                              | public            |
| Lifecycle flows                   | `client.lifecycle.subscribeOpenFlows` / `subscribeFlowDetail`                                               | public or private |
| Zipped asset supply               | `client.zipper.subscribeZippedAssetSupply`                                                                  | public            |
| Your orders                       | `client.orders.subscribe`                                                                                   | private           |
| Your triggers                     | `client.triggers.subscribe` / `subscribeEvents`                                                             | private           |
| Your trades                       | `client.trades.subscribe`                                                                                   | private           |
| Balances                          | `client.balances.subscribe`                                                                                 | private           |
| Ledger transfers                  | `client.transfers.subscribe`                                                                                | private           |
| Subaccounts / API keys / policies | `client.subaccounts.subscribe`, `client.apiKeys.subscribe`, `client.subaccounts.policies.subscribePolicies` | private           |
| Address book invalidations        | `client.addressBook.subscribeViewInvalidations`                                                             | private           |
| Profile identity                  | `client.auth.profile.subscribeIdentity`                                                                     | public            |

Private channels authenticate with the client's auth provider (JWT or API key). Without usable auth, a private subscription calls `onError` when supplied; otherwise `subscribe` throws.

The SDK reads the current JWT provider value for every realtime token request. Let the provider read its current session state rather than capturing a JWT when the client is created, so refresh and logout take effect for later subscriptions.

## Snapshot-then-stream

State streams (order book, market overview) first fetch a snapshot over HTTP, buffer publications that arrive meanwhile, then apply deltas in order. After a reconnect or an observed sequence gap, they refetch. Your `onEvent` sees a consistent view relative to the last applied snapshot or delta.

That is the whole recovery contract. A feed that stays subscribed but goes silent does not fire `onError`, does not reconnect, and does not refetch. The SDK cannot tell a dead feed from a quiet market. Applications that need continuity must reconcile after connection gaps and after a connected feed stops publishing.

```ts
const subscription = client.orderbook.createSubscription({
	symbolId,
	depth: 10,
	onEvent: (book) => render(book),
});

subscription.unsubscribe();
```

For the order book, `depth` is any integer in `[1, 500]` (values above `500` clamp to `500`). The SDK maps it onto a published channel and slices levels back to the depth you asked for. Emitted events carry that requested depth. Continuity across a silent-but-connected book is still your job: track the last `onEvent` and resync with `orderbook.get()` when it goes idle. See [Order book](https://testnet.polyester.com/docs/sdk/typescript/reference/order-book).

## Connection management

Shared realtime client: `client.realtime`.

```ts
client.realtime.isConnected; // any active websocket?
client.realtime.activeChannels; // number of attached channels
client.realtime.disconnect(); // tear down everything
client.realtime.disconnectPrivate(); // drop only the private connection (e.g. on logout)
```

You rarely call these. Connections open lazily with the first subscription and close when the last consumer unsubscribes. `auth.logout()` already disconnects private channels.

Terminal server closures remove their channels before `onError` and `onClose` run. Reconcile from an authoritative read, then subscribe again from either callback if the user has refreshed the required credentials or permissions. Calling an unsubscribe handle yourself stays silent.

> **Servers can subscribe; SSR render passes cannot**
>
> Subscriptions run in any long-lived runtime: Node, Bun, a trading bot, a backend service. The one exception is a framework SSR render pass. Opening a persistent socket while rendering one request is a mistake, so the SDK rejects it on purpose. In a web app, fetch a snapshot during render and subscribe after hydration. In a standalone server process, just subscribe. The WebSocket engine (\~300 KB) loads lazily on the first subscription.

## Ordering and reference data

Events with prices or quantities need catalog scale data to decode. Subscriptions gate on readiness: events that arrive early are queued and flushed in order once the catalog is ready. You never see a partially decoded event. That queue is bounded. If the catalog never becomes ready, or the backlog outgrows the bound, the subscription reports it through `onError` and stops instead of staying connected with a hole in the stream.

For entity streams (orders, balances, trades), pair the stream with a snapshot read (`listOpen`, `list`, and so on) when you need a complete starting state. These streams do not promise replay after a disconnect or an ambiguous mutation, so reconcile from an authoritative read before relying on local state again. The same rule applies to a connected stream that goes silent: `onOpen` staying up is not proof the feed is live.
