[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-server-sent-events-eventsource-live-updates":44,"search-suggestions":245,"related-server-sent-events-eventsource-live-updates":286,"code:js:true:e1c9fh":415,"code:js:true:cowdp9":416,"code:js:true:1dfyv8m":417,"code:js:true:fjndlq":418,"code:plain:true:o3im8c":419,"comments-01a03cdb-3743-73ef-ab9d-21f03158ca35":420},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":228,"seo":229,"translationGroupId":232,"series":48,"podcastUrl":48,"verticalId":5,"thread":233,"assessments":235,"translations":241,"quiz":243},"01a03cdb-3743-73ef-ab9d-21f03158ca35","server-sent-events-eventsource-live-updates","You Don't Need a WebSocket for That Live Feed",null,"A live notification badge, a progress bar, a dashboard counter — you reached for socket.io and started hand-rolling reconnect logic. The browser already ships a simpler API that does it for you.","\u002Fmedia\u002Fcovers\u002Fserver-sent-events-eventsource-live-updates.png",6,"2026-08-31T13:11:42.385Z",9,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"javascript","JavaScript",[64,66,69,72],{"slug":61,"name":65,"color":48},"Javascript",{"slug":67,"name":68,"color":48},"webdev","Webdev",{"slug":70,"name":71,"color":48},"tutorial","Tutorial",{"slug":73,"name":74,"color":48},"apis","Apis",{"assessments":76},1,{"slug":46,"title":78},"EventSource vs WebSocket — reconnect playground",{"blocks":80,"version":76},[81,85,88,91,95,98,104,107,110,114,117,120,124,127,130,133,137,140,143,147,150,153,158,161,164,167,170,173,176,179,182,189,192,195,198,201,204,207,210,213,216,219,222],{"id":82,"html":83,"type":84},"b1","\u003Cp>You need a live notification badge. The count in the corner should tick up the moment something happens on the server — no refresh, no polling every three seconds hoping you didn&#39;t miss anything. So you do what everyone does: \u003Ccode>npm install socket.io\u003C\u002Fcode>, wire up a client, and ship it.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>Then someone&#39;s wifi hiccups for two seconds. The socket drops. Nothing reconnects, because you didn&#39;t write that part yet. Now you&#39;re writing it — a \u003Ccode>reconnecting\u003C\u002Fcode> flag, a backoff timer that doubles each attempt, a cap so it doesn&#39;t retry forever, a check for whether the tab is even in the foreground. Forty-something lines later you have a hand-rolled connection manager sitting in front of a feature that only ever sent data in one direction: server to browser.\u003C\u002Fp>",{"id":89,"html":90,"type":84},"b3","\u003Cp>There&#39;s a browser API that already does the part you just spent an afternoon on. It&#39;s been sitting in every evergreen browser for over a decade, and it&#39;s not the one you reached for.\u003C\u002Fp>",{"id":92,"html":93,"text":93,"type":94,"level":31},"b4","The wrong way, in more detail than you wanted","heading",{"id":96,"html":97,"type":84},"b5","\u003Cp>Here&#39;s roughly what that WebSocket reconnect logic looks like once it&#39;s &quot;done&quot;:\u003C\u002Fp>",{"id":99,"code":100,"type":101,"language":102,"highlight":103},"b6","let socket;\nlet retryDelay = 1000;\n\nfunction connect() {\n  socket = new WebSocket('wss:\u002F\u002Fapi.example.com\u002Fnotifications');\n\n  socket.onopen = () => { retryDelay = 1000; };\n\n  socket.onmessage = (event) => {\n    updateBadge(JSON.parse(event.data));\n  };\n\n  socket.onclose = () => {\n    setTimeout(connect, retryDelay);\n    retryDelay = Math.min(retryDelay * 2, 30000);\n  };\n\n  socket.onerror = () => socket.close();\n}\n\nconnect();","code","js",[],{"id":105,"html":106,"type":84},"b7","\u003Cp>It works. It&#39;s also code you didn&#39;t want to own: a manual backoff schedule, a \u003Ccode>retryDelay\u003C\u002Fcode> you have to remember to reset on success, and a \u003Ccode>close\u003C\u002Fcode>\u002F\u003Ccode>error\u003C\u002Fcode> interaction that&#39;s easy to get subtly wrong (fire both handlers and you&#39;ll double-schedule a reconnect). And you still haven&#39;t handled \u003Cem>resuming\u003C\u002Fem> — if the connection drops mid-stream, you have no idea what the server sent while you were gone. You just start fresh and hope you didn&#39;t miss the one notification that mattered.\u003C\u002Fp>",{"id":108,"html":109,"type":84},"b8","\u003Cp>None of that is a WebSocket problem, exactly. It&#39;s what you get for using a bidirectional, binary-capable protocol for a feature that only ever pushes text from server to client.\u003C\u002Fp>",{"id":111,"html":112,"text":113,"type":94,"level":31},"b9","What you&#39;re actually asking for","What you're actually asking for",{"id":115,"html":116,"type":84},"b10","\u003Cp>Strip the feature down to what it needs: the server pushes updates, the browser listens, and if the connection drops, it should pick back up without you writing a state machine. That&#39;s \u003Ca href=\"https:\u002F\u002Fhtml.spec.whatwg.org\u002Fmultipage\u002Fserver-sent-events.html\">Server-Sent Events\u003C\u002Fa> — an HTTP response that never ends, plus a browser-side API called \u003Ccode>EventSource\u003C\u002Fcode> that reads it.\u003C\u002Fp>",{"id":118,"html":119,"type":84},"b11","\u003Cp>The client side is almost embarrassingly small:\u003C\u002Fp>",{"id":121,"code":122,"type":101,"language":102,"highlight":123},"b12","const stream = new EventSource('\u002Fapi\u002Fnotifications');\n\nstream.onmessage = (event) => {\n  updateBadge(JSON.parse(event.data));\n};\n\nstream.onerror = () => {\n  \u002F\u002F the browser is already retrying — this just tells you it happened\n  console.warn('connection dropped, reconnecting…');\n};",[],{"id":125,"html":126,"type":84},"b13","\u003Cp>That&#39;s it. No \u003Ccode>retryDelay\u003C\u002Fcode>, no \u003Ccode>setTimeout\u003C\u002Fcode>, no \u003Ccode>connect()\u003C\u002Fcode> function to call again. If the connection drops — the wifi hiccup, a proxy timeout, the server restarting — the browser reconnects on its own. You didn&#39;t write that part because it isn&#39;t yours to write.\u003C\u002Fp>",{"id":128,"html":129,"text":129,"type":94,"level":31},"b14","The part that replaces your backoff timer",{"id":131,"html":132,"type":84},"b15","\u003Cp>The server side is a plain HTTP response with one specific content type and a text format the browser knows how to parse:\u003C\u002Fp>",{"id":134,"code":135,"type":101,"language":102,"highlight":136},"b16","app.get('\u002Fapi\u002Fnotifications', (req, res) => {\n  res.set({\n    'Content-Type': 'text\u002Fevent-stream',\n    'Cache-Control': 'no-cache',\n    Connection: 'keep-alive',\n  });\n\n  const send = (data) => res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n\n  const unsubscribe = notifications.subscribe(send);\n  req.on('close', unsubscribe);\n});",[],{"id":138,"html":139,"type":84},"b17","\u003Cp>Each message is a \u003Ccode>data:\u003C\u002Fcode> line followed by a blank line — that blank line is what tells the browser &quot;this event is complete, hand it to \u003Ccode>onmessage\u003C\u002Fcode>.&quot; Send that content type and keep the connection open, and you&#39;ve built the entire server half of what \u003Ccode>socket.io\u003C\u002Fcode> was doing for you, minus the parts you didn&#39;t need.\u003C\u002Fp>",{"id":141,"html":142,"type":84},"b18","\u003Cp>The reconnection is where it earns the comparison to your backoff timer. When the browser reconnects after a drop, it&#39;s not just retrying blindly — it sends a \u003Ccode>Last-Event-ID\u003C\u002Fcode> header carrying the ID of the last event it successfully received, so the server can pick the stream back up instead of replaying everything or losing the gap:\u003C\u002Fp>",{"id":144,"code":145,"type":101,"language":102,"highlight":146},"b19","const send = (data, id) => {\n  res.write(`id: ${id}\\n`);\n  res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n};\n\napp.get('\u002Fapi\u002Fnotifications', (req, res) => {\n  res.set({ 'Content-Type': 'text\u002Fevent-stream' });\n  const lastId = req.headers['last-event-id'];\n  const missed = notifications.since(lastId); \u002F\u002F your own backlog logic\n  missed.forEach((n) => send(n.payload, n.id));\n  const unsubscribe = notifications.subscribe(send);\n  req.on('close', unsubscribe);\n});",[],{"id":148,"html":149,"type":84},"b20","\u003Cp>You still have to write \u003Ccode>notifications.since()\u003C\u002Fcode> — SSE gives you the \u003Cem>mechanism\u003C\u002Fem> for catching up, not the backlog itself. But that&#39;s a data-layer problem you&#39;d have had with a WebSocket too, except there you&#39;d also be reimplementing the ID and the reconnect trigger by hand.\u003C\u002Fp>",{"id":151,"html":152,"type":84},"b21","\u003Cp>Want to change how long the browser waits before its first reconnect attempt? The server can set that too, with a \u003Ccode>retry:\u003C\u002Fcode> line — the value&#39;s in milliseconds:\u003C\u002Fp>",{"id":154,"code":155,"type":101,"language":156,"highlight":157},"b22","retry: 5000\ndata: {\"count\": 3}\n","plain",[],{"id":159,"html":160,"type":84},"b23","\u003Cp>Skip it and the browser falls back to its own default of a few seconds, which is fine for most dashboards and badges.\u003C\u002Fp>",{"id":162,"html":163,"type":84},"b24","\u003C!-- playground:start -->",{"id":165,"html":166,"text":166,"type":94,"level":31},"b25","🎮 Try it yourself",{"id":168,"html":169,"type":84},"b26","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fserver-sent-events-eventsource-live-updates\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":171,"html":172,"type":84},"b27","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":174,"html":175,"type":84},"b28","\u003C!-- playground:end -->",{"id":177,"html":178,"text":178,"type":94,"level":31},"b29","Where this stops being the right tool",{"id":180,"html":181,"type":84},"b30","\u003Cp>SSE isn&#39;t a WebSocket replacement — it&#39;s the right tool for a narrower job, and it&#39;s worth being honest about the edges before you reach for it everywhere:\u003C\u002Fp>",{"id":183,"type":184,"items":185,"ordered":18},"b31","list",[186,187,188],"\u003Cstrong>It&#39;s one-way.\u003C\u002Fstrong> The stream only carries server → client. If the browser needs to send something back — a chat message, a cursor position, a game input — that&#39;s a normal \u003Ccode>fetch\u003C\u002Fcode> call on the side, not a message over the same connection. The moment the client needs to \u003Cem>talk back\u003C\u002Fem> in real time, you&#39;re back to WebSockets.","\u003Cstrong>It&#39;s text only.\u003C\u002Fstrong> Every event is UTF-8. Binary data — audio chunks, protobuf, anything you&#39;d otherwise send as a \u003Ccode>Blob\u003C\u002Fcode> or \u003Ccode>ArrayBuffer\u003C\u002Fcode> — has to be base64-encoded first, which is exactly the kind of tax that makes WebSockets or WebRTC the better call for that payload.","\u003Cstrong>HTTP\u002F1.1 caps you at six connections per browser, per origin.\u003C\u002Fstrong> Because each \u003Ccode>EventSource\u003C\u002Fcode> holds a request open indefinitely, a user with several tabs open to your app can exhaust that limit and starve other requests on the same domain. Serve your app over HTTP\u002F2 (most CDNs and reverse proxies do this by default now) and the cap disappears, because HTTP\u002F2 multiplexes many streams over one connection — but it&#39;s worth checking, not assuming.",{"id":190,"html":191,"type":84},"b32","\u003Cp>If your feature needs the client to send data in the same real-time loop, or needs binary frames, that&#39;s your WebSocket. If it&#39;s a one-way trickle of small text updates — a badge, a progress indicator, a live dashboard number, a &quot;someone else is editing this doc&quot; banner — you were reaching for the heavier tool.\u003C\u002Fp>",{"id":193,"html":194,"type":84},"b33","\u003C!-- quiz:start -->",{"id":196,"html":197,"text":197,"type":94,"level":31},"b34","🧠 Test yourself",{"id":199,"html":200,"type":84},"b35","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fserver-sent-events-eventsource-live-updates\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":202,"html":203,"type":84},"b36","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":205,"html":206,"type":84},"b37","\u003C!-- quiz:end -->",{"id":208,"html":209,"type":84},"b38","\u003Cp>Next time a feature description is &quot;push updates from the server, nothing coming back,&quot; open your network tab before you open your \u003Ccode>package.json\u003C\u002Fcode>. Check whether \u003Ccode>EventSource\u003C\u002Fcode> covers it first — you might delete a reconnect handler instead of writing one.\u003C\u002Fp>",{"id":211,"html":212,"type":84},"b39","\u003Cp>Have you shipped a WebSocket for something that turned out to be one-way the whole time? Tell me what it was — I&#39;d bet it&#39;s more common than the socket.io download numbers suggest.\u003C\u002Fp>",{"id":214,"type":215},"b40","divider",{"id":217,"html":218,"type":84},"b41","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":220,"html":221,"type":84},"b42","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":223,"type":184,"items":224,"ordered":18},"b43",[225,226,227],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","You need a live notification badge. The count in the corner should tick up the moment something happens on the server — no refresh, no polling every three seconds hoping you didn't miss anything. So you do what everyone does: `npm install socket.io`, wire up a client, and ship it.\n\nThen someone's wifi hiccups for two seconds. The socket drops. Nothing reconnects, because you didn't write that part yet. Now you're writing it — a `reconnecting` flag, a backoff timer that doubles each attempt, a cap so it doesn't retry forever, a check for whether the tab is even in the foreground. Forty-something lines later you have a hand-rolled connection manager sitting in front of a feature that only ever sent data in one direction: server to browser.\n\nThere's a browser API that already does the part you just spent an afternoon on. It's been sitting in every evergreen browser for over a decade, and it's not the one you reached for.\n\n## The wrong way, in more detail than you wanted\n\nHere's roughly what that WebSocket reconnect logic looks like once it's \"done\":\n\n```js\nlet socket;\nlet retryDelay = 1000;\n\nfunction connect() {\n  socket = new WebSocket('wss:\u002F\u002Fapi.example.com\u002Fnotifications');\n\n  socket.onopen = () => { retryDelay = 1000; };\n\n  socket.onmessage = (event) => {\n    updateBadge(JSON.parse(event.data));\n  };\n\n  socket.onclose = () => {\n    setTimeout(connect, retryDelay);\n    retryDelay = Math.min(retryDelay * 2, 30000);\n  };\n\n  socket.onerror = () => socket.close();\n}\n\nconnect();\n```\n\nIt works. It's also code you didn't want to own: a manual backoff schedule, a `retryDelay` you have to remember to reset on success, and a `close`\u002F`error` interaction that's easy to get subtly wrong (fire both handlers and you'll double-schedule a reconnect). And you still haven't handled *resuming* — if the connection drops mid-stream, you have no idea what the server sent while you were gone. You just start fresh and hope you didn't miss the one notification that mattered.\n\nNone of that is a WebSocket problem, exactly. It's what you get for using a bidirectional, binary-capable protocol for a feature that only ever pushes text from server to client.\n\n## What you're actually asking for\n\nStrip the feature down to what it needs: the server pushes updates, the browser listens, and if the connection drops, it should pick back up without you writing a state machine. That's [Server-Sent Events](https:\u002F\u002Fhtml.spec.whatwg.org\u002Fmultipage\u002Fserver-sent-events.html) — an HTTP response that never ends, plus a browser-side API called `EventSource` that reads it.\n\nThe client side is almost embarrassingly small:\n\n```js\nconst stream = new EventSource('\u002Fapi\u002Fnotifications');\n\nstream.onmessage = (event) => {\n  updateBadge(JSON.parse(event.data));\n};\n\nstream.onerror = () => {\n  \u002F\u002F the browser is already retrying — this just tells you it happened\n  console.warn('connection dropped, reconnecting…');\n};\n```\n\nThat's it. No `retryDelay`, no `setTimeout`, no `connect()` function to call again. If the connection drops — the wifi hiccup, a proxy timeout, the server restarting — the browser reconnects on its own. You didn't write that part because it isn't yours to write.\n\n## The part that replaces your backoff timer\n\nThe server side is a plain HTTP response with one specific content type and a text format the browser knows how to parse:\n\n```js\napp.get('\u002Fapi\u002Fnotifications', (req, res) => {\n  res.set({\n    'Content-Type': 'text\u002Fevent-stream',\n    'Cache-Control': 'no-cache',\n    Connection: 'keep-alive',\n  });\n\n  const send = (data) => res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n\n  const unsubscribe = notifications.subscribe(send);\n  req.on('close', unsubscribe);\n});\n```\n\nEach message is a `data:` line followed by a blank line — that blank line is what tells the browser \"this event is complete, hand it to `onmessage`.\" Send that content type and keep the connection open, and you've built the entire server half of what `socket.io` was doing for you, minus the parts you didn't need.\n\nThe reconnection is where it earns the comparison to your backoff timer. When the browser reconnects after a drop, it's not just retrying blindly — it sends a `Last-Event-ID` header carrying the ID of the last event it successfully received, so the server can pick the stream back up instead of replaying everything or losing the gap:\n\n```js\nconst send = (data, id) => {\n  res.write(`id: ${id}\\n`);\n  res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n};\n\napp.get('\u002Fapi\u002Fnotifications', (req, res) => {\n  res.set({ 'Content-Type': 'text\u002Fevent-stream' });\n  const lastId = req.headers['last-event-id'];\n  const missed = notifications.since(lastId); \u002F\u002F your own backlog logic\n  missed.forEach((n) => send(n.payload, n.id));\n  const unsubscribe = notifications.subscribe(send);\n  req.on('close', unsubscribe);\n});\n```\n\nYou still have to write `notifications.since()` — SSE gives you the *mechanism* for catching up, not the backlog itself. But that's a data-layer problem you'd have had with a WebSocket too, except there you'd also be reimplementing the ID and the reconnect trigger by hand.\n\nWant to change how long the browser waits before its first reconnect attempt? The server can set that too, with a `retry:` line — the value's in milliseconds:\n\n```\nretry: 5000\ndata: {\"count\": 3}\n\n```\n\nSkip it and the browser falls back to its own default of a few seconds, which is fine for most dashboards and badges.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fserver-sent-events-eventsource-live-updates\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Where this stops being the right tool\n\nSSE isn't a WebSocket replacement — it's the right tool for a narrower job, and it's worth being honest about the edges before you reach for it everywhere:\n\n- **It's one-way.** The stream only carries server → client. If the browser needs to send something back — a chat message, a cursor position, a game input — that's a normal `fetch` call on the side, not a message over the same connection. The moment the client needs to *talk back* in real time, you're back to WebSockets.\n- **It's text only.** Every event is UTF-8. Binary data — audio chunks, protobuf, anything you'd otherwise send as a `Blob` or `ArrayBuffer` — has to be base64-encoded first, which is exactly the kind of tax that makes WebSockets or WebRTC the better call for that payload.\n- **HTTP\u002F1.1 caps you at six connections per browser, per origin.** Because each `EventSource` holds a request open indefinitely, a user with several tabs open to your app can exhaust that limit and starve other requests on the same domain. Serve your app over HTTP\u002F2 (most CDNs and reverse proxies do this by default now) and the cap disappears, because HTTP\u002F2 multiplexes many streams over one connection — but it's worth checking, not assuming.\n\nIf your feature needs the client to send data in the same real-time loop, or needs binary frames, that's your WebSocket. If it's a one-way trickle of small text updates — a badge, a progress indicator, a live dashboard number, a \"someone else is editing this doc\" banner — you were reaching for the heavier tool.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fserver-sent-events-eventsource-live-updates\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\nNext time a feature description is \"push updates from the server, nothing coming back,\" open your network tab before you open your `package.json`. Check whether `EventSource` covers it first — you might delete a reconnect handler instead of writing one.\n\nHave you shipped a WebSocket for something that turned out to be one-way the whole time? Tell me what it was — I'd bet it's more common than the socket.io download numbers suggest.\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":47,"canonical":230,"description":231},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fserver-sent-events-eventsource-live-updates","A live notification badge, a progress bar, a dashboard counter — you reached for socket.io and started hand-rolling reconnect logic. The browser already ships a simpler API that do","01a03cdb-3743-73ef-ab9d-25e4d109ada7",{"id":234,"locked":18},"01a03cdb-37ed-734f-a019-a1c5a1af27cc",[236],{"id":237,"slug":46,"title":238,"_count":239},"01a03cdb-3829-70a8-9009-a2b5bcefec2c","Server-Sent Events and EventSource",{"questions":240},8,[242],{"locale":13,"slug":46},{"id":237,"slug":46,"title":238,"_count":244,"questionCount":240},{"questions":240},[246,248,252,254,258,260,264,268,272,275,279,283],{"slug":67,"name":68,"articles":247},75,{"slug":249,"name":250,"articles":251},"frontend","Frontend",64,{"slug":61,"name":65,"articles":253},63,{"slug":255,"name":256,"articles":257},"css","Css",29,{"slug":70,"name":71,"articles":259},15,{"slug":261,"name":262,"articles":263},"typescript","Typescript",11,{"slug":265,"name":266,"articles":267},"performance","Performance",10,{"slug":269,"name":270,"articles":271},"react","React",7,{"slug":273,"name":274,"articles":51},"grammar","Grammar",{"slug":276,"name":277,"articles":278},"node","Node",5,{"slug":280,"name":281,"articles":282},"programming","Programming",4,{"slug":284,"name":285,"articles":282},"html","Html",{"items":287,"meta":414},[288,307,318,340,359,377,396],{"id":289,"slug":290,"title":291,"subtitle":48,"excerpt":292,"coverUrl":293,"locale":13,"readingMinutes":294,"publishedAt":295,"viewCount":296,"likeCount":19,"commentCount":19,"author":297,"vertical":298,"topic":299,"tags":300,"_count":305,"playground":306,"hasQuiz":17,"hasPlayground":17},"019ff73a-fb5b-73ce-b725-2912167a7dde","weekly-debounce-and-throttle","Debounce and Throttle in JavaScript: The Complete Guide","Learn debounce and throttle in JavaScript step by step, with worked examples, edge cases, and a copy-paste cheat sheet for search, scroll, and resize.","\u002Fmedia\u002Fcovers\u002Fweekly-debounce-and-throttle.png",14,"2026-08-31T13:12:14.139Z",28,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[301,302,303,304],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":265,"name":266,"color":48},{"assessments":76},{"slug":290},{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":267,"likeCount":19,"commentCount":19,"author":308,"vertical":309,"topic":310,"tags":311,"_count":316,"playground":317,"hasQuiz":17,"hasPlayground":17},{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[312,313,314,315],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":319,"slug":320,"title":321,"subtitle":48,"excerpt":322,"coverUrl":323,"locale":13,"readingMinutes":294,"publishedAt":324,"viewCount":325,"likeCount":19,"commentCount":19,"author":326,"vertical":327,"topic":328,"tags":331,"_count":338,"playground":339,"hasQuiz":17,"hasPlayground":17},"01a04446-3f08-701c-8cdc-e5ab5e87086e","nuxt-weekly-cross-request-state-leak","Nuxt useState vs ref(): Why Server State Leaks Across Users","A module-scope ref() in Nuxt is shared by every request that hits your server. Learn why useState isolates state per user, and how to fix the leak.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-cross-request-state-leak.png","2026-08-30T11:37:11.335Z",86,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":329,"name":330},"nuxt","Nuxt",[332,333,336,337],{"slug":329,"name":330,"color":48},{"slug":334,"name":335,"color":48},"ssr","SSR",{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":320},{"id":341,"slug":342,"title":343,"subtitle":48,"excerpt":344,"coverUrl":345,"locale":13,"readingMinutes":346,"publishedAt":347,"viewCount":348,"likeCount":19,"commentCount":19,"author":349,"vertical":350,"topic":351,"tags":352,"_count":357,"playground":358,"hasQuiz":17,"hasPlayground":17},"01a04446-373a-775b-8aaf-b72c8952123d","react-weekly-rerender-vs-remount","React Re-render vs Remount: What Actually Triggers Each","A practical guide to React re-render vs remount: what type, position, and key decide, why state resets unexpectedly, and how to force a remount.","\u002Fmedia\u002Fcovers\u002Freact-weekly-rerender-vs-remount.png",13,"2026-08-29T12:32:41.200Z",143,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":269,"name":270},[353,354,355,356],{"slug":269,"name":270,"color":48},{"slug":61,"name":65,"color":48},{"slug":70,"name":71,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":342},{"id":360,"slug":361,"title":362,"subtitle":48,"excerpt":363,"coverUrl":364,"locale":13,"readingMinutes":278,"publishedAt":365,"viewCount":366,"likeCount":19,"commentCount":19,"author":367,"vertical":368,"topic":369,"tags":370,"_count":375,"playground":376,"hasQuiz":17,"hasPlayground":17},"01a02839-480f-7428-914a-2c0442d00855","intl-segmenter-grapheme-word-sentence","You're truncating bios with `.slice()`. `Intl.Segmenter` knows where the emoji actually end.","A 30-character bio limit that cuts off mid-emoji isn't a rendering bug — it's .length counting UTF-16 code units instead of what's on screen. Intl.Segmenter counts graphemes, words, and sentences the way a reader actually sees them, and every major browser supports it now.","\u002Fmedia\u002Fcovers\u002Fintl-segmenter-grapheme-word-sentence.png","2026-08-27T17:30:57.707Z",199,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[371,372,373,374],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":249,"name":250,"color":48},{"slug":70,"name":71,"color":48},{"assessments":76},{"slug":361},{"id":378,"slug":379,"title":380,"subtitle":48,"excerpt":381,"coverUrl":382,"locale":13,"readingMinutes":278,"publishedAt":383,"viewCount":384,"likeCount":19,"commentCount":19,"author":385,"vertical":386,"topic":387,"tags":389,"_count":394,"playground":395,"hasQuiz":17,"hasPlayground":17},"01a02319-4485-7310-b494-dc94bdbfb49c","css-field-sizing-auto-grow-textarea","Delete Your Auto-Resize Textarea JS. CSS Does It Now.","The scrollHeight hack everyone copy-pastes for auto-growing textareas has a one-line CSS replacement — plus the one gotcha that bites if you skip max-height.","\u002Fmedia\u002Fcovers\u002Fcss-field-sizing-auto-grow-textarea.png","2026-08-26T06:56:38.974Z",216,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":255,"name":388},"CSS",[390,391,392,393],{"slug":255,"name":256,"color":48},{"slug":67,"name":68,"color":48},{"slug":249,"name":250,"color":48},{"slug":61,"name":65,"color":48},{"assessments":76},{"slug":379},{"id":397,"slug":398,"title":399,"subtitle":48,"excerpt":400,"coverUrl":401,"locale":13,"readingMinutes":294,"publishedAt":402,"viewCount":403,"likeCount":19,"commentCount":19,"author":404,"vertical":405,"topic":406,"tags":407,"_count":412,"playground":413,"hasQuiz":17,"hasPlayground":17},"019fe661-0c1b-7408-8419-531dae95bdf1","weekly-javascript-closures","JavaScript Closures: The Complete Guide (with Cheat Sheet)","Learn how JavaScript closures really work — lexical scope, private state, memoization, and the classic loop bug — with worked examples and a cheat sheet.","\u002Fmedia\u002Fcovers\u002Fweekly-javascript-closures.png","2026-08-25T08:53:48.904Z",281,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[408,409,410,411],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":280,"name":281,"color":48},{"assessments":76},{"slug":398},{"page":76,"perPage":271,"total":253,"totalPages":53},"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003Cdiv>14\u003C\u002Fdiv>\u003Cdiv>15\u003C\u002Fdiv>\u003Cdiv>16\u003C\u002Fdiv>\u003Cdiv>17\u003C\u002Fdiv>\u003Cdiv>18\u003C\u002Fdiv>\u003Cdiv>19\u003C\u002Fdiv>\u003Cdiv>20\u003C\u002Fdiv>\u003Cdiv>21\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> socket;\n\u003Cspan class=\"shj-kwd\">let\u003C\u002Fspan> retryDelay \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1000\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">function\u003C\u002Fspan> \u003Cspan class=\"shj-func\">connect\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  socket \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">WebSocket\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'wss:\u002F\u002Fapi.example.com\u002Fnotifications'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  socket\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">onopen\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> retryDelay \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1000\u003C\u002Fspan>; \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\n  socket\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">onmessage\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-func\">updateBadge\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">parse\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\n  socket\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">onclose\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-func\">setTimeout\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>connect\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> retryDelay\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    retryDelay \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-class\">Math\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">min\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>retryDelay \u003Cspan class=\"shj-oper\">*\u003C\u002Fspan> \u003Cspan class=\"shj-num\">2\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">30000\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\n  socket\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">onerror\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> socket\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">close\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\n\u003Cspan class=\"shj-func\">connect\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> stream \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">EventSource\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'\u002Fapi\u002Fnotifications'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nstream\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">onmessage\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-func\">updateBadge\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">parse\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\nstream\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">onerror\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F the browser is already retrying — this just tells you it happened\n\u003C\u002Fspan>  console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">warn\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'connection dropped, reconnecting…'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">app\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'\u002Fapi\u002Fnotifications'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>req\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> res\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  res\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">set\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    'Content-Type'\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'text\u002Fevent-stream'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    'Cache-Control'\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'no-cache'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    Connection\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'keep-alive'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> \u003Cspan class=\"shj-func\">send\u003C\u002Fspan> \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> res\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">write\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`data: \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>\u003Cspan class=\"shj-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">stringify\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\\n\\n`\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> unsubscribe \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> notifications\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">subscribe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>send\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  req\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">on\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'close'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> unsubscribe\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> send \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  res\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">write\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`id: \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>id\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\\n`\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  res\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">write\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">`data: \u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">${\u003C\u002Fspan>\u003Cspan class=\"shj-num\">JSON\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">stringify\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>data\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">}\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\\n\\n`\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>;\n\napp\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">get\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'\u002Fapi\u002Fnotifications'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>req\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> res\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  res\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-kwd\">set\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> 'Content-Type'\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'text\u002Fevent-stream'\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> lastId \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> req\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>headers\u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'last-event-id'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> missed \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> notifications\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">since\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>lastId\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F your own backlog logic\n\u003C\u002Fspan>  missed\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>n\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-func\">send\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>n\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>payload\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> n\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> unsubscribe \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> notifications\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">subscribe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>send\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  req\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">on\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'close'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> unsubscribe\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-plain shj-multiline\" data-lang=\"plain\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">retry: 5000\ndata: {\u003Cspan class=\"shj-str\">\"count\"\u003C\u002Fspan>: 3}\n\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":421},[]]