@gettersethya/yt-livechat-client
v0.5.1
Published
TypeScript client for the yt-livechat-api server. It handles session bootstrap, polling, token discipline, pacing, and re-bootstrap on token expiry. It never talks to YouTube directly.
Readme
@gettersethya/yt-livechat-client
TypeScript client for the yt-livechat-api server. It handles session bootstrap, polling, token discipline, pacing, and re-bootstrap on token expiry. It never talks to YouTube directly.
Requirements
- A running yt-livechat-api server (default
http://localhost:3000) - Any environment with
fetch— browsers, Node.js 18+, Bun, or Deno. The client only usesglobalThis.fetch(overridable via thefetchFnoption) andsetTimeout, so it runs in the browser as long as the server is reachable (the server sends CORS headers).
Install
npm install @gettersethya/yt-livechat-clientUsage
import { LiveChatApiClient } from '@gettersethya/yt-livechat-client'
const client = new LiveChatApiClient({
baseUrl: 'http://localhost:3000',
videoUrl: 'https://www.youtube.com/watch?v=zvwJ29RFVww', // full URL or bare id
})
client.on('connected', () => console.log('connected:', client.videoId))
client.on('message', (message) => console.log(`${message.author}: ${message.message}`))
client.on('error', (error) => console.error(error.code, error.message))
client.on('end', (reason) => console.log('ended:', reason))
await client.connect()
await client.start()Call client.stop() at any time to stop the loop; it only sets a flag and never blocks.
Events
| Event | Payload |
| ----------- | -------------------------------------- |
| connected | none |
| message | normalized chat message (§5 of SPECS) |
| error | ApiHttpError (code, message, status) |
| end | reason string, emitted exactly once |
End reasons: "no more continuations", "giving up after N error(s)", "stopped".
message events are delivered sequentially: every poll's batch is enqueued
into an internal effect Queue and a consumer fiber emits them one at a time,
in order. Messages are spaced at least messageSpacingMs apart (constructor
option, default 500 ms; pass 0 to disable) so a large initial batch doesn't
flood the UI. Spacing only delays when messages arrive faster than the gap —
naturally slower streams are not slowed further. On a natural end the queue is
drained fully before end fires; calling stop() interrupts the consumer, so
queued-but-undelivered messages are dropped.
Rendering message bodies (message vs parts)
Every message carries two views of its body:
message— flattened plain text. Emoji runs become their shortcut (:crown:,:yt:). Use it for logs, notifications, and plain-text consumers.parts— the structured body; render from this for chat UI. Each part is{ kind: 'text', text }or{ kind: 'emoji', shortcut, emoji_id, is_custom, mapped_unicode, thumbnails }.
Render rules per part:
kind === 'text'→ rendertextas a normal text node.kind === 'emoji'andis_customis true → render an image: pick the largestthumbnailsentry (the array is size-ascending, so the last one) and use itsurlas an<img>src.kind === 'emoji', not custom → render a unicode character:mapped_unicodewhen non-empty, elseemoji_idif it is already a raw emoji character (YouTube sometimes sends"👑"directly), else fall back to theshortcuttext.
Example renderer (React; PartSchema is exported by this package):
import type { PartSchema } from '@gettersethya/yt-livechat-client'
function MessageBody({ parts, message }: { parts: readonly PartSchema[]; message: string }) {
if (parts.length === 0) return <>{message}</>
return (
<>
{parts.map((part, i) => {
if (part.kind === 'text') return <span key={i}>{part.text}</span>
if (part.is_custom) {
const src = part.thumbnails[part.thumbnails.length - 1]?.url
return src ? <img key={i} src={src} alt={part.shortcut} className="emoji" /> : <span key={i}>{part.shortcut}</span>
}
const unicode = part.mapped_unicode !== '' ? part.mapped_unicode : part.emoji_id
return <span key={i}>{unicode !== '' ? unicode : part.shortcut}</span>
})}
</>
)
}Console version (text-only; custom emojis print their shortcut or image URL):
function bodyForConsole(parts: readonly PartSchema[]): string {
return parts
.map((part) => {
if (part.kind === 'text') return part.text
const src = part.thumbnails[part.thumbnails.length - 1]?.url
if (part.is_custom) return src !== undefined ? `[img ${src}]` : part.shortcut
if (part.mapped_unicode !== '') return part.mapped_unicode
return part.emoji_id !== '' ? part.emoji_id : part.shortcut
})
.join('')
}
client.on('message', (message) => {
console.log(`${message.author}: ${bodyForConsole(message.parts)}`)
})Note: thumbnails can be populated even when is_custom is false (YouTube
ships image thumbnails for some standard emojis); is_custom is the deciding
flag for image vs. unicode rendering.
