مؤشر التحميل لا يخبرك بشيء. عندما تستغرق مهمة ذكاء اصطناعي عدة دقائق — أو تعود إلى قائمة الانتظار للمحاولة الثالثة — فأنت بحاجة لرؤية الحالة. تمنحك أحداث الخادم المرسلة (Server-Sent Events) هذه الرؤية دون عبء المصافحة (handshake overhead) الخاص بـ WebSockets أو تعقيدات الاستطلاع الطويل (long polling). يحتفظ الخادم باستجابة HTTP واحدة مفتوحة ويقوم بدفع تحديثات نصية بسيطة مع تغير الأشياء. ويقوم العميل بقراءتها فور وصولها.

إذا انقطع الاتصال، فمن المحتمل أنك لا تريد البدء من جديد. تدفق SSE المصمم جيدًا يتذكر أين كنت. باستخدام Node.js 20 والمكتبة القياسية وحدها، يمكنك إعداد ذلك. لا حاجة لحزم خارجية.

كيف يبدو تنسيق البيانات عبر الشبكة

رسالة SSE هي نص بسيط. يكتب الخادم ثلاثة أشياء: اسم حدث اختياري، وحقل data مطلوب، وحقل id يصبح نقطة الحفظ الخاصة بك. ينتهي كل سجل برمزين للسطر الجديد — سطر فارغ يحدد الحدود.

قد يبدو التدفق السليم عبر الشبكة بهذا الشكل:

id: 14
event: status
data: {"phase":"testing","progress":43}

id: 15
event: status
data: {"phase":"retrying","attempt":2}

يقرأ عميل EventSource في المتصفح هذه الأسطر تلقائيًا. حيث يطلق حدثًا لكل كتلة ويخزن أحدث id داخليًا. إذا تعثر اتصال TCP، ينتظر العميل، ثم يعيد الاتصال، ويرسل المعرف المخزن مرة أخرى إلى الخادم عبر ترويسة Last-Event-ID. هذه الترويسة هي السبب الوحيد لعمل هذا النمط؛ فبدونها، لن يكون لديك مؤشر (cursor) مستدام.

ربط الخادم في Node.js

يمكن لوحدة http المدمجة في Node التعامل مع هذا مباشرة. عندما يصل طلب، قم بتعيين الترويسات الصحيحة ليعرف العميل أن هذا تدفق (stream) وليس صفحة:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

تخلص من التخزين المؤقت (buffering). تقوم البروكسيات (proxies) وأطر العمل أحيانًا بتجميع الاستجابات، مما يقتل الشعور بالوقت الفعلي، لذا قم بتفريغ البيانات (flush) بعد كل جزء.

أرسل المعرف (ID) أولاً، ثم نوع الحدث، ثم بيانات الحمولة (payload)، ثم السطر الفارغ النهائي. الترتيب مهم فقط من حيث وجوب وصول المعرف قبل السطر الفارغ حتى يتمكن العميل من التقاطه. إذا كنت تستخدم response.write() الأصلية، فإن المخرج سيكون حرفيًا:

response.write(`id: ${cursor}\n`);
response.write(`event: ${eventName}\n`);
response.write(`data: ${JSON.stringify(payload)}\n\n`);

إن الرمز \n\n في النهاية ليس للزينة. تتعامل محللات SSE معه كمنهي للسجل. إذا فاتك، سيعلق العميل في انتظار المزيد من البيانات.

المؤشر هو كل شيء

الاتصال الجديد عبر HTTP لا يضمن حالة جديدة. عندما يعيد العميل الاتصال، تخبرك ترويسة Last-Event-ID بآخر رسالة تلقاها. مهمتك هي الاستئناف من الرسالة التالية، وليس من البداية.

هذا يعني الاحتفاظ بسجل أو يومية مرتبة للأحداث في جانب الخادم. المصفوفة في الذاكرة (in-memory array) تفي بالغرض في العروض التجريبية. أما في بيئة الإنتاج، فأنت بحاجة إلى شيء مستدام — مثل الإضافة إلى سجل قاعدة بيانات، أو Redis stream، أو سجل كتابة مسبقة (write-ahead journal) — لأن إعادة تشغيل الخادم يجب ألا تمسح التاريخ وتجبر كل عميل على البدء من الصفر.

قم بفهرسة أحداثك باستخدام عدد صحيح متزايد بشكل مطرد أو ULID. عندما يأتي طلب إعادة الاتصال، استعلم عن الأحداث حيث يكون id > lastEventId وأعد تشغيلها بالترتيب. أضف تأخيرًا اصطناعيًا بسيطًا أو قم بتجميع البيانات إذا كان لديك مئات الرسائل المتراكمة، ولكن أرسلها من الأقدم إلى الأحدث حتى يتمكن العميل من إعادة بناء الحالة زمنيًا.

توقع تكرار البيانات

الشبكات ليست موثوقة دائمًا. قد يرسل الخادم حدثًا، ويفقد إقرار TCP، ثم يرسله مرة أخرى بعد انتهاء المهلة. صمم نظامك ليعمل بمبدأ التسليم "مرة واحدة على الأقل" (at-least-once delivery) منذ البداية.

