diff --git a/src/components/dashboard/__tests__/medication-intake-quick-add.test.tsx b/src/components/dashboard/__tests__/medication-intake-quick-add.test.tsx index 584b08b36..1d7d1eed5 100644 --- a/src/components/dashboard/__tests__/medication-intake-quick-add.test.tsx +++ b/src/components/dashboard/__tests__/medication-intake-quick-add.test.tsx @@ -393,21 +393,22 @@ describe("submit invalidation — the quick-add is a capture-picker surface, not ); it("routes the intake submit through the blessed invalidateMedicationReads (source pin)", () => { - expect(componentSrc).toContain( - "await invalidateMedicationReads(queryClient);", - ); expect(componentSrc).not.toContain( "invalidateKeys(queryClient, medicationDependentKeys)", ); - // The submit handler pairs the blessed helper with the inline - // compliance-chart fan-out AFTER the intake POST landed. + // AFTER the intake POST landed, and after the sheet was told it is done: + // the save does not wait for the refetches before it closes. expect(componentSrc).toMatch( - /async function handleSubmit\([\s\S]*?apiPost[\s\S]*?await invalidateMedicationReads\(queryClient\);[\s\S]*?complianceChartInline\(\)/, + /async function handleSubmit\([\s\S]*?apiPost[\s\S]*?onSuccess\?\.\(\);\s*(?:\/\/[^\n]*\n\s*)*void invalidateMedicationReads\(queryClient\);/, + ); + // The bundle carries every inline compliance chart, so no second sweep. + expect(medicationDependentKeys).toContainEqual( + queryKeys.complianceChartInline(), ); }); it("forces the inactive Today hero + dashboard reads to refetch after submit", async () => { - // The helper the submit path awaits — asserted directly, same shape as + // The helper the submit path calls — asserted directly, same shape as // the take-all-due suite: the bundle fans out (N keys), then the two // daily reads are invalidated with `refetchType: "inactive"`, which is // what actually refetches them while unmounted. diff --git a/src/components/dashboard/medication-intake-quick-add.tsx b/src/components/dashboard/medication-intake-quick-add.tsx index c67561b12..38bd99fcf 100644 --- a/src/components/dashboard/medication-intake-quick-add.tsx +++ b/src/components/dashboard/medication-intake-quick-add.tsx @@ -235,13 +235,6 @@ export function MedicationIntakeQuickAdd({ ); const eventId = created?.id; - await invalidateMedicationReads(queryClient); - // Fan-out to every inline compliance chart key (one per medication) - // so the detail-page tile refreshes when the user re-enters it. - await queryClient.invalidateQueries({ - queryKey: queryKeys.complianceChartInline(), - }); - // v1.36.x — the shared decision, not a fourth copy of it. This surface // was left out of the collapse that took the other three: it offered a // delegate an Undo hitting `DELETE /api/medications/{id}/intake/{id}`, @@ -272,6 +265,9 @@ export function MedicationIntakeQuickAdd({ toast.success(t("common.saved")); } onSuccess?.(); + // The dose is stored; the sheet does not wait for the refetches. The + // bundle covers every inline compliance chart as well. + void invalidateMedicationReads(queryClient); } catch (err) { setError( err instanceof ApiError && err.message diff --git a/src/components/measurements/list/use-measurement-edit.ts b/src/components/measurements/list/use-measurement-edit.ts index e7ae637e7..f4213ba97 100644 --- a/src/components/measurements/list/use-measurement-edit.ts +++ b/src/components/measurements/list/use-measurement-edit.ts @@ -127,15 +127,15 @@ export function useMeasurementEdit({ }) => { await apiPut(`/api/measurements/${id}`, { value, measuredAt, notes }); }, - onSuccess: async () => { - await invalidateKeys(queryClient, measurementDependentKeys); - await refetchInactiveDailyReads(queryClient); + onSuccess: () => { setEditSeed({ value: editValue, measuredAt: editMeasuredAt, notes: editNotes, }); dismissEdit(); + void invalidateKeys(queryClient, measurementDependentKeys); + void refetchInactiveDailyReads(queryClient); }, onError: (err) => { // A 409 names its reason in `meta.errorCode`: the edit collides with diff --git a/src/components/measurements/measurement-form.tsx b/src/components/measurements/measurement-form.tsx index 76e7b2f95..9f7ba309b 100644 --- a/src/components/measurements/measurement-form.tsx +++ b/src/components/measurements/measurement-form.tsx @@ -528,8 +528,6 @@ export function MeasurementForm({ setPulse(""); setPairedGlucose(""); setNotes(""); - await invalidateKeys(queryClient, measurementDependentKeys); - await refetchInactiveDailyReads(queryClient); // v1.36.x — a delegate's receipt names the record. "Saved" alone is // the one confirmation somebody acting for another person does not // need, and the reading has just left their own history for good. @@ -544,6 +542,10 @@ export function MeasurementForm({ : undefined, ); onSuccess?.(); + // The reading is stored; the sheet does not wait for every read that + // shows it to come back. + void invalidateKeys(queryClient, measurementDependentKeys); + void refetchInactiveDailyReads(queryClient); } catch (err) { setError(localizedApiError(err, t, "measurements.saveError")); } finally { diff --git a/src/components/mood/mood-form.tsx b/src/components/mood/mood-form.tsx index 2fecf8747..e79bf95c6 100644 --- a/src/components/mood/mood-form.tsx +++ b/src/components/mood/mood-form.tsx @@ -257,13 +257,13 @@ export function MoodForm({ if (tagKeys.length > 0) recordUse(tagKeys); resetForm(); setMoreTagsOpen(false); - await invalidateKeys(queryClient, moodDependentKeys); - await refetchInactiveDailyReads(queryClient); toast.success(t("common.saved")); // The entry landed, but a tag picked here may have been archived or // deleted elsewhere since the picker loaded; the server says so. if (droppedAnyLinkKeys(saved)) toast.warning(t("mood.tagsNotSaved")); onSuccess?.(); + void invalidateKeys(queryClient, moodDependentKeys); + void refetchInactiveDailyReads(queryClient); } catch (err) { setError(localizedApiError(err, t, "mood.saveError")); } finally { diff --git a/src/components/symptoms/use-symptoms.ts b/src/components/symptoms/use-symptoms.ts index 41790f6e9..711a7c96a 100644 --- a/src/components/symptoms/use-symptoms.ts +++ b/src/components/symptoms/use-symptoms.ts @@ -137,9 +137,11 @@ export function invalidateSymptomReads(qc: QueryClient) { ]); } +// Not returned, so a save reports as soon as it lands rather than after +// every read it touches has refetched. function useInvalidateSymptoms() { const qc = useQueryClient(); - return () => invalidateSymptomReads(qc); + return () => void invalidateSymptomReads(qc); } export function useCreateSymptomDefinition() { diff --git a/src/components/vaccinations/use-vaccinations.ts b/src/components/vaccinations/use-vaccinations.ts index c67f2b77f..a2c9c8656 100644 --- a/src/components/vaccinations/use-vaccinations.ts +++ b/src/components/vaccinations/use-vaccinations.ts @@ -125,8 +125,10 @@ export function useVaccination(id: string | null) { */ export function useVaccinationMutations() { const qc = useQueryClient(); + // Not returned: a mutation would wait for every refetch before it settles, + // and the sheet would stay open with its spinner until then. const invalidate = () => - Promise.all([ + void Promise.all([ invalidateKeys(qc, vaccinationDependentKeys), invalidateReminderReads(qc), ]); diff --git a/src/components/workouts/__tests__/manual-workout-save.test.ts b/src/components/workouts/__tests__/manual-workout-save.test.ts index 4156d4a47..b7a97beba 100644 --- a/src/components/workouts/__tests__/manual-workout-save.test.ts +++ b/src/components/workouts/__tests__/manual-workout-save.test.ts @@ -105,6 +105,22 @@ describe("saveManualWorkout — the wire", () => { expect(keys).toContain(JSON.stringify(queryKeys.timelineRoot())); }); + it("returns once the entry is stored, without waiting for the refetches", async () => { + // The sheet closes on this answer; on a slow connection the reads it + // refreshes can take seconds, and the person would watch a spinner. + fetchSpy.mockResolvedValue( + envelope({ entries: [{ index: 0, status: "inserted" }] }), + ); + const client = { + ...fakeClient(), + invalidateQueries: vi.fn(() => new Promise(() => {})), + }; + + await expect( + saveManualWorkout(ENTRY, client as unknown as QueryClient), + ).resolves.toBe("inserted"); + }); + it("treats a duplicate (the same form submitted twice) as saved", async () => { fetchSpy.mockResolvedValue( envelope({ entries: [{ index: 0, status: "duplicate" }] }), diff --git a/src/components/workouts/delete-workout-button.tsx b/src/components/workouts/delete-workout-button.tsx index 2feb89a1e..b04d1c417 100644 --- a/src/components/workouts/delete-workout-button.tsx +++ b/src/components/workouts/delete-workout-button.tsx @@ -53,8 +53,8 @@ export async function deleteManualWorkout( ): Promise { await apiDelete(`/api/workouts/${encodeURIComponent(id)}`); queryClient.removeQueries({ queryKey: queryKeys.workoutDetail(id) }); - await invalidateKeys(queryClient, workoutDependentKeys); - await refetchInactiveDailyReads(queryClient); + void invalidateKeys(queryClient, workoutDependentKeys); + void refetchInactiveDailyReads(queryClient); } /** diff --git a/src/components/workouts/manual-workout-form.tsx b/src/components/workouts/manual-workout-form.tsx index 708f55819..b10aafc1d 100644 --- a/src/components/workouts/manual-workout-form.tsx +++ b/src/components/workouts/manual-workout-form.tsx @@ -94,7 +94,8 @@ interface BatchResult { export type ManualWorkoutSaveOutcome = "inserted" | "duplicate" | "updated"; /** - * Post the entry and refresh what shows workouts. A `duplicate` is a success: + * Post the entry and start refreshing what shows workouts; the caller reports + * the save without waiting for those reads. A `duplicate` is a success: * the same form already landed this session (a second tap, a retry), so the * row exists exactly once. A `skipped` entry was refused and throws. */ @@ -112,8 +113,8 @@ export async function saveManualWorkout( // The form also opens from the capture picker on any page, where the // Today hero and the dashboard snapshot are unmounted, so those two are // refetched outright. - await invalidateKeys(queryClient, workoutDependentKeys); - await refetchInactiveDailyReads(queryClient); + void invalidateKeys(queryClient, workoutDependentKeys); + void refetchInactiveDailyReads(queryClient); return status; } diff --git a/src/hooks/use-encounters.ts b/src/hooks/use-encounters.ts index dc947d9de..d140be832 100644 --- a/src/hooks/use-encounters.ts +++ b/src/hooks/use-encounters.ts @@ -166,10 +166,11 @@ export function useEncounterMutations() { * * So the fan-out is the visit roots PLUS the reminder reads, through the same * helper the Vorsorge surfaces use, rather than a second sweep that could - * drift from it. + * drift from it. Not returned: a mutation would wait for every refetch + * before it settles, and the sheet would stay open until then. */ const invalidate = () => - Promise.all([ + void Promise.all([ invalidateKeys(qc, encounterDependentKeys), invalidateReminderReads(qc), ]); diff --git a/src/hooks/use-measurement-reminders.ts b/src/hooks/use-measurement-reminders.ts index b2376dddc..d60766302 100644 --- a/src/hooks/use-measurement-reminders.ts +++ b/src/hooks/use-measurement-reminders.ts @@ -117,7 +117,8 @@ export function useMeasurementReminders(enabled = true) { export function useMeasurementReminderMutations() { const qc = useQueryClient(); - const invalidate = () => invalidateReminderReads(qc); + // Not returned: a mutation would wait for every refetch before it settles. + const invalidate = () => void invalidateReminderReads(qc); const create = useMutation({ mutationKey: queryKeys.measurementReminderCreate(), diff --git a/src/hooks/use-practitioners.ts b/src/hooks/use-practitioners.ts index bab014841..4b083a47f 100644 --- a/src/hooks/use-practitioners.ts +++ b/src/hooks/use-practitioners.ts @@ -62,9 +62,10 @@ export function usePractitionerMutations() { const qc = useQueryClient(); // Renaming a practice changes the label a rail appointment renders under, // and the daily reads are unmounted from the address book — pair the - // bundle invalidation with the forced inactive refetch. + // bundle invalidation with the forced inactive refetch. Not returned, so the + // sheet closes without waiting for the refetches. const invalidate = () => - Promise.all([ + void Promise.all([ invalidateKeys(qc, encounterDependentKeys), refetchInactiveDailyReads(qc), ]);