ایک لوڈنگ اسپنر آپ کو کچھ نہیں بتاتا۔ جب کوئی AI ٹاسک منٹوں تک جاری رہے—یا تیسری بار ری ٹرائی (retry) کے لیے کیو (queue) میں واپس چلا جائے—تو آپ کو اس کی حالت (state) دیکھنے کی ضرورت ہوتی ہے۔ Server-Sent Events آپ کو وہ ویزیبلٹی (visibility) فراہم کرتے ہیں جو WebSockets کے ہینڈ شیک اوورہیڈ (handshake overhead) یا long polling کی پیچیدگیوں کے بغیر ممکن ہے۔ سرور ایک ہی HTTP رسپانس کو کھلا رکھتا ہے اور جیسے جیسے چیزیں تبدیل ہوتی ہیں، سادہ ٹیکسٹ (plain-text) اپ ڈیٹس بھیجتا رہتا ہے۔ کلائنٹ انہیں پہنچتے ہی پڑھ لیتا ہے۔
اگر کنکشن ٹوٹ جائے، تو آپ غالباً دوبارہ شروع نہیں کرنا چاہیں گے۔ ایک بہتر طریقے سے بنایا گیا SSE اسٹریم یاد رکھتا ہے کہ آپ کہاں تھے۔ Node.js 20 اور صرف اسٹینڈرڈ لائبریری کے ساتھ، آپ اسے ترتیب دے سکتے ہیں۔ کسی بیرونی پیکج کی ضرورت نہیں ہے۔
وائر فارمیٹ (wire format) کیسا دکھتا ہے
ایک SSE میسج سادہ ٹیکسٹ ہوتا ہے۔ سرور تین چیزیں لکھتا ہے: ایک اختیاری ایونٹ کا نام، ایک لازمی data فیلڈ، اور ایک id فیلڈ جو آپ کے سیو پوائنٹ (save point) کے طور پر کام کرتی ہے۔ ہر ریکارڈ دو نیو لائن (newline) کریکٹرز کے ساتھ ختم ہوتا ہے—ایک خالی لائن جو حد (boundary) کو ظاہر کرتی ہے۔
ایک صحت مند اسٹریم وائر پر کچھ اس طرح نظر آ سکتی ہے:
id: 14
event: status
data: {"phase":"testing","progress":43}
id: 15
event: status
data: {"phase":"retrying","attempt":2}
براؤزر کا EventSource کلائنٹ ان لائنوں کو خود بخود پڑھ لیتا ہے۔ یہ ہر بلاک کے لیے ایک ایونٹ اٹھاتا ہے اور اندرونی طور پر تازہ ترین id کو محفوظ کر لیتا ہے۔ اگر TCP کنکشن میں خرابی آئے، تو کلائنٹ انتظار کرتا ہے، دوبارہ کنیکٹ ہوتا ہے، اور محفوظ شدہ آئی ڈی کو Last-Event-ID ہیڈر کے طور پر سرور کو واپس بھیجتا ہے۔ یہ ہیڈر ہی وہ اصل وجہ ہے جس سے یہ پیٹرن کام کرتا ہے۔ اس کے بغیر، آپ کے پاس کوئی پائیدار کرسر (durable cursor) نہیں ہوگا۔
Node.js میں سرور کو وائر کرنا
Node کا بلٹ ان http ماڈیول اسے براہ راست سنبھال سکتا ہے۔ جب کوئی ریکویسٹ آئے، تو صحیح ہیڈرز سیٹ کریں تاکہ کلائنٹ کو معلوم ہو کہ یہ ایک اسٹریم ہے، کوئی پیج نہیں۔
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
بفنگ (buffering) کو ختم کریں۔ پراکسیز اور فریم ورکس کبھی کبھی رسپانسز کو بیچ (batch) میں جمع کرتے ہیں، جو ریئل ٹائم احساس کو ختم کر دیتا ہے، اس لیے ہر چنک (chunk) کے بعد اسے فلش (flush) کریں۔
پہلے ID بھیجیں، پھر ایونٹ کی قسم، پھر پے لوڈ ڈیٹا، اور پھر ختم کرنے والی خالی لائن۔ ترتیب صرف اس لحاظ سے اہم ہے کہ خالی لائن سے پہلے ID پہنچنا چاہیے تاکہ کلائنٹ اسے محفوظ کر سکے۔ اگر آپ نیٹیو response.write() استعمال کر رہے ہیں، تو آؤٹ پٹ لفظی طور پر یہ ہوگا:
response.write(`id: ${cursor}\n`);
response.write(`event: ${eventName}\n`);
response.write(`data: ${JSON.stringify(payload)}\n\n`);
وہ آخر میں آنے والا \n\n محض سجاوٹ نہیں ہے۔ SSE پارسرز اسے ریکارڈ ٹرمینٹر (terminator) کے طور پر لیتے ہیں۔ اگر آپ اسے چھوڑ دیں، تو کلائنٹ مزید ڈیٹا کے انتظار میں ہینگ ہو جائے گا۔
کرسر (cursor) ہی سب کچھ ہے
ایک نیا HTTP کنکشن تازہ حالت (fresh state) کی ضمانت نہیں دیتا۔ جب کوئی کلائنٹ دوبارہ کنیکٹ ہوتا ہے، تو Last-Event-ID ہیڈر آپ کو بتاتا ہے کہ انہوں نے آخری میسج کون سا وصول کیا تھا۔ آپ کا کام اگلے میسج سے شروع کرنا ہے، نہ کہ شروع سے۔
اس کا مطلب ہے سرور سائیڈ پر ایونٹس کا ایک ترتیب وار لاگ (log) یا جرنل (journal) برقرار رکھنا۔ ڈیمو کے لیے ان میموری ایرے (in-memory array) کام کر سکتا ہے۔ پروڈکشن میں آپ کو کچھ پائیدار چاہیے ہوگا—جیسے ڈیٹا بیس لاگ، Redis اسٹریم، یا write-ahead journal میں ڈیٹا شامل کرنا—کیونکہ سرور ری اسٹارٹ ہونے سے ہسٹری ختم نہیں ہونی چاہیے اور ہر کلائنٹ کو صفر سے شروع کرنے پر مجبور نہیں کرنا چاہیے۔
اپنے ایونٹس کو ایک بڑھتے ہوئے انٹیجر (monotonically increasing integer) یا ULID کے ذریعے انڈیکس کریں۔ جب دوبارہ کنکشن (reconnect) آئے، تو ان ایونٹس کے لیے کوئری کریں جہاں id > lastEventId ہو، اور انہیں ترتیب سے دوبارہ چلائیں۔ اگر آپ کے پاس سینکڑوں بیک لاگ میسجز ہیں تو ایک چھوٹا مصنوعی تاخیر (delay) یا بیچ (batch) استعمال کریں، لیکن انہیں پرانے سے نئے کی ترتیب میں بھیجیں تاکہ کلائنٹ کرونولوجیکل (chronological) طور پر حالت کو دوبارہ بنا سکے۔
ڈپلیکیٹس (duplicates) کی توقع رکھیں
نیٹ ورکس قابل بھروسہ نہیں ہوتے۔ سرور ایک ایونٹ بھیج سکتا ہے، TCP ایکنولیجمنٹ (acknowledgment) کھو سکتا ہے، اور ٹائم آؤٹ کے بعد اسے دوبارہ بھیج سکتا ہے۔ شروع سے ہی "at-least-once delivery" کے لیے ڈیزائن بنائیں۔
کلائنٹ پر، ڈی-ڈپلیکیشن (deduplication) آسان ہے۔ ایونٹ آئی ڈی کے ذریعے ایک Map رکھیں۔ جب نیا ایونٹ آئے، تو میپ کو چیک کریں۔ اگر آئی ڈی موجود ہے، تو ڈپلیکیٹ کو خاموشی سے چھوڑ دیں۔ چونکہ آپ کا سرور یقینی (deterministic) آئی ڈیز تفویض کرتا ہے، اس لیے یہ ڈپلیکیٹس کو بے ضرر بنا دیتا ہے۔ میپ کو ہمیشہ کے لیے بڑھنے کی ضرورت نہیں ہے۔ ایک بار جب آپ تصدیق کر لیں کہ ایونٹ محفوظ طریقے سے پروسیس ہو گیا ہے، تو پرانی آئی ڈیز کو نکال دیں۔ براؤزر کلائنٹس کے لیے چند سو اندراجات کی سلائیڈنگ ونڈو (sliding window) عام طور پر کافی ہوتی ہے۔
جب کرسر ایکسپائر (expire) ہو جائے
آخر کار کوئی کلائنٹ گھنٹوں یا دنوں کے بعد دوبارہ کنیکٹ ہوگا۔ اگر آپ کا ہسٹری بفر صرف آخری ہزار ایونٹس تک محدود ہے اور کلائنٹ دو ہزار ایونٹس پیچھے ہے، تو گیپس (gaps) کو دوبارہ چلانا ناممکن ہے۔
جزوی ہسٹری (partial history) اسٹریم نہ کریں۔ یہ کلائنٹ کو ایک غیر مستقل حالت (inconsistent state) میں چھوڑ دیتا ہے۔ اس کے بجائے، ایکسپائر شدہ کرسر کا پتہ لگائیں اور اگلے ایونٹ کے طور پر مکمل اسنیپ شاٹ (snapshot) بھیجیں۔ اسنیپ شاٹ میں ایک نیا کرسر ہونا چاہیے جو کلائنٹ کو موجودہ حالت سے جوڑ دے۔ وہاں سے، لائیو ڈیلٹاس (live deltas) معمول کے مطابق دوبارہ شروع ہو جائیں گے۔ اپنے پروٹوکول میں اس حد کو واضح طور پر بیان کریں تاکہ کلائنٹ کوڈ کو معلوم ہو کہ اسے مقامی ماڈل میں ڈیٹا شامل کرنے کے بجائے اسے کب ری سیٹ کرنا ہے۔
اسٹریم کی حفاظت کریں
کھلے SSE اینڈ پوائنٹس پرکشش اہداف ہو سکتے ہیں۔ کوئی بھی کنکشن برقرار رکھ سکتا ہے، اور ری پلے ریکویسٹس آپ کے اسٹوریج پر ریڈ لوڈ (read load) کو بڑھا سکتی ہیں۔
اینڈ پوائنٹ کو مناسب اتھارائزیشن کے ذریعے محفوظ بنائیں۔ چونکہ براؤزر کا EventSource کسٹم ہیڈرز کو سپورٹ نہیں کرتا، اس لیے ٹوکن کو کوئری اسٹرنگ میں پاس کریں یا سخت SameSite پالیسیوں کے ساتھ کوکیز کا استعمال کریں۔ اسٹریم ریسورسز مختص کرنے سے پہلے ٹوکن کی تصدیق کریں۔
ہسٹری کی حدود اور فی صارف کوٹہ مقرر کریں۔ فی ٹاسک محفوظ شدہ ایونٹس کی تعداد اور فی کلائنٹ بیک وقت ہونے والے کنکشنز کی تعداد کو محدود کریں۔ ڈس کنکٹس اور ری پلے کو لاگ کریں تاکہ آپ کسی ایسے غیر متعلقہ کلائنٹ کی نشاندہی کر سکیں جو آپ کے کرسر اینڈ پوائنٹ پر مسلسل دباؤ ڈال رہا ہو۔
یہ پیٹرن ہر جگہ لاگو ہوتا ہے
یہ طریقہ کار صرف HTTP تک محدود نہیں ہے۔ یہی اصول تب بھی لاگو ہوتے ہیں جب آپ WebSockets، میسج کیوز، یا ایجنٹ-ٹو-ایجنٹ انٹرفیسز کی طرف منتقل ہوتے ہیں۔ ٹرانسپورٹ تبدیل ہو سکتی ہے—آپ بائنری فریمز یا ٹاپک سبسکرپشنز استعمال کر سکتے ہیں—لیکن بنیادی مسئلہ وہی رہتا ہے۔ آپ کو ایک کرسر، ایک پائیدار لاگ، at-least-once semantics، کلائنٹ ڈی ڈپلیکیشن، اور کرسر کے پرانا ہونے پر مکمل اسنیپ شاٹس پر واپسی (fallback) کی ضرورت ہوگی۔ اسٹیٹ کنورجنس کا مسئلہ ایک بار حل کر لیں، اور آپ بنیادی لاجک کو دوبارہ ڈیزائن کیے بغیر اسے TCP، WebSocket، یا RabbitMQ جیسے بروک ر پر بھی منتقل کر سکتے ہیں۔
اسے سادہ رکھیں
Server-Sent Events اس لیے کام کرتے ہیں کیونکہ وہ عام HTTP پر مبنی ہوتے ہیں۔ پراکسیز انہیں سمجھتے ہیں۔ لوڈ بیلنسرز ان کا ہیلتھ چیک کر سکتے ہیں۔ ڈی بگنگ curl کی طرح آسان ہے۔ لیکن اگر آپ ایج کیسز کو نظر انداز کریں گے تو یہ سادگی ختم ہو جائے گی۔ کرسر بنائیں، ری پلے کی توقع رکھیں، کلائنٹ پر ڈی ڈپلیکیشن کریں، اور جب ہسٹری ختم ہو جائے تو اسنیپ شاٹ لیں۔ ایسا کرنے سے، آپ کے طویل عرصے تک چلنے والے AI ٹاسک اپنی پیشرفت کی درست رپورٹ دیں گے، چاہے وائی فائی کمزور ہو، سرور ری اسٹارٹ ہو رہا ہو، یا رات کے وقت براؤزر سلیپ موڈ میں چلا جائے۔
ماخذ: Build a Reconnecting SSE Task Stream with Node.js
بحث میں شامل ہوں: GyaanSetu AI Community
