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

@zakkster/lite-twitch

v1.0.1

Published

Signal-native, state-machine-backed iframe SDK for Twitch Extensions. Wraps Twitch.ext in lite-signal/lite-statechart/lite-await primitives. Zero-dep ESM.

Downloads

210

Readme

@zakkster/lite-twitch

npm version Zero-GC sponsor npm bundle size npm downloads npm total downloads lite-signal peer TypeScript Dependencies License: MIT

Signal-native, state-machine-backed iframe SDK for Twitch Extensions. Wraps window.Twitch.ext in lite-signal / lite-statechart / lite-await primitives so the rest of your extension code stays in the same reactive vocabulary as everything else in the @zakkster/lite-* ecosystem.

npm i @zakkster/lite-twitch

Peer deps: @zakkster/lite-signal @zakkster/lite-statechart @zakkster/lite-await @zakkster/lite-twitch-jwt. Zero runtime dependencies.


What this is

The Twitch Extensions iframe helper (window.Twitch.ext) is a callback-shaped API:

Twitch.ext.onAuthorized((auth) => { /* token reissue */ });
Twitch.ext.onContext((ctx, changed) => { /* player state */ });
Twitch.ext.onVisibilityChanged((isVisible) => { /* tab focus */ });
Twitch.ext.listen("broadcast", (target, contentType, msg) => { /* PubSub */ });

It also has lifecycle quirks (JWT reissue every ~30min, expiry ~60min, viewers can minimize the extension) that every extension reinvents handling for, usually badly. lite-twitch lifts those callbacks into signals, models the auth lifecycle as a real state machine, and gives you one composition primitive: pubsubSignal(target).

import {createTwitchSdk} from "@zakkster/lite-twitch";
import {effect} from "@zakkster/lite-signal";

const sdk = createTwitchSdk({helper: window.Twitch.ext});

await sdk.ready();           // resolves once authenticated
const role = sdk.claims().role;  // "viewer" | "broadcaster" | ...
const broadcast = sdk.pubsubSignal("broadcast");

effect(() => {
    if (!sdk.visible()) return;
    const msg = broadcast();
    if (msg !== null) handle(msg);
});

// Later, on extension teardown:
sdk.close();

Auth machine

The auth lifecycle is a @zakkster/lite-statechart flat FSM. The shape:

                       +-----------------------+
                       |      anonymous        |  initial
                       +-----------+-----------+
              JWT_RECEIVED---------|
                                   v
                       +-----------------------+
                  VALID|      validating       |
                       +----+-------------+----+
                       +----+             +----INVALID--+  (or EXPIRED)
                       v                                v
             +----------------------+              (back to anonymous)
             |     authenticated    |
             +---+--------------+---+
                 | JWT_RECEIVED | JWT_EXPIRING (timer)
                 v              v
            (validating)    (anonymous)

  Wildcard (applies from every non-final state):
    CLOSE  -> closed  (final, terminal)
    FATAL  -> error   (final, terminal)

JWT_EXPIRING fires from an internal timer scheduled 60s before the current token's exp. It transitions to anonymous, not back to validating -- by the time the timer fires, Twitch should have already reissued via onAuthorized (typical reissue is at ~30min into a ~60min token's life). If JWT_EXPIRING fires, it means the reissue channel is broken, and dropping to anonymous tells consumers their auth is unreliable.

The authenticated entry action takes the lite-statechart 1.1 ctx.signal so the refresh timer is scoped to the state -- when the machine leaves authenticated for any reason (CLOSE, FATAL, JWT_RECEIVED, JWT_EXPIRING), ctx.signal aborts and the timer is cleared automatically. No leak windows.

Visibility is deliberately NOT in the machine -- it's orthogonal to auth. Use sdk.visible if you care, or compose: computed(() => sdk.matches("authenticated") && sdk.visible()).


Signals you get

