Full type-safe Redis PubSub system with async iterators
- Type-safety with Zod
- Out-of-the-box support for
Date/Map/Set/BigIntserialization with superjson - Full usage of Async Iterators
- Support for AbortController / AbortSignal
- Support for type-safe filtering using Type Guards / Type predicates
- Support for Entity + Identifier pattern subscriptions
- Support for Async Zod Refines and Async Zod Transforms
- GraphQL API ready
pnpm add @soundxyz/redis-pubsubnpm install @soundxyz/redis-pubsubyarn add @soundxyz/redis-pubsubPeer dependencies
pnpm add zod ioredisnpm install zod ioredisyarn add zod ioredisimport Redis from "ioredis";
import { z } from "zod";
import { RedisPubSub } from "@soundxyz/redis-pubsub";
const { createChannel } = RedisPubSub({
publisher: new Redis({
port: 6379,
}),
subscriber: new Redis({
port: 6379,
}),
});const schema = z.object({
id: z.string(),
name: z.string(),
});
const userChannel = createChannel({
name: "User",
schema,
});
const nonLazyUserChannel = createChannel({
name: "User",
schema,
// By default the channels are lazily connected with redis
isLazy: false,
});// Using async iterators / async generators to subscribe
(async () => {
for await (const user of userChannel.subscribe()) {
console.log("User", {
id: user.id,
name: user.name,
});
}
})();
// You can explicitly wait until the channel is sucessfully connected with Redis
await userChannel.isReady();
// Publish data into the channel
await userChannel.publish(
{
value: {
id: "1",
name: "John",
},
},
// You can also publish more than a single value
{
value: {
id: "2",
name: "Peter",
},
}
);(async () => {
for await (const user of userChannel.subscribe({
filter(value) {
return value.id === "1";
},
})) {
console.log("User 1", {
id: user.id,
name: user.name,
});
}
})();
// You can also use type predicates / type guards
(async () => {
for await (const user of userChannel.subscribe({
filter(value): value is { id: "1"; name: string } {
return value.id === "1";
},
})) {
// typeof user.id == "1"
console.log("User 1", {
id: user.id,
name: user.name,
});
}
})();It will create a separate redis channel for every identifier, concatenating "name" and "identifier", for example, with "name"="User" and "identifier" = 1, the channel trigger name will be "User1"
(async () => {
for await (const user of userChannel.subscribe({
// number or string
identifier: 1,
})) {
console.log("User with identifier=1", {
id: user.id,
name: user.name,
});
}
})();
await userChannel.isReady({
// number or string
identifier: 1,
});
await userChannel.publish({
value: {
id: "1",
name: "John",
},
identifier: 1,
});You can levarage Zod Transforms to be able to separate input types from the output types, and receive any custom class or output on your subscriptions.
class CustomClass {
constructor(public name: string) {}
}
const inputSchema = z.string();
const outputSchema = z.string().transform((input) => new CustomClass(input));
const channel = pubSub.createChannel({
name: "separate-type",
inputSchema,
outputSchema,
});
const subscription = (async () => {
for await (const data of channel.subscribe()) {
return data;
}
})();
await channel.isReady();
await channel.publish({
value: "test",
});
const result = await subscription;
// true
console.log(result instanceof CustomClass);
// true
console.log(result.name === "test");If isLazy is not disabled, the last subscription to a channel will be automatically unsubscribed from Redis.
const abortController = new AbortController();
const abortedSubscription = (() => {
for await (const data of userChannel.subscribe({
abortSignal: abortController.signal,
})) {
console.log({ data });
}
})();
// ...
firstSubscribeAbortController.abort();
await abortedSubscription;await userChannel.unsubscribe(
{
identifier: 1,
},
// You can specify more than a single identifer at once
{
identifier: 2,
}
);await userChannel.unsubscribeAll();const pubSub = RedisPubSub({
publisher: new Redis({
port: 6379,
}),
subscriber: new Redis({
port: 6379,
}),
});
// ...
await pubSub.close();