perf: collapse the medication compliance hot path and the dashboard fan-out

The medication list fired one compliance request per card every 30
seconds, and each request scanned the medication's full intake history
and expanded the recurrence calendar five to six times. The endpoint now
mints bands once per request, serves the sub-windows from that single
ledger, bounds the event read to a year of selected columns and sits
behind a write-invalidated 15-minute server cache; the cards poll at
five minutes. The dashboard drops two redundant checklist queries by
reading counts from the snapshot, the BMI chart derives from the weight
series instead of refetching it, the detail page no longer serialises
behind the auth probe and seeds its query from the list row. Expensive
analytics reads gain a shared per-user rate limit, and the service
worker stops serving cross-version shells, enables navigation preload
and trims caches in one pass.
This commit is contained in:
Marc-André Bombeck committed 2026-06-10 12:55:56 +02:00
1 parent 41ba4c6c17
commit 298e4c34b4
24 files changed
+1483 -319

No files matched your search

+26
View File
@@ -50,6 +50,18 @@ async function primeAllCaches(): Promise<void> {
c: 2,
}));
// v1.15.20 — per-medication compliance payload cache.
await cached(
caches.medicationCompliance,
`${USER_A}|med-1|compliance`,
async () => ({ mc: 1 }),
);
await cached(
caches.medicationCompliance,
`${USER_B}|med-1|compliance`,
async () => ({ mc: 2 }),
);
await cached(caches.achievements, USER_A, async () => ({ ach: 1 }));
await cached(caches.achievements, USER_B, async () => ({ ach: 2 }));
@@ -149,6 +161,20 @@ describe("invalidateUserMedications", () => {
expect(caches.medications.get(USER_B)).not.toBeNull();
expect(caches.medicationsIntake.get(`${USER_B}|compliance|30`)).not.toBeNull();
});
it("evicts the per-medication compliance payload for the target user only", async () => {
await primeAllCaches();
invalidateUserMedications(USER_A);
// v1.15.20 — the compliance route caches 15 min; an intake write must
// flush the `${userId}|` prefix so the next card read is fresh.
expect(
caches.medicationCompliance.get(`${USER_A}|med-1|compliance`),
).toBeNull();
expect(
caches.medicationCompliance.get(`${USER_B}|med-1|compliance`),
).not.toBeNull();
});
});
describe("invalidateUserDashboardWidgets", () => {
+4
View File
@@ -105,6 +105,10 @@ export function invalidateUserMood(userId: string): void {
export function invalidateUserMedications(userId: string): void {
caches.medications.deleteByPrefix(userId);
caches.medicationsIntake.deleteByPrefix(`${userId}|`);
// v1.15.20 — the per-medication compliance payload is cached 15 min;
// every intake / medication / schedule write must flush it so the card
// rates and the heatmap reflect the action on the next read.
caches.medicationCompliance.deleteByPrefix(`${userId}|`);
caches.achievements.deleteByPrefix(userId);
// v1.4.36 W1 — medication writes change the MEDICATION_COMPLIANCE
// target rollup (compliance7 / compliance30 / consistency strip).
+16
View File
@@ -393,6 +393,22 @@ export const caches = {
maxEntries: 1000,
ttlMs: 900_000,
}),
/**
* v1.15.20 — per-medication compliance payload
* (`GET /api/medications/[id]/compliance`). The medications list fans
* the endpoint out once per card, and each cold build runs the full
* band-expansion pass — so the warm path has to live server-side.
* Keyed `${userId}|${medicationId}|compliance`; every intake /
* medication write flushes the `${userId}|` prefix via
* `invalidateUserMedications`, so the 15-minute TTL only bounds
* wall-clock drift (a dose flipping overdue), never user-action
* staleness. Sized for a few hundred users × a handful of meds.
*/
medicationCompliance: new ServerCache<unknown>({
name: "medicationCompliance",
maxEntries: 2000,
ttlMs: 900_000,
}),
moodAnalytics: new ServerCache<unknown>({
name: "moodAnalytics",
maxEntries: 1000,