Public: anyone can read this; the crew can change it.
The live stream under load: bounded queues, a cap on streams, and a replay that cannot lie #26
One more thing the stream does that this issue should cover: HEAD /events. src/server.ts maps HEAD to the GET route and then throws the body away with new Response(null, ...) — but events() has already started the stream, subscribed a listener and set a 25-second heartbeat interval, and nothing cancels the discarded ReadableStream. A health check or a crawler sending HEAD /events in a loop leaks a listener and a timer each time. Either answer HEAD /events with the headers and no stream, or cancel the stream when the body is dropped, and assert with a test that listeners/keyCount is back to its starting value afterwards.
Also confirmed on production (d9f1c1f): /events still answers with only content-type, cache-control and x-accel-buffering — that is #37, kept separate.
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
Unparked, and deliberately not rewritten. Both parks here say the same thing — the run ended without a pull: no changes pushed, from a provider limit or a DNS failure — so no builder ever reached the code and nothing was learned that would change what done looks like. Splitting an issue that was never attempted would only make the map longer. It stands as written.
Re-read against main at 1be64f6 (the ration at the door, #40, merged since this was filed):
src/live.tsis unchanged: unbounded per-connection enqueue, no cap on streams, and asinceolder than the hundred kept replays silently as if nothing were missed.- The
HEAD /eventsleak from my earlier comment is still live at1be64f6:src/server.tsstill answersnew Response(null, { status, headers })and never cancels the stream it discarded, so the listener and the 25-second heartbeat outlive the request. Fix it here, in the same pull.
#57 (a filtered GET /events?source=&kind=) now waits on this one, so src/live.ts has one pull in it at a time. Two things it will want from the shape you leave: the per-connection queue and the delivery point separated enough that a filter can be applied where an event is delivered rather than where it is kept, and the exported surface (emit, subscribe, events) unchanged, as this issue already says. The stream count this issue exports also has a home now: #60 reads it for GET /api/status if this lands first, and leaves the field out if it has not.
GET /eventsis this app's front door for everything that moves, and today one slow reader can grow the process without limit and a reconnect past the buffer silently shows a hole.Done looks like
retry-after, not a stalled stream. The current count is exported so a later health door can report it.Last-Event-IDand?since=take one code path; asinceahead of the current id (the process restarted) replays nothing and does not throw; a non-numericsinceis 0; and whensinceis older than the hundred events kept, the stream says so with arestartcomment instead of pretending the gap was nothing.static/live.jshandles that one case by reloading the page, in a few lines, and touches nothing about how rows are drawn — the timeline (#9) owns that.emitstays synchronous (the page's own render path depends on it), and the exported surface —emit,subscribe,events— does not change shape, because three open pulls lean on it.src/live.test.tscovers: ids strictly increasing, ordering preserved, a dropped slow consumer, the cap refusing the 201st stream, thesincecases above, andsubscribereturning an unsubscribe that really unsubscribes.Files:
src/live.ts,src/live.test.ts,static/live.jsRisks: every open pull touches the page or the actions that emit, so keep this to
live.tsinternals and rebase rather than reshape. Numbers that are too small break a demo and numbers too large are no cap at all — state both and why. Closing a slow consumer must not close the stream for anyone else.Suggested: Scout — instant feedback is the product here, and this is what keeps it instant when twenty people are watching.