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-ebs

v1.0.0

Published

Reactive RPC layer to a Twitch Extension's EBS. JWT auto-attached, typed HttpError, SSE via streamQuery. Built on @zakkster/lite-query 1.1. Zero-dep ESM.

Downloads

80

Readme

@zakkster/lite-twitch-ebs

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

Reactive RPC layer to a Twitch Extension's own backend (the "EBS", in Twitch's terminology). Wraps @zakkster/lite-query 1.1's query / mutation / streamQuery with three things every consumer otherwise rebuilds: bearer-token auto-attachment, typed HttpError, and Server-Sent Events parsing.

npm i @zakkster/lite-twitch-ebs

Peer deps: @zakkster/lite-signal @zakkster/lite-await @zakkster/lite-stream @zakkster/lite-query ^1.1.0. Zero runtime dependencies. ESM only.


What this is

A Twitch Extension's iframe talks to two backends: Twitch's Helix API (for read-mostly Twitch-owned data) and your own EBS (for everything your extension does). The EBS is where your business logic lives. The iframe authenticates to it by sending the helper JWT (the one Twitch hands to Twitch.ext.onAuthorized) in the Authorization header; your EBS verifies the signature with the extension secret via @zakkster/lite-twitch-jwt.verify().

lite-twitch-ebs is the iframe-side adapter for that flow. It does three concrete things:

  • Reads the token accessor on every request. Pass token: () => sdk.token() from @zakkster/lite-twitch; the SDK's signal accessor is evaluated each fetch start, so token rotation (after JWT_RECEIVED) reaches the next request without restarting anything.
  • Wraps non-2xx responses in HttpError. Carries status, statusText, url, and a best-effort parsed body. Surfaces through lite-query's error() signal unchanged so consumers branch on err.status for retry/UI decisions.
  • Parses SSE as a streamQuery. Default parser yields { event, data, id } with data JSON-parsed when valid. Backed by lite-stream's pipeToSignal, so backpressure (mode: "buffer", maxBuffer: N) and abort-on-detach are inherited from lite-query/stream.

Usage

import {createTwitchSdk} from "@zakkster/lite-twitch";
import {queryClient} from "@zakkster/lite-query";
import {createEbs} from "@zakkster/lite-twitch-ebs";
import {effect} from "@zakkster/lite-signal";

const sdk = createTwitchSdk({helper: window.Twitch.ext});
const qc = queryClient({defaultStaleTime: 30_000, crossTab: true});
const ebs = createEbs({
    qc,
    baseUrl: "https://my-ebs.example.com",
    token: () => sdk.token(),
});

await sdk.ready();

Query (GET)

const poll = ebs.query({
    key: () => ["poll", pollId()],
    path: (key) => `/polls/${key[1]}`,
    staleTime: 5_000,
});

effect(() => {
    if (poll.loading()) return showSpinner();
    if (poll.error()) return showError(poll.error());
    renderPoll(poll.data());
});

The query handle is a standard lite-query handle: data(), error(), status(), loading(), fetching(), refetch(), dispose(). Everything lite-query supports (reactive keys, enabled gates, staleTime, cacheTime, retry/backoff, cross-tab cache coherence) is available -- path and headers are the only options ebs consumes; everything else passes through.

Mutation (POST/PUT/PATCH/DELETE)

const vote = ebs.mutation({
    method: "POST",
    path: (vars) => `/polls/${vars.pollId}/votes`,
    body: (vars) => ({choice: vars.choice}),
    onSuccess: (_, vars) => qc.invalidate(["poll", vars.pollId]),
});

await vote.mutate({pollId: "abc", choice: 2});

If you omit body, POST/PUT/PATCH default to sending vars as the JSON body -- the common case where the mutation's variables match the request shape gets zero boilerplate. DELETE sends no body by default. Content-Type: application/json is set automatically when serializing an object.

Stream (SSE)

const events = ebs.stream({
    key: ["events", channelId],
    path: `/events/${channelId}/stream`,
    mode: "buffer",
    maxBuffer: 100,
});

