mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
fix(query-keys): refresh the day view and timeline after every write
Symptoms, workouts, lab results, allergies, documents and questionnaires evicted only their own lists. An open day, its calendar dots and the timeline kept the pre-write answer for up to five minutes, so "Capture for this day" saved a symptom or a workout and the day showed the old list, and a deleted row stayed on it. Workouts, labs, allergies and documents get a bundle beside the seven that already carry the day and timeline roots; the six lab lists become one (the OCR dialog's own copy of the commit hook's list goes with it). Symptoms and questionnaires add the two roots to their list. Symptoms stay off the illness bundle on purpose: nothing on the dashboard or the Today digest reads them. Custom metrics need nothing: neither the day nor the timeline reads them. invalidateKeys now drops a key listed twice, since spread bundles share the two roots.
This commit is contained in:
1 parent
aae029d94e
commit
2e73b0ffec
24 files changed
+257
-118
No files matched your search
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* The day view and the timeline read every record table, and the day query
|
||||
* stays fresh for five minutes because every record write evicts its root.
|
||||
* A write whose invalidation leaves the two roots out keeps an open day and
|
||||
* the timeline on their pre-write answer: "Capture for this day" saved a
|
||||
* symptom or a workout and the day showed the old list.
|
||||
*/
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { invalidateSymptomReads } from "@/components/symptoms/use-symptoms";
|
||||
import {
|
||||
allergyDependentKeys,
|
||||
cycleDependentKeys,
|
||||
documentDependentKeys,
|
||||
encounterDependentKeys,
|
||||
illnessDependentKeys,
|
||||
labDependentKeys,
|
||||
measurementDependentKeys,
|
||||
medicationDependentKeys,
|
||||
moodDependentKeys,
|
||||
queryKeys,
|
||||
vaccinationDependentKeys,
|
||||
workoutDependentKeys,
|
||||
} from "@/lib/query-keys";
|
||||
|
||||
const RECORD_BUNDLES = {
|
||||
allergy: allergyDependentKeys,
|
||||
cycle: cycleDependentKeys,
|
||||
document: documentDependentKeys,
|
||||
encounter: encounterDependentKeys,
|
||||
illness: illnessDependentKeys,
|
||||
lab: labDependentKeys,
|
||||
measurement: measurementDependentKeys,
|
||||
medication: medicationDependentKeys,
|
||||
mood: moodDependentKeys,
|
||||
vaccination: vaccinationDependentKeys,
|
||||
workout: workoutDependentKeys,
|
||||
};
|
||||
|
||||
describe("record writes reach the day view and the timeline", () => {
|
||||
it.each(Object.entries(RECORD_BUNDLES))(
|
||||
"a %s write evicts the day and timeline roots",
|
||||
(_name, bundle) => {
|
||||
expect(bundle).toContainEqual(queryKeys.dayRoot());
|
||||
expect(bundle).toContainEqual(queryKeys.timelineRoot());
|
||||
},
|
||||
);
|
||||
|
||||
it("a symptom write marks an open day and the timeline stale", async () => {
|
||||
const client = new QueryClient();
|
||||
const day = queryKeys.day("2026-11-10");
|
||||
const lanes = queryKeys.timeline("week", null, null, "");
|
||||
client.setQueryData(day, { sections: [] });
|
||||
client.setQueryData(lanes, { lanes: [] });
|
||||
|
||||
await invalidateSymptomReads(client);
|
||||
|
||||
expect(client.getQueryState(day)?.isInvalidated).toBe(true);
|
||||
expect(client.getQueryState(lanes)?.isInvalidated).toBe(true);
|
||||
client.clear();
|
||||
});
|
||||
});
|
||||
@@ -359,6 +359,52 @@ export const vaccinationDependentKeys = [
|
||||
queryKeys.timelineRoot(),
|
||||
];
|
||||
|
||||
/**
|
||||
* Keys invalidated when a workout is logged, edited or deleted. The Today
|
||||
* digest names a workout that just arrived and the write evicts the server's
|
||||
* daily caches, so the daily reads ride along; pair call sites with
|
||||
* `refetchInactiveDailyReads`, because both are unmounted wherever the form
|
||||
* opens outside the dashboard.
|
||||
*/
|
||||
export const workoutDependentKeys = [
|
||||
queryKeys.workouts(),
|
||||
queryKeys.dashboardSnapshot(),
|
||||
queryKeys.dailyDigest(),
|
||||
queryKeys.dayRoot(),
|
||||
queryKeys.timelineRoot(),
|
||||
];
|
||||
|
||||
/**
|
||||
* Keys invalidated when a lab reading is created, edited, deleted or restored,
|
||||
* or a marker is renamed, hidden, merged or deleted. The marker list carries
|
||||
* each marker's latest reading, and the "what changed since your last panel"
|
||||
* card compares the two newest panels.
|
||||
*/
|
||||
export const labDependentKeys = [
|
||||
queryKeys.labResults(),
|
||||
queryKeys.biomarkers(),
|
||||
queryKeys.insightsLabsChanges(),
|
||||
queryKeys.dayRoot(),
|
||||
queryKeys.timelineRoot(),
|
||||
];
|
||||
|
||||
/** Keys invalidated when an allergy is created, edited or deleted. */
|
||||
export const allergyDependentKeys = [
|
||||
queryKeys.allergies(),
|
||||
queryKeys.dayRoot(),
|
||||
queryKeys.timelineRoot(),
|
||||
];
|
||||
|
||||
/**
|
||||
* Keys invalidated when a document is uploaded, imported, edited, deleted or
|
||||
* restored. A document's date places it on its day and on the timeline.
|
||||
*/
|
||||
export const documentDependentKeys = [
|
||||
queryKeys.documents(),
|
||||
queryKeys.dayRoot(),
|
||||
queryKeys.timelineRoot(),
|
||||
];
|
||||
|
||||
/**
|
||||
* Keys invalidated when an input of the `ai` block on `GET /api/auth/me`
|
||||
* changes: the account's own provider (key, local model, compatible endpoint,
|
||||
@@ -385,7 +431,8 @@ export const aiInputDependentKeys = [
|
||||
/**
|
||||
* Invalidate every key in the bundle in parallel. Use this from mutation
|
||||
* `onSuccess` handlers so the call site stays a one-liner instead of repeating
|
||||
* `Promise.all(keys.map(...))` everywhere.
|
||||
* `Promise.all(keys.map(...))` everywhere. A key listed twice (two bundles
|
||||
* spread into one list share the day and timeline roots) is invalidated once.
|
||||
*
|
||||
* Uses `allSettled` so one transient network failure doesn't abort subsequent
|
||||
* invalidations (cache would otherwise be left half-stale) and so the `void
|
||||
@@ -404,8 +451,11 @@ export function invalidateKeys(
|
||||
queryClient: QueryClient,
|
||||
keys: readonly QueryKey[],
|
||||
): Promise<PromiseSettledResult<unknown>[]> {
|
||||
const unique = new Map(keys.map((key) => [JSON.stringify(key), key]));
|
||||
return Promise.allSettled(
|
||||
keys.map((queryKey) => invalidateKey(queryClient, queryKey)),
|
||||
[...unique.values()].map((queryKey) =>
|
||||
invalidateKey(queryClient, queryKey),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -441,9 +491,8 @@ async function invalidateKey(
|
||||
* remount re-fetches under `refetchOnMount: false`.
|
||||
*
|
||||
* Originates from the v1.16.11 dashboard-snapshot fix and the v1.29.1
|
||||
* medication-intake fix (`invalidateMedicationReads` in
|
||||
* `components/medications/use-medication-intake.ts`, which predates this
|
||||
* shared helper and keeps its own inline copy). Call this alongside
|
||||
* medication-intake fix (`invalidateMedicationReads` below wraps the pair
|
||||
* for medication writes). Call this alongside
|
||||
* `invalidateKeys(queryClient, xDependentKeys)` from any write whose server
|
||||
* route already hard-evicts the snapshot bucket — the forced refetch then
|
||||
* returns post-write data immediately, no server change needed.
|
||||
|
||||
Reference in new issue
Block a user