Skip to content

fix(client): recover SSE on browser visibilitychange#325

Merged
dimakis merged 1 commit into
mainfrom
fix/sse-visibility-reconnect
May 16, 2026
Merged

fix(client): recover SSE on browser visibilitychange#325
dimakis merged 1 commit into
mainfrom
fix/sse-visibility-reconnect

Conversation

@dimakis
Copy link
Copy Markdown
Owner

@dimakis dimakis commented May 15, 2026

Summary

  • iOS Safari kills EventSource connections when the tab is backgrounded without firing an error event
  • The Capacitor lifecycle hook already handled this, but browser clients (e.g. phone via Safari PWA) had no recovery
  • This caused the health SSE stream to die permanently after a single background cycle, hiding the voice mic button

Fix

Add a visibilitychange listener in event-bus-singleton.ts that calls ensureConnected() when the page becomes visible.

Test plan

  • New test: event-bus-singleton.test.ts — verifies ensureConnected called on visible, not called on hidden
  • Full test suite passes (5 pre-existing failures unrelated to this change)
  • Manual: open Mitzo in mobile Safari, background the tab, return — mic button should persist

🤖 Generated with Claude Code

iOS Safari kills EventSource connections when the tab is backgrounded
without firing an error event, so the browser's native auto-reconnect
never triggers. The Capacitor lifecycle hook already called
ensureConnected() on resume, but browser clients had no equivalent
recovery path — leaving health events (and thus the voice mic button)
permanently dead after a single background cycle.

Add a visibilitychange listener in the EventBus singleton that calls
ensureConnected() when the page becomes visible again.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dimakis dimakis force-pushed the fix/sse-visibility-reconnect branch from 96d4fc3 to 3cbdca1 Compare May 15, 2026 10:35
Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Centaur Review

Found 3 issue(s) (1 critical) (1 warning).

frontend/src/lib/event-bus-singleton.ts

The PR ships tests for a visibilitychange recovery feature whose implementation is missing from the diff — the singleton module has no listener, so both tests will fail.

  • 🔴 bugs: The PR adds tests for a visibilitychange listener that calls eventBus.ensureConnected(), but the listener itself was never added to event-bus-singleton.ts. The module still only exports the EventBus and calls connect() — there is no document.addEventListener('visibilitychange', ...). Both tests will fail because dispatching the event does nothing. The commit message says 'Add a visibilitychange listener in the EventBus singleton' but the implementation is missing from the diff. [fixable]

frontend/src/lib/__tests__/event-bus-singleton.test.ts

The PR ships tests for a visibilitychange recovery feature whose implementation is missing from the diff — the singleton module has no listener, so both tests will fail.

  • 🔵 missing_tests: No test verifies the actual recovery behavior — that ensureConnected() recreates a CLOSED EventSource. The tests only check that ensureConnected is called, but a test confirming the EventSource is recreated when readyState is CLOSED (the real bug scenario from the commit message) would add meaningful coverage beyond the spy check. [fixable]
  • 🟡 unsafe_assumptions (L30): The test mutates document.visibilityState via Object.defineProperty without restoring it in an afterEach. Since both tests modify the same global, a test ordering dependency exists — the second test ('hidden') could inadvertently pass if it relies on state left by the first test. Use afterEach to restore the original descriptor. [fixable]

const ensureConnectedSpy = vi.spyOn(eventBus, 'ensureConnected');

// Simulate page becoming visible
Object.defineProperty(document, 'visibilityState', {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 unsafe_assumptions: The test mutates document.visibilityState via Object.defineProperty without restoring it in an afterEach. Since both tests modify the same global, a test ordering dependency exists — the second test ('hidden') could inadvertently pass if it relies on state left by the first test. Use afterEach to restore the original descriptor. [fixable]

Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Centaur Review

LGTM — no issues found.

@dimakis dimakis merged commit e8d9f15 into main May 16, 2026
1 check passed
@dimakis dimakis deleted the fix/sse-visibility-reconnect branch May 16, 2026 09:53
dimakis added a commit that referenced this pull request May 16, 2026
iOS Safari kills EventSource connections when the tab is backgrounded
without firing an error event, so the browser's native auto-reconnect
never triggers. The Capacitor lifecycle hook already called
ensureConnected() on resume, but browser clients had no equivalent
recovery path — leaving health events (and thus the voice mic button)
permanently dead after a single background cycle.

Add a visibilitychange listener in the EventBus singleton that calls
ensureConnected() when the page becomes visible again.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant