alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
217 lines (209 loc) • 7.65 kB
text/typescript
import {
createPrimitive,
KIND,
PipelinePrimitive,
type PipelinePrimitiveOptions,
} from "alepha";
import type {
TopicHandler,
TopicMessageSchema,
TopicPrimitive,
} from "./$topic.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()
* });
* }
* }
* ```
*/
export const $subscriber = <T extends TopicMessageSchema>(
options: SubscriberPrimitiveOptions<T>,
): SubscriberPrimitive<T> => {
return createPrimitive(SubscriberPrimitive<T>, options);
};
// ---------------------------------------------------------------------------------------------------------------------
export interface SubscriberPrimitiveOptions<T extends TopicMessageSchema>
extends PipelinePrimitiveOptions {
/**
* The topic primitive that this subscriber will listen to for messages.
*
* This establishes the connection between the subscriber and its source topic:
* - The subscriber inherits the topic's message schema for type safety
* - Messages published to the topic will be automatically delivered to this subscriber
* - Multiple subscribers can listen to the same topic independently
* - The subscriber will use the topic's provider and configuration settings
*
* **Topic Integration Benefits**:
* - Type safety: Subscriber handler gets fully typed message payloads
* - Schema validation: Messages are validated before reaching the subscriber
* - Real-time delivery: Messages are delivered immediately upon publication
* - Error isolation: Subscriber errors don't affect the topic or other subscribers
* - Monitoring: Topic metrics include subscriber processing statistics
*
* @example
* ```ts
* // First, define a topic
* userEvents = $topic({
* name: "user-activity",
* schema: {
* payload: z.object({ userId: z.text(), action: z.text() })
* }
* });
*
* // Then, create a subscriber for that topic
* activitySubscriber = $subscriber({
* topic: this.userEvents, // Reference the topic primitive
* handler: async (message) => { } // Process messages here
* });
* ```
*/
topic: TopicPrimitive<T>;
/**
* Message handler function that processes individual messages from the topic.
*
* This function:
* - Receives fully typed and validated message payloads from the connected topic
* - Executes immediately when messages are published to the topic
* - Should implement the core business logic for reacting to these events
* - Runs independently of other subscribers to the same topic
* - Should handle errors gracefully to avoid affecting other subscribers
* - Has access to the full Alepha dependency injection container
*
* **Handler Design Guidelines**:
* - Keep handlers focused on a single responsibility
* - Use proper error handling and logging
* - Consider performance impact for high-frequency topics
* - Make handlers idempotent when possible for reliability
* - Validate business rules within the handler logic
* - Log important processing steps for debugging and monitoring
*
* **Error Handling Strategy**:
* - Log errors but don't re-throw to avoid affecting other subscribers
* - Use try-catch blocks for external service calls
* - Implement circuit breakers for resilience with external systems
* - Monitor error rates and patterns for system health
* - Consider implementing retry logic for temporary failures
*
* **Performance Considerations**:
* - Keep handler execution time minimal for high-throughput topics
* - Use background queues for heavy processing triggered by events
* - Implement batching for efficiency when processing many similar events
* - Consider async processing patterns for non-critical operations
*
* @param message - The topic message with validated payload and optional headers
* @param message.payload - The typed message data based on the topic's schema
* @returns Promise that resolves when processing is complete
*
* @example
* ```ts
* handler: async (message) => {
* const { userId, eventType, timestamp } = message.payload;
*
* try {
* // Log event receipt
* this.logger.info(`Processing ${eventType} event for user ${userId}`, {
* timestamp,
* userId,
* eventType
* });
*
* // Perform event-specific processing
* switch (eventType) {
* case 'user.login':
* await this.updateLastLogin(userId, timestamp);
* await this.sendWelcomeNotification(userId);
* break;
* case 'user.logout':
* await this.updateSessionDuration(userId, timestamp);
* break;
* case 'user.purchase':
* await this.updateRewardsPoints(userId, message.payload.purchaseAmount);
* await this.triggerRecommendations(userId);
* break;
* default:
* this.logger.warn(`Unknown event type: ${eventType}`);
* }
*
* // Update analytics
* await this.analytics.track(eventType, {
* userId,
* timestamp,
* source: 'topic-subscriber'
* });
*
* this.logger.info(`Successfully processed ${eventType} for user ${userId}`);
*
* } catch (error) {
* // Log error but don't re-throw to avoid affecting other subscribers
* this.logger.error(`Failed to process ${eventType} for user ${userId}`, {
* error: error.message,
* stack: error.stack,
* userId,
* eventType,
* timestamp
* });
*
* // Optionally send to error tracking service
* await this.errorTracker.captureException(error, {
* context: { userId, eventType, timestamp },
* tags: { component: 'topic-subscriber' }
* });
* }
* }
* ```
*/
handler: TopicHandler<T>;
}
// ---------------------------------------------------------------------------------------------------------------------
export class SubscriberPrimitive<
T extends TopicMessageSchema,
> extends PipelinePrimitive<SubscriberPrimitiveOptions<T>> {}
$subscriber[KIND] = SubscriberPrimitive;