refactor(insights): fold Math.min/max spreads in the six status helpers

`bmi`, `pulse`, `general`, `weight`, `blood-pressure`, and `mood`
status helpers all built their `summarizeSeries` block with
`Math.min(...series.map(...))` and `Math.max(...series.map(...))`.
The same anti-pattern hit `summarize()` in `src/lib/analytics/trends.ts`
on 2026-05-16 and crashed `/api/analytics` with
`RangeError: Maximum call stack size exceeded` once an
Apple-Health-synced PULSE series outgrew V8's ~125 000-arg ceiling;
`61107e0c` folded that one. The six insight helpers are fed bounded
windows today so they never reached the ceiling, but the spread still
allocates a transient args array on every call and the fold is both
stack-safe and cheaper.

Replace each `Math.min(...) / Math.max(...) / average(...)` triplet
with a single walk that maintains running sum / min / max. The
now-unused `average()` helper is dropped from each file. Pinned by
the existing per-helper unit tests (326 passing in
`src/lib/insights` / `src/lib/analytics` / `src/app/api/analytics`).
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 17:13:34 +02:00
1 parent 13b2c13caf
commit b5060f1422
6 files changed
+109 -47

No files matched your search

+18 -8
View File
@@ -57,11 +57,6 @@ function round(value: number, digits = 1): number {
return Math.round(value * factor) / factor;
}
function average(values: number[]): number {
if (values.length === 0) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function normalizeSummaryText(value: string): string {
return stripChartTokens(value).replace(/\s+/g, " ").trim();
}
@@ -74,14 +69,29 @@ function summarizeSeries(series: Array<{ value: number }>) {
if (series.length === 0) return null;
const first = series[0].value;
const last = series[series.length - 1].value;
// v1.4.33 — fold sum/min/max into a single walk. The previous
// `Math.min(...series.map(...))` / `Math.max(...series.map(...))`
// spread tripped V8's ~125 000-arg ceiling on the bound /api/analytics
// path; see `.planning/round-v1433-analytics-500-report.md` §"Carry-
// over". These helpers are fed bounded windows today so the crash
// never reached them, but the spread allocates a transient args array
// on every call — the fold is both stack-safe and cheaper.
let sum = 0;
let minVal = series[0].value;
let maxVal = series[0].value;
for (const entry of series) {
sum += entry.value;
if (entry.value < minVal) minVal = entry.value;
if (entry.value > maxVal) maxVal = entry.value;
}
return {
points: series.length,
start: round(first, 2),
end: round(last, 2),
delta: round(last - first, 2),
mean: round(average(series.map((entry) => entry.value)), 2),
min: round(Math.min(...series.map((entry) => entry.value)), 2),
max: round(Math.max(...series.map((entry) => entry.value)), 2),
mean: round(sum / series.length, 2),
min: round(minVal, 2),
max: round(maxVal, 2),
};
}
+19 -8
View File
@@ -42,11 +42,6 @@ function round(value: number, digits = 1): number {
return Math.round(value * factor) / factor;
}
function average(values: number[]): number {
if (values.length === 0) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function normalizeSummaryText(value: string): string {
return stripChartTokens(value).replace(/\s+/g, " ").trim();
}
@@ -59,14 +54,30 @@ function summarizeSeries(series: Array<{ value: number }>) {
if (series.length === 0) return null;
const first = series[0].value;
const last = series[series.length - 1].value;
// v1.4.33 — fold sum/min/max into a single walk. The previous
// `Math.min(...series.map(...))` / `Math.max(...series.map(...))`
// spread tripped V8's ~125 000-arg ceiling on the bound /api/analytics
// path and is the same anti-pattern fixed in `summarize()` (see
// `.planning/round-v1433-analytics-500-report.md` §"Carry-over").
// These helpers feed off bounded windows today so they did not crash,
// but the spread allocates a transient args array on every call;
// folding once is both stack-safe and cheaper.
let sum = 0;
let minVal = series[0].value;
let maxVal = series[0].value;
for (const entry of series) {
sum += entry.value;
if (entry.value < minVal) minVal = entry.value;
if (entry.value > maxVal) maxVal = entry.value;
}
return {
points: series.length,
start: round(first, 2),
end: round(last, 2),
delta: round(last - first, 2),
mean: round(average(series.map((entry) => entry.value)), 2),
min: round(Math.min(...series.map((entry) => entry.value)), 2),
max: round(Math.max(...series.map((entry) => entry.value)), 2),
mean: round(sum / series.length, 2),
min: round(minVal, 2),
max: round(maxVal, 2),
};
}
+18 -7
View File
@@ -51,10 +51,6 @@ function round(value: number, digits = 1): number {
return Math.round(value * factor) / factor;
}
function average(values: number[]): number {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function normalizeSummaryText(value: string): string {
return stripChartTokens(value).replace(/\s+/g, " ").trim();
}
@@ -63,14 +59,29 @@ function summarizeSeries(series: Array<{ value: number }>) {
if (series.length === 0) return null;
const first = series[0].value;
const last = series[series.length - 1].value;
// v1.4.33 — fold sum/min/max into a single walk. The previous
// `Math.min(...series.map(...))` / `Math.max(...series.map(...))`
// spread tripped V8's ~125 000-arg ceiling on the bound /api/analytics
// path; see `.planning/round-v1433-analytics-500-report.md` §"Carry-
// over". These helpers are fed bounded windows today so the crash
// never reached them, but the spread allocates a transient args array
// on every call — the fold is both stack-safe and cheaper.
let sum = 0;
let minVal = series[0].value;
let maxVal = series[0].value;
for (const entry of series) {
sum += entry.value;
if (entry.value < minVal) minVal = entry.value;
if (entry.value > maxVal) maxVal = entry.value;
}
return {
points: series.length,
start: round(first, 2),
end: round(last, 2),
delta: round(last - first, 2),
mean: round(average(series.map((entry) => entry.value)), 2),
min: round(Math.min(...series.map((entry) => entry.value)), 2),
max: round(Math.max(...series.map((entry) => entry.value)), 2),
mean: round(sum / series.length, 2),
min: round(minVal, 2),
max: round(maxVal, 2),
};
}
+18 -8
View File
@@ -49,11 +49,6 @@ function round(value: number, digits = 1): number {
return Math.round(value * factor) / factor;
}
function average(values: number[]): number {
if (values.length === 0) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function normalizeSummaryText(value: string): string {
return stripChartTokens(value).replace(/\s+/g, " ").trim();
}
@@ -66,14 +61,29 @@ function summarizeSeries(series: Array<{ value: number }>) {
if (series.length === 0) return null;
const first = series[0].value;
const last = series[series.length - 1].value;
// v1.4.33 — fold sum/min/max into a single walk. The previous
// `Math.min(...series.map(...))` / `Math.max(...series.map(...))`
// spread tripped V8's ~125 000-arg ceiling on the bound /api/analytics
// path; see `.planning/round-v1433-analytics-500-report.md` §"Carry-
// over". These helpers are fed bounded windows today so the crash
// never reached them, but the spread allocates a transient args array
// on every call — the fold is both stack-safe and cheaper.
let sum = 0;
let minVal = series[0].value;
let maxVal = series[0].value;
for (const entry of series) {
sum += entry.value;
if (entry.value < minVal) minVal = entry.value;
if (entry.value > maxVal) maxVal = entry.value;
}
return {
points: series.length,
start: round(first, 2),
end: round(last, 2),
delta: round(last - first, 2),
mean: round(average(series.map((entry) => entry.value)), 2),
min: round(Math.min(...series.map((entry) => entry.value)), 2),
max: round(Math.max(...series.map((entry) => entry.value)), 2),
mean: round(sum / series.length, 2),
min: round(minVal, 2),
max: round(maxVal, 2),
};
}
+18 -8
View File
@@ -48,11 +48,6 @@ function round(value: number, digits = 1): number {
return Math.round(value * factor) / factor;
}
function average(values: number[]): number {
if (values.length === 0) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function normalizeSummaryText(value: string): string {
return stripChartTokens(value).replace(/\s+/g, " ").trim();
}
@@ -65,14 +60,29 @@ function summarizeSeries(series: Array<{ value: number }>) {
if (series.length === 0) return null;
const first = series[0].value;
const last = series[series.length - 1].value;
// v1.4.33 — fold sum/min/max into a single walk. The previous
// `Math.min(...series.map(...))` / `Math.max(...series.map(...))`
// spread tripped V8's ~125 000-arg ceiling on the bound /api/analytics
// path; see `.planning/round-v1433-analytics-500-report.md` §"Carry-
// over". These helpers are fed bounded windows today so the crash
// never reached them, but the spread allocates a transient args array
// on every call — the fold is both stack-safe and cheaper.
let sum = 0;
let minVal = series[0].value;
let maxVal = series[0].value;
for (const entry of series) {
sum += entry.value;
if (entry.value < minVal) minVal = entry.value;
if (entry.value > maxVal) maxVal = entry.value;
}
return {
points: series.length,
start: round(first, 2),
end: round(last, 2),
delta: round(last - first, 2),
mean: round(average(series.map((entry) => entry.value)), 2),
min: round(Math.min(...series.map((entry) => entry.value)), 2),
max: round(Math.max(...series.map((entry) => entry.value)), 2),
mean: round(sum / series.length, 2),
min: round(minVal, 2),
max: round(maxVal, 2),
};
}
+18 -8
View File
@@ -52,11 +52,6 @@ function round(value: number, digits = 1): number {
return Math.round(value * factor) / factor;
}
function average(values: number[]): number {
if (values.length === 0) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function normalizeSummaryText(value: string): string {
return stripChartTokens(value).replace(/\s+/g, " ").trim();
}
@@ -69,14 +64,29 @@ function summarizeSeries(series: Array<{ value: number }>) {
if (series.length === 0) return null;
const first = series[0].value;
const last = series[series.length - 1].value;
// v1.4.33 — fold sum/min/max into a single walk. The previous
// `Math.min(...series.map(...))` / `Math.max(...series.map(...))`
// spread tripped V8's ~125 000-arg ceiling on the bound /api/analytics
// path; see `.planning/round-v1433-analytics-500-report.md` §"Carry-
// over". These helpers are fed bounded windows today so the crash
// never reached them, but the spread allocates a transient args array
// on every call — the fold is both stack-safe and cheaper.
let sum = 0;
let minVal = series[0].value;
let maxVal = series[0].value;
for (const entry of series) {
sum += entry.value;
if (entry.value < minVal) minVal = entry.value;
if (entry.value > maxVal) maxVal = entry.value;
}
return {
points: series.length,
start: round(first, 2),
end: round(last, 2),
delta: round(last - first, 2),
mean: round(average(series.map((entry) => entry.value)), 2),
min: round(Math.min(...series.map((entry) => entry.value)), 2),
max: round(Math.max(...series.map((entry) => entry.value)), 2),
mean: round(sum / series.length, 2),
min: round(minVal, 2),
max: round(maxVal, 2),
};
}