alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
185 lines (177 loc) • 6.32 kB
text/typescript
import {
createPrimitive,
KIND,
PipelinePrimitive,
type PipelinePrimitiveOptions,
type Static,
type TSchema,
} from "alepha";
import type { QueuePrimitive } from "./$queue.ts";
/**
* Creates a consumer primitive to process messages from a specific queue.
*
* Provides a dedicated message consumer that connects to a queue and processes messages
* with custom handler logic, enabling scalable architectures where multiple consumers
* can process messages from the same queue.
*
* **Key Features**
* - Seamless integration with any $queue primitive
* - Full type safety inherited from queue schema
* - Automatic worker management for background processing
* - Built-in error handling and retry mechanisms
* - Support for multiple consumers per queue for horizontal scaling
*
* **Common Use Cases**
* - Email sending and notification services
* - Image and media processing workers
* - Data synchronization and background jobs
*
* @example
* ```ts
* class EmailService {
* emailQueue = $queue({
* name: "emails",
* schema: z.object({
* to: z.text(),
* subject: z.text(),
* body: z.text()
* })
* });
*
* emailConsumer = $consumer({
* queue: this.emailQueue,
* handler: async (message) => {
* const { to, subject, body } = message.payload;
* await this.sendEmail(to, subject, body);
* }
* });
*
* async sendWelcomeEmail(userEmail: string) {
* await this.emailQueue.push({
* to: userEmail,
* subject: "Welcome!",
* body: "Thanks for joining."
* });
* }
* }
* ```
*/
export const $consumer = <T extends TSchema>(
options: ConsumerPrimitiveOptions<T>,
): ConsumerPrimitive<T> => {
return createPrimitive(ConsumerPrimitive<T>, options);
};
// ---------------------------------------------------------------------------------------------------------------------
export interface ConsumerPrimitiveOptions<T extends TSchema>
extends PipelinePrimitiveOptions {
/**
* The queue primitive that this consumer will process messages from.
*
* This establishes the connection between the consumer and its source queue:
* - The consumer inherits the queue's message schema for type safety
* - Messages pushed to the queue will be automatically routed to this consumer
* - Multiple consumers can be attached to the same queue for parallel processing
* - The consumer will use the queue's provider and configuration settings
*
* **Queue Integration Benefits**:
* - Type safety: Consumer handler gets fully typed message payloads
* - Schema validation: Messages are validated before reaching the consumer
* - Error handling: Failed messages can be retried or moved to dead letter queues
* - Monitoring: Queue metrics include consumer processing statistics
*
* @example
* ```ts
* // First, define a queue
* emailQueue = $queue({
* name: "emails",
* schema: z.object({ to: z.text(), subject: z.text() })
* });
*
* // Then, create a consumer for that queue
* emailConsumer = $consumer({
* queue: this.emailQueue, // Reference the queue primitive
* handler: async (message) => { } // process email
* });
* ```
*/
queue: QueuePrimitive<T>;
/**
* Message handler function that processes individual messages from the queue.
*
* This function:
* - Receives fully typed and validated message payloads from the connected queue
* - Runs in the background worker system for non-blocking operation
* - Should implement the core business logic for processing this message type
* - Can throw errors to trigger the queue's retry mechanisms
* - Has access to the full Alepha dependency injection container
* - Should be idempotent to handle potential duplicate deliveries
*
* **Handler Design Guidelines**:
* - Keep handlers focused on a single responsibility
* - Use proper error handling and meaningful error messages
* - Log important processing steps for debugging and monitoring
* - Consider transaction boundaries for data consistency
* - Make operations idempotent when possible
* - Validate business rules within the handler logic
*
* **Error Handling Strategy**:
* - Throw errors for temporary failures that should be retried
* - Log and handle permanent failures gracefully
* - Use specific error types to control retry behavior
* - Consider implementing circuit breakers for external service calls
*
* @param message - The queue message containing the validated payload
* @param message.payload - The typed message data based on the queue's schema
* @returns Promise that resolves when processing is complete
*
* @example
* ```ts
* handler: async (message) => {
* const { userId, action, data } = message.payload;
*
* try {
* // Log processing start
* this.logger.info(`Processing ${action} for user ${userId}`);
*
* // Validate business rules
* if (!await this.userService.exists(userId)) {
* throw new Error(`User ${userId} not found`);
* }
*
* // Perform the main processing logic
* switch (action) {
* case "create":
* await this.processCreation(userId, data);
* break;
* case "update":
* await this.processUpdate(userId, data);
* break;
* default:
* throw new Error(`Unknown action: ${action}`);
* }
*
* // Log successful completion
* this.logger.info(`Successfully processed ${action} for user ${userId}`);
*
* } catch (error) {
* // Log error with context
* this.logger.error(`Failed to process ${action} for user ${userId}`, {
* error: error.message,
* userId,
* action,
* data
* });
*
* // Re-throw to trigger queue retry mechanism
* throw error;
* }
* }
* ```
*/
handler: (message: { payload: Static<T> }) => Promise<void>;
}
// ---------------------------------------------------------------------------------------------------------------------
export class ConsumerPrimitive<T extends TSchema> extends PipelinePrimitive<
ConsumerPrimitiveOptions<T>
> {}
$consumer[KIND] = ConsumerPrimitive;