العمل مع قوائم المهام في TypeScript: عمليًا
\nفي تطبيقات الويب الحديثة، غالبًا ما تكون هناك حاجة لتنفيذ عمليات طويلة أو كثيفة الاستخدام للموارد بشكل غير متزامن، دون حظر التدفق الرئيسي للمستخدم. إرسال البريد الإلكتروني الجماعي، إنشاء تقارير PDF، معالجة الصور المرفوعة - كلها مرشحة مثالية للنقل إلى مهام خلفية. قائمة المهام (task queue) هي نمط معماري مثبت يسمح بتنظيم تنفيذ موثوق لمثل هذه العمليات. في هذه المقالة، سنشرح كيفية إنشاء واستخدام قوائم المهام في TypeScript عمليًا، باستخدام المكتبات والأساليب الشائعة.
\nسننظر في إنشاء قائمة مهام بسيطة، والعمل مع الأولويات، ومعالجة الأخطاء، بالإضافة إلى التكامل مع Redis عبر مكتبة BullMQ. جميع الأمثلة ستكون مكتوبة بلغة TypeScript مع التركيز على الكتابة الصارمة للأنواع.
\n\n1. الأساسيات: إنشاء قائمة مهام بسيطة
\nلنبدأ بتنفيذ قائمة مهام بسيطة بدون تبعيات خارجية. سيساعدنا هذا على فهم المبادئ الأساسية للعمل. ستخزن قائمتنا وظائف المهام وتنفذها بالتسلسل.
\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// الاستخدام\nconst queue = new SimpleQueue();\n\nqueue.add(async () => {\n console.log('بدأت المهمة 1');\n await new Promise(r => setTimeout(r, 1000));\n console.log('اكتملت المهمة 1');\n return 'النتيجة 1';\n});\n\nqueue.add(async () => {\n console.log('بدأت المهمة 2');\n return 'النتيجة 2';\n});\nيوضح هذا المثال الآلية الأساسية: تتم إضافة المهام إلى مصفوفة وتنفيذها بالتسلسل. كل مهمة تُرجع Promise، مما يسمح بالعمل بسهولة مع النتائج. ومع ذلك، فإن هذا النهج له قيود: لا يوجد دعم للأولويات، أو إعادة المحاولة، أو حفظ الحالة عند إعادة تشغيل التطبيق.
\n\n2. تحديد الأولويات ومعالجة الأخطاء
\nفي المشاريع الحقيقية، غالبًا ما يكون للمهام أهمية مختلفة. على سبيل المثال، يجب تنفيذ مهمة إرسال بريد إلكتروني معاملة قبل مهمة تنظيف الملفات المؤقتة. دعنا نضيف الأولويات إلى قائمتنا.
\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