UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

124 lines (108 loc) 3.06 kB
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], ); } }