Client Library
Events & Options
The four events, the constructor options, and the pacing and retry 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 session bootstrap, including automatic re-bootstraps |
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 recovered
TOKEN_EXPIRED emits error and then connected, and the stream carries on.
end reasons
| Reason | Meaning |
|---|---|
"no more continuations" |
The chat finished; YouTube returned a null token |
"giving up after N error(s)" |
Consecutive failures hit the limit |
"stopped" |
stop() was called |
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 |
Minimum gap between message events. 0 disables spacing |
fetchFn |
FetchFn |
globalThis.fetch |
Replacement fetch, for proxies, auth headers or tests |
messageSpacingMs
Messages are not emitted as they arrive in a batch. Each poll's messages go
into an internal queue and a consumer emits them one at a time, at least
messageSpacingMs apart — so an initial backlog of two hundred messages
trickles into your UI instead of arriving in a single frame.
Spacing only ever delays; it never speeds anything up. A stream slower than
the gap is not affected. Set 0 to deliver as fast as messages arrive:
const client = new LiveChatApiClient({ baseUrl, videoUrl }, { messageSpacingMs: 0 })Ordering is preserved regardless of the value.
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.
Pacing the poll loop
The client waits timeoutMs from each poll response, clamped:
| Value | |
|---|---|
| Minimum poll interval | 500 ms |
| Maximum poll interval | 15 000 ms |
The wait is interruptible: stop() is noticed within about 250 ms rather than
after the full delay.
Retries and recovery
| Value | |
|---|---|
| Backoff | Exponential from 500 ms |
| Backoff cap | 15 000 ms |
| Consecutive failures tolerated | 3 |
A successful poll resets the failure counter. When the limit is reached, the
client emits end with "giving up after 3 error(s)".
TOKEN_EXPIRED is handled specially: instead of counting as a failure, the
client re-bootstraps the session, emits connected again, resets the counter
and continues. Only if that re-bootstrap itself fails does it count against the
limit.
Instance properties
| Property | Type | Description |
|---|---|---|
videoId |
string |
The resolved 11-character id, available immediately after construction |
session |
SessionResponseSchema | null |
The current session; null before connect() |
session.token changes on every poll — read it for debugging, don't cache it.
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.