From e4d46de22d1bb0f89005af68d56624fe3c748f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 21 May 2026 18:16:27 +0200 Subject: [PATCH] refactor(insights): extract shared timeout-stub persist helper Pull the 40-line auditLog.create block out of bmi-status into a dedicated `persist-timeout-stub.ts` helper so every InsightStatus route can share one persist path instead of copy-pasting the sentinel-row shape. The bmi route now calls the helper in a 6-line return; behaviour and persisted body are byte-equivalent to the inline form. Sets up the next commit, which wires the six remaining sibling routes (blood-pressure, weight, general, pulse, mood, medication-compliance) to the same helper and closes the last bare-fallback shape on the dashboard hot path. --- src/lib/insights/bmi-status.ts | 53 ++++------------- src/lib/insights/persist-timeout-stub.ts | 73 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 43 deletions(-) create mode 100644 src/lib/insights/persist-timeout-stub.ts diff --git a/src/lib/insights/bmi-status.ts b/src/lib/insights/bmi-status.ts index 0acebf85d..697e91798 100644 --- a/src/lib/insights/bmi-status.ts +++ b/src/lib/insights/bmi-status.ts @@ -13,6 +13,7 @@ import { withTimeout, STATUS_PROVIDER_TIMEOUT_MS, } from "@/lib/insights/with-timeout"; +import { persistTimeoutStubAndReturn } from "@/lib/insights/persist-timeout-stub"; import { annotate } from "@/lib/logging/context"; const BERLIN_DAY_FORMATTER = new Intl.DateTimeFormat("en-US", { @@ -294,49 +295,15 @@ export async function generateBmiStatusForUser( // v1.4.37 — persist a sentinel row keyed to today so the next // mount short-circuits at the cache lookup above instead of // re-racing the same 20 s provider call on every cold visit. - // Without this, a single stall (provider hiccup, network blip, - // model warm-up) leaves the user staring at the loading state - // on every reload until the daily 02:20 pre-warm job runs. - // The body is the same no-key fallback the caller returned - // before, so the user-facing UI is identical; only the - // re-fire frequency drops to zero for the rest of the day. - // `meta.timeout` is set so the daily pg-boss pre-warm worker - // (the same job that originally seeded the cache) can - // recognise and overwrite the stub rather than respect it as - // a real assessment. - const stubText = getNoKeyBmiStatusText(locale); - let stubUpdatedAt: string | null = null; - try { - const stub = await prisma.auditLog.create({ - data: { - userId, - action: cacheAction, - details: JSON.stringify({ - dateKey: todayKey, - locale, - text: stubText, - providerType: provider.type, - model: "timeout-stub", - tokensUsed: null, - timeout: true, - }), - }, - select: { createdAt: true }, - }); - stubUpdatedAt = stub.createdAt.toISOString(); - } catch { - // The persist is best-effort — if the row write fails the - // caller still sees the deterministic fallback text and the - // next mount falls back to the (still expensive) race. Do - // not surface the error: the user does not care that the - // cache write missed, only that the page rendered. - } - return { - hasProvider: true, - text: stubText, - cached: true, - updatedAt: stubUpdatedAt, - }; + // See `persistTimeoutStubAndReturn` for the full rationale. + return persistTimeoutStubAndReturn({ + userId, + cacheAction, + todayKey, + locale, + providerType: provider.type, + stubText: getNoKeyBmiStatusText(locale), + }); } const result = raced.value; diff --git a/src/lib/insights/persist-timeout-stub.ts b/src/lib/insights/persist-timeout-stub.ts new file mode 100644 index 000000000..cfbb4a3e9 --- /dev/null +++ b/src/lib/insights/persist-timeout-stub.ts @@ -0,0 +1,73 @@ +import { prisma } from "@/lib/db"; + +/** + * v1.4.37 / v1.4.41 — shared timeout-stub persist for InsightStatus routes. + * + * When the upstream provider call exceeds `STATUS_PROVIDER_TIMEOUT_MS` + * the caller persists a sentinel `auditLog` row keyed to today's Berlin + * day so the next mount short-circuits at the cache lookup instead of + * re-racing the same 20 s provider call on every cold visit. Without + * this, a single stall (provider hiccup, network blip, model warm-up) + * leaves the user staring at the loading state on every reload until + * the daily 02:20 pre-warm job runs. + * + * The persisted body is the deterministic no-key fallback the caller + * would have returned in the bare-fallback shape, so the user-facing + * UI is identical; only the re-fire frequency drops to zero for the + * rest of the day. + * + * `meta.timeout` + `meta.model = "timeout-stub"` are set so the daily + * pg-boss pre-warm worker (the same job that originally seeded the + * cache) can recognise and overwrite the stub rather than respect it + * as a real assessment. + * + * The write is best-effort — if it fails the caller still sees the + * deterministic fallback text and the next mount falls back to the + * (still expensive) race. We do not surface the error: the user does + * not care that the cache write missed, only that the page rendered. + */ +export async function persistTimeoutStubAndReturn(input: { + userId: string; + cacheAction: string; + todayKey: string; + locale: string; + providerType: string; + stubText: string; +}): Promise<{ + hasProvider: true; + text: string; + cached: true; + updatedAt: string | null; +}> { + const { userId, cacheAction, todayKey, locale, providerType, stubText } = + input; + let stubUpdatedAt: string | null = null; + try { + const stub = await prisma.auditLog.create({ + data: { + userId, + action: cacheAction, + details: JSON.stringify({ + dateKey: todayKey, + locale, + text: stubText, + providerType, + model: "timeout-stub", + tokensUsed: null, + timeout: true, + }), + }, + select: { createdAt: true }, + }); + stubUpdatedAt = stub.createdAt.toISOString(); + } catch { + // The persist is best-effort — silently swallow so the user-facing + // payload still renders even when the row write missed. + } + return { + hasProvider: true, + text: stubText, + cached: true, + updatedAt: stubUpdatedAt, + }; +}