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

@foghorn/sdk

v0.5.0

Published

Browser client for foghorn — connect, subscribe, and receive real-time WebSocket messages with reconnect/backoff built in.

Readme

@foghorn/sdk

Browser client for foghorn — a thin wrapper over the native WebSocket for connecting, subscribing, and receiving real-time messages. Handles reconnect with backoff and auto-resubscribe for you, plus a ping/pong heartbeat that detects a connection API Gateway (or anything in between) dropped without ever delivering a close frame — otherwise invisible, since readyState stays OPEN on a connection like that indefinitely. There's still no way to emit with the secret key from here, and never will be — that stays a server-to-server call. This client does support emit() for scoped client-emit, authorized by a short-lived token minted server-side (see @foghorn/sdk/server below).

Install

npm install @foghorn/sdk

Usage

import { FoghornClient } from "@foghorn/sdk";

const client = new FoghornClient({
  url: "wss://<your-project-ws-url>", // from your project's dashboard page
  publicKey: "pk_live_xxxxxxxx",
});

client.onOpen(() => client.subscribe("room-1"));
client.onMessage((msg) => console.log(msg.channel, msg.payload));
client.connect();

Document edits and anything needing a durable, revision-checked write still go through your own backend over plain HTTP with the secret key — that path isn't part of this client. For ephemeral, high-frequency signals (cursor/selection/focus/blur/identity) where that round trip doesn't pull its weight, emit() lets the browser publish directly, authorized by a short-lived, channel-scoped, clientId-bound, type-allowlisted token (minted server-side via mintClientEmitToken from @foghorn/sdk/server) — never the secret key:

client.subscribe("template:42", { presence: true, clientId: "user-123" });
client.emit("template:42", "cursor", { x: 120, y: 84 }, token); // token from your backend, see @foghorn/sdk/server below

Optional per-subscription features: presence (who else is in a channel, via onPresence — join/leave/sync events, no per-user metadata beyond clientId) and a short-lived replay buffer for reconnects (onReplayGap fires when a since request couldn't fully catch up), both opt-in via subscribe(channel, options).

API

| Method | Signature | Behavior | |---|---|---| | connect() | (): void | Opens the connection | | disconnect() | (): void | Closes the connection; suppresses auto-reconnect | | subscribe(channel, options?) | (channel: string, options?: {presence?, clientId?, since?}): void | Subscribes; remembered (with options) so a future reconnect resubscribes automatically, requesting replay from the last seq actually seen | | unsubscribe(channel) | (channel: string): void | Unsubscribes; forgets the channel (and its tracked seq) | | getLastSeq(channel) | (channel: string): number \| undefined | Highest seq seen on channel this session — persist it yourself (e.g. localStorage) to resume via since after a full page reload | | emit(channel, type, payload, token) | (channel: string, type: string, payload: unknown, token: string): void | Emits directly to channel over this connection using a scoped token minted by @foghorn/sdk/server | | onMessage(handler) | (msg: {channel, payload, seq, replay?, type?, clientId?}) => void → unsubscribe fn | Fires for emitted messages on subscribed channels; replay: true if delivered from the buffer rather than live; type/clientId present when sent via client-emit | | onPresence(handler) | (event: {type: "presence", channel, event, ...}) => void → unsubscribe fn | Fires for join/leave/sync events on channels subscribed with presence: true | | onReplayGap(handler) | (event: {type: "replay_gap", channel, since}) => void → unsubscribe fn | Fires when a replay request couldn't fully catch up — fall back to a full resync | | onAck(handler) | (ack: {action, channel, ok, seq?}) => void → unsubscribe fn | Fires on subscribe/unsubscribe/emit acks; seq present for emit | | onErrorMessage(handler) | (err: {error, ...}) => void → unsubscribe fn | Fires on server-sent protocol errors | | onOpen(handler) | () => void → unsubscribe fn | Fires when the socket opens, including after a reconnect | | onClose(handler) | ({code, reason}) => void → unsubscribe fn | Fires on every close | | onSocketError(handler) | (event: unknown) => void → unsubscribe fn | Fires on the underlying WebSocket's error event |

Options

| Option | Type | Default | |---|---|---| | url | string | required | | publicKey | string | required | | reconnect | boolean | true | | maxReconnectDelayMs | number | 30000 | | heartbeatIntervalMs | number | 240000 (4 min) — how often to send ping; 0 disables it | | heartbeatAckTimeoutMs | number | 15000 — how long to wait for pong before closing the socket and letting reconnect logic take over |

Heartbeat

Detects a connection API Gateway (or anything in between) dropped without ever delivering a close frame — sleep/wake, a NAT/proxy timeout, a backgrounded tab. readyState stays OPEN on a connection like that indefinitely, so the client pings itself on heartbeatIntervalMs and, if heartbeatAckTimeoutMs passes with no pong, closes the socket itself (HEARTBEAT_TIMEOUT_CLOSE_CODE, reason "heartbeat_timeout") — which feeds the existing reconnect path the same as any other unexpected close:

import { FoghornClient, HEARTBEAT_TIMEOUT_CLOSE_CODE } from "@foghorn/sdk";

const client = new FoghornClient({
  url: "wss://<your-project-ws-url>",
  publicKey: "pk_live_xxxxxxxx",
  heartbeatIntervalMs: 60_000, // ping every minute instead of the 4 min default
  heartbeatAckTimeoutMs: 5_000, // give up after 5s instead of 15s
});

client.onClose(({ code, reason }) => {
  if (code === HEARTBEAT_TIMEOUT_CLOSE_CODE) {
    console.warn("connection died silently; reconnecting", reason);
  }
});

Set heartbeatIntervalMs: 0 to disable it entirely.

@foghorn/sdk/server

A separate, Node-only entry point — never import this from browser code. Mints and revokes scoped client-emit tokens for emit() above. Both functions are dependency-free (only node:crypto/fetch) and derive everything they need from values you already have — no extra endpoint URLs to look up or configure.

import { mintClientEmitToken, revokeClientEmitToken } from "@foghorn/sdk/server";

const token = mintClientEmitToken({
  secret: process.env.FOGHORN_CLIENT_EMIT_SECRET!, // from the dashboard, never the public/secret API key
  projectId: "...",
  channel: "template:42",
  clientId: "user-123", // must match the clientId this connection subscribes with (presence: true)
  types: ["cursor", "selection"],
  ttlSeconds: 300,
});

// Invalidate a token (or every token for a clientId) ahead of its TTL —
// e.g. on logout. Derives the revoke URL from the same emitUrl you already
// use for backend emit() calls — it's a sibling path, not a separate one.
await revokeClientEmitToken({
  emitUrl: process.env.FOGHORN_EMIT_URL!,
  secretKey: process.env.FOGHORN_SECRET_KEY!,
  clientId: "user-123", // and/or jti to revoke one specific token
});

License

MIT