Skip to content

fix(sse): update readyState to CLOSED on owner disposal; document silent-drop limitation#837

Merged
davedbase merged 5 commits intosolidjs-community:mainfrom
JavanPoirier:fix-sse-state
Mar 2, 2026
Merged

fix(sse): update readyState to CLOSED on owner disposal; document silent-drop limitation#837
davedbase merged 5 commits intosolidjs-community:mainfrom
JavanPoirier:fix-sse-state

Conversation

@JavanPoirier
Copy link
Contributor

Two related gaps in the SSE primitive:

  1. readyState stayed stale after owner disposalonCleanup called teardown() (closes EventSource, clears source signal) but never called setReadyState(CLOSED), leaving the signal inconsistently at OPEN or CONNECTING.

  2. No documentation of the fundamental EventSource limitation: a server that drops without a TCP close never fires an error event, so readyState stays OPEN forever and neither browser-native nor app-level reconnect triggers.

Changes

src/sse.ts

Added setReadyState(SSEReadyState.CLOSED) to onCleanup, making it consistent with every other teardown path (disconnect, retries-exhausted, budget-exhausted):

onCleanup(() => {
  clearReconnectTimer();
  teardown();
  setReadyState(SSEReadyState.CLOSED); // ← was missing
});

test/index.test.ts

Two new tests:

  • "readyState is CLOSED after owner disposal" — directly covers the fix
  • "readyState updates to CONNECTING when server drops connection" — documents the transient-error state transition

README.md

New "A note on server disconnection detection" section covering:

  • Why EventSource cannot detect silent server drops
  • Heartbeat pattern as the only reliable workaround — client timeout + server-side periodic event
  • Why SSE comment lines (: keep-alive) are insufficient (not exposed to JS)
function createSSEWithHeartbeat(url: string) {
  let timer: ReturnType<typeof setTimeout> | undefined;

  const { reconnect, ...rest } = createSSE(url, {
    events: { heartbeat: resetTimer },
    onMessage: resetTimer,
    reconnect: true,
  });

  function resetTimer() {
    clearTimeout(timer);
    timer = setTimeout(() => reconnect(), 15_000);
  }

  onCleanup(() => { clearTimeout(timer); timer = undefined; });
  resetTimer();
  return { reconnect, ...rest };
}

🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Copilot AI and others added 5 commits March 1, 2026 16:10
…ory leak

Co-authored-by: JavanPoirier <42590458+JavanPoirier@users.noreply.github.com>
Co-authored-by: JavanPoirier <42590458+JavanPoirier@users.noreply.github.com>
Co-authored-by: JavanPoirier <42590458+JavanPoirier@users.noreply.github.com>
Co-authored-by: JavanPoirier <42590458+JavanPoirier@users.noreply.github.com>
@changeset-bot
Copy link

changeset-bot bot commented Mar 1, 2026

🦋 Changeset detected

Latest commit: f507436

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solid-primitives/sse Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@JavanPoirier JavanPoirier marked this pull request as ready for review March 1, 2026 21:13
@davedbase
Copy link
Member

Nice work on this. I like the changes!

@davedbase davedbase merged commit 029032a into solidjs-community:main Mar 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants