Travailler avec les files d'attente de tâches en TypeScript : en pratique
\nDans les applications web modernes, il est souvent nécessaire d'effectuer des opérations longues ou gourmandes en ressources de manière asynchrone, sans bloquer le fil principal de l'utilisateur. L'envoi de campagnes email, la génération de rapports PDF, le traitement d'images téléchargées — ce sont tous des candidats idéaux pour être déportés dans des tâches d'arrière-plan. La file d'attente de tâches (task queue) est un modèle architectural éprouvé qui permet d'organiser l'exécution fiable de ces opérations. Dans cet article, nous allons voir comment créer et utiliser des files d'attente de tâches en TypeScript en pratique, en utilisant des bibliothèques et des approches populaires.
\nNous aborderons la création d'une file d'attente simple, le travail avec les priorités, la gestion des erreurs, ainsi que l'intégration avec Redis via la bibliothèque BullMQ. Tous les exemples seront écrits en TypeScript avec un accent sur le typage strict.
\n\n1. Les bases : créer une file d'attente de tâches simple
\nPour commencer, implémentons une file d'attente de tâches simple sans dépendances externes. Cela aidera à comprendre les principes de base de fonctionnement. Notre file d'attente stockera des fonctions-tâches et les exécutera séquentiellement.
\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// Utilisation\nconst queue = new SimpleQueue();\n\nqueue.add(async () => {\n console.log('Tâche 1 commencée');\n await new Promise(r => setTimeout(r, 1000));\n console.log('Tâche 1 terminée');\n return 'Résultat 1';\n});\n\nqueue.add(async () => {\n console.log('Tâche 2 commencée');\n return 'Résultat 2';\n});\nCet exemple illustre la mécanique de base : les tâches sont ajoutées dans un tableau et exécutées les unes après les autres. Chaque tâche retourne une Promise, ce qui permet de travailler facilement avec les résultats. Cependant, cette approche a des limites : pas de support des priorités, des tentatives répétées ni de sauvegarde d'état lors du redémarrage de l'application.
\n\n2. Priorisation et gestion des erreurs
\nDans les projets réels, les tâches ont souvent des importantes différentes. Par exemple, une tâche d'envoi d'email transactionnel doit être exécutée avant une tâche de nettoyage de fichiers temporaires. Ajoutons des priorités à notre file d'attente.
\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