alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
124 lines (108 loc) • 3.06 kB
text/typescript
import { $hook } from "alepha";
import { $logger } from "alepha/logger";
import {
type SubscribeCallback,
TopicProvider,
type TopicPublishOptions,
type TopicSubscribeOptions,
type UnSubscribeFn,
} from "./TopicProvider.ts";
export class MemoryTopicProvider extends TopicProvider {
protected readonly log = $logger();
protected readonly subscriptions: Record<string, SubscribeCallback[]> = {};
protected readonly retained: Record<string, string> = {};
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}'`);
}
}
},
});
/**
* Publish a message to a topic.
*
* @param topic
* @param message
* @param options
*/
public async publish(
topic: string,
message: string,
options?: TopicPublishOptions,
): Promise<void> {
if (options?.retain) {
this.retained[topic] = message;
}
for (const [pattern, callbacks] of Object.entries(this.subscriptions)) {
if (this.topicMatches(pattern, topic)) {
for (const callback of callbacks) {
await callback(message, topic);
}
}
}
}
/**
* Subscribe to a topic.
*
* @param topic - The topic to subscribe to.
* @param callback
*/
public async subscribe(
topic: string,
callback: SubscribeCallback,
_options?: TopicSubscribeOptions,
): Promise<UnSubscribeFn> {
if (!this.subscriptions[topic]) {
this.subscriptions[topic] = [];
}
this.subscriptions[topic].push(callback);
// Deliver retained messages matching the pattern
for (const [retainedTopic, retainedMessage] of Object.entries(
this.retained,
)) {
if (this.topicMatches(topic, retainedTopic)) {
await callback(retainedMessage, retainedTopic);
}
}
return async () => {
const callbacks = this.subscriptions[topic];
if (!callbacks) {
return;
}
this.subscriptions[topic] = callbacks.filter((cb) => cb !== callback);
if (this.subscriptions[topic].length === 0) {
delete this.subscriptions[topic];
}
};
}
/**
* Unsubscribe from a topic.
*
* @param topic - The topic to unsubscribe from.
*/
public async unsubscribe(topic: string): Promise<void> {
delete this.subscriptions[topic];
}
/**
* Check if a topic matches a subscription pattern.
* Supports `+` single-level wildcard.
*/
protected topicMatches(pattern: string, topic: string): boolean {
if (pattern === topic) {
return true;
}
const patternParts = pattern.split("/");
const topicParts = topic.split("/");
if (patternParts.length !== topicParts.length) {
return false;
}
return patternParts.every(
(part, i) => part === "+" || part === topicParts[i],
);
}
}