@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. Broadcasters get live viewer counts and the ability to create and close polls on the fly. Reactions are aggregated into bursts so a wave of emoji becomes one animation rather than a thousand. Your app owns one core NoLag client and injects it into NoLagStream; the wrapper attaches its behaviour to that connection.

Key Features

  • Comment stream with per-viewer author info
  • Ephemeral reaction bursts for fire-and-forget emoji animations
  • Live polls with real-time vote tallying, closed with closePoll()
  • Live viewer count updated as viewers join and leave
  • Viewer roles (viewer, moderator, host) carried in presence
  • Automatic reconnection with subscriptions and presence restored

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 viewer comments, _reactions for emoji bursts, and polls for poll creation, votes, and close events. 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.

TopicPurpose
commentsViewer comments: text, author info, timestamp
_reactionsEmoji reaction bursts
pollsPoll creation, vote updates, and close events

A viewer who joins sees comments and polls published from that moment on. A fresh subscribe or an ordinary reconnect never replays history; see Replay for the one case where the broker replays messages.

Before you start. Create an app from the nolag-stream-sdk blueprint. That seeds the room this SDK expects (live-stream) and the online lobby, and returns an app slug with a random suffix. That slug is the appName you pass to the wrapper. You can also do this in the portal: Apps, New App, pick the blueprint.

import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
const app = await api.apps.create({ name: "My App", blueprintId: "nolag-stream-sdk" });
console.log(app.slug); // e.g. "my-app-a3f9": this is your appName

const actor = await api.actors.create({ name: "web-client", actorType: "user" });
console.log(actor.accessToken); // shown once; keep it

Installation

npm install @nolag/stream @nolag/js-sdk

Shared connection. 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,
  appName: APP_SLUG, // the slug returned when you created the app
  username: 'Alice',
  role: 'viewer',
})

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

// Join the seeded stream
const room = stream.joinStream('live-stream')

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

// Send a comment. Publishers never receive their own comments back, so the
// wrapper adds it to the local store and emits `commentSent`.
room.sendComment('This is amazing!')

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

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

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

// Create a poll (typically broadcaster-side). Polls stay open until closePoll().
const poll = room.createPoll({
  question: 'Which feature next?',
  options: ['Dark mode', 'Mobile app', 'API access'],
})

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

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

// Close it when you are done
room.closePoll(poll.id)

// 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 carried in presence (default 'viewer').
metadataRecord<string, unknown>Optional custom data attached to viewer presence.
appNamestringThe app slug returned when you created the app (default 'stream', which only works if an app with exactly that slug exists).
streamsstring[]Streams to join once the wrapper is ready.
maxCommentCachenumberMax comments kept in memory per stream (default 500).
reactionWindownumberReaction burst aggregation window in ms (default 3000).
debugbooleanEnable wrapper debug logging (default false).
Method / PropertyReturnsDescription
ready()Promise<void>Resolves once wrapper setup completed
detach()voidRelease this wrapper's handlers and topics; terminal, never closes the socket
joinStream(name, opts?)StreamRoomJoin a live stream session; opts.filters limits comments and polls to matching values; throws before ready()
leaveStream(name)voidLeave a stream and unsubscribe from its topics
getRooms()StreamRoom[]All joined streams
getOnlineViewers()StreamViewer[]All viewers currently online in the lobby
viewerCountnumberCurrent lobby viewer count, including you

Events: NoLagStream

EventPayloadDescription
connectednoneWrapper setup completed on a fresh connection
disconnectedreason: stringConnection closed
reconnectingnoneThe client is attempting to reconnect
reconnectednoneReconnection successful; streams are restored automatically
errorerror: ErrorUnrecoverable error occurred
viewerOnlineviewer: StreamViewerA viewer appeared in the lobby
viewerOfflineviewer: StreamViewerA viewer left the lobby
viewerCountChangedcount: numberLobby viewer count changed

StreamRoom

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

Method / PropertyReturnsDescription
sendComment(text, opts?)StreamCommentPublish a comment to this stream; returns the optimistic local copy
getComments()StreamComment[]All comments in the local store
sendReaction(emoji)voidFire an ephemeral reaction to all viewers
createPoll(opts)PollCreate a poll from { question, options: string[] }; it stays open until closePoll()
votePoll(pollId, optionIndex)voidSubmit a vote for an option by its zero-based index
closePoll(pollId)voidClose the poll and broadcast final results
getViewers()StreamViewer[]Remote viewers present in this stream
setFilters(values, opts?)voidReplace the comment and poll filters; see Filters
commentsStreamComment[]All comments in the local store
activePollPoll | undefinedThe currently open poll, if any
viewerCountnumberViewers present in this stream, including you

A StreamComment has id, viewerId, username, avatar?, text, data?, timestamp, status, and isReplay. A Poll has id, question, options: { text, votes }[], createdBy, closed, totalVotes, and timestamp.

Events: StreamRoom

EventPayloadDescription
commentStreamCommentA comment arrived from another viewer
commentSentStreamCommentYour own comment was added to the local store and published
reactionReactionBurstReactions aggregated over reactionWindow: { emoji, count, windowStart, windowEnd }
pollCreatedPollA new poll was opened
pollUpdatedPollVote tallies updated
pollClosedPollPoll closed with final results
viewerJoinedStreamViewerA viewer joined this stream
viewerLeftStreamViewerA viewer left this stream
viewerCountChangedcount: numberViewer count for this stream changed