ഒരു ലോഡിംഗ് സ്പിന്നറും നിങ്ങൾക്ക് ഒന്നും പറഞ്ഞുതരില്ല. ഒരു AI ടാസ്ക് മിനിറ്റുകൾ നീണ്ടുനിൽക്കുകയോ അല്ലെങ്കിൽ മൂന്നാം തവണ റീട്രൈ ചെയ്യാൻ ക്യൂവിലേക്ക് തിരികെ പോകുകയോ ചെയ്യുമ്പോൾ, അതിന്റെ അവസ്ഥ (state) നിങ്ങൾക്ക് കാണേണ്ടതുണ്ട്. WebSockets-ന്റെ ഹാൻഡ്ഷേക്ക് ഓവർഹെഡോ അല്ലെങ്കിൽ ലോങ്ങ് പോളിംഗിന്റെ സങ്കീർണ്ണതയോ ഇല്ലാതെ 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
ബഫറിംഗ് ഒഴിവാക്കുക. പ്രോക്സികളും ഫ്രെയിംവർക്കുകളും ചിലപ്പോൾ റെസ്പോൺസുകൾ ബച്ച് (batch) ചെയ്യാറുണ്ട്, ഇത് റിയൽ-ടൈം അനുഭവം ഇല്ലാതാക്കും, അതിനാൽ ഓരോ ചങ്ക് (chunk) കഴിഞ്ഞും ഫ്ലഷ് (flush) ചെയ്യുക.
ആദ്യം 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 ഹെഡർ നിങ്ങളോട് പറയുന്നു. നിങ്ങളുടെ ജോലി അടുത്ത മെസ്സേജിൽ നിന്ന് പുനരാരംഭിക്കുക എന്നതാണ്, ആദ്യമെ നിന്നല്ല.
ഇതിനർത്ഥം സെർവർ സൈഡിൽ ഇവന്റുകളുടെ ക്രമത്തിലുള്ള ഒരു ലോഗ് അല്ലെങ്കിൽ ജേണൽ സൂക്ഷിക്കണം എന്നാണ്. ഒരു ഡെമോയ്ക്ക് ഇൻ-മെമ്മറി അറേ (in-memory array) ഉപയോഗിക്കാം. എന്നാൽ പ്രൊഡക്ഷനിൽ നിങ്ങൾക്ക് കൂടുതൽ ഡ്യൂറബിൾ ആയ ഒന്ന് വേണം—ഒരു ഡാറ്റാബേസ് ലോഗിലോ, ഒരു Redis സ്ട്രീമിലോ, അല്ലെങ്കിൽ ഒരു റൈറ്റ്-അഹെഡ് ജേണലിലോ വിവരങ്ങൾ ചേർക്കുക—കാരണം ഒരു സെർവർ റീസ്റ്റാർട്ട് ഹിസ്റ്ററി മായ്ച്ചു കളയുകയും എല്ലാ ക്ലയന്റുകളെയും പൂജ്യത്തിൽ നിന്ന് തുടങ്ങാൻ നിർബന്ധിക്കുകയും ചെയ്യരുത്.
നിങ്ങളുടെ ഇവന്റുകൾ ഒരു മോണോട്ടോണിക്ലി ഇൻക്രീസിംഗ് ഇന്റീജർ (monotonically increasing integer) അല്ലെങ്കിൽ ഒരു ULID ഉപയോഗിച്ച് ഇൻഡക്സ് ചെയ്യുക. വീണ്ടും കണക്ട് ചെയ്യുമ്പോൾ, id > lastEventId ആയ ഇവന്റുകൾക്കായി ക്വറി ചെയ്യുകയും അവ ക്രമത്തിൽ വീണ്ടും പ്ലേ ചെയ്യുകയും ചെയ്യുക. നൂറുകണക്കിന് ബാക്ക്ലോഗ് മെസ്സേജുകൾ ഉണ്ടെങ്കിൽ ചെറിയൊരു കൃത്രിമമായ ഡിലേ അല്ലെങ്കിൽ ബാച്ച് ഉപയോഗിക്കാം, എന്നാൽ ക്ലയന്റിന് സ്റ്റേറ്റ് ക്രമമായി പുനർനിർമ്മിക്കാൻ കഴിയുന്നതിനായി അവ പഴയതിൽ നിന്ന് പുതിയതിലേക്ക് അയക്കുക.
ഡ്യൂപ്ലിക്കേറ്റുകൾ പ്രതീക്ഷിക്കുക
നെറ്റ്വർക്കുകൾ എപ്പോഴും വിശ്വസനീയമല്ല. ഒരു സെർവർ ഒരു ഇവന്റ് അയച്ചേക്കാം, എന്നാൽ TCP അക്നോളജ്മെന്റ് നഷ്ടപ്പെടുകയും ടൈമൗട്ടിന് ശേഷം അത് വീണ്ടും അയക്കുകയും ചെയ്തേക്കാം. തുടക്കം മുതൽ തന്നെ അറ്റ്-ലീസ്റ്റ്-വൺസ് ഡെലിവറി (at-least-once delivery) രീതിയിൽ ഡിസൈൻ ചെയ്യുക.
ക്ലയന്റിൽ, ഡ്യൂപ്ലിക്കേഷൻ ഒഴിവാക്കുന്നത് എളുപ്പമാണ്. ഇവന്റ് ഐഡി ഉപയോഗിച്ച് ഒരു Map സൂക്ഷിക്കുക. ഒരു പുതിയ ഇവന്റ് വരുമ്പോൾ മാപ്പ് പരിശോധിക്കുക. ഐഡി നിലവിലുണ്ടെങ്കിൽ, ഡ്യൂപ്ലിക്കേറ്റ് നിശബ്ദമായി ഒഴിവാക്കുക. നിങ്ങളുടെ സെർവർ കൃത്യമായ ഐഡികൾ നൽകുന്നതിനാൽ, ഇത് ഡ്യൂപ്ലിക്കേറ്റുകളെ ദോഷകരമല്ലാതാക്കുന്നു. മാപ്പ് എപ്പോഴും വളർന്നുകൊണ്ടിരിക്കേണ്ടതില്ല. ഒരു ഇവന്റ് സുരക്ഷിതമായി പ്രോസസ്സ് ചെയ്തതായി ഉറപ്പാക്കിയാൽ പഴയ ഐഡികൾ നീക്കം ചെയ്യുക. ബ്രൗസർ ക്ലയന്റുകൾക്ക് ഏതാനും നൂറ് എൻട്രികൾ മാത്രമുള്ള ഒരു സ്ലൈഡിംഗ് വിൻഡോ (sliding window) സാധാരണയായി മതിയാകും.
കഴ്സർ കാലാവധി കഴിയുമ്പോൾ
ഒടുവിൽ ഒരു ക്ലയന്റ് മണിക്കൂറുകൾക്കോ ദിവസങ്ങൾക്കോ ശേഷം വീണ്ടും കണക്ട് ചെയ്തേക്കാം. നിങ്ങളുടെ ഹിസ്റ്ററി ബഫർ അവസാന ആയിരം ഇവന്റുകൾ മാത്രമേ ഉൾക്കൊള്ളുന്നുള്ളൂ എങ്കിലും ക്ലയന്റ് രണ്ടായിരം ഇവന്റുകൾ പിന്നിലാണെങ്കിൽ, വിട്ടുപോയ ഭാഗങ്ങൾ വീണ്ടും പ്ലേ ചെയ്യുന്നത് അസാധ്യമാണ്.
ഭാഗികമായ ഹിസ്റ്ററി മാത്രം സ്ട്രീം ചെയ്യരുത്. അത് ക്ലയന്റിനെ അസ്ഥിരമായ ഒരു അവസ്ഥയിൽ (inconsistent state) എത്തിക്കും. പകരം, കഴ്സർ കാലാവധി കഴിഞ്ഞതായി തിരിച്ചറിയുകയും അടുത്ത ഇവന്റായി ഒരു ഫുൾ സ്നാപ്പ്ഷോട്ട് (full snapshot) അയക്കുകയും ചെയ്യുക. സ്നാപ്പ്ഷോട്ടിൽ ക്ലയന്റിനെ നിലവിലെ സ്റ്റേറ്റിലേക്ക് എത്തിക്കുന്ന പുതിയൊരു കഴ്സർ ഉണ്ടായിരിക്കണം. അതിനുശേഷം, ലൈവ് ഡെൽറ്റകൾ (live deltas) സാധാരണ പോലെ തുടരാം. ക്ലയന്റ് കോഡ് എപ്പോൾ ലോക്കൽ മോഡൽ റീസെറ്റ് ചെയ്യണമെന്ന് അറിയുന്നതിനായി പ്രോട്ടോക്കോളിൽ ഈ പരിധി വ്യക്തമായി രേഖപ്പെടുത്തുക.
സ്ട്രീം സംരക്ഷിക്കുക
തുറന്നുകിടക്കുന്ന SSE എൻഡ്പോയിന്റുകൾ ആക്രമിക്കപ്പെടാൻ സാധ്യതയുള്ളവയാണ്. ആർക്കും ഒരു കണക്ഷൻ നിലനിർത്താൻ കഴിയും, കൂടാതെ റീപ്ലേ റിക്വസ്റ്റുകൾ നിങ്ങളുടെ സ്റ്റോറേജിലെ റീഡ് ലോഡ് (read load) വർദ്ധിപ്പിക്കാനും കാരണമാകും.
ശരിയായ ഓതറൈസേഷനിലൂടെ എൻഡ്പോയിന്റ് സുരക്ഷിതമാക്കുക. ബ്രൗസറിലെ EventSource-ന് കസ്റ്റം ഹെഡറുകൾ (custom headers) പിന്തുണയ്ക്കുന്നില്ലാത്തതിനാൽ, ടോക്കൺ ക്വറി സ്ട്രിംഗിലൂടെ (query string) കൈമാറുക അല്ലെങ്കിൽ കർശനമായ SameSite പോളിസികളുള്ള കുക്കികൾ (cookies) ഉപയോഗിക്കുക. സ്ട്രീം റിസോഴ്സുകൾ (stream resources) അനുവദിക്കുന്നതിന് മുമ്പ് ടോക്കൺ പരിശോധിക്കുക.
ഹിസ്റ്ററി പരിധികളും ഓരോ ഉപയോക്താവിനും വേണ്ടിയുള്ള ക്വാട്ടകളും നിശ്ചയിക്കുക. ഓരോ ടാസ്കിലും സംഭരിച്ചുവെക്കുന്ന ഇവന്റുകളുടെ എണ്ണവും ഓരോ ക്ലയന്റിനും അനുവദനീയമായ ഒരേസമയം കണക്ട് ചെയ്യാവുന്ന കണക്ഷനുകളുടെ (concurrent connections) എണ്ണവും പരിമിതപ്പെടുത്തുക. ഡിസ്കണക്റ്റുകളും റീപ്ലേകളും (replays) ലോഗ് ചെയ്യുക, അങ്ങനെ നിങ്ങളുടെ കർസർ എൻഡ്പോയിന്റിൽ (cursor endpoint) അമിതമായി റിക്വസ്റ്റുകൾ അയക്കുന്ന ഒരു മോശം ക്ലയന്റിനെ (rogue client) നിങ്ങൾക്ക് കണ്ടെത്താനാകും.
ഈ രീതി എല്ലാടത്തും ബാധകമാണ്
ഈ സമീപനം HTTP-യിൽ മാത്രം ഒതുങ്ങിനിൽക്കുന്നതല്ല. നിങ്ങൾ WebSockets, മെസ്സേജ് ക്യൂകൾ (message queues), അല്ലെങ്കിൽ ഏജന്റ്-ടു-ഏജന്റ് ഇന്റർഫേസുകളിലേക്ക് മാറുമ്പോഴും ഇതേ നിയമങ്ങൾ ബാധകമാണ്. ട്രാൻസ്പോർട്ട് രീതി മാറിയേക്കാം—നിങ്ങൾ ബൈനറി ഫ്രെയിമുകളോ (binary frames) ടോപ്പിക് സബ്സ്ക്രിപ്ഷനുകളോ ഉപയോഗിച്ചേക്കാം—എന്നാൽ അടിസ്ഥാന പ്രശ്നം ഒന്നുതന്നെയായിരിക്കും. നിങ്ങൾക്ക് ഒരു കർസർ (cursor), ഒരു ഡ്യൂറബിൾ ലോഗ് (durable log), at-least-once semantics, ക്ലയന്റ് ഡ്യൂപ്ലിക്കേഷൻ (client deduplication), കൂടാതെ കർസർ കാലഹരണപ്പെടുമ്പോൾ (stale) ഫുൾ സ്നാപ്പ്ഷോട്ടുകളിലേക്കുള്ള (full snapshots) ഒരു ബാക്കപ്പ് സംവിധാനം എന്നിവ ആവശ്യമാണ്. സ്റ്റേറ്റ് കൺവേർജൻസ് (state convergence) ഒരിക്കൽ പരിഹരിച്ചാൽ, കോർ ലോജിക് (core logic) വീണ്ടും രൂപകൽപ്പന ചെയ്യാതെ തന്നെ നിങ്ങൾക്ക് ഇത് TCP, WebSocket, അല്ലെങ്കിൽ RabbitMQ പോലുള്ള ഒരു ബ്രോക്കർ വഴി ഉപയോഗിക്കാം.
ലളിതമായി സൂക്ഷിക്കുക
സാധാരണ HTTP ഉപയോഗിക്കുന്നതുകൊണ്ടാണ് Server-Sent Events പ്രവർത്തിക്കുന്നത്. പ്രോക്സികൾക്ക് (Proxies) അവ മനസ്സിലാക്കാൻ സാധിക്കും. ലോഡ് ബാലൻസറുകൾക്ക് (Load balancers) അവ ഹെൽത്ത്-ചെക്ക് ചെയ്യാം. curl ഉപയോഗിക്കുന്നത് പോലെ വളരെ എളുപ്പത്തിൽ ഡീബഗ് (debug) ചെയ്യാം. എന്നാൽ എഡ്ജ് കേസുകൾ (edge cases) നിങ്ങൾ അവഗണിച്ചാൽ ആ ലാളിത്യം നഷ്ടപ്പെടും. കർസർ നിർമ്മിക്കുക. റീപ്ലേകൾ പ്രതീക്ഷിക്കുക. ക്ലയന്റിൽ തന്നെ ഡ്യൂപ്ലിക്കേഷനുകൾ ഒഴിവാക്കുക (Deduplicate). ഹിസ്റ്ററി അവസാനിക്കുമ്പോൾ സ്നാപ്പ്ഷോട്ട് എടുക്കുക. അങ്ങനെ ചെയ്താൽ, മോശം വൈഫൈ (spotty Wi-Fi), സെർവർ റീസ്റ്റാർട്ടുകൾ, രാത്രികാലങ്ങളിൽ ബ്രൗസർ സ്ലീപ്പ് മോഡിലേക്ക് മാറുന്നത് എന്നിവ ഉണ്ടായാലും നിങ്ങളുടെ ദീർഘനേരത്തെ AI ടാസ്ക്കുകൾ അവയുടെ പുരോഗതി കൃത്യമായി റിപ്പോർട്ട് ചെയ്യും.
സ്രോതസ്സ്: Build a Reconnecting SSE Task Stream with Node.js
ചർച്ചയിൽ പങ്കുചേരുക: GyaanSetu AI Community