على جانب العميل، عملية إزالة التكرار (deduplication) بسيطة. احتفظ بـ Map مفتاحه هو معرف الحدث (event ID). عندما يصل حدث جديد، تحقق من الخريطة. إذا كان المعرف موجودًا، فقم بإسقاط التكرار بصمت. وبما أن خادمك يخصص معرفات حتمية (deterministic IDs)، فإن هذا يجعل التكرارات غير ضارة. لا تحتاج الخريطة للنمو إلى الأبد؛ فبمجرد التأكد من معالجة الحدث بأمان، قم بإزالة المعرفات القديمة. عادة ما تكون النافذة المنزلقة (sliding window) التي تضم بضع مئات من المدخلات كافية لعملاء المتصفح.

عندما تنتهي صلاحية المؤشر

في النهاية، سيعيد العميل الاتصال بعد ساعات أو أيام. إذا كان مخزن التاريخ لديك يغطي فقط آخر ألف حدث وكان العميل متأخرًا بألفي حدث، فسيكون من المستحيل تعويض الفجوات.

لا تقم ببث تاريخ جزئي، لأن ذلك يترك العميل في حالة غير متسقة. بدلاً من ذلك، اكتشف انتهاء صلاحية المؤشر وأرسل لقطة كاملة (full snapshot) كحدث تالٍ. يجب أن تحمل اللقطة مؤشرًا جديدًا يربط العميل بالحالة الحالية. ومن هناك، تستأنف التغييرات المباشرة (live deltas) كالمعتاد. قم بتوثيق هذه الحدود بوضوح في البروتوكول الخاص بك حتى يعرف كود العميل متى يجب عليه إعادة ضبط نموذجه المحلي بدلاً من الإضافة إليه.

حماية التدفق

تعتبر نقاط نهاية SSE المفتوحة أهدافًا جذابة. يمكن لأي شخص الاحتفاظ باتصال، ويمكن لطلبات إعادة التشغيل أن تضاعف حمل القراءة على وحدة التخزين لديك.

قم بتأمين نقطة النهاية باستخدام تفويض مناسب. نظرًا لأن EventSource في المتصفح لا يدعم الترويسات المخصصة (custom headers)، قم بتمرير الرمز (token) في سلسلة الاستعلام (query string) أو استخدم ملفات تعريف الارتباط (cookies) مع سياسات SameSite صارمة. تحقق من صحة الرمز قبل تخصيص موارد البث.

ضع حدوداً للسجل (history) وحصصاً لكل مستخدم. حدد عدد الأحداث المخزنة لكل مهمة، وحدد عدد الاتصالات المتزامنة لكل عميل. قم بتسجيل عمليات الفصل وإعادة التشغيل (replays) حتى تتمكن من رصد أي عميل مارق يغرق نقطة نهاية المؤشر (cursor endpoint) بالطلبات.

النمط ينتقل عبر الوسائط

هذا النهج ليس محصوراً داخل بروتوكول HTTP. تنطبق القواعد نفسها عند الانتقال إلى WebSockets، أو طوابير الرسائل (message queues)، أو واجهات الوكيل للوكيل (agent-to-agent interfaces). تتغير وسيلة النقل — فقد تستخدم إطارات ثنائية (binary frames) أو اشتراكات المواضيع (topic subscriptions) — ولكن المشكلة الأساسية تظل كما هي. أنت بحاجة إلى مؤشر (cursor)، وسجل دائم (durable log)، ودلالات "التسليم مرة واحدة على الأقل" (at-least-once semantics)، وإزالة التكرار من جانب العميل (client deduplication)، والرجوع إلى لقطات كاملة (full snapshots) عندما يصبح المؤشر قديماً. حل مشكلة تقارب الحالة (state convergence) مرة واحدة، ويمكنك نقلها عبر TCP أو WebSocket أو وسيط (broker) مثل RabbitMQ دون الحاجة لإعادة تصميم المنطق الأساسي.

حافظ على البساطة

تعمل أحداث الخادم المرسلة (Server-Sent Events) لأنها تعتمد على بروتوكول HTTP العادي. الخوادم الوكيلة (Proxies) تفهمها، وموازنات الحمل (Load balancers) يمكنها إجراء فحص الحالة (health-check) لها. كما أن تصحيح الأخطاء (Debugging) سهل تماماً مثل استخدام curl. لكن هذه البساطة تختفي إذا تجاهلت الحالات الاستثنائية (edge cases). ابنِ المؤشر. توقع عمليات إعادة التشغيل. قم بإزالة التكرار عند العميل. التقط لقطة (snapshot) عندما ينتهي السجل. افعل ذلك، وستقوم مهام الذكاء الاصطناعي طويلة الأمد بالإبلاغ عن تقدمها بصدق، حتى عبر شبكة Wi-Fi غير مستقرة، أو إعادة تشغيل الخادم، أو وضع السكون المفاجئ للمتصفح أثناء الليل.

المصدر: Build a Reconnecting SSE Task Stream with Node.js

انضم إلى النقاش: GyaanSetu AI Community