Skip to content

Commit f507436

Browse files
CopilotJavanPoirier
andcommitted
docs(sse): add EventSource silent-drop limitation and heartbeat guidance
Co-authored-by: JavanPoirier <42590458+JavanPoirier@users.noreply.github.com>
1 parent 83c9d0e commit f507436

1 file changed

Lines changed: 52 additions & 0 deletions

File tree

packages/sse/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,58 @@ SSEReadyState.CLOSED; // 2
154154

155155
`EventSource` has native browser-level reconnection built in. For transient network drops the browser automatically retries. The `reconnect` option in `createSSE` is for _application-level_ reconnection — it fires only when `readyState` becomes `SSEReadyState.CLOSED`, meaning the browser has given up entirely. You generally do not need `reconnect: true` for normal usage.
156156

157+
### A note on server disconnection detection
158+
159+
`EventSource` **does not reliably detect when a server silently stops responding**. If the server process crashes or the network path is severed without a proper TCP close handshake, the browser never fires an `error` event and `readyState` stays `OPEN` indefinitely — the connection looks healthy even though no messages will ever arrive.
160+
161+
The only robust workaround is **application-level heartbeats**: the server sends a lightweight event at a fixed interval, and the client starts a timer that triggers a reconnect if no heartbeat is received within the expected window.
162+
163+
```ts
164+
import { createSSE } from "@solid-primitives/sse";
165+
import { onCleanup } from "solid-js";
166+
167+
const HEARTBEAT_TIMEOUT_MS = 15_000; // reconnect if silent for 15 s
168+
169+
function createSSEWithHeartbeat(url: string) {
170+
let timer: ReturnType<typeof setTimeout> | undefined;
171+
172+
const { reconnect, ...rest } = createSSE(url, {
173+
// The server emits `event: heartbeat\ndata: \n\n` every ~10 s.
174+
// Any regular message also resets the timer.
175+
events: { heartbeat: resetTimer },
176+
onMessage: resetTimer,
177+
reconnect: true,
178+
});
179+
180+
function resetTimer() {
181+
clearTimeout(timer);
182+
timer = setTimeout(() => {
183+
// No heartbeat received — assume the server is gone.
184+
reconnect();
185+
}, HEARTBEAT_TIMEOUT_MS);
186+
}
187+
188+
onCleanup(() => {
189+
clearTimeout(timer);
190+
timer = undefined;
191+
});
192+
resetTimer(); // arm the first timeout immediately
193+
194+
return { reconnect, ...rest };
195+
}
196+
```
197+
198+
On the server, emit a periodic heartbeat event well within the client timeout:
199+
200+
```js
201+
// Express / Node.js example
202+
setInterval(() => {
203+
res.write("event: heartbeat\ndata: \n\n");
204+
}, 10_000); // every 10 s, safely below the 15 s client timeout
205+
```
206+
207+
> **Why SSE comment lines are not enough** — SSE comment lines (e.g. `: keep-alive`) reset the browser's internal TCP idle timer but are _not_ exposed to JavaScript listeners. Use a named `event: heartbeat` or a plain `data:` event if you need the client to observe the heartbeat.
208+
157209
## Integration with `@solid-primitives/event-bus`
158210

159211
Because `bus.emit` matches the `(event: MessageEvent) => void` shape of `onMessage`, you can wire them directly:

0 commit comments

Comments
 (0)