@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
npm install @nolag/stream @nolag/js-sdkOne 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
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 |