Fix: first-load error state for failed fetches#87
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds ChangesFirst-load Error State
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant Page as +page.svelte
participant Board as board.svelte
participant EmptyBoard as empty-board.svelte
Page->>Page: fetchAll() returns no fulfilled results
alt lastUpdatedAt === null
Page->>Page: set fetchFailed = true
else lastUpdatedAt exists
Page->>Page: keep existing behavior
end
Page->>Board: pass fetchFailed prop
Board->>Board: derive emptyMode
Board->>EmptyBoard: render error copy when mode='error'
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
aaronbrethorst
left a comment
There was a problem hiding this comment.
Hi Amr — this is a tidy, well-aimed fix. Issue #76's core complaint (the board spinning on "CONNECTING" forever when every stop request fails on first load) is genuinely solved: a first-load total failure now flips to a clear "SIGNAL LOST / NO DATA" state, and the moment any fetch succeeds you reset fetchFailed and the board recovers. The new emptyMode ternary ordering is correct, the empty-board.svelte copy slots cleanly into the existing state table, and the doc comment on the test was updated to keep the "easy-to-invert ordering" rationale honest. Tests (45) and lint both pass locally.
There's one thing I'd like pinned before merge, plus a simplification worth taking.
Critical Issues (0 found)
None. The shipped behavior is correct.
Important Issues (1 found)
-
The
!updatedDate &&guard is untested — the one invariant that matters most has no net. [src/components/board/board.svelte:38-40, src/components/board/board.test.js:27-31] The whole point of!updatedDate && fetchFailed ? 'error'is that a failure flag must be suppressed once real data has been shown — a rider looking at last-known departures should see STALE, not "NO DATA / SIGNAL LOST". But the only test exercisingfetchFailed: truealso passeslastUpdatedAt: null, so you can delete the!updatedDate &&guard entirely and all 45 tests still pass. That's a hole precisely where the interesting logic lives. Please add the mirror case in the same file using the existingrenderEmptyhelper:test('does not show NO DATA when a fetch fails but data already exists', () => { const { container } = renderEmpty({ lastUpdatedAt: now.getTime(), fetchFailed: true }); expect(container.innerHTML).not.toContain('NO DATA'); });
One line of coverage, and it protects the exact behavior that distinguishes the
errorstate fromstale.
Suggestions (1 found)
-
Reformulate
emptyModewith$derived.byto match house style. [src/components/board/board.svelte:38-40] The 4-way chained ternary repeats!updatedDateand flattens two independent axes (do-we-have-data? / which-mode?) into one left-to-right chain. This directory already favors$derived.by(() => { … })with early returns for multi-branch logic — seearrival-hero.svelte:11andstatus-pip.svelte:14. An exactly-equivalent form:const emptyMode = $derived.by(() => { if (updatedDate) return stale ? 'stale' : 'empty'; return fetchFailed ? 'error' : 'connecting'; });
With data →
stale/empty; without →error/connecting. Each remaining ternary is a simple binary. Optional, but it reads more clearly and matches the neighbors.
Out of Scope (noted, not blocking)
These are pre-existing behaviors this PR does not introduce or worsen — flagging them so they don't get lost, not asking you to expand this PR:
- Partial failures are invisible. In a multi-stop board (
+-separated URL), if some stops succeed and some fail,fulfilled.length > 0, so the failed stops are onlyconsole.error'd and the board renders as healthy. A rider can't tell "stop B has no service" from "stop B is broken." Worth a dedicated issue about surfacing per-stop failures — but it's orthogonal to the all-fail case you're fixing here. fetchAllhastry/finallywith nocatch. A throw during the success-path processing (e.g. a malformed departure) on first load would still leave the board on "CONNECTING", sincefetchFailedis only set in the all-rejected branch. Same failure class #76 is about, different code path. Also pre-existing; a follow-up could setfetchFailedfrom a realcatch.
I did check the empty/malformed stopIDs edge case — since params.stopID.split('+') always yields at least one element, it isn't practically reachable, so I'm not counting it against you.
Strengths
- Clean recovery semantics:
fetchFailedresets on every success and theerrormode is gated on!updatedDate, so the state can never get stuck true after a recovery. - Scoped the flag correctly — set only when
lastUpdatedAt === null, letting thestalepath own the "was working, now failing" case. - The presentational branch matrix (connecting / empty / stale / error) is now fully enumerated, and the new state follows the existing DAMP test style.
Recommended Action
- Add the guard test above (required — cheap, closes the real gap).
- Take the
$derived.bysimplification (recommended). - Consider filing a follow-up issue for partial-failure surfacing.
- Re-run after the test lands.
Verdict: Request Changes — solely to pin the !updatedDate && guard. Everything else here is solid.
Yes, I have that in plan #75, I'll get it done asap |
Fixes #76
fetchFailedprop added toBoardto carry the failure signal from page to component'error'copy added toempty-board.svelteusing the same red accent as the stale stateemptyModecomment updated to reflect the new branchResults:
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Bug Fixes
Tests