Bug Report
Package: @solid-primitives/sse
File: packages/sse/src/sse.ts
Description
In createSSE, the handleError function inside _open correctly guards app-level reconnects behind the es.readyState === SSEReadyState.CLOSED check. However, when retriesLeft reaches 0 and the browser has permanently closed the connection, there is no cleanup performed. This causes:
currentCleanup is never called — source.close() and removeEventListener calls inside the makeSSE cleanup are never invoked, leaving the EventSource instance and its event listeners alive in memory.
source signal is never set to undefined — callers continue to see a stale SSESourceHandle.
currentCleanup reference is never cleared — the closure is retained unnecessarily.
Relevant Code
// packages/sse/src/sse.ts ~L258
if (es.readyState === SSEReadyState.CLOSED && retriesLeft > 0) {
retriesLeft--;
reconnectTimer = setTimeout(() => _open(resolvedUrl), reconnectConfig.delay ?? 3000);
}
// ❌ No else branch: when retriesLeft === 0, cleanup is never called
Expected Behavior
When retries are exhausted and es.readyState === CLOSED, the library should:
- Call
currentCleanup() to invoke source.close() and remove all event listeners
- Set
currentCleanup = undefined
- Set
source signal to undefined
Suggested Fix
if (es.readyState === SSEReadyState.CLOSED && retriesLeft > 0) {
retriesLeft--;
reconnectTimer = setTimeout(() => _open(resolvedUrl), reconnectConfig.delay ?? 3000);
} else if (es.readyState === SSEReadyState.CLOSED) {
// Retries exhausted — clean up fully to avoid memory/listener leaks
currentCleanup?.();
currentCleanup = undefined;
setSource(undefined);
}
Steps to Reproduce
const { readyState } = createSSE("https://unreachable.example.com/events", {
reconnect: { retries: 3, delay: 500 },
});
// After 3 failed attempts, event listeners remain attached and
// currentCleanup is never called.