లోడింగ్ స్పిన్నర్ మీకు ఏమీ చెప్పదు. ఒక AI టాస్క్ నిమిషాల పాటు కొనసాగినప్పుడు—లేదా మూడవసారి రీట్రై కోసం క్యూలోకి వెళ్ళినప్పుడు—మీరు దాని స్టేట్ను చూడాల్సిన అవసరం ఉంటుంది. WebSockets యొక్క హ్యాండ్షేక్ ఓవర్హెడ్ లేదా long polling యొక్క సంక్లిష్టత లేకుండా Server-Sent Events మీకు ఆ విజిబిలిటీని అందిస్తాయి. సర్వర్ ఒకే ఒక 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 హెడర్గా సర్వర్కు పంపుతుంది. ఈ ప్యాటర్న్ పని చేయడానికి కారణం అంతా ఆ హెడర్ మాత్రమే. అది లేకపోతే, మీ వద్ద డ్యూరబుల్ కర్సర్ ఉండదు.
Node.jsలో సర్వర్ను వైరింగ్ చేయడం
Node యొక్క బిల్ట్-ఇన్ http మాడ్యూల్ దీనిని నేరుగా హ్యాండిల్ చేయగలదు. ఒక రిక్వెస్ట్ వచ్చినప్పుడు, ఇది ఒక పేజీ కాదు, ఒక స్ట్రీమ్ అని క్లయింట్కు తెలియజేయడానికి సరైన హెడర్లను సెట్ చేయండి:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
బఫరింగ్ను తొలగించండి. ప్రాక్సీలు మరియు ఫ్రేమ్వర్క్లు కొన్నిసార్లు రెస్పాన్స్లను బ్యాచ్లుగా చేస్తాయి, ఇది రియల్-టైమ్ అనుభూతిని దెబ్బతీస్తుంది, కాబట్టి ప్రతి చంక్ తర్వాత ఫ్లష్ చేయండి.
ముందుగా 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 పార్సర్లు దానిని రికార్డ్ టెర్మినేటర్గా పరిగణిస్తాయి. దానిని మర్చిపోతే, క్లయింట్ మరిన్ని డేటా కోసం వేచి చూస్తూ హ్యాంగ్ అవుతుంది.
కర్సర్ అనేది అన్నిటికంటే ముఖ్యం
కొత్త HTTP కనెక్షన్ కొత్త స్టేట్ను గ్యారెంటీ చేయదు. క్లయింట్ మళ్ళీ కనెక్ట్ అయినప్పుడు, Last-Event-ID హెడర్ వారు చివరిగా అందుకున్న మెసేజ్ను మీకు చెబుతుంది. మీ పని మొదటి నుండి కాకుండా, తదుపరి మెసేజ్ నుండి తిరిగి ప్రారంభించడం.
దీని అర్థం సర్వర్ సైడ్లో ఈవెంట్ల యొక్క క్రమబద్ధమైన లాగ్ లేదా జర్నల్ను నిర్వహించడం. డెమో కోసం ఇన్-మెమరీ అర్రే సరిపోతుంది. ప్రొడక్షన్లో మీరు ఏదైనా డ్యూరబుల్ పద్ధతిని ఉపయోగించాలి—డేటాబేస్ లాగ్, Redis స్ట్రీమ్ లేదా write-ahead జర్నల్కు యాపెండ్ చేయండి—ఎందుకంటే సర్వర్ రీస్టార్ట్ అయినప్పుడు హిస్టరీ మొత్తం పోయి, ప్రతి క్లయింట్ సున్నా నుండి ప్రారంభించాల్సి రాకూడదు.
మీ ఈవెంట్లను మోనోటోనిక్లీ పెరిగే ఇంటిజర్ లేదా ULID ద్వారా ఇండెక్స్ చేయండి. రీకనెక్ట్ వచ్చినప్పుడు, id > lastEventId ఉన్న ఈవెంట్ల కోసం క్వెరీ చేయండి మరియు వాటిని క్రమ పద్ధతిలో ప్లే చేయండి. మీ వద్ద వందల కొద్దీ బ్యాక్లాగ్ మెసేజ్లు ఉంటే చిన్న ఆర్టిఫిషియల్ డిలే లేదా బ్యాచింగ్ను ఉపయోగించండి, కానీ క్లయింట్ క్రోనాలజికల్గా స్టేట్ను రీబిల్డ్ చేయడానికి వీలుగా పాత మెసేజ్ల నుండి పంపండి.
డూప్లికేట్లను ఆశించండి
నెట్వర్క్లు నమ్మదగినవి కావు. సర్వర్ ఒక ఈవెంట్ను పంపవచ్చు, TCP అక్నాలెడ్జ్మెంట్ను కోల్పోవచ్చు మరియు టైమ్ అవుట్ అయిన తర్వాత దానిని మళ్ళీ పంపవచ్చు. మొదటి నుండినే at-least-once delivery కోసం డిజైన్ చేయండి.
క్లయింట్ వైపు, డూప్లికేషన్ తొలగింపు (deduplication) సులభం. ఈవెంట్ ID ద్వారా కీ చేయబడిన Mapను ఉంచుకోండి. కొత్త ఈవెంట్ వచ్చినప్పుడు, మ్యాప్ను తనిఖీ చేయండి. ID ఇప్పటికే ఉంటే, డూప్లికేట్ను సైలెంట్గా వదిలేయండి. మీ సర్వర్ డిటర్మినిస్టిక్ IDలను కేటాయించడం వల్ల, డూప్లికేట్లు హాని చేయవు. మ్యాప్ ఎప్పటికీ పెరగాల్సిన అవసరం లేదు. ఒక ఈవెంట్ సురక్షితంగా ప్రాసెస్ చేయబడిందని మీరు నిర్ధారించుకున్న తర్వాత, పాత IDలను తొలగించండి. బ్రౌజర్ క్లయింట్ల కోసం కొన్ని వందల ఎంట్రీల స్లైడింగ్ విండో సరిపోతుంది.
కర్సర్ ఎక్స్పైర్ అయినప్పుడు
చివరికి ఒక క్లయింట్ గంటలు లేదా రోజుల తర్వాత రీకనెక్ట్ అవుతుంది. మీ హిస్టరీ బఫర్ కేవలం చివరి వెయ్యి ఈవెంట్లను మాత్రమే కలిగి ఉండి, క్లయింట్ రెండు వేల ఈవెంట్ల వెనుకబడి ఉంటే, గ్యాప్లను ప్లే చేయడం అసాధ్యం.
పాక్షిక హిస్టరీని స్ట్రీమ్ చేయవద్దు. అది క్లయింట్ను అస్థిరమైన (inconsistent) స్టేట్లో ఉంచుతుంది. బదులుగా, ఎక్స్పైర్ అయిన కర్సర్ను గుర్తించి, తదుపరి ఈవెంట్గా పూర్తి స్నాప్షాట్ను పంపండి. ఆ స్నాప్షాట్ క్లయింట్ను ప్రస్తుత స్టేట్కు అనుసంధానించే కొత్త కర్సర్ను కలిగి ఉండాలి. అక్కడి నుండి, లైవ్ డెల్టాస్ సాధారణంగా కొనసాగుతాయి. క్లయింట్ కోడ్ ఎప్పుడు లోకల్ మోడల్ను రీసెట్ చేయాలో తెలియజేయడానికి, మీ ప్రోటోకాల్లో ఈ బౌండరీని స్పష్టంగా డాక్యుమెంట్ చేయండి.
స్ట్రీమ్ను రక్షించండి
ఓపెన్ SSE ఎండ్పాయింట్లు ఆకర్షణీయమైన లక్ష్యాలు. ఎవరైనా కనెక్షన్ను పట్టుకుని ఉండవచ్చు మరియు రీప్లే రిక్వెస్ట్లు మీ స్టోరేజ్ పై రీడ్ లోడ్ను పెంచవచ్చు.
Gate the endpoint with proper authorization. Because the browser EventSource does not support custom headers, pass the token in the query string or use cookies with strict SameSite policies. Validate the token before you allocate stream resources.
Set history limits and per-user quotas. Cap the number of stored events per task, and cap the number of concurrent connections per client. Log disconnects and replays so you can spot a rogue client hammering your cursor endpoint.
The pattern travels
This approach is not trapped inside HTTP. The same rules apply when you move to WebSockets, message queues, or agent-to-agent interfaces. The transport changes—you might use binary frames or topic subscriptions—but the underlying problem stays identical. You need a cursor, a durable log, at-least-once semantics, client deduplication, and a fallback to full snapshots when the cursor goes stale. Solve state convergence once, and you can ship it over TCP, WebSocket, or a broker like RabbitMQ without redesigning the core logic.
Keep it simple
Server-Sent Events work because they ride on ordinary HTTP. Proxies understand them. Load balancers can health-check them. Debugging is as easy as curl. But that simplicity disappears if you ignore the edge cases. Build the cursor. Expect replays. Deduplicate on the client. snapshot when history runs out. Do that, and your long-running AI tasks will report their progress honestly, even through spotty Wi-Fi, server restarts, and the occasional overnight browser sleep.
Source: Build a Reconnecting SSE Task Stream with Node.js
Join the discussion: GyaanSetu AI Community
