mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
perf(medications): one compliance round trip and a memoised formatter hot path
Intl.DateTimeFormat instances are memoised per locale, timezone and options signature — construction dominated the 365-day ledger build and the hot path drops about fivefold. A batched GET /api/medications/compliance serves every card from one request through the same per-medication cache cells the detail page reads, replacing the per-card fan-out; the list response stays untouched so the due line never waits on a ledger build. The medications and compliance buckets serve stale-while-revalidate, interactive writes evict while background bulk sync marks stale, the per-id route defers ownership and schedule reads until a cache miss, and the card body reserves constant slots for the status and cycle lines so late data stops shifting the layout.
This commit is contained in:
1 parent
23f7c74c04
commit
b346e78cc7
36 files changed
+1453
-500
No files matched your search
+51
@@ -213,6 +213,57 @@ describe("invalidateUserMedications", () => {
|
||||
caches.medicationCompliance.get(`${USER_B}|med-1|compliance`),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("marks the SWR buckets stale by default — background sync posture", async () => {
|
||||
await primeAllCaches();
|
||||
invalidateUserMedications(USER_A);
|
||||
|
||||
// v1.16.8 — the default (bulk sync / cron) path marks stale: the
|
||||
// entries are gone for plain `get()` (asserted above) but stay
|
||||
// serveable for the `cachedSwr` readers (medications list, both
|
||||
// compliance routes, the dashboard snapshot), so a high-frequency
|
||||
// iOS sync never busts every card into an inline cold rebuild.
|
||||
expect(caches.medications.getAllowStale(USER_A)).toEqual({
|
||||
value: { m: 1 },
|
||||
stale: true,
|
||||
});
|
||||
expect(
|
||||
caches.medicationCompliance.getAllowStale(`${USER_A}|med-1|compliance`),
|
||||
).toEqual({ value: { mc: 1 }, stale: true });
|
||||
expect(
|
||||
caches.analytics.getAllowStale(dashboardSnapshotCacheKey(USER_A)),
|
||||
).toEqual({ value: { snap: 1 }, stale: true });
|
||||
|
||||
// User B stays fresh.
|
||||
expect(caches.medications.getAllowStale(USER_B)).toEqual({
|
||||
value: { m: 2 },
|
||||
stale: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("hard-evicts the SWR buckets with { evict: true } — interactive write posture", async () => {
|
||||
await primeAllCaches();
|
||||
invalidateUserMedications(USER_A, { evict: true });
|
||||
|
||||
// v1.16.8 — an interactive take / skip / CRUD write must NOT leave a
|
||||
// stale-serveable body behind: the SWR readers would hand the user
|
||||
// back the pre-write card rates. The evict drops the entries.
|
||||
expect(caches.medications.getAllowStale(USER_A)).toBeNull();
|
||||
expect(
|
||||
caches.medicationCompliance.getAllowStale(`${USER_A}|med-1|compliance`),
|
||||
).toBeNull();
|
||||
expect(
|
||||
caches.analytics.getAllowStale(dashboardSnapshotCacheKey(USER_A)),
|
||||
).toBeNull();
|
||||
|
||||
// User B stays warm; the non-SWR buckets evict as before.
|
||||
expect(caches.medications.getAllowStale(USER_B)).not.toBeNull();
|
||||
expect(
|
||||
caches.medicationCompliance.getAllowStale(`${USER_B}|med-1|compliance`),
|
||||
).not.toBeNull();
|
||||
expect(caches.medicationsIntake.get(`${USER_A}|compliance|30`)).toBeNull();
|
||||
expect(caches.achievements.get(USER_A)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalidateUserDashboardWidgets", () => {
|
||||
|
||||
Vendored
+39
-12
@@ -117,24 +117,51 @@ export function invalidateUserMood(userId: string): void {
|
||||
* Invalidate every cache that may reflect a user's medication state.
|
||||
*
|
||||
* Covers the medications cache, the medication-intake compliance cache
|
||||
* (cached daily), and achievement progress.
|
||||
* (cached daily), the per-medication compliance payload, and achievement
|
||||
* progress.
|
||||
*
|
||||
* v1.16.8 — `evict` splits the SWR buckets (medications, the
|
||||
* per-medication compliance payload, analytics) by write origin, the
|
||||
* `invalidateUserMeasurements` pattern. Interactive writes (a card take /
|
||||
* skip, a CRUD edit, an intake edit / delete / purge / import) pass
|
||||
* `{ evict: true }`: the user must see their own action on the very next
|
||||
* read, and a stale-serveable entry would hand back the pre-write body.
|
||||
* Background paths (the iOS bulk-intake sync, the auto-miss cron, slot
|
||||
* dedup) keep the default mark-stale so a high-frequency sync never busts
|
||||
* the buckets into inline cold-rebuild storms — the `cachedSwr` readers
|
||||
* serve the prior payload while one coalesced recompute warms each cell.
|
||||
*/
|
||||
export function invalidateUserMedications(userId: string): void {
|
||||
caches.medications.deleteByPrefix(userId);
|
||||
export function invalidateUserMedications(
|
||||
userId: string,
|
||||
opts?: { evict?: boolean },
|
||||
): void {
|
||||
if (opts?.evict) {
|
||||
caches.medications.deleteByPrefix(userId);
|
||||
// v1.15.20 — the per-medication compliance payload is cached 15 min;
|
||||
// an interactive 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}|`);
|
||||
// v1.4.38 W-F — `/api/dashboard/summary` lives under the analytics
|
||||
// cache and reads `medicationIntakeEvent` for both today's compliance
|
||||
// tally and the 365-day streak feed. A taken / skipped event must
|
||||
// evict the user-bucket so the next iOS poll reflects the change.
|
||||
caches.analytics.deleteByPrefix(`${userId}|`);
|
||||
} else {
|
||||
// Mark-stale collapses the fresh TTL to "now": the SWR readers (the
|
||||
// medications list, both compliance routes, the dashboard snapshot)
|
||||
// serve the prior value while a single background recompute warms a
|
||||
// fresh one; the plain `cached` readers in the analytics bucket see
|
||||
// a clean miss — identical to the old evict for those keys.
|
||||
caches.medications.markStaleByPrefix(userId);
|
||||
caches.medicationCompliance.markStaleByPrefix(`${userId}|`);
|
||||
caches.analytics.markStaleByPrefix(`${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).
|
||||
caches.insightsTargets.deleteByPrefix(userId);
|
||||
// v1.4.38 W-F — `/api/dashboard/summary` lives under the analytics
|
||||
// cache and reads `medicationIntakeEvent` for both today's compliance
|
||||
// tally and the 365-day streak feed. A taken / skipped event must
|
||||
// evict the user-bucket so the next iOS poll reflects the change.
|
||||
caches.analytics.deleteByPrefix(`${userId}|`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+22
@@ -363,10 +363,22 @@ export const caches = {
|
||||
ttlMs: 60_000,
|
||||
staleTtlMs: 600_000,
|
||||
}),
|
||||
/**
|
||||
* v1.16.8 — stale-while-revalidate on the list bucket. The 60 s fresh
|
||||
* TTL meant every visit minutes apart paid the full cold list build
|
||||
* (five parallel reads + the next-due engine per medication). The list
|
||||
* GET reads via `cachedSwr` now: inside the 10-minute stale window the
|
||||
* prior list serves immediately while one background recompute warms a
|
||||
* fresh one. Interactive writes hard-evict the bucket through
|
||||
* `invalidateUserMedications({ evict: true })`, so the window only
|
||||
* bounds wall-clock drift (`todayEventCount`, `nextDueAt`), never
|
||||
* user-action staleness.
|
||||
*/
|
||||
medications: new ServerCache<unknown>({
|
||||
name: "medications",
|
||||
maxEntries: 1000,
|
||||
ttlMs: 60_000,
|
||||
staleTtlMs: 600_000,
|
||||
}),
|
||||
achievements: new ServerCache<unknown>({
|
||||
name: "achievements",
|
||||
@@ -405,11 +417,21 @@ export const caches = {
|
||||
* `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.
|
||||
*
|
||||
* v1.16.8 — stale-while-revalidate. Both compliance routes (the per-id
|
||||
* detail read and the batched card read) consume via `cachedSwr`:
|
||||
* inside the 10-minute stale window an expired cell serves the prior
|
||||
* payload immediately while one coalesced rebuild warms it. Interactive
|
||||
* intake / medication writes hard-evict (the user must see their own
|
||||
* dose on the next read); background sync paths (iOS bulk intake, the
|
||||
* auto-miss cron, slot dedup) mark stale so a high-frequency sync never
|
||||
* busts every card into an inline cold rebuild.
|
||||
*/
|
||||
medicationCompliance: new ServerCache<unknown>({
|
||||
name: "medicationCompliance",
|
||||
maxEntries: 2000,
|
||||
ttlMs: 900_000,
|
||||
staleTtlMs: 600_000,
|
||||
}),
|
||||
moodAnalytics: new ServerCache<unknown>({
|
||||
name: "moodAnalytics",
|
||||
|
||||
Reference in new issue
Block a user