Hoist SSE connection to module-singleton so route nav stops spamming Firefox console #605
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks#605
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
User story
As an operator using the dashboard in Firefox, I want a single shared
EventSourcefor/eventsthat survives route navigation, so that the console stops printingLa connexion avec https://claude.jacquin.app/events a été interrompue pendant le chargement de la page.every time I move between pages.Background
apps/web/src/lib/sse.ts:68exposesuseSSE(), and every top-level route mounts its own copy:apps/web/src/routes/planner.board.tsx:143apps/web/src/routes/agents.tsx:155apps/web/src/routes/workspace.index.tsx:162apps/web/src/routes/flows.$flowId.tsx:35apps/web/src/routes/flows.$flowId.v.$version.tsx:20apps/web/src/routes/flows.$flowId.versions.tsx:21apps/web/src/routes/flows.new.tsx:24apps/web/src/routes/settings.index.tsx:44apps/web/src/routes/settings.repos.tsx:38Each
useSSEcall creates its ownEventSourceinuseEffect(sse.ts:123) and closes it on cleanup (sse.ts:215-223). Switching routes therefore tears down the previous stream and opens a new one. Firefox logs the cited "interrompue pendant le chargement" message on every torn-down connection. Functionally harmless — the new stream connects fine, the conn pill stays green — but the console spam is annoying and easy to mistake for a real problem.StrictModeinapps/web/src/main.tsx:40adds one extra mount/cleanup pair per page in dev mode, which compounds the issue locally.The pre-existing
initialConnectionToastShownmodule-level latch (sse.ts:36) hints that this came up already in #493, but only the toast got de-duped — the underlying connection still re-opens.Acceptance criteria
Singleton connection
EventSourcelives at module scope insideapps/web/src/lib/sse.ts, not inside the hook'suseEffect.EventSourcekeeps running.Multiplexed callbacks
onEventandonReconnectfrom each subscriber are stored in module-level listener sets and fired for every active subscriber on each SSE message / reconnect — no subscriber misses an event because another subscriber happened to mount more recently.live/reconnecting/disconnected,reconnectCount,offlineFor) is also module-level and broadcast to every hook instance via a small store (plain listener set +useStatere-render, oruseSyncExternalStore).Public API stays stable
useSSE(opts)continues to return{ state, reconnectCount, offlineFor, forceReconnect }with the same semantics. No call sites need to change.forceReconnect()still tears down + reopens the shared connection (does not close it permanently)._resetInitialConnectionToastForTestis preserved, plus an analogous reset hook for the new module state so unit tests can isolate.Firefox console
/planner/board,/agents,/workspace,/flows/*,/settings,/settings/reposrepeatedly. Theinterrompue pendant le chargementwarning no longer appears for/events(one initial open + one close on tab close is acceptable).live→reconnecting→disconnectedcorrectly when the server is killed; reconnects without a manual page reload.Tests
apps/web/src/lib/sse.test.tscovering: refcount open-on-first / close-after-last, grace-window cancel on quick remount, multiplexedonEventto two subscribers,forceReconnectkeeps the shared connection alive for other subscribers.Out of scope
/eventshandler changes (apps/server/src/main.ts:1108) — unaffected.srv.timeout(req, 0)for/events(main.ts:2984); revisit only if the conn pill flaps after this fix.EventSourcewith WebSocket orfetch-based streaming.References
apps/web/src/lib/sse.ts— current per-hookEventSourcelifecycleapps/web/src/main.tsx:40—StrictModewrapper (compounds the issue in dev)useSSEconsumers listed under Background