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

@foony/realtime

v0.15.2

Published

TypeScript SDK for the Foony Realtime service.

Readme

@foony/realtime

TypeScript SDK for the Foony Realtime service. A small client for the wire protocol implemented by services/realtime-saas: connect, subscribe, publish, presence, and history, plus a REST client for backends.

Install

npm install @foony/realtime

The package ships compiled ESM output and TypeScript declarations.

Quick start

Browser / Foony client

import { Realtime } from '@foony/realtime';

const realtime = new Realtime({
  authCallback: async () => {
    const response = await fetch('/api/realtime/token');
    return await response.text();
  },
});

const channel = realtime.channels.get('chat:room-1');

channel.subscribe((message) => {
  console.log('chat message:', message.data);
});

await channel.publish('chat', { text: 'hello world' });

channel.presence.on((event) => {
  console.log(event.action, event.clientId, event.data);
});
await channel.presence.enter({ name: 'Alice' });

Node / server (browser auth)

Browser clients should fetch a short-lived JWT from your backend via the SDK's authCallback option. Your backend mints that JWT locally with createJwt (it signs with your Realtime API key, with no network call), or asks the service to mint one with rest.auth.requestToken. Either way the API key stays on your backend and never ships to browsers.

Local development against the realtime backend

Start the backend following services/realtime-saas/README.md. Then mint a dev token:

cd services/realtime-saas
JWT_SIGNING_KEY=local-dev-key go run ./cmd/devtoken -app foony -client alice

Use the printed token in the SDK:

const realtime = new Realtime({
  endpoint: 'ws://localhost:3000',
  token: process.env.FOONY_REALTIME_DEV_TOKEN!,
});

Omit endpoint in production to use wss://realtime.foony.io.

Transports

The client connects over WebSocket. When the WebSocket cannot be established, for example behind a corporate proxy that blocks upgrades, it automatically falls back to HTTP long-polling within about 5 seconds, and everything keeps working: publish, subscribe, presence, history, resume, and exactly-once delivery. Long-polling has higher latency and more per-request overhead, so the WebSocket is always tried first. Force a transport with the transport option ('auto' is the default):

const realtime = new Realtime({
  token,
  transport: 'long-polling', // or 'websocket' to never fall back
});

Channel names

Channel names are 1 to 255 ASCII characters from A-Z a-z 0-9 : - _ and cannot start with a :. Use colons to express hierarchy (chat:rooms:42). Dots are not allowed. The server rejects invalid names with error code 40001 (BadFrame).

API surface

  • Realtime — top-level client. Owns the WebSocket; channels attach lazily.
  • client.channels.get(name) — returns a stable Channel for that name.
  • channel.subscribe(fn) — message listener. Returns an unsubscribe fn.
  • channel.subscribe(name, fn) — message listener for one message name.
  • channel.on(fn) / channel.on('attached', fn) — channel lifecycle state listener.
  • channel.publish(name, data) — publish one message. Resolves on ack.
  • channel.presence.on(fn) — presence listener.
  • channel.presence.enter|update|leave(data?) — mutate this connection's membership.
  • client.connection.on(fn) — observe all connection events.
  • client.connection.on('connected', fn) — observe one connection event.
  • client.connection.off() — remove all connection listeners.
  • client.connection.once('connected') — await the next matching connection event.
  • Rest — HTTP client for publish, history, presence, and token minting without a connection (see REST).

REST

For backends and integrations that publish or read without holding a connection open (cron jobs, serverless functions, webhooks), use the Rest client. It talks to the same service over HTTPS, and its publishes are identical to WebSocket publishes for subscribers, history, and billing.

import { Rest } from '@foony/realtime';

const rest = new Rest({ key: process.env.REALTIME_API_KEY });
const channel = rest.channels.get('chat:room:42');

// Publish one message, or an array (stored and delivered as one atomic batch).
await channel.publish('greeting', { text: 'hello' });

// History, newest first. Page through older messages with next().
let page = await channel.history({ limit: 100 });
for (const message of page.items) {
  console.log(message.name, message.data);
}
while (page.hasNext()) {
  page = (await page.next())!;
}

// Current presence members.
const members = await channel.presence.get();

// Mint a client JWT from your API key (for handing to browser clients).
const details = await rest.auth.requestToken({
  clientId: 'user-42',
  capability: { 'chat:*': ['subscribe', 'publish'] },
});

Auth accepts the same options as the realtime client: key (server-side), token, or authCallback (refreshed automatically when the service reports it expired). Channels accept the same cipher option for end-to-end encryption. Errors reject with RestError, which carries the numeric protocol code plus the HTTP statusCode.

Reconnect

When the connection drops unexpectedly the client retries with exponential backoff (1s, 2s, 4s, ..., capped at 30s). Everything is restored automatically on reconnect: subscriptions are re-issued (with a resume cursor, so missed messages within retention are replayed), presence watchers are re-opened, and whatever presence membership this connection had entered is re-entered. Call presence.leave() if you no longer want to be present.

Pass autoReconnect: false to disable retries entirely (useful in tests).

Publishes made while the connection is establishing or temporarily down are queued locally and flushed on the next successful (re)connect, so a publish during a brief blip resolves rather than rejects. A publish that was already in flight when the connection dropped is resent on reconnect too. Every publish carries a stable client-assigned id, so the server collapses any duplicate that a resend would otherwise create (exactly-once). Pass queueMessages: false to disable buffering/resend and reject such publishes immediately.

Tests

npm test

Runs wire unit tests plus an in-process end-to-end test that drives the SDK against a fake edge built on ws. No external services required.

License

Apache-2.0 © Foony Limited