mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(insights): regenerate only the user's locale on a measurement invalidation
The write-path regenerate-on-invalidate enqueued a fresh assessment for both de and en on every dirtying measurement, while the read-path only ever serves the user's resolved locale. A frequent Apple-Health sync thus fanned out two provider calls per scope, half for a language the user never opens. Resolve the user's locale the way the read-path does (normalizeLocale of user.locale) and enqueue that single locale. The nightly warm pass still covers both locales for the rare locale switch.
This commit is contained in:
1 parent
ae82b3e484
commit
aeb72d1a08
2 files changed
+33
-8
No files matched your search
@@ -12,10 +12,12 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
const deleteMany = vi.fn();
|
||||
const enqueueStatusGeneration = vi.fn();
|
||||
const userFindUnique = vi.fn();
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
auditLog: { deleteMany: (...a: unknown[]) => deleteMany(...a) },
|
||||
user: { findUnique: (...a: unknown[]) => userFindUnique(...a) },
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -29,6 +31,7 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
deleteMany.mockResolvedValue({ count: 0 });
|
||||
enqueueStatusGeneration.mockResolvedValue(undefined);
|
||||
userFindUnique.mockResolvedValue({ locale: "de" });
|
||||
});
|
||||
|
||||
/** Distinct scopes the invalidator enqueued a regenerate for. */
|
||||
@@ -100,17 +103,29 @@ describe("invalidateStatusInsightsForTypes", () => {
|
||||
expect(enqueueStatusGeneration).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("enqueues a debounced regenerate for every dirtied scope, both locales", async () => {
|
||||
it("enqueues a debounced regenerate for every dirtied scope, the user's locale only", async () => {
|
||||
userFindUnique.mockResolvedValue({ locale: "de" });
|
||||
await invalidateStatusInsightsForTypes("u1", ["WEIGHT"]);
|
||||
// weight + bmi + general, each warmed for de + en.
|
||||
// weight + bmi + general, each warmed once for the user's resolved locale.
|
||||
expect(enqueuedScopes()).toEqual(["bmi", "general", "weight"]);
|
||||
expect(enqueueStatusGeneration).toHaveBeenCalledTimes(6);
|
||||
expect(enqueueStatusGeneration).toHaveBeenCalledTimes(3);
|
||||
const locales = new Set(
|
||||
enqueueStatusGeneration.mock.calls.map(
|
||||
(c) => (c[0] as { locale: string }).locale,
|
||||
),
|
||||
);
|
||||
expect([...locales].sort()).toEqual(["de", "en"]);
|
||||
expect([...locales]).toEqual(["de"]);
|
||||
});
|
||||
|
||||
it("resolves the user's locale (en) for the regenerate, never the unused one", async () => {
|
||||
userFindUnique.mockResolvedValue({ locale: "en" });
|
||||
await invalidateStatusInsightsForTypes("u1", ["WEIGHT"]);
|
||||
const locales = new Set(
|
||||
enqueueStatusGeneration.mock.calls.map(
|
||||
(c) => (c[0] as { locale: string }).locale,
|
||||
),
|
||||
);
|
||||
expect([...locales]).toEqual(["en"]);
|
||||
});
|
||||
|
||||
it("does not blanket-evict general for an unrelated synced type", async () => {
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "@/lib/ai/provider-runner";
|
||||
import { invalidateUserInsights } from "@/lib/cache/invalidate";
|
||||
import { enqueueStatusGeneration } from "@/lib/jobs/insight-status-generate-shared";
|
||||
import { normalizeLocale } from "@/lib/insights/status-shared";
|
||||
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
@@ -240,13 +241,22 @@ export async function invalidateStatusInsightsForTypes(
|
||||
// the cache is re-warmed in the background. The enqueue is coalesced per
|
||||
// `(user, metric, locale)` via the queue's `singletonKey` (120 s window),
|
||||
// so the constant Apple-Health sync cannot fan out a storm of jobs — a
|
||||
// burst of samples collapses into one regenerate per scope per locale.
|
||||
// burst of samples collapses into one regenerate per scope.
|
||||
// The stale-while-revalidate read keeps the previous assessment visible
|
||||
// until the fresh one lands, so the user never waits on a card.
|
||||
//
|
||||
// v1.8.7 — regenerate only the user's resolved locale, matching the
|
||||
// read-path (every `*-status` GET serves `normalizeLocale(user.locale)`).
|
||||
// Warming both locales doubled provider spend on every sync, half of it
|
||||
// for a language the user never opens. The nightly warm pass still covers
|
||||
// both locales for the rare locale switch.
|
||||
const localeRow = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { locale: true },
|
||||
});
|
||||
const locale = normalizeLocale(localeRow?.locale);
|
||||
for (const scope of scopes) {
|
||||
for (const locale of ["de", "en"] as const) {
|
||||
void enqueueStatusGeneration({ userId, metric: scope, locale });
|
||||
}
|
||||
void enqueueStatusGeneration({ userId, metric: scope, locale });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user