mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(concurrency): echo the base token from the prefs write clients and document it
Wire the notification-prefs cards, the module toggles, the coach-preferences save, and the two mood-tag manage cards to echo the optimistic-concurrency token they last read and to reload on a 409 instead of resaving over a newer change. Document the token and the 409 on the four write endpoints in the OpenAPI registry via the shared base-token field and conflict-response fragments.
This commit is contained in:
1 parent
8849f2cecd
commit
3f33a30415
12 files changed
+352
-44
No files matched your search
@@ -4,11 +4,24 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiFetchRaw, apiPut } from "@/lib/api/api-fetch";
|
||||
import {
|
||||
readUpdatedAtToken,
|
||||
withBaseToken,
|
||||
isConflict,
|
||||
} from "@/lib/api/optimistic-token";
|
||||
import {
|
||||
DEFAULT_COACH_PREFS,
|
||||
type CoachPrefs,
|
||||
} from "@/lib/validations/coach-prefs";
|
||||
|
||||
/**
|
||||
* v1.32.22 (R5b) — the cached Coach-prefs row carries the optimistic-
|
||||
* concurrency token (`User.updatedAt`) alongside its payload so the writer
|
||||
* can echo it. The read query and the save mutation both speak this shape;
|
||||
* the token is opaque (the client only ever echoes a server-returned value).
|
||||
*/
|
||||
type CoachPrefsWithToken = CoachPrefs & { updatedAt?: string };
|
||||
|
||||
/**
|
||||
* v1.4.23 W6 (S-03) — shared accessor for the per-user Coach
|
||||
* preferences row. Both the Coach drawer's settings sheet and the
|
||||
@@ -25,14 +38,14 @@ import {
|
||||
* row (message thread) leave it unset (defaults to true).
|
||||
*/
|
||||
export function useCoachPrefs(opts?: { enabled?: boolean }) {
|
||||
return useQuery<CoachPrefs>({
|
||||
return useQuery<CoachPrefsWithToken>({
|
||||
queryKey: queryKeys.coachPrefs(),
|
||||
queryFn: async () => {
|
||||
// `apiFetchRaw` (no .ok throw) — a non-OK read soft-fails to the
|
||||
// defaults instead of surfacing an error state.
|
||||
const res = await apiFetchRaw("/api/auth/me/coach-prefs");
|
||||
if (!res.ok) return DEFAULT_COACH_PREFS;
|
||||
const env = (await res.json()) as { data: CoachPrefs };
|
||||
const env = (await res.json()) as { data: CoachPrefsWithToken };
|
||||
return env.data;
|
||||
},
|
||||
enabled: opts?.enabled,
|
||||
@@ -48,15 +61,48 @@ export function useCoachPrefs(opts?: { enabled?: boolean }) {
|
||||
* every surface that reads the hook re-renders against one source of
|
||||
* truth — the rail and the cog can never drift.
|
||||
*/
|
||||
export function useSaveCoachPrefs(opts?: { onSuccess?: () => void }) {
|
||||
export function useSaveCoachPrefs(opts?: {
|
||||
onSuccess?: () => void;
|
||||
/** Fired when a write 409s because the row advanced since it was loaded. */
|
||||
onConflict?: () => void;
|
||||
/** Fired for every non-conflict write error, if the caller wants to surface it. */
|
||||
onError?: (err: unknown) => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationKey: queryKeys.coachPrefs(),
|
||||
// v1.32.22 (R5b) — echo the base token this edit was based on so a
|
||||
// concurrent write to the same row (another tab, the chat sources rail)
|
||||
// 409s instead of silently overwriting the newer state. Read straight from
|
||||
// the cache at mutate time so it reflects the latest settled write.
|
||||
mutationFn: async (next: CoachPrefs) =>
|
||||
apiPut<CoachPrefs>("/api/auth/me/coach-prefs", next),
|
||||
apiPut<CoachPrefsWithToken>(
|
||||
"/api/auth/me/coach-prefs",
|
||||
withBaseToken(
|
||||
next,
|
||||
readUpdatedAtToken(queryClient, queryKeys.coachPrefs()),
|
||||
),
|
||||
),
|
||||
onSuccess: (data) => {
|
||||
queryClient.setQueryData<CoachPrefs>(queryKeys.coachPrefs(), data);
|
||||
queryClient.setQueryData<CoachPrefsWithToken>(
|
||||
queryKeys.coachPrefs(),
|
||||
data,
|
||||
);
|
||||
opts?.onSuccess?.();
|
||||
},
|
||||
onError: (err) => {
|
||||
// v1.32.22 (R5b) — a 409 means the stored prefs advanced since this edit
|
||||
// was based. Refetch so the token advances and the form re-syncs to the
|
||||
// fresh server state, then let the caller nudge gently — nothing was
|
||||
// clobbered. Every other error is handed back to the caller.
|
||||
if (isConflict(err)) {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.coachPrefs(),
|
||||
});
|
||||
opts?.onConflict?.();
|
||||
return;
|
||||
}
|
||||
opts?.onError?.(err);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user