From 79c86a0652faef4bf32172c1a6b90dc76b628387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 12:10:42 +0200 Subject: [PATCH] refactor(tz): consolidate startOfUtcDay into src/lib/tz/start-of-utc-day.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mood-rollup and measurement-rollup writers carried byte-identical copies of the helper that could drift independently. Promote one canonical export under src/lib/tz/ so both writers — and any future rollup table — import the same UTC-anchored truncation. --- src/lib/measurements/rollups.ts | 12 +----------- src/lib/mood/rollups.ts | 13 +------------ src/lib/tz/start-of-utc-day.ts | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 23 deletions(-) create mode 100644 src/lib/tz/start-of-utc-day.ts diff --git a/src/lib/measurements/rollups.ts b/src/lib/measurements/rollups.ts index 41c23336e..897789a30 100644 --- a/src/lib/measurements/rollups.ts +++ b/src/lib/measurements/rollups.ts @@ -31,6 +31,7 @@ import { prisma } from "@/lib/db"; import { getGlobalBoss } from "@/lib/jobs/boss-instance"; import { annotate } from "@/lib/logging/context"; +import { startOfUtcDay } from "@/lib/tz/start-of-utc-day"; import type { MeasurementType, RollupGranularity, @@ -518,17 +519,6 @@ async function persistRollupRows( return touched; } -/** - * Truncate a `Date` to the start of its UTC day. The rollup table - * stores TIMESTAMPTZ values so we anchor on UTC to keep the bucket - * key deterministic across timezone-shifting consumers. - */ -function startOfUtcDay(d: Date): Date { - return new Date( - Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0), - ); -} - /** * Span of the bucket containing `measuredAt` at the given granularity. * Postgres `date_trunc` is the canonical truncator; we mirror its diff --git a/src/lib/mood/rollups.ts b/src/lib/mood/rollups.ts index 4601db8e8..bb7df0069 100644 --- a/src/lib/mood/rollups.ts +++ b/src/lib/mood/rollups.ts @@ -40,6 +40,7 @@ import { prisma } from "@/lib/db"; import { getGlobalBoss } from "@/lib/jobs/boss-instance"; import { annotate } from "@/lib/logging/context"; +import { startOfUtcDay } from "@/lib/tz/start-of-utc-day"; import type { Prisma, PrismaClient, @@ -611,18 +612,6 @@ async function persistMoodRollupRows( return touched; } -/** - * Truncate a `Date` to the start of its UTC day. The rollup table - * stores TIMESTAMPTZ values so we anchor on UTC to keep the bucket - * key deterministic across timezone-shifting consumers — same - * convention as `measurement-rollups`. - */ -function startOfUtcDay(d: Date): Date { - return new Date( - Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0), - ); -} - /** * Span of the bucket containing `moodLoggedAt` at the given * granularity. Mirrors `measurement-rollups` so the WEEK / MONTH / diff --git a/src/lib/tz/start-of-utc-day.ts b/src/lib/tz/start-of-utc-day.ts new file mode 100644 index 000000000..eb9cc53be --- /dev/null +++ b/src/lib/tz/start-of-utc-day.ts @@ -0,0 +1,20 @@ +/** + * UTC-anchored start-of-day truncation shared by the measurement + + * mood rollup writers. + * + * Both rollup tables store TIMESTAMPTZ bucket starts. Anchoring on the + * UTC calendar day (rather than the user's local day) keeps the bucket + * key deterministic across timezone-shifting consumers — the same + * convention `date_trunc('day', measured_at AT TIME ZONE 'UTC')` would + * produce on the read side. The display surfaces re-bucket into the + * user's timezone via `userDayKey()` from `./format`. + * + * v1.4.40 W-GHOSTS consolidated two byte-identical local copies + * (`src/lib/measurements/rollups.ts` + `src/lib/mood/rollups.ts`) into + * this module so future writers cannot drift independently. + */ +export function startOfUtcDay(d: Date): Date { + return new Date( + Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 0, 0, 0, 0), + ); +}