Search documentation

Search pages and headings

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.