UNPKG

alepha

Version:

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

336 lines (335 loc) 10.8 kB
import { $hook, $inject, $module, Alepha, AlephaError, KIND, PipelinePrimitive, Primitive, createPrimitive } from "alepha"; import { $logger } from "alepha/logger"; import { DateTimeProvider } from "alepha/datetime"; import { TemplatedPathParser } from "alepha/router"; //#region ../../src/topic/core/primitives/$subscriber.ts /** * Creates a subscriber primitive to listen for messages from a specific topic. * * Provides a dedicated message subscriber that connects to a topic and processes messages * with custom handler logic, enabling scalable pub/sub architectures where multiple * subscribers can react to the same events independently. * * **Key Features** * - Seamless integration with any $topic primitive * - Full type safety inherited from topic schema * - Real-time message delivery when events are published * - Error isolation between subscribers * - Support for multiple independent subscribers per topic * * **Common Use Cases** * - Notification services and audit logging * - Analytics and metrics collection * - Data synchronization and real-time UI updates * * @example * ```ts * class UserActivityService { * userEvents = $topic({ * name: "user-activity", * schema: { * payload: z.object({ * userId: z.text(), * action: z.enum(["login", "logout", "purchase"]), * timestamp: z.number() * }) * } * }); * * activityLogger = $subscriber({ * topic: this.userEvents, * handler: async (message) => { * const { userId, action, timestamp } = message.payload; * await this.auditLogger.log({ * userId, * action, * timestamp * }); * } * }); * * async trackUserLogin(userId: string) { * await this.userEvents.publish({ * userId, * action: "login", * timestamp: Date.now() * }); * } * } * ``` */ const $subscriber = (options) => { return createPrimitive(SubscriberPrimitive, options); }; var SubscriberPrimitive = class extends PipelinePrimitive {}; $subscriber[KIND] = SubscriberPrimitive; //#endregion //#region ../../src/topic/core/errors/TopicTimeoutError.ts var TopicTimeoutError = class extends AlephaError { topic; timeout; constructor(topic, timeout) { super(`Timeout of ${timeout}ms exceeded for topic ${topic}`); this.timeout = timeout; this.topic = topic; } }; //#endregion //#region ../../src/topic/core/providers/TopicProvider.ts /** * Base class for topic providers. */ var TopicProvider = class { alepha = $inject(Alepha); log = $logger(); dateTimeProvider = $inject(DateTimeProvider); /** * Encode and publish a typed message to a topic. */ async publishMessage(name, schema, payload, options) { let topicName = name; if (options?.params) topicName = new TemplatedPathParser(name).interpolate(options.params); await this.publish(topicName, JSON.stringify({ payload: this.alepha.codec.encode(schema, payload) }), options); } /** * Parse a raw message string into a typed topic message. */ parseMessage(schema, message) { const { payload } = JSON.parse(message); return { payload: this.alepha.codec.decode(schema, payload) }; } /** * Subscribe a typed handler to a topic, with error wrapping and message parsing. */ async subscribeHandler(name, schema, handler, options) { const parser = new TemplatedPathParser(name); const subscribeTopic = parser.hasParams ? parser.wildcardize(this.wildcardChar()) : name; return this.subscribe(subscribeTopic, async (message, receivedTopic) => { try { const parsed = this.parseMessage(schema, message); if (parser.hasParams && receivedTopic) { const params = parser.extract(receivedTopic) ?? {}; await handler({ ...parsed, params }); } else await handler(parsed); } catch (error) { this.log.error("Message processing has failed", error); } }, options); } /** * Wait for a single message matching an optional filter, with timeout. */ async waitForMessage(name, schema, options = {}) { const filter = options.filter ?? (() => true); return new Promise((resolve, reject) => { const ref = {}; (async () => { const clear = await this.subscribe(name, (raw) => { const message = this.parseMessage(schema, raw); if (!filter(message)) return; ref.timeout?.clear(); if (ref.clear) ref.clear(); resolve(message); }); ref.clear = clear; const timeoutDuration = options.timeout ?? [10, "seconds"]; ref.timeout = this.dateTimeProvider.createTimeout(() => { clear(); reject(new TopicTimeoutError(name, this.dateTimeProvider.duration(timeoutDuration).asMilliseconds())); }, timeoutDuration); })(); }); } /** * The wildcard character used for pattern subscriptions. * Override in subclasses for provider-specific wildcards. */ wildcardChar() { return "+"; } /** * Returns the list of $subscribers for this provider. */ subscribers() { const handlers = []; const topics = this.alepha.primitives($topic); for (const topic of topics) { if (topic.provider !== this) continue; const handler = topic.options.handler; if (handler && topic.provider === this) handlers.push(() => topic.subscribe(handler)); } const subscribers = this.alepha.primitives($subscriber); for (const subscriber of subscribers) { if (subscriber.options.topic.provider !== this) continue; const handler = subscriber.handler.run.bind(subscriber.handler); handlers.push(() => subscriber.options.topic.subscribe(handler)); } return handlers; } }; //#endregion //#region ../../src/topic/core/providers/MemoryTopicProvider.ts var MemoryTopicProvider = class extends TopicProvider { log = $logger(); subscriptions = {}; retained = {}; 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 */ async publish(topic, message, options) { 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 */ async subscribe(topic, callback, _options) { if (!this.subscriptions[topic]) this.subscriptions[topic] = []; this.subscriptions[topic].push(callback); 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. */ async unsubscribe(topic) { delete this.subscriptions[topic]; } /** * Check if a topic matches a subscription pattern. * Supports `+` single-level wildcard. */ topicMatches(pattern, topic) { 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]); } }; //#endregion //#region ../../src/topic/core/primitives/$topic.ts /** * Creates a topic primitive for publish/subscribe messaging and event-driven architecture. * * Enables decoupled communication through a pub/sub pattern where publishers send messages * and multiple subscribers receive them. Supports type-safe messages, real-time delivery, * event filtering, and pluggable backends (memory, Redis, custom providers). * * **Use Cases**: User notifications, real-time chat, event broadcasting, microservice communication * * @example * ```ts * class NotificationService { * userActivity = $topic({ * name: "user-activity", * schema: { * payload: z.object({ * userId: z.text(), * action: z.enum(["login", "logout", "purchase"]), * timestamp: z.number() * }) * }, * handler: async (message) => { * console.log(`User ${message.payload.userId}: ${message.payload.action}`); * } * }); * * async trackLogin(userId: string) { * await this.userActivity.publish({ userId, action: "login", timestamp: Date.now() }); * } * * async subscribeToEvents() { * await this.userActivity.subscribe(async (message) => { * // Additional subscriber logic * }); * } * } * ``` */ const $topic = (options) => { return createPrimitive(TopicPrimitive, options); }; var TopicPrimitive = class extends Primitive { provider = this.$provider(); get name() { return this.options.name || this.config.propertyKey; } async publish(message) { const hasParams = this.options.schema.params; const payload = hasParams ? message.payload : message; const params = hasParams ? message.params : void 0; await this.provider.publishMessage(this.name, this.options.schema.payload, payload, { retain: this.options.retain, mqtt: this.options.mqtt, params }); } async subscribe(handler) { return this.provider.subscribeHandler(this.name, this.options.schema.payload, handler, { mqtt: this.options.mqtt }); } async wait(options = {}) { return this.provider.waitForMessage(this.name, this.options.schema.payload, options); } $provider() { if (!this.options.provider) return this.alepha.inject(TopicProvider); if (this.options.provider === "memory") return this.alepha.inject(MemoryTopicProvider); return this.alepha.inject(this.options.provider); } }; $topic[KIND] = TopicPrimitive; //#endregion //#region ../../src/topic/core/index.ts /** * Publish/subscribe messaging for event-driven architectures. * * **Features:** * - Pub/sub topics with type-safe messages * - Topic subscription handlers * - Multiple subscriber support * - Message filtering and routing * - Providers: Memory (dev), Redis (production) * * @module alepha.topic */ const AlephaTopic = $module({ name: "alepha.topic", primitives: [$topic, $subscriber], services: [TopicProvider], variants: [MemoryTopicProvider], register: (alepha) => alepha.with({ optional: true, provide: TopicProvider, use: MemoryTopicProvider }) }); //#endregion export { $subscriber, $topic, AlephaTopic, MemoryTopicProvider, SubscriberPrimitive, TopicPrimitive, TopicProvider, TopicTimeoutError }; //# sourceMappingURL=index.js.map