sdk.auth     // ReadSignal<TwitchAuthPayload | null>  -- raw helper auth payload
sdk.context  // ReadSignal<TwitchContextPayload | null>  -- latest onContext
sdk.claims   // ReadSignal<TwitchClaims | null>  -- decoded JWT claims
sdk.token    // ReadSignal<string | null>  -- raw JWT string
sdk.visible  // ReadSignal<boolean>  -- from onVisibilityChanged
sdk.state    // ReadSignal<TwitchAuthState>  -- machine state name

All read-only by convention. The SDK is the only writer.


ready()

await sdk.ready({timeout: 5000, signal: ctrl.signal});

Resolves when the machine reaches authenticated. Rejects with:

  • LiteTwitchError("closed") if sdk.close() is called before authentication completes, or if the machine is already closed when ready() is called.
  • LiteTwitchError("fatal") if a FATAL event drives the machine to error.
  • TimeoutError if opts.timeout elapses (from @zakkster/lite-await).
  • AbortError if opts.signal aborts.

The implementation uses raceOf to race "reached authenticated" against "reached terminal", so terminal-state rejection is immediate rather than dependent on caller timeout.


pubsubSignal

const broadcast = sdk.pubsubSignal("broadcast");
effect(() => {
    const msg = broadcast();
    if (msg === null) return;
    // msg is whatever the helper delivered: Uint8Array, ArrayBuffer, or string.
});

The signal value is the most recent payload received on that PubSub target. Auto-unsubscribed on sdk.close(). Composes naturally with @zakkster/lite-twitch-room's transport, which expects to peel multi-packet envelopes -- you'd typically not consume pubsubSignal directly for room traffic, since the room transport owns that path.


Composition with @zakkster/lite-twitch-room

The intended Phase 2 pattern (released as lite-twitch-room v0.2.0):

import {createTwitchSdk} from "@zakkster/lite-twitch";
import {createRoom} from "@zakkster/lite-room";
import {createTwitchPubsubTransport} from "@zakkster/lite-twitch-room";
import {allOf} from "@zakkster/lite-await";

const sdk = createTwitchSdk({helper: window.Twitch.ext});
await sdk.ready();

const transport = createTwitchPubsubTransport({
    helper: window.Twitch.ext,
    ebsUrl: "https://my-ebs.example.com/room/" + sdk.auth().channelId + "/packets",
    jwt: () => sdk.token(),
});

const room = createRoom({transport, username: "alice"});
await allOf([
    [room.status, (s) => s === "connected"],
    [sdk.state,   (s) => s === "authenticated"],
]);

Or, after lite-twitch-room v0.2.0 ships, createTwitchRoom(opts) will wrap all of this into a single factory.


Testing

Use createTwitchMock -- it's the canonical mock for the entire ecosystem.

import {test} from "node:test";
import {createTwitchSdk, createTwitchMock} from "@zakkster/lite-twitch";

test("my extension behaves on auth", async () => {
    const mock = createTwitchMock();
    const sdk = createTwitchSdk({helper: mock.helper});
    mock.fire.authorized({
        channelId: "12345",
        clientId: "test-client",
        token: makeValidJwt(),
        userId: "UABCDEF",
    });
    await sdk.ready();
    // ... assertions
    sdk.close();
});

mock.fire exposes authorized, context, broadcast, global, visibility for driving callbacks deterministically. mock.sentMessages records every helper.send call. mock.reset() clears all state between tests.


What this package is NOT

  • Not the room transport. That's @zakkster/lite-twitch-room.
  • Not the Helix client. That's @zakkster/lite-twitch-helix (Phase 3).
  • Not the EBS RPC layer. That's @zakkster/lite-twitch-ebs (Phase 3).
  • Not Bits commerce. That's @zakkster/lite-twitch-bits (Phase 3).
  • Not a server-side library. JWT signature verification lives in @zakkster/lite-twitch-jwt; this SDK only does local decode + freshness checks (sufficient for iframe-side code -- the EBS is the source of truth on signature validity).

License

MIT. Copyright (c) Zahary Shinikchiev <[email protected]>.