refactor(web): shared SSE connection, reconnect with backoff, app-level notifications #7

Merged
pregno merged 2 commits from fix/web-shared-stream into main 2026-06-13 01:29:32 +02:00
Owner

Fixes three Important findings from the repo-wide review in one coherent refactor:

  1. One shared EventSource instead of three. New src/api/stream.ts module owns a single connection, parses each frame once with the right zod schema (demuxed by the type discriminator), and fans out to per-type subscribers. Reference-counted: opens on first subscriber, tears down (including pending retry timers) on last. Previously the Notifications page ran 3 concurrent connections each parsing the full scanner payload every ~8s.

  2. Recovery from a permanently closed stream. When onerror fires with readyState === CLOSED (e.g. proxy returns non-200 during a server restart — browser EventSource never retries from there), the connection is recreated with 1s→30s capped doubling backoff, reset on successful open. Transient errors keep the existing behavior (conn "stale", browser retries). Connection-status semantics in the Layout are unchanged.

  3. Desktop notifications fire on every page. new Notification(...) moved out of the page-mounted alert hooks into useDesktopNotifications() mounted once in App — previously notifications only worked while the user was already looking at the Notifications page. Permission flow unchanged.

API changes: useScannerStream's unused initial param and lastTs return removed (no callers). The two alert hooks keep their public APIs and were NOT merged into one generic hook — post-refactor each is 5 lines and the generic form needed type gymnastics for zero dedup gain.

All pre-existing tests pass untouched (including the view tests); 8 new tests cover connection sharing, fan-out, refcount teardown, backoff (fake timers), and notification firing from a non-Notifications view. 58 files / 319 passing, typecheck clean.

Fixes three Important findings from the repo-wide review in one coherent refactor: 1. **One shared EventSource instead of three.** New `src/api/stream.ts` module owns a single connection, parses each frame once with the right zod schema (demuxed by the `type` discriminator), and fans out to per-type subscribers. Reference-counted: opens on first subscriber, tears down (including pending retry timers) on last. Previously the Notifications page ran 3 concurrent connections each parsing the full scanner payload every ~8s. 2. **Recovery from a permanently closed stream.** When `onerror` fires with `readyState === CLOSED` (e.g. proxy returns non-200 during a server restart — browser EventSource never retries from there), the connection is recreated with 1s→30s capped doubling backoff, reset on successful open. Transient errors keep the existing behavior (conn "stale", browser retries). Connection-status semantics in the Layout are unchanged. 3. **Desktop notifications fire on every page.** `new Notification(...)` moved out of the page-mounted alert hooks into `useDesktopNotifications()` mounted once in App — previously notifications only worked while the user was already looking at the Notifications page. Permission flow unchanged. API changes: `useScannerStream`'s unused `initial` param and `lastTs` return removed (no callers). The two alert hooks keep their public APIs and were NOT merged into one generic hook — post-refactor each is 5 lines and the generic form needed type gymnastics for zero dedup gain. All pre-existing tests pass untouched (including the view tests); 8 new tests cover connection sharing, fan-out, refcount teardown, backoff (fake timers), and notification firing from a non-Notifications view. 58 files / 319 passing, typecheck clean.
Add apps/web/src/api/stream.ts: a module-level singleton EventSource that
parses each frame once (demuxed by the frame's type field), fans it out to
per-type subscribers, and is reference-counted (opens on first subscriber,
closes on last). When the browser gives up (readyState CLOSED, e.g. proxy
returns non-200 during a server restart) the connection is recreated with
exponential backoff (1s doubling to a 30s cap, reset on successful open)
instead of silently freezing until reload.

Rewrite useScannerStream/useAlertStream/useMarketAlertStream as thin
subscribers. useScannerStream's unused 'initial' parameter and 'lastTs'
return value are removed (no callers). Conn semantics are unchanged:
connecting -> open on open or valid frame, stale on error.
Move the new Notification(...) side effects out of the page-mounted alert
hooks into useDesktopNotifications, mounted once in App, so desktop
notifications fire on any page instead of only while the Notifications
view is open. The permission-request button stays in Notifications.
pregno force-pushed fix/web-shared-stream from 037b29caa3 to c7576e1d82 2026-06-13 01:29:09 +02:00 Compare
pregno merged commit 2e3a48b4d9 into main 2026-06-13 01:29:32 +02:00
pregno deleted branch fix/web-shared-stream 2026-06-13 01:29:32 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
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
pregno/polymarket-screener!7
No description provided.