alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
117 lines (99 loc) • 3.14 kB
text/typescript
import { $atom, $hook, $inject, $state, Alepha, type Static, z } from "alepha";
import { $logger } from "alepha/logger";
import { RedisProvider, RedisSubscriberProvider } from "alepha/redis";
import {
type SubscribeCallback,
TopicProvider,
type TopicPublishOptions,
type TopicSubscribeOptions,
type UnSubscribeFn,
} from "alepha/topic";
// ---------------------------------------------------------------------------------------------------------------------
/**
* Redis topic configuration atom.
*/
export const redisTopicOptions = $atom({
name: "alepha.topic.redis.options",
schema: z.object({
prefix: z.text({
default: "topic",
description: "Prefix for all topic channels in Redis.",
}),
}),
default: {
prefix: "topic",
},
});
export type RedisTopicOptions = Static<typeof redisTopicOptions.schema>;
declare module "alepha" {
interface State {
[redisTopicOptions.key]: RedisTopicOptions;
}
}
// ---------------------------------------------------------------------------------------------------------------------
export class RedisTopicProvider extends TopicProvider {
protected readonly options = $state(redisTopicOptions);
protected readonly alepha = $inject(Alepha);
protected readonly redisProvider = $inject(RedisProvider);
protected readonly redisSubscriberProvider = $inject(RedisSubscriberProvider);
protected readonly log = $logger();
protected readonly start = $hook({
on: "start",
handler: async () => {
const subscribers = this.subscribers();
if (subscribers.length) {
await Promise.all(subscribers.map((fn) => fn()));
for (const subscriber of subscribers) {
this.log.debug(`Subscribed to topic '${subscriber.name}'`);
}
}
},
});
protected override wildcardChar(): string {
return "*";
}
public prefix(queue: string): string {
return `${this.options.prefix}:${queue}`;
}
/**
* Publish a message to a topic.
*/
public async publish(
topic: string,
message: string,
options?: TopicPublishOptions,
): Promise<void> {
if (options?.retain) {
await this.redisProvider.set(`${this.prefix(topic)}:retained`, message);
}
await this.redisProvider.publish(this.prefix(topic), message);
}
/**
* Subscribe to a topic.
*/
public async subscribe(
name: string,
callback: SubscribeCallback,
_options?: TopicSubscribeOptions,
): Promise<UnSubscribeFn> {
const topic = this.prefix(name);
// Subscribe first to avoid message loss window
await this.redisSubscriberProvider.subscribe(topic, callback);
// Then deliver retained message if exists
const retained = await this.redisProvider.get(`${topic}:retained`);
if (retained) {
await callback(retained.toString());
}
return () => this.unsubscribe(name, callback);
}
/**
* Unsubscribe from a topic.
*/
public async unsubscribe(
name: string,
callback?: SubscribeCallback,
): Promise<void> {
const topic = this.prefix(name);
await this.redisSubscriberProvider.unsubscribe(topic, callback);
}
}