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

@chatpack/transport-redis

v0.2.0

Published

Redis pub/sub transport for Chatpack - multi-node SSE fan-out

Readme

@chatpack/transport-redis

Redis pub/sub transport for Chatpack - multi-node SSE fan-out.

Chatpack's default transport is in-process: correct for one server, silently wrong for two. With two app servers behind a load balancer, Alice's /stream connection lives on node A while Bob's POST /messages is handled by node B - and node B has no way to reach Alice's stream. This package relays every event through a Redis channel so all nodes see all events.

npm install @chatpack/transport-redis ioredis

Usage

import { chatpack } from "@chatpack/core";
import { drizzleAdapter } from "@chatpack/adapter-drizzle";
import { redisPresenceStore, redisTransport } from "@chatpack/transport-redis";
import { presence } from "@chatpack/core/plugins";
import { Redis } from "ioredis";

// Two connections: a Redis client in subscriber mode cannot issue PUBLISH.
const publisher = new Redis(process.env.REDIS_URL!);
export const chat = chatpack({
  storage: drizzleAdapter(db),
  auth: async (req) => getSessionUser(req),
  transport: redisTransport({
    publisher,
    subscriber: new Redis(process.env.REDIS_URL!),
  }),
  plugins: [presence({ store: redisPresenceStore({ client: publisher }) })],
});

That is the entire change. Every route, event, and client behaves identically - Transport is the seam core was designed around, so nothing else moves.

Import ioredis's named Redis, as above. Its default export is the same class at runtime, but ioredis is CommonJS, so on "module": "nodenext" in an ESM package TypeScript types that default as the module object and new Redis(url) fails to compile. The named import works in every module setting.

Works with node-redis too:

import { createClient } from "redis";

const publisher = createClient({ url: process.env.REDIS_URL });
const subscriber = publisher.duplicate();
await Promise.all([publisher.connect(), subscriber.connect()]);

const transport = redisTransport({ publisher, subscriber });

Options

| Option | Default | Notes | | ------------ | ----------------- | ------------------------------------------------------------------------- | | publisher | (required) | Redis client used to PUBLISH. | | subscriber | (required) | A second client, used to SUBSCRIBE. Passing the same one throws. | | channel | chatpack:events | Override to isolate staging from production on a shared Redis. | | nodeId | random | This process's id, used to drop its own echoed events. Override in tests. | | onError | console.error | (error, "publish" \| "receive" \| "subscribe"). Wire to your tracker. |

redisTransport() returns the standard Transport plus nodeId and close() (unsubscribes and drops local listeners; the connections are yours to close, since this package did not open them).

What you get, and what you don't

Multi-node: messages (message.created / updated / deleted), reactions (reaction.added / reaction.removed), group membership changes (participant.added / participant.removed / conversation.updated), typing indicators, and read receipts. All of them travel on the transport - TransportEvent has four members (ChatEvent, ReactionEvent, ConversationEvent, EphemeralEvent) and this package relays all four, reviving the Date fields on whichever snapshot the event carries: a message, or a conversation and its participants.

Multi-node: presence() with redisPresenceStore(). Pass the same shared store to every node. It tracks one expiring lease per SSE connection, so GET /presence and online/offline transitions reflect all nodes. The transport publisher must be a normal Redis connection because the presence store runs atomic EVAL scripts; keep the subscriber connection dedicated to SUBSCRIBE.

Without a shared store, presence remains process-local for backward compatibility.

Real Redis integration test

The normal suite uses an in-memory Redis double. To execute the Lua scripts against Redis itself, start a Redis server with redis-cli available and run:

CHATPACK_REDIS_URL=redis://127.0.0.1:6379 \
  pnpm --filter @chatpack/transport-redis test -- redis.integration.test.ts

When redis-cli exists only inside Docker, use CHATPACK_REDIS_CLI=docker CHATPACK_REDIS_CLI_ARGS="exec redis redis-cli".

Failure behavior

A Redis outage degrades live delivery; it never fails a send.

publish() is synchronous and never throws, as the Transport contract requires - the message is already in storage before anyone is notified (durable-first). Local subscribers are notified inline, so clients on the publishing node are unaffected by Redis health. The PUBLISH itself is fire-and-forget, and failures go to onError.

So during an outage: senders succeed, history stays correct, same-node clients stay live, and other-node clients miss events until they reconnect - at which point Last-Event-ID gap-fill replays what they missed from storage.

Redis pub/sub is at-most-once (no replay buffer), which is why durable events remain replayable from storage and ephemeral ones are defined as droppable. Reaction and conversation events sit in between: they're stored, but they have no seq, so Last-Event-ID can't replay them either - a reaction or a membership change missed during an outage shows up on the next conversation refetch (ADR 0013, ADR 0017).

Notes

  • Serverless is still not a fit for SSE, whatever the transport - function lifetime is the blocker, not fan-out. Poll GET /conversations/:id/messages there.
  • Ordering is per-publisher; two nodes publishing concurrently can interleave. Clients sort by seq, so message order stays correct.
  • Sticky sessions are not required. Any node can serve any stream.
  • Presence needs both shared pieces. Configure redisTransport() for event fan-out and redisPresenceStore() for connection state. Lease expiry removes connections from crashed nodes, so snapshots become offline; the default five-second offline grace absorbs reconnect flaps.

Links

Community

  • Discord — chat with the team and other developers
  • X — releases and updates
  • Docs — the full documentation site
  • GitHub Discussions — questions, show-and-tell, and feedback

MIT