feat(frontend): shared API client with session-expiry, abort and dedup (#103) #181
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/103-shared-api-client"
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?
Fixes #103. Second of the v3.5.0 (Frontend Platform & PWA) milestone, after #180.
Why
request()was copy-pasted acrosscampaigns.js,sessions.js,users.js,planning.js, with more variants inlined inauth.js. That duplication was the visible problem; the invisible ones were worse:AuthGuardcarried on rendering a dead page. The user was stuck rather than sent to login.AbortControlleranywhere. A slow response from a route you'd already left could resolve late and overwrite fresher state.What changed
New
src/api/client.js; everysrc/api/*module routes through it.AuthProvider(clear user → redirect/login). The client exposes a registration callback rather than importing auth state, because the api modules it serves are themselves imported byuseAuth— importing back would be circular.AbortSignal; Dashboard, CampaignDetail and WikiArticle abort in effect cleanup (8 effects). Every fetching effect swallowsAbortErrorinstead of treating navigation as a failure.Two subtleties worth reviewing
1. Abort and dedup interact badly if done naively. When two callers share one in-flight request, letting either one's signal abort the underlying fetch breaks the other caller. Subscribers are refcounted instead: your signal rejects your promise only, and the shared fetch is aborted just when the last subscriber leaves. Pinned by a test ("one subscriber aborting does not break another sharing the request").
2. A blanket TTL cache would introduce a bug the old code couldn't have. Create a campaign → return to the dashboard inside the window → your new campaign is missing (today every mount refetches). So any successful mutation clears the cached GETs. Blunt, but it beats shipping a regression. Also pinned by a test.
Two endpoints must NOT get blanket 401 treatment
Both opt out explicitly — these were easy to get wrong:
/api/meanswers 401 simply because nobody is logged in yet. Firing the session-expiry handler there would redirect-loop every anonymous visitor. →handleUnauthorized: false/public/analytics/<token>is a genuinely public share link fetched without credentials, rendered for logged-out viewers. →auth: falseThere's a test for each.
Acceptance criteria
src/hooks/useAuth.test.jsxDashboard.test.jsxdrives the real client so the cache genuinely runs:fetchNextSessionis called 4× (2 per mount, 2 mounts) but only 2 HTTP requests are madesrc/api/*import the shared client;grep "async function request"insrc/api/returns nothing (and nofetch(remains outsideclient.js)detailstringVerification
Run in node:20, matching CI:
npm run lint→ exit 0, no output (exhaustive-depsis genuinely enforced now, thanks to #21)npx vitest run→ 97/97 passing across 13 files (was 69; +25 client tests, +2 useAuth, +1 dashboard dedup). No regressions.npx vite build→ succeedsEntry chunk is currently 685.50 kB (172.36 kB gzip) — that's the baseline for #105 (route-level code splitting), next in this milestone.
🤖 Generated with Claude Code