Client Library
Events & Options
The four events, the constructor options, and the reconnection, rate-limit and pacing behaviour the client implements for you.
Events
on() is chainable and may be called more than once per event — every
registered listener is invoked.
| Event | Payload | When |
|---|---|---|
connected |
none | After each successful connection, including automatic reconnects |
message |
MessageSchema |
Once per message, in order |
error |
ApiHttpError |
On every failure, including ones the client then recovers from |
end |
string (reason) |
Exactly once, when the stream is over |
client
.on('connected', () => console.log('connected'))
.on('message', (message) => render(message))
.on('error', (error) => console.error(error.code, error.message))
.on('end', (reason) => console.log('ended:', reason))error is not fatal
The client emits error and then decides whether to continue. Receiving one
does not mean the stream stopped — wait for end to know that. A rate limit,
for instance, emits error, waits, reconnects, and emits connected again.
end reasons
| Reason | Meaning |
|---|---|
"no more continuations" |
The chat finished; YouTube returned no next token |
"room released" |
The server stopped polling because nobody was watching |
"giving up after N error(s)" |
Consecutive failures hit the limit |
"giving up after N rate limit(s)" |
Rate limits hit their separate limit |
"stopped" |
stop() was called |
The first two come from the server as an end frame; the rest are the
client's own decisions. end fires exactly once per stream, whichever path
got there.
Options
new LiveChatApiClient(
{ baseUrl, videoUrl },
{ messageSpacingMs: 0, fetchFn: myFetch },
)| Option | Type | Default | Description |
|---|---|---|---|
messageSpacingMs |
number |
500 |
The idle gap between message events. 0 disables spacing |
fetchFn |
FetchFn |
globalThis.fetch |
Replacement fetch, for proxies, auth headers or tests |
messageSpacingMs
Messages do not go straight to your listeners. They pass through an internal queue, and a consumer emits them one at a time — so a burst of arrivals trickles into your UI instead of landing in a single frame. Bursts are normal: 50 backfilled messages on connect, and several messages per upstream poll on a busy chat.
Spacing is adaptive, and messageSpacingMs is the ceiling, not a fixed
interval. When a backlog exists the consumer works to clear it within about
two seconds, so the gap shrinks as the queue deepens:
| Queued | Effective gap |
|---|---|
| 0–3 | 500 ms (the idle pace) |
| 10 | ~200 ms |
| 50 | ~40 ms |
The property this buys you is a bound on lateness rather than on rate: the display settles roughly two seconds behind live at any arrival rate, instead of falling permanently further behind. A fixed 500 ms gap would cap delivery at two messages per second, which any moderately busy chat exceeds.
A 50-message backfill therefore drains in about three seconds, not twenty-five.
Set 0 to deliver as fast as messages arrive:
const client = new LiveChatApiClient({ baseUrl, videoUrl }, { messageSpacingMs: 0 })Ordering is preserved regardless of the value.
Above roughly 60 messages/second the gap hits its 16 ms floor and the queue grows again. Nothing is dropped — messages are delivered late rather than lost.
fetchFn
import type { FetchFn } from '@gettersethya/yt-livechat-client'
const authorized: FetchFn = (url, init) =>
fetch(url, { ...init, headers: { ...init.headers, Authorization: token } })
const client = new LiveChatApiClient({ baseUrl, videoUrl }, { fetchFn: authorized })Useful for putting the API behind your own gateway, or for injecting a stub in
tests. This is why the client parses SSE frames off a fetch body rather than
using EventSource, which cannot be replaced or configured this way.
Reconnection
The client holds a cursor — the id of the last message it received — and
sends it as Last-Event-ID on every reconnect, so the server resumes from
exactly that point. Nothing is lost or duplicated across a drop.
A server that simply closes the connection is treated as a reconnect, not a failure, and costs nothing from either budget below. It still waits before reconnecting: a tight reconnect loop would drive a fresh bootstrap on the server every time around.
Retries and rate limits
There are two independent budgets, because a rate limit is not a broken stream.
| Ordinary errors | Rate limits | |
|---|---|---|
| Tolerated | 3 consecutive | 8 consecutive |
| Delay | Exponential from 500 ms, capped at 15 000 ms | Retry-After, or 30 s when absent |
| Reset by | A successful frame | A successful reconnect |
A 429 carries a Retry-After header, which the client reads in both of its
legal forms and exposes as error.retryAfterMs. A RATE_LIMITED error frame
carries no headers, so the 30-second fallback applies.
Keeping these separate matters: on one shared budget, three rate limits inside a few seconds would retire the stream permanently, and reconnecting on the ordinary 500 ms backoff would hammer a server that has just reported it is being throttled upstream — each reconnect driving the very bootstrap that is being rate-limited. See Upstream Pacing.
Instance properties
| Property | Type | Description |
|---|---|---|
videoId |
string |
The resolved 11-character id, available immediately after construction |
init |
StreamInitSchema | null |
Metadata from the last init frame; null before connect() |
init carries videoId, chatType and the cursor the connection started
from. It is refreshed on every reconnect.
Typing against the interface
import type { ILiveChatApiClient } from '@gettersethya/yt-livechat-client'
function attach(client: ILiveChatApiClient) {
client.on('message', (message) => render(message))
}Depending on ILiveChatApiClient rather than the concrete class keeps a fake
substitutable in tests.