ITADN

[sse] Memory leak and missing cleanup when app-level retries are exhausted

#835OpenJavanPoirier 创建于 2026-03-01
J
JavanPoiriercommented
## Bug Report **Package:** `@solid-primitives/sse` **File:** [`packages/sse/src/sse.ts`](https://github.com/solidjs-community/solid-primitives/blob/af34b836baba599c525d0db4b1c9871dd0b13f27/packages/sse/src/sse.ts#L252) --- ### 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: 1. **`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. 2. **`source` signal is never set to `undefined`** — callers continue to see a stale `SSESourceHandle`. 3. **`currentCleanup` reference is never cleared** — the closure is retained unnecessarily. --- ### Relevant Code ```ts // 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 ```ts 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 ```ts 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. ```
0 条评论