---
title: Stream SDK
description: Live streaming engagement with comments, reactions, and polls.
---

# @nolag/stream

Live streaming engagement with comments, reactions, and polls.

## Overview

`@nolag/stream` adds real-time viewer engagement to any live stream. Viewers can post comments, fire reaction bursts, and vote in polls, all delivered with sub-100ms latency. Broadcasters get live viewer counts, comment moderation hooks, and the ability to create and close polls on the fly. Comments are replayed on join so late arrivals see the full conversation; reactions and poll votes are ephemeral to keep things snappy. Your app owns one core NoLag client and injects it into `NoLagStream`; the wrapper attaches its behaviour to that connection.

### Key Features

- Threaded comment stream with 7-day replay for late joiners
- Ephemeral reaction bursts for fire-and-forget emoji animations
- Live polls with real-time vote tallying and automatic close
- Live viewer count updated as viewers join and leave
- 1-day poll result replay so viewers can see recent poll outcomes
- Automatic reconnection with state restoration

## How It Works

`NoLagStream` attaches to an injected `@nolag/js-sdk` client and maintains a lobby for viewer counts. Calling `joinStream()` creates a `StreamRoom` that subscribes to three topics: `comments` for durable comment history, `_reactions` for ephemeral emoji bursts, and `polls` for durable poll state. A `CommentStore` accumulates comments while a `PollManager` tracks the active poll and its running vote totals. The app owns the socket lifecycle; the wrapper never opens or closes it.

| Topic | Purpose | Replay |
| --- | --- | --- |
| `comments` | Viewer comments: text, author info, timestamp | 7 days |
| `_reactions` | Emoji reaction bursts (ephemeral) | None |
| `polls` | Poll creation, vote updates, and close events | 1 day |

## Installation

```bash [Terminal]
npm install @nolag/stream @nolag/js-sdk
```

One core NoLag client can back several wrapper SDKs at once, for example stream engagement, chat, and notify on a single socket, as long as each wrapper uses a distinct `appName`. Each wrapper attaches its handlers on construction and releases them with `detach()`, and never touches the socket itself. Your app owns `connect()` and `disconnect()`.

## Quick Start

```typescript [TypeScript]
import { NoLag } from '@nolag/js-sdk'
import { NoLagStream } from '@nolag/stream'

// The app owns one core client. In a browser, pass a token provider so the
// SDK can mint fresh short-lived client tokens from your backend.
const client = NoLag(async () => (await (await fetch('/api/nolag-token')).json()).token)

// Inject the client into the stream wrapper (role: 'viewer', 'moderator', or 'host')
const stream = new NoLagStream({ client, username: 'Alice', role: 'viewer' })

await client.connect()   // the app owns the connection
await stream.ready()     // wrapper setup complete

// Join a stream session
const room = await stream.joinStream('live-event-2026')

// Display live viewer count
stream.on('viewerCountChanged', ({ count }) => {
  console.log('Viewers:', count)
})

// Send a comment
await room.sendComment('This is amazing!')

// Listen for comments from other viewers
room.on('comment', (c) => {
  console.log(`[${c.author.name}]: ${c.text}`)
})

// Send a reaction burst
await room.sendReaction('🔥')

room.on('reaction', ({ emoji, count }) => {
  console.log(`${count}x ${emoji}`)
})

// Create a poll (typically broadcaster-side)
const poll = await room.createPoll({
  question: 'Which feature next?',
  options: ['Dark mode', 'Mobile app', 'API access'],
  durationMs: 60_000,
})

// Vote on the active poll
await room.votePoll(poll.pollId, 1) // vote for index 1

room.on('pollUpdated', (p) => {
  console.log('Results:', p.options.map(o => `${o.label}: ${o.votes}`))
})

// Teardown: the wrapper releases its handlers; the app closes the socket.
stream.detach()
client.disconnect()
```

## API Reference

### NoLagStream

The main class. Attaches to the injected core client, manages lobby viewer counts, and the stream room lifecycle.

#### Constructor Options

| Option | Type | Description |
| --- | --- | --- |
| `client` | `NoLagSocket` | **Required.** The injected core NoLag client the app owns and connects. |
| `username` | `string` | **Required.** Display name for this viewer. |
| `avatar` | `string` | Optional avatar URL. |
| `role` | `'viewer' \| 'moderator' \| 'host'` | Viewer role (default `'viewer'`). |
| `metadata` | `Record<string, unknown>` | Optional custom data attached to viewer presence. |
| `appName` | `string` | NoLag app for topic prefixes (default `'stream'`). |
| `streams` | `string[]` | Streams to join once the wrapper is ready. |
| `maxCommentCache` | `number` | Max comments kept in memory per stream. |
| `reactionWindow` | `number` | Reaction burst aggregation window in ms. |
| `debug` | `boolean` | Enable wrapper debug logging (default `false`). |

| Method / Property | Description |
| --- | --- |
| `ready()` | Resolves once wrapper setup completed |
| `detach()` | Release this wrapper's handlers and topics; terminal, never closes the socket |
| `joinStream(name)` | Join a live stream session; returns a `StreamRoom` instance |
| `leaveStream(name)` | Leave a stream and unsubscribe from its topics |
| `getOnlineViewers()` | Return all viewers currently online across all joined streams |
| `viewerCount` | Reactive property with the current total viewer count across all joined streams |

### Events: NoLagStream

| Event | Payload | Description |
| --- | --- | --- |
| `connected` | none | WebSocket connection established |
| `disconnected` | `reason: string` | Connection closed |
| `reconnected` | none | Reconnection successful; streams are restored automatically |
| `error` | `error: Error` | Unrecoverable error occurred |
| `viewerOnline` | `viewer: StreamViewer` | A viewer joined any stream |
| `viewerOffline` | `viewer: StreamViewer` | A viewer left any stream |
| `viewerCountChanged` | `{ count: number }` | Total viewer count changed |

### StreamRoom

Returned by `joinStream()`. Handles comments, reactions, polls, and per-stream viewer presence.

| Method / Property | Description |
| --- | --- |
| `sendComment(text)` | Publish a comment to this stream |
| `sendReaction(emoji)` | Fire an ephemeral reaction burst to all viewers |
| `createPoll(opts)` | Create a new poll with a question, options array, and optional duration |
| `votePoll(pollId, optionIndex)` | Submit a vote for an option by its zero-based index |
| `closePoll(pollId)` | Close the poll early and broadcast final results |
| `comments` | Reactive array of all comments in the local store |
| `activePoll` | The currently open poll, or `null` if none |
| `viewerCount` | Number of viewers currently in this stream room |

### Events: StreamRoom

| Event | Payload | Description |
| --- | --- | --- |
| `comment` | `StreamComment` | A comment arrived from another viewer |
| `commentSent` | `StreamComment` | Confirmation that your own comment was delivered |
| `reaction` | `{ emoji: string, count: number }` | A reaction burst arrived; animate accordingly |
| `pollCreated` | `StreamPoll` | A new poll was opened |
| `pollUpdated` | `StreamPoll` | Vote tallies updated |
| `pollClosed` | `StreamPoll` | Poll closed with final results |
| `viewerJoined` | `StreamViewer` | A viewer joined this stream room |
| `viewerLeft` | `StreamViewer` | A viewer left this stream room |
| `viewerCountChanged` | `{ count: number }` | Viewer count for this stream changed |
| `replayStart` | `{ count: number }` | Historical comment replay is beginning |
| `replayEnd` | `{ replayed: number }` | Historical comment replay is complete |
