mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(insights): unify the derived-metric query hook onto one factory key
Two parallel loaders for the generic derived-metric route had grown — a components-local hook keyed on a 4-element tuple and a hooks/ hook keyed on a 3-or-4-element tuple. Both shared the ["insights","derived",metric,…] prefix with different tuple shapes, which silently poisons the TanStack cache between surfaces. Collapse to the single components-local hook (auth-gated, 8 s client ceiling, retry-free) on the one insightsDerived factory key; the fourth slot is always present so the metric prefix can no longer carry two shapes.
This commit is contained in:
1 parent
c5ba38cebb
commit
02c50acd46
4 files changed
+52
-108
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useDerivedMetric } from "@/hooks/use-derived-metric";
|
||||
import { useDerivedMetric } from "./use-derived-metric";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type {
|
||||
ReadinessValue,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type {
|
||||
DerivedCoverage,
|
||||
@@ -9,14 +10,22 @@ import type {
|
||||
} from "@/lib/insights/derived/types";
|
||||
|
||||
/**
|
||||
* v1.10.0 — TanStack Query hook for the generic derived-metric route.
|
||||
* v1.10.0 — the single TanStack Query hook for the generic derived-metric
|
||||
* route, shared by every client surface (vitals dashboard tiles, composite
|
||||
* score-anatomy, the home wellness strip).
|
||||
*
|
||||
* `GET /api/insights/derived?metric=<id>[&type=<vital>]` returns the
|
||||
* compute-once flattened `Derived<T>` the W2b dashboard tiles + per-metric
|
||||
* detail cards read. The queryKey comes from the centralised factory
|
||||
* (`queryKeys.insightsDerived`) so the bare-array ESLint rule stays
|
||||
* satisfied and the cache never poisons across metrics; the read unwraps
|
||||
* `(await res.json()).data` per the envelope convention.
|
||||
* compute-once flattened `Derived<T>` the surfaces read. The queryKey comes
|
||||
* from the centralised factory (`queryKeys.insightsDerived`) so the bare-array
|
||||
* ESLint rule stays satisfied and the cache never poisons across metrics; the
|
||||
* read unwraps `(await res.json()).data` per the envelope convention.
|
||||
*
|
||||
* Pure compute on the server (sub-second on a warm rollup tenant) — so the
|
||||
* hook bounds the fetch on an 8 s client ceiling (a slow response means a
|
||||
* degraded network, not a provider call), keeps a 60 s `staleTime`, gates on
|
||||
* auth, and never inline-retries. These reads NEVER warm/generate on visit —
|
||||
* they read already-computed rollup values (the v1.4.36 / v1.9.1 lesson: a
|
||||
* warm-on-mount fan-out looks like the app hanging).
|
||||
*
|
||||
* Type-only import of the `Derived<T>` facets — no server module enters the
|
||||
* client bundle (the v1.9.0 lesson). The value `T` is left generic so each
|
||||
@@ -24,6 +33,8 @@ import type {
|
||||
* imported as `import type`.
|
||||
*/
|
||||
|
||||
const DERIVED_TIMEOUT_MS = 8_000;
|
||||
|
||||
/** The flattened wire shape the route emits in the `data` envelope slot. */
|
||||
export interface DerivedMetricResponse<T> {
|
||||
metric: string;
|
||||
@@ -38,7 +49,7 @@ export interface DerivedMetricResponse<T> {
|
||||
export interface UseDerivedMetricOptions {
|
||||
/** Optional sub-target (the chosen vital for `VITALS_BASELINE`). */
|
||||
type?: string | null;
|
||||
/** Gate the fetch (e.g. on auth). Defaults to enabled. */
|
||||
/** Gate the fetch (e.g. on a surface flag). Defaults to enabled. */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -47,21 +58,33 @@ export function useDerivedMetric<T>(
|
||||
options: UseDerivedMetricOptions = {},
|
||||
) {
|
||||
const { type = null, enabled = true } = options;
|
||||
const { isAuthenticated } = useAuth();
|
||||
const gated = enabled && isAuthenticated && metric !== "";
|
||||
|
||||
return useQuery({
|
||||
queryKey: queryKeys.insightsDerived(metric, type),
|
||||
queryFn: async (): Promise<DerivedMetricResponse<T>> => {
|
||||
const params = new URLSearchParams({ metric });
|
||||
if (type) params.set("type", type);
|
||||
const res = await fetch(`/api/insights/derived?${params.toString()}`);
|
||||
if (!res.ok) {
|
||||
throw new Error(`derived ${metric} request failed (${res.status})`);
|
||||
const controller = new AbortController();
|
||||
const timeoutHandle = setTimeout(
|
||||
() => controller.abort(),
|
||||
DERIVED_TIMEOUT_MS,
|
||||
);
|
||||
try {
|
||||
const params = new URLSearchParams({ metric });
|
||||
if (type) params.set("type", type);
|
||||
const res = await fetch(`/api/insights/derived?${params.toString()}`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`derived ${metric} request failed (${res.status})`);
|
||||
}
|
||||
return (await res.json()).data as DerivedMetricResponse<T>;
|
||||
} finally {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
return (await res.json()).data as DerivedMetricResponse<T>;
|
||||
},
|
||||
enabled,
|
||||
// Pure compute over the rollup tier — cheap to recompute, but the
|
||||
// numbers only move on a fresh ingest. A short stale window keeps the
|
||||
// dashboard snappy without hammering the route on every focus.
|
||||
enabled: gated,
|
||||
staleTime: 60_000,
|
||||
retry: 0,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user