Trabalhando com Filas de Tarefas em TypeScript: na prática
\nEm aplicações web modernas, surge frequentemente a necessidade de executar operações longas ou que consomem muitos recursos de forma assíncrona, sem bloquear o fluxo principal do usuário. Envio de campanhas de e-mail, geração de relatórios em PDF, processamento de imagens carregadas — todos esses são candidatos ideais para serem transferidos para tarefas em segundo plano. Uma fila de tarefas (task queue) é um padrão arquitetural comprovado que permite organizar a execução confiável de tais operações. Neste artigo, vamos analisar como criar e usar filas de tarefas em TypeScript na prática, utilizando bibliotecas e abordagens populares.
\nAbordaremos a criação de uma fila simples, o trabalho com prioridades, o tratamento de erros e a integração com Redis através da biblioteca BullMQ. Todos os exemplos serão escritos em TypeScript com ênfase em tipagem estrita.
\n\n1. Fundamentos: criando uma fila de tarefas simples
\nPara começar, vamos implementar uma fila de tarefas simples sem dependências externas. Isso ajudará a entender os princípios básicos de funcionamento. Nossa fila armazenará funções-tarefa e as executará sequencialmente.
\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('Tarefa 1 iniciada');\n await new Promise(r => setTimeout(r, 1000));\n console.log('Tarefa 1 concluída');\n return 'Resultado 1';\n});\n\nqueue.add(async () => {\n console.log('Tarefa 2 iniciada');\n return 'Resultado 2';\n});\nEste exemplo demonstra a mecânica básica: as tarefas são adicionadas a um array e executadas em ordem. Cada tarefa retorna uma Promise, o que permite trabalhar convenientemente com os resultados. No entanto, essa abordagem tem limitações: não há suporte para prioridades, novas tentativas e preservação de estado ao reiniciar a aplicação.
\n\n2. Priorização e tratamento de erros
\nEm projetos reais, as tarefas geralmente têm diferentes níveis de importância. Por exemplo, a tarefa de envio de um e-mail transacional deve ser executada antes da tarefa de limpeza de arquivos temporários. Vamos adicionar prioridades à nossa fila.
\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