Trabajo con colas de tareas en TypeScript: en la práctica
\nEn las aplicaciones web modernas, a menudo surge la necesidad de ejecutar operaciones largas o que consumen muchos recursos de forma asíncrona, sin bloquear el flujo principal del usuario. Envío de correos electrónicos masivos, generación de informes PDF, procesamiento de imágenes cargadas: todos estos son candidatos ideales para ser trasladados a tareas en segundo plano. Una cola de tareas (task queue) es un patrón arquitectónico probado que permite organizar la ejecución confiable de dichas operaciones. En este artículo, analizaremos cómo crear y utilizar colas de tareas en TypeScript en la práctica, utilizando bibliotecas y enfoques populares.
\nVeremos la creación de una cola simple, el trabajo con prioridades, el manejo de errores y la integración con Redis a través de la biblioteca BullMQ. Todos los ejemplos estarán escritos en TypeScript con énfasis en la tipificación estricta.
\n\n1. Fundamentos: creando una cola de tareas simple
\nPara empezar, implementaremos una cola de tareas simple sin dependencias externas. Esto ayudará a comprender los principios básicos de funcionamiento. Nuestra cola almacenará funciones de tarea y las ejecutará secuencialmente.
\ntype Task<T> = () => Promise<T>;\n\ninterface TaskItem<T> {\n task: Task<T>;\n resolve: (value: T | PromiseLike<T>) => void;\n reject: (reason?: any) => void;\n}\n\nclass SimpleQueue {\n private queue: TaskItem<any>[] = [];\n private isProcessing = false;\n\n add<T>(task: Task<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n this.queue.push({ task, resolve, reject });\n this.process();\n });\n }\n\n private async process(): Promise<void> {\n if (this.isProcessing || this.queue.length === 0) return;\n this.isProcessing = true;\n\n const item = this.queue.shift()!;\n try {\n const result = await item.task();\n item.resolve(result);\n } catch (error) {\n item.reject(error);\n } finally {\n this.isProcessing = false;\n this.process();\n }\n }\n}\n\n// Uso\nconst queue = new SimpleQueue();\n\nqueue.add(async () => {\n console.log('Tarea 1 iniciada');\n await new Promise(r => setTimeout(r, 1000));\n console.log('Tarea 1 completada');\n return 'Resultado 1';\n});\n\nqueue.add(async () => {\n console.log('Tarea 2 iniciada');\n return 'Resultado 2';\n});\nEste ejemplo demuestra la mecánica básica: las tareas se agregan a un array y se ejecutan en orden. Cada tarea devuelve una Promise, lo que permite trabajar cómodamente con los resultados. Sin embargo, este enfoque tiene limitaciones: no hay soporte para prioridades, reintentos ni persistencia del estado al reiniciar la aplicación.
\n\n2. Priorización y manejo de errores
\nEn proyectos reales, las tareas suelen tener diferente importancia. Por ejemplo, la tarea de enviar un correo electrónico transaccional debe ejecutarse antes que la tarea de limpiar archivos temporales. Agreguemos prioridades a nuestra cola.
\ntype Priority = 'high' | 'medium' | 'low';\n\ninterface PriorityTask<T> {\n task: Task<T>;\n priority: Priority;\n resolve: (value: T) => void;\n reject: (reason?: any) => void;\n retries: number;\n}\n\nclass PriorityQueue {\n private high: PriorityTask<any>[] = [];\n private medium: PriorityTask<any>[] = [];\n private low: PriorityTask<any>[] = [];\n private isProcessing = false;\n private maxRetries = 3;\n\n add<T>(task: Task<T>, priority: Priority = 'medium'): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const taskItem: PriorityTask<T> = {\n task,\n priority,\n resolve,\n reject,\n retries: 0\n };\n this.getQueue(priority).push(taskItem);\n this.process();\n });\n }\n\n private getQueue(priority: Priority): PriorityTask<any>[] {\n switch (priority) {\n case 'high': return this.high;\n case 'medium': return this.medium;\n