Skip to content

[#850] Fix EventSource reconnect storm/leak in useStreamEvents - #996

Open
Hollujay wants to merge 2 commits into
LabsCrypt:mainfrom
Hollujay:fix/850-usestreamevents-eventsource-recreation
Open

[#850] Fix EventSource reconnect storm/leak in useStreamEvents#996
Hollujay wants to merge 2 commits into
LabsCrypt:mainfrom
Hollujay:fix/850-usestreamevents-eventsource-recreation

Conversation

@Hollujay

@Hollujay Hollujay commented Jul 27, 2026

Copy link
Copy Markdown

Summary

useStreamEvents was tearing down and recreating the EventSource on every render, and since each incoming event re-renders the consumer, the stream ended up reconnecting on every event in a self-sustaining reconnect loop.

Root cause

buildUrl was a useCallback depending directly on streamIds, which is always a fresh array reference every render (both the default [] and every call site passing inline array literals). New reference → new buildUrl → new connect → mount useEffect([connect]) tears down the old EventSource and creates a new one. Incoming events → setEvents → re-render → cycle repeats.

Changes

frontend/src/hooks/useStreamEvents.ts

  1. Stable subscription identity — A subscriptionKey string is derived from sorted streamIds, subscribeToAll, and jwtToken via useMemo. buildUrl depends on this key rather than the raw values, so it only changes when the subscription content changes — inline array identity changes on every render no longer cause a reconnect.

  2. Reconnect timer cleared before rescheduling — Both the connect entry point and the onerror handler now clear any pending reconnect timeout before scheduling a new one, preventing duplicate or stale timers.

  3. Reconnect attempt cap — Added MAX_RECONNECT_ATTEMPTS = 20 with a reconnectAttemptsRef counter. Incremented on each reconnect attempt; once the cap is exceeded no further reconnection timers are set. The counter resets to 0 on a successful onopen.

frontend/src/__tests__/useStreamEvents.test.tsx

Two new regression tests:

  • "creates only one EventSource across multiple re-renders and incoming events" — Renders the hook, re-renders multiple times with the same inline streamIds array, emits events (which trigger setEvents → consumer re-render), re-renders again. Asserts instanceCount === 1 and the same MockEventSource instance is reused throughout.

  • "stops reconnecting after reaching the cap" — Triggers 25 consecutive errors without a successful connection. Asserts at most 21 EventSource instances were created (1 initial + 20 capped reconnects).

Call sites (no changes needed)

All three call sites pass inline arrays — this is exactly the pattern that was broken before, and the fix handles it transparently because the stable key is derived from array contents, not reference:

  • NotificationDropdown.tsxuserPublicKeys: [publicKey]
  • dashboard-view.tsxuserPublicKeys: [session.publicKey]
  • stream-details-content.tsxstreamIds: [streamId]

Verification

  • npm test — 11/11 tests pass (9 existing + 2 new)
  • npm run lint — no new warnings or errors
  • npx tsc --noEmit — no new type errors

Closes #850

Use a stable subscription key derived from sorted streamIds,
subscribeToAll, and jwtToken so buildUrl/connect only change
when the subscription actually changes — not on every render
due to inline array literals or default [] identity.

- Derive subscriptionKey as a stable memoized string
- Clear pending reconnect timer before scheduling a new one
- Cap reconnect attempts at 20 (configurable via constant)

Fixes LabsCrypt#850
@Hollujay

Copy link
Copy Markdown
Author

CI failures here are pre-existing and unrelated to this fix:

  • Frontend CI / Backend CI: npm ci fails on a peer dependency conflict —
    @vitest/ui@3.2.7 requires vitest ^3.2.7, but @vitest/coverage-v8@2.1.9
    requires vitest ^2.1.9. These can't both resolve; unrelated to this PR's
    scope (a frontend hook fix touches no dependencies).
  • Soroban Contracts CI: fails compiling the ethnum crate with
    error[E0512]: cannot transmute between types of different sizes — a
    toolchain/crate version incompatibility, unrelated to any frontend code.

Local verification (noted in the PR description) passed cleanly: 11/11 tests,
no lint/type errors.

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.

[Frontend] useStreamEvents tears down and recreates the EventSource on every render - SSE reconnect storm/leak

1 participant