fix(measurements): isolate per-type failures in the series batch

The batched daily-series endpoint ran every type through one Promise.all
with no per-task catch, so a single type's transient reject 500'd the whole
batch and blanked every preloaded chart in the dashboard row at once.

Wrap each per-type read so a failure degrades to an empty slice for that
type only; the failing chart self-fetches through its existing fallback.
A degraded_types counter rides the wide event.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 18:29:46 +02:00
1 parent 1a73afa7e9
commit 04b0f38537
2 files changed
+39 -9

No files matched your search

@@ -117,4 +117,24 @@ describe("GET /api/measurements/series-batch", () => {
expect(res.status).toBe(200);
expect(vi.mocked(readDailySeries)).toHaveBeenCalledTimes(1);
});
it("isolates a single type's failure to an empty slice, others still return", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
const weightRows = [
{ type: "WEIGHT", value: 80, measuredAt: FROM, count: 1 },
];
vi.mocked(readDailySeries).mockImplementation(async ({ type }) => {
if (type === "WEIGHT") return weightRows as never;
throw new Error("transient db reject");
});
const res = await GET(
req(`types=WEIGHT,BLOOD_PRESSURE_SYS&from=${FROM}&to=${TO}`),
);
// One type rejecting must NOT 500 the whole batch.
expect(res.status).toBe(200);
const body = await res.json();
expect(body.data.series.WEIGHT).toEqual(weightRows);
expect(body.data.series.BLOOD_PRESSURE_SYS).toEqual([]);
});
});
+19 -9
View File
@@ -63,18 +63,27 @@ export const GET = apiHandler(async (request: NextRequest) => {
// so the batched reader doesn't re-query it per type.
const priorityJson = await loadUserSourcePriority(user.id);
// Per-type isolation: a single type's transient DB reject degrades to an
// empty slice for THAT type only, never a 500 that blanks every chart in
// the row. The failing chart self-fetches through its existing fallback.
const limit = pLimit(SERIES_BATCH_CONCURRENCY);
let degradedTypes = 0;
const entries = await Promise.all(
uniqueTypes.map((type) =>
limit(async () => {
const rows = await readDailySeries({
userId: user.id,
type,
from,
to,
priorityJson,
});
return [type, rows] as const;
limit(async (): Promise<readonly [MeasurementType, DailySeriesRow[]]> => {
try {
const rows = await readDailySeries({
userId: user.id,
type,
from,
to,
priorityJson,
});
return [type, rows] as const;
} catch {
degradedTypes += 1;
return [type, []] as const;
}
}),
),
);
@@ -89,6 +98,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
meta: {
type_count: uniqueTypes.length,
total: entries.reduce((sum, [, rows]) => sum + rows.length, 0),
degraded_types: degradedTypes,
},
});