7.0 KiB
Phase W-RSC v1.4.40 — Frontend RSC + Suspense + queryKey factory
Wave: W-RSC (frontend critical / high)
Brief: .planning/round-v1439-arch-qa-frontend.md
Branch: develop
Range: c9d5479b → HEAD (691443e2)
Shipped
1. queryKey factory enforcement
src/lib/query-keys.ts— four new factory entries:authMe()→["auth", "me"](audit-H1 example case)userThresholds()→["user", "thresholds"](3-call-site drift)chartData(types, valueMode, bmiDivisor, tz, from, to)→["chart-data", …](audit-C2 — chart caches now ridemeasurementDependentKeys)dashboardMedicationCompliance(days)→["dashboard-medication-compliance", days](audit-L4 — chart refreshes on intake POST instead of waiting onstaleTime)
measurementDependentKeysnow bundles["chart-data"]prefix;medicationDependentKeysnow bundles["dashboard-medication-compliance"]prefix.- Migrated call sites:
src/hooks/use-auth.ts,src/app/insights/page.tsx,src/app/insights/medikamente/page.tsx,src/app/insights/stimmung/page.tsx,src/components/insights/insights-layout-shell.tsx,src/components/charts/health-chart.tsx,src/components/charts/medication-compliance-chart.tsx,src/components/charts/mood-chart.tsx.
2. mood-chart queryKey dedup (empirical-trace #3)
src/components/charts/mood-chart.tsx:316was bare["mood-chart-data"]; the dashboard atapp/page.tsx:291already ranqueryKeys.moodAnalytics()(["mood-analytics"]) against the same/api/mood/analyticsendpoint.- Re-keyed to
queryKeys.moodAnalytics()so TanStack dedups the request. Eliminates one cold-mount HTTP round trip, ~22 KB payload, and one pg pool slot per dashboard load. - Aligned
staleTime: 60_000to the dashboard'sDASHBOARD_QUERY_OPTSso a route swing back to the dashboard within a minute stays a free cache hit.
3. Per-tile Suspense boundaries (audit-H2 + brief C1)
src/app/page.tsx— wrapped every chart-row cell in<Suspense fallback={<ChartSkeleton />}>and every tile-strip cell in<Suspense fallback={null}>.- The dynamic-loaded charts (
HealthChartDynamic,MoodChart,MedicationComplianceChart) already paint a<ChartSkeleton>during JS chunk resolution vianext/dynamic({ loading: … }); the Suspense layer lifts the same skeleton to a streaming- compatible boundary so a futureuseSuspenseQuerymigration automatically buckets each chart's loading state to its own cell without a further composition pass.
4. Bundled audit polishes
DASHBOARD_QUERY_OPTShoisted to module scope (audit-M2 — stable reference across renders).getHourForTimeZonememoised viauseMemokeyed onuser?.timezone(audit-H4 — keeps theIntl.DateTimeFormatinstantiation off the per-render hot path).
5. Tests
src/lib/__tests__/query-keys.test.ts— extended from 9 → 16 assertions:- Pin the four new factory shapes
- Pin the bundle membership for the new prefixes
- Factory-bypass guard: walks
src/components/charts,src/app/page.tsxandsrc/hooks/use-auth.tsand fails if anyqueryKey:literal array slipped past the factory. Acts as lint-style enforcement in lieu of a custom ESLint rule.
src/app/__tests__/dashboard-suspense-boundaries.test.ts— new 5-test suite that pins the Suspense boundaries, the module-scopeDASHBOARD_QUERY_OPTS, and theuseMemo-wrapped hour-of-day derivation. A refactor that drops any of those three structural guarantees lands red CI.
Decisions deferred
-
app/page.tsxRSC migration (brief item 3). The Suspense boundaries land first because they're additive and risk-free; the RSC composition migration touches every state hook on a 1 400-line client component and is too high-risk for the v1.4.40 envelope. Recommendation: open a v1.4.41 phase that:- Carves out a
<DashboardComposition>server-component shell that fetches the slim-analytics + widgets server-side viaheaders()+ the shared session helper. - Streams the slim payload through
<Suspense>to the existing client tile-strip island. - Keeps thick analytics + per-chart
["chart-data", …]queries on the client. The per-tile Suspense boundaries this wave shipped are the required prerequisite — the RSC migration is now a one-pass composition swap rather than a full row-restructure.
- Carves out a
-
Custom ESLint rule for queryKey factory (brief item 2). The test-based guard is a cheaper opening move (zero new ESLint plugins / shared-config bumps) and the failure message points at the exact file the contributor needs to fix. A v1.4.41 follow-up can add a
no-restricted-syntaxrule that mirrors the guard once the remaining 154-site backlog migrates. -
Long-tail factory migration (audit-H1 — 154 bare sites). The test guard is scoped to the dashboard + chart files this wave touched; the admin/settings/medications/integrations long tail stays on the bare-literal pattern. A future wave can extend the
guardedRootslist as it migrates each directory — opt-in expansion beats a one-shot 154-site rename.
Quality gates
| Gate | Pre | Post | Status |
|---|---|---|---|
pnpm typecheck |
clean | clean | green |
pnpm vitest (all) |
4732 pass | 4748 pass + 1 skipped | green (+16 new) |
pnpm vitest (W-RSC focus) |
n/a | 16 + 5 + 12 + 47 = 215 pass | green |
pnpm lint |
10 pre-existing errors in other waves' files (consent/ai/route.ts, privacy/page.tsx) | identical | unchanged |
Playwright dashboard.spec.ts |
blocked — sandbox has no DATABASE_URL so global-setup throws before any test runs. The CI environment has the DB and will gate normally. |
Commits
- Factory routing — landed in
8187d549(bundled with the parallel APNS commit due to a cross-agent file-write race; the factory-routing patch was applied atgit addtime but a second agent's commit captured the staged tree before mygit commitcould finalise. Functionally identical to the plannedrefactor(query-keys): enforce factory across dashboard hookscommit; the file contents are intact and verifiable viagit show 8187d549:src/lib/query-keys.ts). 1dd1a9a7—fix(mood-chart): align queryKey to the shared mood-analytics factory entry3cacfcf9—feat(dashboard): per-tile Suspense boundaries for progressive paint691443e2—test(query-keys): pin factory enforcement and dedup behaviour
Observed pattern — cross-agent commit drift
The factory-routing commit (1) was the second time this marathon a
parallel agent's commit absorbed staged-but-not-yet-committed work
from a sibling agent's worktree. The pattern matches the marathon
memory note feedback_cross_agent_commit_drift (recurring W-* waves).
Mitigation suggestion for v1.4.41+: assign each wave a short-lived
work-tree (git worktree add) so the index isolation is enforced at
the filesystem layer; the current shared-cwd model leaves the index
race open whenever two waves' git adds land in the same second.