UNPKG

alepha

Version:

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

578 lines 20.3 kB
import { Alepha, KIND, PipelinePrimitive, PipelinePrimitiveOptions, Primitive, Service, Static, TSchema } from "alepha"; import { DateTimeProvider } from "alepha/datetime"; //#region ../../src/queue/core/providers/QueueProvider.d.ts /** * Minimalist Queue interface. * * Will be probably enhanced in the future to support more advanced features. But for now, it's enough! */ declare abstract class QueueProvider { /** * Push a message to the queue. * * @param queue Name of the queue to push the message to. * @param message String message to be pushed to the queue. Buffer messages are not supported for now. */ abstract push(queue: string, message: string): Promise<void>; /** * Pop a message from the queue. * * @param queue Name of the queue to pop the message from. * * @returns The message popped or `undefined` if the queue is empty. */ abstract pop(queue: string): Promise<string | undefined>; } //#endregion //#region ../../src/queue/core/providers/MemoryQueueProvider.d.ts declare class MemoryQueueProvider implements QueueProvider { protected readonly log: import("alepha/logger").Logger; protected queueList: Record<string, string[]>; push(queue: string, ...messages: string[]): Promise<void>; pop(queue: string): Promise<string | undefined>; } //#endregion //#region ../../src/queue/core/providers/WorkerProvider.d.ts /** * Queue worker configuration atom. */ declare const queueWorkerOptions: import("alepha").Atom<import("zod").ZodObject<{ interval: import("zod").ZodDefault<import("zod").ZodInt>; maxInterval: import("zod").ZodDefault<import("zod").ZodInt>; concurrency: import("zod").ZodDefault<import("zod").ZodInt>; }, import("zod/v4/core").$strip>, "alepha.queue.worker.options">; type QueueWorkerOptions = Static<typeof queueWorkerOptions.schema>; declare module "alepha" { interface State { [queueWorkerOptions.key]: QueueWorkerOptions; } } declare class WorkerProvider { protected readonly log: import("alepha/logger").Logger; protected readonly options: Readonly<{ interval: number; maxInterval: number; concurrency: number; }>; protected readonly alepha: Alepha; protected readonly queueProvider: QueueProvider; protected readonly dateTimeProvider: DateTimeProvider; protected workerPromises: Array<Promise<void>>; protected workersRunning: number; protected abortController: AbortController | undefined; protected workerIntervals: Record<number, number>; protected consumers: Array<Consumer>; protected nextConsumerIndex: number; get isRunning(): boolean; protected readonly start: import("alepha").HookPrimitive<"start">; /** * Start the workers. * This method will create an endless loop that will check for new messages! */ protected startWorkers(): void; protected readonly stop: import("alepha").HookPrimitive<"stop">; /** * Wait for the next message, where `n` is the worker number. * * This method will wait for a certain amount of time, increasing the wait time again if no message is found. */ protected waitForNextMessage(n: number): Promise<void>; /** * Get the next message. */ protected getNextMessage(): Promise<undefined | NextMessage>; /** * Process a message from a queue. */ protected processMessage(response: { message: any; consumer: Consumer; }): Promise<void>; /** * Stop the workers. * * This method will stop the workers and wait for them to finish processing. */ protected stopWorkers(): Promise<void>; /** * Force the workers to get back to work. */ wakeUp(): void; } interface Consumer<T extends TSchema = TSchema> { queue: QueuePrimitive<T>; handler: (message: QueueMessage<T>) => Promise<void>; } interface NextMessage { consumer: Consumer; message: string; } //#endregion //#region ../../src/queue/core/primitives/$queue.d.ts /** * Creates a queue primitive for asynchronous message processing with background workers. * * `$queue` is the **raw transport layer**: it fans messages out to background * workers over the configured backend with type-safe payloads. Delivery is * **at-most-once** — a message is popped from the backend before the handler * runs, so a handler error or a process crash loses it. There is no retry, * no dead-letter queue, and no delivery guarantee at this layer. * * **For work that must not be lost, use `$job` (alepha/api/jobs) instead.** * It layers a durable, DB-backed outbox over this transport: at-least-once * delivery, retries, idempotency keys, priorities, crash recovery via a * reconciliation sweep, and failure records. * * **What $queue gives you** * - Type-safe payloads with schema validation at push and receive * - Background workers with graceful shutdown and lifecycle management * - Pluggable backends: memory (dev/test), Redis, Cloudflare Queues * - Cheap fire-and-forget fan-out where occasional loss is acceptable * (cache invalidation, presence pings, metrics, live notifications) * * @example Loss-tolerant event fan-out * ```typescript * const activityQueue = $queue({ * name: "activity-events", * schema: z.object({ * userId: z.text(), * event: z.text(), * at: z.number() * }), * handler: async (message) => { * await metrics.track(message.payload); * } * }); * * // Push messages for background processing * await activityQueue.push({ * userId: "u1", * event: "page-view", * at: 1700000000000 * }); * ``` * * @example Batch processing with Redis * ```typescript * const imageQueue = $queue({ * name: "image-processing", * provider: RedisQueueProvider, * schema: z.object({ * imageId: z.text(), * operations: z.array(z.enum(["resize", "compress", "thumbnail"])) * }), * handler: async (message) => { * for (const op of message.payload.operations) { * await processImage(message.payload.imageId, op); * } * } * }); * * // Batch processing multiple images * await imageQueue.push( * { imageId: "img1", operations: ["resize", "thumbnail"] }, * { imageId: "img2", operations: ["compress"] }, * { imageId: "img3", operations: ["resize", "compress", "thumbnail"] } * ); * ``` * * @example Development with memory provider * ```typescript * const taskQueue = $queue({ * name: "dev-tasks", * provider: "memory", * schema: z.object({ * taskType: z.enum(["cleanup", "backup", "report"]), * data: z.record(z.text(), z.any()) * }), * handler: async (message) => { * switch (message.payload.taskType) { * case "cleanup": * await performCleanup(message.payload.data); * break; * case "backup": * await createBackup(message.payload.data); * break; * case "report": * await generateReport(message.payload.data); * break; * } * } * }); * ``` */ declare const $queue: { <T extends TSchema>(options: QueuePrimitiveOptions<T>): QueuePrimitive<T>; [KIND]: typeof QueuePrimitive; }; interface QueuePrimitiveOptions<T extends TSchema> { /** * Unique name for the queue. * * This name is used for: * - Queue identification across the system * - Storage backend key generation * - Logging and monitoring * - Worker assignment and routing * * If not provided, defaults to the property key where the queue is declared. * * @example "email-notifications" * @example "image-processing" * @example "order-fulfillment" */ name?: string; /** * Human-readable description of the queue's purpose. * * Used for: * - Documentation generation * - Monitoring dashboards * - Development team communication * - Queue management interfaces * * @example "Process user registration emails and welcome sequences" * @example "Handle image uploads, resizing, and thumbnail generation" * @example "Manage order processing, payment, and shipping workflows" */ description?: string; /** * Queue storage provider configuration. * * Options: * - **"memory"**: In-memory queue (default for development, lost on restart) * - **Service<QueueProvider>**: Custom provider class (e.g., RedisQueueProvider) * - **undefined**: Uses the default queue provider from dependency injection * * **Provider Selection Guidelines**: * - Development: Use "memory" for fast, simple testing * - Production: Use Redis so multiple instances share one queue (note: * a popped message is gone — the at-most-once caveat above applies) * - High-throughput: Use specialized providers with connection pooling * - Distributed systems: Use Redis or message brokers for scalability * * @default Uses injected QueueProvider * @example "memory" * @example RedisQueueProvider * @example DatabaseQueueProvider */ provider?: "memory" | Service<QueueProvider>; /** * TypeBox schema defining the structure of messages in this queue. * * This schema: * - Validates all messages pushed to the queue * - Provides full TypeScript type inference * - Ensures type safety between producers and consumers * - Enables automatic serialization/deserialization * * **Schema Design Best Practices**: * - Keep schemas simple and focused on the specific task * - Use optional fields for data that might not always be available * - Include version fields for schema evolution * - Use union types for different message types in the same queue * * @example * ```ts * z.object({ * userId: z.text(), * action: z.enum(["create", "update"]), * data: z.record(z.text(), z.any()), * timestamp: z.number().optional() * }) * ``` */ schema: T; /** * Message handler function that processes queue messages. * * This function: * - Runs in background worker threads for non-blocking processing * - Receives type-safe message payloads based on the schema * - Has access to the full Alepha dependency injection container * - **Loses the message if it throws** — errors are logged, not retried. * Use `$job` when failed work must be retried or recorded. * * **Handler Best Practices**: * - Keep handlers focused on a single responsibility * - Use proper error handling and logging * - Make operations idempotent when possible * - Validate critical business logic within handlers * - Consider using transactions for data consistency * * @param message - The queue message with validated payload * @returns Promise that resolves when processing is complete * * @example * ```ts * handler: async (message) => { * const { userId, email, template } = message.payload; * * try { * await this.emailService.send({ * to: email, * template, * data: { userId } * }); * * await this.userService.markEmailSent(userId, template); * } catch (error) { * // The message is NOT redelivered after a throw — record the * // failure yourself, or use $job for retryable work. * this.logger.error(`Failed to send email to ${email}`, error); * throw error; * } * } * ``` */ handler?: (message: QueueMessage<T>) => Promise<void>; } declare class QueuePrimitive<T extends TSchema> extends Primitive<QueuePrimitiveOptions<T>> { protected readonly log: import("alepha/logger").Logger; protected readonly workerProvider: WorkerProvider; readonly provider: MemoryQueueProvider | QueueProvider; push(...payloads: Array<Static<T>>): Promise<void>; get name(): string; protected $provider(): MemoryQueueProvider | QueueProvider; } interface QueueMessageSchema { payload: TSchema; } interface QueueMessage<T extends TSchema> { payload: Static<T>; } //#endregion //#region ../../src/queue/core/primitives/$consumer.d.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." * }); * } * } * ``` */ declare const $consumer: { <T extends TSchema>(options: ConsumerPrimitiveOptions<T>): ConsumerPrimitive<T>; [KIND]: typeof ConsumerPrimitive; }; 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>; } declare class ConsumerPrimitive<T extends TSchema> extends PipelinePrimitive<ConsumerPrimitiveOptions<T>> {} //#endregion //#region ../../src/queue/core/providers/CloudflareQueueProvider.d.ts /** * Cloudflare Queue interface matching the CF Workers Queue API. */ interface CloudflareQueue { send(message: unknown): Promise<void>; sendBatch(messages: Array<{ body: unknown; }>): Promise<void>; } /** * Default queue binding name used in wrangler configuration. */ declare const QUEUE_DEFAULT_BINDING = "JOBS_QUEUE"; /** * How many times Cloudflare redelivers a message before routing it to the * consumer's dead-letter queue. Matches Cloudflare's own default. */ declare const QUEUE_DEFAULT_MAX_RETRIES = 3; /** * Cloudflare Queue provider. * * Uses a Queue binding for message dispatch. Messages are wrapped with the * logical queue name so the consumer can route them to the correct handler. * * **Required Cloudflare binding:** * - `JOBS_QUEUE` - A Queue binding in wrangler configuration * * @example * ```toml * # wrangler.toml - automatically generated by alepha build * [[queues.producers]] * binding = "JOBS_QUEUE" * queue = "my-app-queue" * ``` */ declare class CloudflareQueueProvider extends QueueProvider { protected readonly alepha: Alepha; protected readonly log: import("alepha/logger").Logger; protected queue?: CloudflareQueue; protected readonly onStart: import("alepha").HookPrimitive<"start">; push(queue: string, message: string): Promise<void>; /** * Not used on Cloudflare — queue consumption is push-based via the `queue` handler. */ pop(_queue: string): Promise<string | undefined>; protected getQueue(): CloudflareQueue; } //#endregion //#region ../../src/queue/core/index.d.ts /** * Asynchronous message processing with automatic worker management. * * **Features:** * - Background job queues with type-safe payloads * - Queue consumer handlers * - Automatic worker threads for non-blocking processing * - Retry mechanisms with exponential backoff * - Dead letter queues for failed messages * - Batch processing support * - Configurable concurrency and worker pools * - Providers: Memory (dev), Redis (production) * * @module alepha.queue */ declare const AlephaQueue: import("alepha").Service<import("alepha").Module>; //#endregion export { $consumer, $queue, AlephaQueue, CloudflareQueue, CloudflareQueueProvider, Consumer, ConsumerPrimitive, ConsumerPrimitiveOptions, MemoryQueueProvider, NextMessage, QUEUE_DEFAULT_BINDING, QUEUE_DEFAULT_MAX_RETRIES, QueueMessage, QueueMessageSchema, QueuePrimitive, QueuePrimitiveOptions, QueueProvider, QueueWorkerOptions, WorkerProvider, queueWorkerOptions }; //# sourceMappingURL=index.d.ts.map