mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Kill the above-the-fold first-load flash on `/`: the hero digest and the batched chart series still fired client-side after JS download + hydrate, so the hero and charts painted empty and filled in seconds later (LCP ~5.5 s mobile-throttled while the snapshot already dehydrated). Extend the existing `page.tsx` snapshot prefetch to also seed the daily digest and the chart series batch into the same dehydrated TanStack cache, under keys byte-identical to the client's: - Extract the series-batch route core into `readSeriesBatch(...)` so the RSC and the route share one code path (no HTTP-to-self); the route thins to auth + Zod + the shared reader, behaviour unchanged. - New client-safe pure module `batch-chart-types.ts`: `deriveBatchChartTypes` (the single implementation of the visible-with-data type-set derivation, order + resting-HR-vs-pulse pick preserved) and `computeBatchWindow` (profile-tz end-of-day minus the coverage window). Server and client run the same function over the same snapshot payload, and the window is computed once server-side and threaded to the client as a prop, so the `chartSeriesBatch` key matches on both sides by construction. - Render the hero data-first so the SSR HTML carries the populated hero (the LCP element) — hydration-safe because the digest is dehydrated and never restored from the IndexedDB persist allowlist; bound that invariant with comments on both the hero gate and the allowlist. - Warm the shared chart-runtime chunk during hydration. Both prefetches honour the `DASHBOARD_SSR_PREFETCH=false` escape and fail soft to the client-fetch path on any error; a ~400 ms soft budget bounds the cold-day digest cost. No contract change, no visual change.
81 lines
2.6 KiB
TypeScript
81 lines
2.6 KiB
TypeScript
/**
|
|
* v1.30.9 — shared core of the batched dashboard daily-series read.
|
|
*
|
|
* Extracted from `GET /api/measurements/series-batch` so the route AND the
|
|
* RSC dashboard prefetch (`src/app/page.tsx`) call ONE code path instead of
|
|
* one HTTP-to-self hop. Behaviour is byte-identical to the pre-extraction
|
|
* route body: load the source-priority ladder ONCE, fan out per-type through
|
|
* the shared `readDailySeries` reader under a `p-limit(4)` cap, and isolate a
|
|
* single type's transient DB reject to an empty slice for THAT type only
|
|
* (never a 500 that blanks every chart in the row).
|
|
*/
|
|
import pLimit from "p-limit";
|
|
|
|
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
|
import {
|
|
readDailySeries,
|
|
type DailySeriesRow,
|
|
} from "@/lib/measurements/daily-series-read";
|
|
import type { MeasurementType } from "@/generated/prisma/client";
|
|
|
|
/**
|
|
* Concurrency cap for the per-type rollup reads. Mirrors the W-POOL
|
|
* `p-limit(4)` discipline applied across the analytics fan-out so a wide
|
|
* dashboard never packs the shared Prisma pool.
|
|
*/
|
|
export const SERIES_BATCH_CONCURRENCY = 4;
|
|
|
|
export interface SeriesBatchResult {
|
|
/** Per-type daily series, keyed by `MeasurementType` — the wire shape. */
|
|
series: Record<string, DailySeriesRow[]>;
|
|
/** How many types degraded to an empty slice on a transient reject. */
|
|
degradedTypes: number;
|
|
}
|
|
|
|
/**
|
|
* Read every requested type's rollup-backed daily series for `[from, to]`.
|
|
* De-dupes the type list (a caller repeating a type doesn't double-read it)
|
|
* while preserving order.
|
|
*/
|
|
export async function readSeriesBatch(
|
|
userId: string,
|
|
types: readonly MeasurementType[],
|
|
from: Date,
|
|
to: Date,
|
|
): Promise<SeriesBatchResult> {
|
|
const uniqueTypes = Array.from(new Set(types)) as MeasurementType[];
|
|
|
|
// Load the source-priority ladder ONCE and thread it across every type so
|
|
// the batched reader doesn't re-query it per type.
|
|
const priorityJson = await loadUserSourcePriority(userId);
|
|
|
|
const limit = pLimit(SERIES_BATCH_CONCURRENCY);
|
|
let degradedTypes = 0;
|
|
const entries = await Promise.all(
|
|
uniqueTypes.map((type) =>
|
|
limit(async (): Promise<readonly [MeasurementType, DailySeriesRow[]]> => {
|
|
try {
|
|
const rows = await readDailySeries({
|
|
userId,
|
|
type,
|
|
from,
|
|
to,
|
|
priorityJson,
|
|
});
|
|
return [type, rows] as const;
|
|
} catch {
|
|
degradedTypes += 1;
|
|
return [type, []] as const;
|
|
}
|
|
}),
|
|
),
|
|
);
|
|
|
|
const series: Record<string, DailySeriesRow[]> = {};
|
|
for (const [type, rows] of entries) {
|
|
series[type] = rows;
|
|
}
|
|
|
|
return { series, degradedTypes };
|
|
}
|