mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
The mood rollup warm-up on the dashboard summary was started with a bare `void`, so `settleBackgroundTasks()` never saw it. On a stale account the warm-up started a full recompute that could still be writing when the statement counter closed its window, and the hot-route budget read 30 against 28 depending on where it landed. Every read-path start of that shape now goes through `trackBackgroundTask(runDetached(...))`: the measurement and mood rollup warm-ups (seven call sites), the dashboard's today-briefing warm, and the two delegated-access bookkeeping writes in the record guard. A guard refuses a bare `void` on any of them. `trackBackgroundTask` now wraps its handle in `Promise.resolve`: a test double that returns nothing used to leave `undefined` in the set, and settle spun on it at full CPU. The budget fixture mints the mood rollups in its seed, as it already did for the measurement rollups. Eight serial runs of query-budget-hot-routes read the same counts every time (dashboard summary 28, snapshot 99, day 47, health-status 59, correlations 43).
93 lines
3.9 KiB
TypeScript
93 lines
3.9 KiB
TypeScript
/**
|
|
* The registry of detached background work, so a test can await what a
|
|
* request-path shortcut deliberately does not.
|
|
*
|
|
* ## The failure this exists for
|
|
*
|
|
* Several code paths start a promise and return without awaiting it, then log
|
|
* on the detached handle so the failure is not swallowed — `fireAndForget`,
|
|
* the stale-while-revalidate cache refresh, the offline query persister. That
|
|
* is right for production and hostile to a test runner: vitest replaces
|
|
* `globalThis.console` with a sink that forwards every write to the main
|
|
* thread as an `onUserConsoleLog` RPC call, and when a worker finishes a test
|
|
* file it awaits the calls in flight and then REJECTS whatever was started
|
|
* after that point. A detached write that lands in the window between those
|
|
* two steps fails the entire run with
|
|
*
|
|
* EnvironmentTeardownError: [vitest-worker]: Closing rpc while
|
|
* "onUserConsoleLog" was pending
|
|
*
|
|
* blamed on the file the worker was tearing down — which need not contain a
|
|
* single console call of its own, and never points at the test that started
|
|
* the work. Muting the console in tests would hide the log line the detached
|
|
* path exists to produce and leave the race in place; awaiting the work
|
|
* removes the precondition.
|
|
*
|
|
* ## Why the registry is test-only
|
|
*
|
|
* Production never awaits it, so tracking there would buy nothing and cost
|
|
* something real: a strong reference to every background promise for as long
|
|
* as it stays unsettled. One wedged task on a long-lived pg-boss worker would
|
|
* then be retained for the life of the process, which turns an observability
|
|
* affordance into a slow leak. Under test the worker is short-lived and the
|
|
* set is drained after every test, so neither concern applies. Outside test
|
|
* every call site keeps exactly the behaviour and cost it had before — start
|
|
* the promise, discard the handle, one boolean check.
|
|
*
|
|
* The module deliberately imports nothing: it is reachable from client code
|
|
* (`lib/pwa/query-persister.ts`) as well as from the server tree, and must not
|
|
* drag `node:async_hooks` into the browser bundle the way `./context` would.
|
|
*/
|
|
const TRACK_IN_FLIGHT =
|
|
typeof process !== "undefined" &&
|
|
(process.env?.VITEST === "true" || process.env?.NODE_ENV === "test");
|
|
|
|
const inFlight = new Set<Promise<unknown>>();
|
|
|
|
/**
|
|
* Register a detached promise so `settleBackgroundTasks()` can await it.
|
|
*
|
|
* Pass a handle that CANNOT reject — the `.catch(...)` result, not the raw
|
|
* work. A rejecting handle would surface as an unhandled rejection through the
|
|
* bookkeeping below, which is a louder signal than this helper should invent.
|
|
*
|
|
* A no-op outside test.
|
|
*/
|
|
export function trackBackgroundTask(settled: Promise<unknown>): void {
|
|
if (!TRACK_IN_FLIGHT) return;
|
|
|
|
// A test double that stands in for the background work often returns
|
|
// nothing at all. Filed as is, `undefined` would never leave the set and
|
|
// `settleBackgroundTasks` would spin on it forever at full CPU. A real
|
|
// promise passes through `Promise.resolve` as the same object.
|
|
const handle = Promise.resolve(settled);
|
|
inFlight.add(handle);
|
|
void handle.finally(() => {
|
|
inFlight.delete(handle);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Await every registered task still in flight, including work that a settling
|
|
* task starts itself.
|
|
*
|
|
* Both suite setups call this from `afterEach`, so no test can leave
|
|
* background work running past its own end. A no-op outside test, where
|
|
* nothing registers.
|
|
*/
|
|
export async function settleBackgroundTasks(): Promise<void> {
|
|
while (inFlight.size > 0) {
|
|
await Promise.allSettled([...inFlight]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* How many tasks are registered right now. Test-only introspection for the
|
|
* registration guard, which asserts both that a task registers and that the
|
|
* set drains back to empty — a retained reference would otherwise pass
|
|
* unnoticed.
|
|
*/
|
|
export function pendingBackgroundTaskCount(): number {
|
|
return inFlight.size;
|
|
}
|