npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 uses globalThis.fetch (overridable via the fetchFn option) and setTimeout, so it runs in the browser as long as the server is reachable (the server sends CORS headers).

Install

npm install @gettersethya/yt-livechat-client

Usage

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:

  1. kind === 'text' → render text as a normal text node.
  2. kind === 'emoji' and is_custom is true → render an image: pick the largest thumbnails entry (the array is size-ascending, so the last one) and use its url as an <img> src.
  3. kind === 'emoji', not custom → render a unicode character: mapped_unicode when non-empty, else emoji_id if it is already a raw emoji character (YouTube sometimes sends "👑" directly), else fall back to the shortcut text.

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.