effect(() => {
    const recent = events.data();   // array of SseEvent in buffer mode
    if (Array.isArray(recent)) renderEventList(recent);
});

The default event shape is { event, data, id }. data is JSON-parsed when valid, raw string when not, undefined for empty data: fields. To extract just the payload, pass a parse transform:

const ticks = ebs.stream({
    key: ["ticks"], path: "/ticks",
    parse: (ev) => ev.event === "tick" ? ev.data : undefined,  // skip non-tick frames
});

parse returning undefined skips the frame entirely (no signal write, no count bump). Throws inside parse are caught and routed to createEbs's onError hook without breaking the stream.


Error shapes

class LiteTwitchEbsError extends Error {
    name: "LiteTwitchEbsError";
    code: "misconfigured" | "no_token" | "closed";
    cause?: unknown;
}

class HttpError extends Error {
    name: "HttpError";
    status: number;
    statusText: string;
    url: string;
    body: unknown;        // parsed JSON | raw string | undefined | null
}

HttpError for anything the server said (4xx, 5xx); LiteTwitchEbsError for things the SDK refused to attempt (no token, SDK closed, misconfiguration).

The no_token case fires when the token accessor returns null or empty string. The cleanest fix is to gate the query with enabled:

const q = ebs.query({
    key: () => ["user", userId()],
    path: () => `/users/${userId()}`,
    enabled: () => sdk.matches("authenticated"),
});

With enabled false, lite-query skips the fetch entirely -- no no_token error fires, the query just sits in idle until authenticated.


Headers

const ebs = createEbs({
    qc, baseUrl, token,
    defaultHeaders: {"X-Extension-Version": "1.2.3"},
});

const q = ebs.query({
    key: ["x"], path: "/x",
    headers: {"X-Trace": "specific"},   // merged on top of defaultHeaders
});

Precedence: defaultHeaders < per-request headers < Accept (auto, "application/json" for queries, "text/event-stream" for streams) < Content-Type (auto when serializing JSON) < Authorization (always wins; consumer cannot override the JWT).


Lifecycle

ebs.close() stops accepting new requests but does NOT dispose the queryClient or any in-flight queries -- those are owned by the consumer and may be shared with other adapters (e.g. @zakkster/lite-twitch-helix). To tear everything down:

ebs.close();
qc.clear();         // remove all cached entries and abort pending fetches
qc.dispose();       // release the queryClient's own signals

Per-handle: q.dispose() / m.dispose() / s.dispose() release the handle's signal node back to lite-signal's pool. Important for SPAs that mount/unmount many ebs queries over their lifetime.


Testing

createEbs accepts a fetch injectable. Wire a mock and assert exact request shapes:

import {createEbs} from "@zakkster/lite-twitch-ebs";
import {queryClient} from "@zakkster/lite-query";

const calls = [];
const mockFetch = async (url, init) => {
    calls.push({url: String(url), method: init.method, headers: init.headers, body: init.body});
    return new Response(JSON.stringify({ok: true}), {status: 200});
};

const qc = queryClient();
const ebs = createEbs({qc, baseUrl: "https://x", token: "test-jwt", fetch: mockFetch});

const m = ebs.mutation({path: "/action"});
await m.mutate({foo: 1});

assert.equal(calls[0].headers.Authorization, "Bearer test-jwt");

For SSE the test fixtures in this package's test/_fixtures.js show how to build a streaming Response with a ReadableStream body -- copy/paste as needed.


What this package is NOT

  • Not the Helix client. Helix uses helixToken (the user's OAuth token, nullable for unlinked viewers); that's @zakkster/lite-twitch-helix.
  • Not the JWT verifier. Verification runs on your EBS via @zakkster/lite-twitch-jwt. This SDK only attaches the existing helper JWT as the request bearer.
  • Not a websocket adapter. Stream support is SSE only in 1.0; ws can be added as a 1.x minor without changing the existing surface.
  • Not a generic HTTP client. The auth model and error shape are specific to Twitch Extension EBS conventions. For Twitch-side APIs, use lite-twitch-helix; for non-Twitch backends, use lite-query directly.

License

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