Lesson 0002 · ~10 minutes
One skill: predict connection lifecycle and wire a correct client — open, listen, recover vs stop, close on unmount.
You can sketch a React/Vue cleanup-safe client and explain what
onerror means by reading readyState — not by
guessing.
EventSource
is intentionally small. From the
WHATWG interface: construct with a URL, optional withCredentials, read
readyState, listen for open /
message / error (plus named types), call
close() when done.
const es = new EventSource("/api/stream");
es.addEventListener("open", () => {
console.log("open", es.readyState); // EventSource.OPEN === 1
});
es.onmessage = (e) => {
// Default wire type: no `event:` field on the server
console.log(e.data); // always a string
console.log(e.lastEventId);
};
es.addEventListener("invoice.paid", (e) => {
// Only if server sent: event: invoice.paid
handle(JSON.parse(e.data));
});
es.onerror = () => {
// See section 3 — readyState tells retry vs dead
};
// SPA unmount / leave page intent:
es.close(); // readyState → CLOSED (2); no more reconnects
Native EventSource does not accept a request body or
arbitrary headers. Auth is usually cookies (optionally
withCredentials: true cross-origin) or a token in the
query string / path. If your platform mandates
Authorization: Bearer … on every call, you either change
the auth model or stream with fetch + manual parse
instead of raw EventSource. Flag that early in design
reviews.
| State | Value | When |
|---|---|---|
CONNECTING |
0 | Just constructed, or mid-reconnect after a drop (spec: reestablish path) |
OPEN |
1 |
Server answered 200 with
Content-Type: text/event-stream; events may flow
|
CLOSED |
2 |
Fatal failure or you called close() — UA
does not reconnect
|
Spec processing model (short version):
announce
→ OPEN + open event;
reestablish
→ CONNECTING + error, wait, refetch (may send
Last-Event-ID);
fail
→ CLOSED + error, stop forever.
The same error event fires when the browser is about to
retry and when the connection is dead. Always branch
on state:
es.onerror = () => {
if (es.readyState === EventSource.CLOSED) {
// Fatal: 404, wrong content-type, auth hard-fail, close(), etc.
showBanner("Live updates stopped");
return;
}
// CONNECTING: transient; browser will retry on its own
showBanner("Reconnecting…");
};
Useful server signals from the intro:
204 No Content tells the client to stop reconnecting;
wrong status or non-text/event-stream fails the connection.
(WHATWG intro)
Wire field event: job-progress does
not hit onmessage. Default type is
message only when the server omits event:.
Use addEventListener("job-progress", …) for named types —
same as the
spec’s add/remove example.
Equal-length options · pick the state after the step
const es = new EventSource("/events"); — no response
yet. What is es.readyState?
Server returns 200 with
Content-Type: text/event-stream and starts writing.
State?
Stream was open; the TCP connection drops mid-flight. Browser begins reestablish. State while waiting to retry?
User navigates away; your effect cleanup runs
es.close(). State?
Endpoint returns 401 Unauthorized with a JSON body
(not an event stream). State after processing?
Server sends event: balance-updated with JSON data.
How should the client subscribe?
React effect opens EventSource for a dashboard route. What belongs in the effect cleanup?
Product wants a “live updates paused” banner only when the stream will not come back by itself. What do you check?
EventSource = tiny client: URL, credentials flag,
three states, three core events, close().
error + CONNECTING = retrying;
error + CLOSED = dead.
addEventListener; always
close() on teardown.
fetch streaming.
WHATWG — The EventSource interface + processing model (sections 9.2.2–9.2.3). Skim the IDL and the announce / reestablish / fail definitions. Use MDN EventSource as the readable companion.