From f01f53a1cc491a5d8662e3591b994a5c9036659c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 20 Oct 2026 20:20:00 +0200 Subject: [PATCH] perf(rollups): refold the buckets of every provider sync in one pass Every provider sync and the shared mutation tail refolded its touched (type, day) buckets one at a time: an aggregate, a transaction and three queue sends per day. They now hand the whole set to recomputeBucketsForKeys, which leaves the DAY tier exactly as the per-key hook did (pinned by a parity test against it) at the cost of one aggregate per chunk of keys and one coarse job per run of buckets. --- src/__tests__/measurement-write-tail-guard.test.ts | 4 +++- src/app/api/import/__tests__/route.test.ts | 12 +++++++++++- src/app/api/import/csv/__tests__/route.test.ts | 12 +++++++++++- .../measurements/bulk-delete/__tests__/route.test.ts | 12 +++++++++++- .../api/measurements/restore/__tests__/route.test.ts | 12 +++++++++++- src/lib/fitbit/sync-core.ts | 6 ++---- src/lib/google-health/sync-core.ts | 6 ++---- src/lib/nightscout/__tests__/sync.test.ts | 7 ++++++- src/lib/nightscout/sync.ts | 6 ++---- src/lib/oura/__tests__/sync.test.ts | 7 ++++++- src/lib/oura/sync.ts | 6 ++---- src/lib/polar/sync.ts | 6 ++---- .../__tests__/after-measurement-mutation.test.ts | 9 +++++++-- src/lib/rollups/after-measurement-mutation.ts | 8 +++----- src/lib/whoop/__tests__/sync.test.ts | 9 +++++++-- src/lib/whoop/sync-core.ts | 8 +++----- src/lib/withings/__tests__/sync-activity.test.ts | 12 +++++++++++- src/lib/withings/__tests__/sync-ecg.test.ts | 12 +++++++++++- src/lib/withings/__tests__/sync-sleep.test.ts | 12 +++++++++++- src/lib/withings/sync-activity.ts | 6 ++---- src/lib/withings/sync-ecg.ts | 6 ++---- src/lib/withings/sync-sleep.ts | 6 ++---- 22 files changed, 128 insertions(+), 56 deletions(-) diff --git a/src/__tests__/measurement-write-tail-guard.test.ts b/src/__tests__/measurement-write-tail-guard.test.ts index 4deacda2d..da045a6ed 100644 --- a/src/__tests__/measurement-write-tail-guard.test.ts +++ b/src/__tests__/measurement-write-tail-guard.test.ts @@ -176,7 +176,9 @@ describe("measurement write tail", () => { const missing = handRolled.filter((file) => { const src = readFileSync(join(SRC, file), "utf8"); return !( - /recomputeBucketsForMeasurement|afterMeasurementMutation/.test(src) && + /recomputeBucketsFor(Measurement|Keys)|afterMeasurementMutation/.test( + src, + ) && /invalidateStatusInsightsForTypes|afterMeasurementMutation/.test(src) ); }); diff --git a/src/app/api/import/__tests__/route.test.ts b/src/app/api/import/__tests__/route.test.ts index 096b72f8c..ef9e668fe 100644 --- a/src/app/api/import/__tests__/route.test.ts +++ b/src/app/api/import/__tests__/route.test.ts @@ -54,9 +54,19 @@ vi.mock("@/lib/rollups/measurement-rollups", async () => { const actual = await vi.importActual< typeof import("@/lib/rollups/measurement-rollups") >("@/lib/rollups/measurement-rollups"); + const perKey = vi.fn().mockResolvedValue(undefined); return { ...actual, - recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined), + recomputeBucketsForMeasurement: perKey, + // The keyed refold, observed as the per-key recomputes it stands for. + recomputeBucketsForKeys: vi.fn( + async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await perKey(userId, k.type, k.measuredAt); + }, + ), }; }); diff --git a/src/app/api/import/csv/__tests__/route.test.ts b/src/app/api/import/csv/__tests__/route.test.ts index 3ca886bea..a6d8dcaa9 100644 --- a/src/app/api/import/csv/__tests__/route.test.ts +++ b/src/app/api/import/csv/__tests__/route.test.ts @@ -58,9 +58,19 @@ vi.mock("@/lib/rollups/measurement-rollups", async () => { const actual = await vi.importActual< typeof import("@/lib/rollups/measurement-rollups") >("@/lib/rollups/measurement-rollups"); + const perKey = vi.fn().mockResolvedValue(undefined); return { ...actual, - recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined), + recomputeBucketsForMeasurement: perKey, + // The keyed refold, observed as the per-key recomputes it stands for. + recomputeBucketsForKeys: vi.fn( + async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await perKey(userId, k.type, k.measuredAt); + }, + ), }; }); diff --git a/src/app/api/measurements/bulk-delete/__tests__/route.test.ts b/src/app/api/measurements/bulk-delete/__tests__/route.test.ts index 3a3438a12..b41d53ccb 100644 --- a/src/app/api/measurements/bulk-delete/__tests__/route.test.ts +++ b/src/app/api/measurements/bulk-delete/__tests__/route.test.ts @@ -47,10 +47,20 @@ vi.mock("@/lib/rollups/measurement-rollups", async () => { const actual = await vi.importActual< typeof import("@/lib/rollups/measurement-rollups") >("@/lib/rollups/measurement-rollups"); + const perKey = vi.fn().mockResolvedValue(undefined); return { // keep the real collapse so the test exercises the actual fan-out logic collapseToTypeDayKeys: actual.collapseToTypeDayKeys, - recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined), + recomputeBucketsForMeasurement: perKey, + // The keyed refold, observed as the per-key recomputes it stands for. + recomputeBucketsForKeys: vi.fn( + async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await perKey(userId, k.type, k.measuredAt); + }, + ), }; }); vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() })); diff --git a/src/app/api/measurements/restore/__tests__/route.test.ts b/src/app/api/measurements/restore/__tests__/route.test.ts index 8ef10c8c3..0fa7e8427 100644 --- a/src/app/api/measurements/restore/__tests__/route.test.ts +++ b/src/app/api/measurements/restore/__tests__/route.test.ts @@ -47,10 +47,20 @@ vi.mock("@/lib/rollups/measurement-rollups", async () => { const actual = await vi.importActual< typeof import("@/lib/rollups/measurement-rollups") >("@/lib/rollups/measurement-rollups"); + const perKey = vi.fn().mockResolvedValue(undefined); return { // keep the real collapse so the test exercises the actual fan-out logic collapseToTypeDayKeys: actual.collapseToTypeDayKeys, - recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined), + recomputeBucketsForMeasurement: perKey, + // The keyed refold, observed as the per-key recomputes it stands for. + recomputeBucketsForKeys: vi.fn( + async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await perKey(userId, k.type, k.measuredAt); + }, + ), }; }); vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() })); diff --git a/src/lib/fitbit/sync-core.ts b/src/lib/fitbit/sync-core.ts index c636b3071..e52c656c9 100644 --- a/src/lib/fitbit/sync-core.ts +++ b/src/lib/fitbit/sync-core.ts @@ -18,7 +18,7 @@ import { getEvent } from "@/lib/logging/context"; import { dropImplausibleMeasurements } from "@/lib/measurements/plausibility-gate"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { refreshAccessToken } from "./client"; import { getUserFitbitCredentials } from "./credentials"; @@ -894,9 +894,7 @@ export async function upsertFitbitMeasurements( try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); invalidateStatusInsightsForTypes( userId, keys.map((k) => k.type), diff --git a/src/lib/google-health/sync-core.ts b/src/lib/google-health/sync-core.ts index 542755bc8..4741df368 100644 --- a/src/lib/google-health/sync-core.ts +++ b/src/lib/google-health/sync-core.ts @@ -41,7 +41,7 @@ import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit import { recordSyncFailure, type FailureKind } from "@/lib/integrations/status"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; import { @@ -1587,9 +1587,7 @@ export async function upsertGoogleHealthMeasurements( try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); invalidateStatusInsightsForTypes( userId, keys.map((k) => k.type), diff --git a/src/lib/nightscout/__tests__/sync.test.ts b/src/lib/nightscout/__tests__/sync.test.ts index e43560469..18f3c8a9b 100644 --- a/src/lib/nightscout/__tests__/sync.test.ts +++ b/src/lib/nightscout/__tests__/sync.test.ts @@ -43,7 +43,12 @@ vi.mock("@/lib/integrations/status", async (importOriginal) => ({ vi.mock("@/lib/rollups/measurement-rollups", () => ({ collapseToTypeDayKeys: (rows: Array<{ type: string; measuredAt: Date }>) => rows.map((r) => ({ type: r.type, measuredAt: r.measuredAt })), - recomputeBucketsForMeasurement: recomputeMock, + recomputeBucketsForKeys: async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await recomputeMock(userId, k.type, k.measuredAt); + }, })); vi.mock("@/lib/insights/comprehensive-generate", () => ({ diff --git a/src/lib/nightscout/sync.ts b/src/lib/nightscout/sync.ts index c1c0edc92..fa3998a0b 100644 --- a/src/lib/nightscout/sync.ts +++ b/src/lib/nightscout/sync.ts @@ -34,7 +34,7 @@ import type { IntegrationClassification } from "@/lib/integrations/http-status-c import type { SyncWriteResult } from "@/lib/outcome/written-outcome"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { emitInsertedMeasurementArrivals, @@ -327,9 +327,7 @@ export async function upsertNightscoutEntries( try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); invalidateStatusInsightsForTypes( userId, keys.map((k) => k.type), diff --git a/src/lib/oura/__tests__/sync.test.ts b/src/lib/oura/__tests__/sync.test.ts index 299f6e892..1f2d22728 100644 --- a/src/lib/oura/__tests__/sync.test.ts +++ b/src/lib/oura/__tests__/sync.test.ts @@ -98,7 +98,12 @@ vi.mock("@/lib/integrations/status", async (importOriginal) => ({ vi.mock("@/lib/rollups/measurement-rollups", () => ({ collapseToTypeDayKeys: (rows: Array<{ type: string; measuredAt: Date }>) => rows.map((r) => ({ type: r.type, measuredAt: r.measuredAt })), - recomputeBucketsForMeasurement: recomputeMock, + recomputeBucketsForKeys: async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await recomputeMock(userId, k.type, k.measuredAt); + }, })); vi.mock("@/lib/insights/comprehensive-generate", () => ({ diff --git a/src/lib/oura/sync.ts b/src/lib/oura/sync.ts index d401fa2b6..0a5c9b384 100644 --- a/src/lib/oura/sync.ts +++ b/src/lib/oura/sync.ts @@ -45,7 +45,7 @@ import { } from "@/lib/integrations/status"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { emitInsertedMeasurementArrivals, @@ -805,9 +805,7 @@ export async function upsertOuraMeasurements( ); try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); invalidateStatusInsightsForTypes( userId, keys.map((k) => k.type), diff --git a/src/lib/polar/sync.ts b/src/lib/polar/sync.ts index 29147bf24..fede4d187 100644 --- a/src/lib/polar/sync.ts +++ b/src/lib/polar/sync.ts @@ -48,7 +48,7 @@ import { } from "@/lib/integrations/status"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; @@ -398,9 +398,7 @@ export async function upsertPolarMeasurements( ); try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); invalidateStatusInsightsForTypes( userId, keys.map((k) => k.type), diff --git a/src/lib/rollups/__tests__/after-measurement-mutation.test.ts b/src/lib/rollups/__tests__/after-measurement-mutation.test.ts index 681356457..cf80d3cb6 100644 --- a/src/lib/rollups/__tests__/after-measurement-mutation.test.ts +++ b/src/lib/rollups/__tests__/after-measurement-mutation.test.ts @@ -4,7 +4,7 @@ * invalidation). * * Watched red: with the recompute loop removed from the helper the - * first test fails naming the missing `recomputeBucketsForMeasurement` + * first test fails naming the missing `recomputeBucketsForKeys` * call; with the `.catch` isolation removed the failure-isolation test * throws. Both verified red against a deliberately broken helper before * the real implementation landed. @@ -27,7 +27,12 @@ vi.mock("@/lib/rollups/measurement-rollups", async (importOriginal) => { await importOriginal(); return { ...actual, - recomputeBucketsForMeasurement: mocks.recompute, + recomputeBucketsForKeys: async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await mocks.recompute(userId, k.type, k.measuredAt); + }, }; }); diff --git a/src/lib/rollups/after-measurement-mutation.ts b/src/lib/rollups/after-measurement-mutation.ts index f12c17160..aa501d1d6 100644 --- a/src/lib/rollups/after-measurement-mutation.ts +++ b/src/lib/rollups/after-measurement-mutation.ts @@ -8,7 +8,7 @@ * * 1. the `(type, day)` rollup buckets the mutation dirtied — recomputed * inline (DAY) + enqueued (WEEK/MONTH/YEAR) via - * `recomputeBucketsForMeasurement`. The recompute handles deletions + * `recomputeBucketsForKeys`. The recompute handles deletions * inherently: a day left empty drops its rollup row. * 2. the cached per-metric status assessments — re-warmed via * `invalidateStatusInsightsForTypes` (fire-and-forget). @@ -20,7 +20,7 @@ */ import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { invalidateStatusInsightsForTypes, @@ -50,9 +50,7 @@ export async function afterMeasurementMutation( // 500-row batch fires at most one DAY recompute per touched bucket. const keys = collapseToTypeDayKeys(rows); try { - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); } catch (err) { console.warn(`[${label}] rollup recompute failed`, err); } diff --git a/src/lib/whoop/__tests__/sync.test.ts b/src/lib/whoop/__tests__/sync.test.ts index 82e4b9e84..969bc0ee3 100644 --- a/src/lib/whoop/__tests__/sync.test.ts +++ b/src/lib/whoop/__tests__/sync.test.ts @@ -90,8 +90,13 @@ vi.mock("@/lib/integrations/status", () => ({ vi.mock("@/lib/rollups/measurement-rollups", () => ({ collapseToTypeDayKeys: (rows: Array<{ type: string; measuredAt: Date }>) => rows.map((r) => ({ type: r.type, measuredAt: r.measuredAt })), - recomputeBucketsForMeasurement: (...a: unknown[]) => - recomputeBucketsMock(...a), + recomputeBucketsForKeys: async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) + await recomputeBucketsMock(userId, k.type, k.measuredAt); + }, })); vi.mock("@/lib/insights/comprehensive-generate", () => ({ diff --git a/src/lib/whoop/sync-core.ts b/src/lib/whoop/sync-core.ts index bb6da4b42..4ba22baeb 100644 --- a/src/lib/whoop/sync-core.ts +++ b/src/lib/whoop/sync-core.ts @@ -14,7 +14,7 @@ * `(userId, type, source = WHOOP, externalId)` so a re-post (WHOOP * re-scores recovery/sleep after the fact) overwrites in place rather than * minting a duplicate. After the upserts the rollup tier is re-folded - * (`recomputeBucketsForMeasurement`) and the status-insight caches are + * (`recomputeBucketsForKeys`) and the status-insight caches are * invalidated, identical to the Withings tail. * * The incremental window starts from `lastSyncedAt - overlap`. WHOOP re-scores @@ -37,7 +37,7 @@ import { } from "@/lib/integrations/status"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; import { @@ -503,9 +503,7 @@ export async function upsertWhoopMeasurements( ); try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); } catch (err) { getEvent()?.addWarning( `whoop: rollup recompute failed for ${userId}: ${err}`, diff --git a/src/lib/withings/__tests__/sync-activity.test.ts b/src/lib/withings/__tests__/sync-activity.test.ts index c9dff410a..7155da2f8 100644 --- a/src/lib/withings/__tests__/sync-activity.test.ts +++ b/src/lib/withings/__tests__/sync-activity.test.ts @@ -69,9 +69,19 @@ vi.mock("@/lib/rollups/measurement-rollups", async () => { const actual = await vi.importActual< typeof import("@/lib/rollups/measurement-rollups") >("@/lib/rollups/measurement-rollups"); + const perKey = vi.fn().mockResolvedValue(undefined); return { ...actual, - recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined), + recomputeBucketsForMeasurement: perKey, + // The keyed refold, observed as the per-key recomputes it stands for. + recomputeBucketsForKeys: vi.fn( + async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await perKey(userId, k.type, k.measuredAt); + }, + ), }; }); diff --git a/src/lib/withings/__tests__/sync-ecg.test.ts b/src/lib/withings/__tests__/sync-ecg.test.ts index dc8cae0b7..916677346 100644 --- a/src/lib/withings/__tests__/sync-ecg.test.ts +++ b/src/lib/withings/__tests__/sync-ecg.test.ts @@ -68,9 +68,19 @@ vi.mock("@/lib/rollups/measurement-rollups", async () => { const actual = await vi.importActual< typeof import("@/lib/rollups/measurement-rollups") >("@/lib/rollups/measurement-rollups"); + const perKey = vi.fn().mockResolvedValue(undefined); return { ...actual, - recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined), + recomputeBucketsForMeasurement: perKey, + // The keyed refold, observed as the per-key recomputes it stands for. + recomputeBucketsForKeys: vi.fn( + async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await perKey(userId, k.type, k.measuredAt); + }, + ), }; }); diff --git a/src/lib/withings/__tests__/sync-sleep.test.ts b/src/lib/withings/__tests__/sync-sleep.test.ts index d5bb12404..ea945c843 100644 --- a/src/lib/withings/__tests__/sync-sleep.test.ts +++ b/src/lib/withings/__tests__/sync-sleep.test.ts @@ -67,9 +67,19 @@ vi.mock("@/lib/rollups/measurement-rollups", async () => { const actual = await vi.importActual< typeof import("@/lib/rollups/measurement-rollups") >("@/lib/rollups/measurement-rollups"); + const perKey = vi.fn().mockResolvedValue(undefined); return { ...actual, - recomputeBucketsForMeasurement: vi.fn().mockResolvedValue(undefined), + recomputeBucketsForMeasurement: perKey, + // The keyed refold, observed as the per-key recomputes it stands for. + recomputeBucketsForKeys: vi.fn( + async ( + userId: string, + keys: Array<{ type: string; measuredAt: Date }>, + ) => { + for (const k of keys) await perKey(userId, k.type, k.measuredAt); + }, + ), }; }); diff --git a/src/lib/withings/sync-activity.ts b/src/lib/withings/sync-activity.ts index 4bcdb7d50..1a075fec6 100644 --- a/src/lib/withings/sync-activity.ts +++ b/src/lib/withings/sync-activity.ts @@ -45,7 +45,7 @@ import { getUnitForType } from "@/lib/validations/measurement"; import { dropImplausibleMeasurements } from "@/lib/measurements/plausibility-gate"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; @@ -540,9 +540,7 @@ export async function syncUserActivity( // catch-up costs at most ~N (type, day) recomputes. try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); // v1.8.0 — drop the per-metric assessment caches the synced types // dirty (steps feed the general overview). Fire-and-forget. diff --git a/src/lib/withings/sync-ecg.ts b/src/lib/withings/sync-ecg.ts index b4fe85fe7..865532dc5 100644 --- a/src/lib/withings/sync-ecg.ts +++ b/src/lib/withings/sync-ecg.ts @@ -56,7 +56,7 @@ import { safeFetch } from "@/lib/safe-fetch"; import { persistEcgRecording } from "@/lib/ecg/persist-recording"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; @@ -519,9 +519,7 @@ export async function syncUserEcg( // then drop the per-metric assessment caches the new events dirty. try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); invalidateStatusInsightsForTypes( userId, keys.map((k) => k.type), diff --git a/src/lib/withings/sync-sleep.ts b/src/lib/withings/sync-sleep.ts index 0611127ec..41dd7f5b1 100644 --- a/src/lib/withings/sync-sleep.ts +++ b/src/lib/withings/sync-sleep.ts @@ -45,7 +45,7 @@ import { safeFetch } from "@/lib/safe-fetch"; import { getUnitForType } from "@/lib/validations/measurement"; import { collapseToTypeDayKeys, - recomputeBucketsForMeasurement, + recomputeBucketsForKeys, } from "@/lib/rollups/measurement-rollups"; import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate"; import { maybeEnqueueMorningRefresh } from "@/lib/daily/morning-refresh-trigger"; @@ -628,9 +628,7 @@ export async function syncUserSleep( // ~10 stage rows per night land in one DAY recompute. try { const keys = collapseToTypeDayKeys(touched); - for (const k of keys) { - await recomputeBucketsForMeasurement(userId, k.type, k.measuredAt); - } + await recomputeBucketsForKeys(userId, keys); // v1.8.0 — drop the per-metric assessment caches the synced types // dirty (sleep + resting-HR feed the pulse / general cards).