feat(sse): hoist EventSource to module singleton #613

Merged
code-lead merged 1 commit from dev/605 into main 2026-04-30 22:13:44 +00:00
Collaborator

Moves EventSource out of each useSSE hook into module scope so route navigation no longer tears down and re-opens the stream — eliminating the Firefox "La connexion … a été interrompue" console spam (#605).

First subscriber opens the connection; last unmount schedules a 1 s grace-window close; a new subscriber within the window cancels it so back-to-back unmount/mount (route nav, StrictMode) keeps the existing EventSource running. onEvent/onReconnect callbacks are multiplexed across all active consumers; connection state is broadcast via useSyncExternalStore. Public API (useSSE, forceReconnect, _resetInitialConnectionToastForTest) is unchanged — no call sites need updating.

Test plan

  • sse.test.ts (new): refcount open-on-first / close-after-last, grace-window cancel on quick remount, multiplexed onEvent to two subscribers, forceReconnect keeps the shared connection alive for other subscribers.
  • bun x turbo run test --filter @claude-hooks/web — 579 tests pass, 50 files.
  • bun x turbo run typecheck --filter @claude-hooks/web — clean.
  • Biome check on changed files — clean.

Closes #605

Moves `EventSource` out of each `useSSE` hook into module scope so route navigation no longer tears down and re-opens the stream — eliminating the Firefox "La connexion … a été interrompue" console spam (#605). First subscriber opens the connection; last unmount schedules a 1 s grace-window close; a new subscriber within the window cancels it so back-to-back unmount/mount (route nav, StrictMode) keeps the existing `EventSource` running. `onEvent`/`onReconnect` callbacks are multiplexed across all active consumers; connection state is broadcast via `useSyncExternalStore`. Public API (`useSSE`, `forceReconnect`, `_resetInitialConnectionToastForTest`) is unchanged — no call sites need updating. ## Test plan - `sse.test.ts` (new): refcount open-on-first / close-after-last, grace-window cancel on quick remount, multiplexed `onEvent` to two subscribers, `forceReconnect` keeps the shared connection alive for other subscribers. - `bun x turbo run test --filter @claude-hooks/web` — 579 tests pass, 50 files. - `bun x turbo run typecheck --filter @claude-hooks/web` — clean. - Biome check on changed files — clean. Closes #605
dev self-assigned this 2026-04-30 22:01:31 +00:00
feat(sse): hoist EventSource to module singleton with refcount + grace window
All checks were successful
qa / dockerfile (pull_request) Successful in 5s
qa / qa (pull_request) Successful in 1m55s
6ceaa82405
Moves the EventSource out of useEffect into module scope so route
navigation no longer tears down and re-opens the stream. First subscriber
opens the connection; last unmount schedules a 1 s grace-window close;
a new subscriber arriving within that window cancels the close and the
existing EventSource keeps running undisturbed (fixes Firefox console spam).

onEvent / onReconnect callbacks are stored in per-subscriber maps and
multiplexed across all active consumers. Connection state is broadcast
via useSyncExternalStore so every hook instance re-renders correctly.

Adds sse.test.ts covering: refcount open/close, grace-window cancel on
quick remount, multiplexed onEvent, and forceReconnect keeping the
shared connection alive for other subscribers. Closes #605.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dev requested review from reviewer 2026-04-30 22:03:29 +00:00
reviewer approved these changes 2026-04-30 22:07:51 +00:00
reviewer left a comment

Singleton connection + refcount + grace-window are all correct. useSyncExternalStore broadcast, stable-ref callback wrappers, and multiplexed dispatch are clean. All 5 AC test cases covered; CI green.

Nit (no block): _resetModuleStateForTest does not call _storeListeners.clear() — harmless in practice because tests unmount their hooks explicitly, but worth adding for completeness.

Singleton connection + refcount + grace-window are all correct. `useSyncExternalStore` broadcast, stable-ref callback wrappers, and multiplexed dispatch are clean. All 5 AC test cases covered; CI green. Nit (no block): `_resetModuleStateForTest` does not call `_storeListeners.clear()` — harmless in practice because tests unmount their hooks explicitly, but worth adding for completeness.
code-lead deleted branch dev/605 2026-04-30 22:13:44 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!613
No description provided.