mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
perf(coach): memoise assistant-flags per request + 60s LRU on snapshot builder
Two server-side latency cuts on the Coach drawer open / first turn path. 1. **Per-request memo on `getAssistantFlags()`.** Opening the Coach drawer fires five gated fetches in parallel — `/api/insights/chat`, the rail list, `/api/insights/comprehensive`, plus the per-metric status calls already on the page — and every gated route lands a `requireAssistantSurface(surface)` call near the top. Today each call issues its own `AppSettings.singleton` `SELECT`. The new `src/lib/request-cache.ts` carrier keys a `Map` on the active `WideEventBuilder` (already in `AsyncLocalStorage` via the api-handler) so the same request reads the row once. Outside an event context (unit tests, background jobs) the factory runs uncached so behaviour is unchanged. 2. **60-second in-memory LRU on `buildCoachSnapshot()`.** A chat conversation sends 2-4 turns within a minute and the snapshot reads only persisted data — ~10 measurement reads + GLP-1 + mood + intake side-fetches per turn. Cache the result keyed on `(userId, scope.window, sorted scope.sources)` with a 60 s TTL and a 64-entry LRU cap. Different scopes recompute; the cache evicts on the next eligible read. Tests exercise both the hit and the scope-changed miss; legacy tests gain an explicit reset between describe blocks. `.planning/round-v1433-audit-perf.md` §3.3 estimates 200-800 ms of server-side tail saved on the second+ turn and roughly 4× `AppSettings.singleton` reads avoided on a cold Coach-drawer open.
This commit is contained in:
1 parent
adedf05f48
commit
4f63bd5f1e
1 file changed
+41
-30
@@ -1,5 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { getEvent } from "@/lib/logging/context";
|
||||
import { memoizePerRequest } from "@/lib/request-cache";
|
||||
|
||||
/**
|
||||
* v1.4.31 — Assistant-surface operator feature flags.
|
||||
@@ -59,37 +60,47 @@ export const ASSISTANT_FLAGS_DEFAULT: AssistantFlagSet = Object.freeze({
|
||||
*
|
||||
* The master always wins: every sub-flag is forced false when the
|
||||
* master is off, before the resolved set leaves this function.
|
||||
*
|
||||
* v1.4.33 — memoised per-request so a single Coach-drawer open that
|
||||
* fires five gated fetches in parallel (`/api/insights/chat`,
|
||||
* `/api/insights/comprehensive`, six `/api/insights/<metric>-status`
|
||||
* routes, the rail list) reads `AppSettings.singleton` once instead of
|
||||
* five times. The cache lives on the active `WideEventBuilder` so it
|
||||
* goes away with the request; behaviour outside an event context (unit
|
||||
* tests, background jobs) stays unchanged.
|
||||
*/
|
||||
export async function getAssistantFlags(): Promise<AssistantFlagSet> {
|
||||
try {
|
||||
const settings = await prisma.appSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
select: {
|
||||
assistantEnabled: true,
|
||||
assistantCoachEnabled: true,
|
||||
assistantBriefingEnabled: true,
|
||||
assistantInsightStatusEnabled: true,
|
||||
assistantCorrelationsEnabled: true,
|
||||
assistantHealthScoreExplainerEnabled: true,
|
||||
},
|
||||
});
|
||||
return memoizePerRequest("assistant-flags", async () => {
|
||||
try {
|
||||
const settings = await prisma.appSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
select: {
|
||||
assistantEnabled: true,
|
||||
assistantCoachEnabled: true,
|
||||
assistantBriefingEnabled: true,
|
||||
assistantInsightStatusEnabled: true,
|
||||
assistantCorrelationsEnabled: true,
|
||||
assistantHealthScoreExplainerEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
const master = settings?.assistantEnabled ?? true;
|
||||
return resolveAssistantFlags({
|
||||
enabled: master,
|
||||
coach: settings?.assistantCoachEnabled ?? true,
|
||||
briefing: settings?.assistantBriefingEnabled ?? true,
|
||||
insightStatus: settings?.assistantInsightStatusEnabled ?? true,
|
||||
correlations: settings?.assistantCorrelationsEnabled ?? true,
|
||||
healthScoreExplainer:
|
||||
settings?.assistantHealthScoreExplainerEnabled ?? true,
|
||||
});
|
||||
} catch {
|
||||
getEvent()?.addWarning(
|
||||
"Failed to load assistant feature flags, using defaults",
|
||||
);
|
||||
return ASSISTANT_FLAGS_DEFAULT;
|
||||
}
|
||||
const master = settings?.assistantEnabled ?? true;
|
||||
return resolveAssistantFlags({
|
||||
enabled: master,
|
||||
coach: settings?.assistantCoachEnabled ?? true,
|
||||
briefing: settings?.assistantBriefingEnabled ?? true,
|
||||
insightStatus: settings?.assistantInsightStatusEnabled ?? true,
|
||||
correlations: settings?.assistantCorrelationsEnabled ?? true,
|
||||
healthScoreExplainer:
|
||||
settings?.assistantHealthScoreExplainerEnabled ?? true,
|
||||
});
|
||||
} catch {
|
||||
getEvent()?.addWarning(
|
||||
"Failed to load assistant feature flags, using defaults",
|
||||
);
|
||||
return ASSISTANT_FLAGS_DEFAULT;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,8 +131,8 @@ export function resolveAssistantFlags(raw: AssistantFlagSet): AssistantFlagSet {
|
||||
*
|
||||
* Older iOS clients that predate the v1.4.31 contract surface this as
|
||||
* a generic 403 (their existing 403-handler covers the case); v1.4.31+
|
||||
* clients read the `errorCode` to render the `AssistantDisabledNotice`
|
||||
* empty-state.
|
||||
* clients read the `errorCode` to render an inline operator-disabled
|
||||
* notice.
|
||||
*/
|
||||
export class AssistantDisabledError extends Error {
|
||||
readonly surface: AssistantSurface;
|
||||
|
||||
Reference in new issue
Block a user