Typed PubSub
TypedPubSub is a thin wrapper around graphql-subscriptions that types channel names and payloads. The publish and subscribe methods accept only channels declared in your event map; typos and payload mismatches fail at compile time.
GraphQL Yoga already ships a type-safe PubSub. If you're on Yoga, you don't need this package.
Yoga's PubSub uses tuple payloads ('channel': [Payload]); TypedPubSub (over graphql-subscriptions) uses the bare type. Don't copy a PubSubMap between the two without flipping the shape.
Installation
- yarn
- npm
- pnpm
- bun
yarn add @baeta/subscriptions-pubsub
npm install @baeta/subscriptions-pubsub
pnpm add @baeta/subscriptions-pubsub
bun add @baeta/subscriptions-pubsub
Basic setup
1. Declare your channels
Map each channel name to its payload type:
import { TypedPubSub } from "@baeta/subscriptions-pubsub";
import { PubSub } from "graphql-subscriptions";
import type { User } from "../__generated__/types.ts";
export type PubSubMap = {
"user-updated": User;
[c: `user-updated-${string}`]: User;
// Add more channels as needed:
// 'post-created': Post;
// 'comment-deleted': { postId: string; commentId: string };
};
2. Create the instance
// In-memory transport — fine for development and single-instance deployments.
// For multi-instance setups, see "Using with Redis" below.
export const pubsub = new TypedPubSub<PubSub, PubSubMap>(new PubSub());
3. Add it to your context
export interface Context {
pubsub: typeof pubsub;
// ... other context properties
}
export function createContext(): Context {
return {
pubsub,
// ... other context properties
};
}
Usage
Publishing events
Publish accepts only declared channels and the right payload type:
const updateUserMutation = Mutation.updateUser.resolve(({ args, ctx }) => {
const updatedUser = {
id: args.where.id,
givenName: args.data.givenName ?? "Jon",
lastName: args.data.lastName ?? "Doe",
};
// Type-safe publish - TypeScript will error if channel or payload type is incorrect
ctx.pubsub.publish("user-updated", updatedUser);
return updatedUser;
});
Subscribing to events
Get a typed async iterable for a channel:
const userUpdatedSubscription = Subscription.userUpdated
.subscribe(({ ctx }) => {
// Type-safe subscription - channel name is autocompleted and type-checked
return ctx.pubsub.asyncIterableIterator("user-updated");
})
.resolve(({ source }) => {
// source is properly inferred as User
return source;
});
Using with Redis
For multi-instance deployments, swap the in-memory transport for graphql-redis-subscriptions:
import { RedisPubSub } from "graphql-redis-subscriptions";
import Redis from "ioredis";
const options = {
host: "redis-server",
port: 6379,
retryStrategy: (times) => Math.min(times * 50, 2000),
};
const pubsub = new TypedPubSub<RedisPubSub, PubSubMap>(
new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options),
}),
{
prefix: "feature-1:", // optional channel prefix
},
);