@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.

TopicPurposeReplay
commentsViewer comments: text, author info, timestamp7 days
_reactionsEmoji reaction bursts (ephemeral)None
pollsPoll creation, vote updates, and close events1 day

Installation

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

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

OptionTypeDescription
clientNoLagSocketRequired. The injected core NoLag client the app owns and connects.
usernamestringRequired. Display name for this viewer.
avatarstringOptional avatar URL.
role'viewer' | 'moderator' | 'host'Viewer role (default 'viewer').
metadataRecord<string, unknown>Optional custom data attached to viewer presence.
appNamestringNoLag app for topic prefixes (default 'stream').
streamsstring[]Streams to join once the wrapper is ready.
maxCommentCachenumberMax comments kept in memory per stream.
reactionWindownumberReaction burst aggregation window in ms.
debugbooleanEnable wrapper debug logging (default false).
Method / PropertyDescription
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
viewerCountReactive property with the current total viewer count across all joined streams

Events: NoLagStream

EventPayloadDescription
connectednoneWebSocket connection established
disconnectedreason: stringConnection closed
reconnectednoneReconnection successful; streams are restored automatically
errorerror: ErrorUnrecoverable error occurred
viewerOnlineviewer: StreamViewerA viewer joined any stream
viewerOfflineviewer: StreamViewerA 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 / PropertyDescription
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
commentsReactive array of all comments in the local store
activePollThe currently open poll, or null if none
viewerCountNumber of viewers currently in this stream room

Events: StreamRoom

EventPayloadDescription
commentStreamCommentA comment arrived from another viewer
commentSentStreamCommentConfirmation that your own comment was delivered
reaction{ emoji: string, count: number }A reaction burst arrived; animate accordingly
pollCreatedStreamPollA new poll was opened
pollUpdatedStreamPollVote tallies updated
pollClosedStreamPollPoll closed with final results
viewerJoinedStreamViewerA viewer joined this stream room
viewerLeftStreamViewerA 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