stop swallowing dashboard persist failures
The optimistic cache made a refused write invisible: the UI stayed correct until the next reload, at which point the change was simply gone. That is tolerable for a pane size and not for a chat panel's agent name, which is the address a peer agent is delivered to. Roll back only if the cache still holds exactly what we wrote — writes to one key overlap freely (a window resize fires one per group) and rolling back over a later successful write would turn one failure into two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import type { UserState } from './useSettings';
|
||||
@@ -29,7 +30,9 @@ export function useDashboardState<T>(key: string, defaultValue: T) {
|
||||
const currentState = queryClient.getQueryData<UserState>(QUERY_KEY) ?? {};
|
||||
if (!(key in currentState)) {
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: defaultValue });
|
||||
clientRef.current.patch('/dashboards', { [key]: defaultValue }).catch(() => { });
|
||||
clientRef.current
|
||||
.patch('/dashboards', { [key]: defaultValue })
|
||||
.catch((err: unknown) => revert(queryClient, key, defaultValue, undefined, err));
|
||||
}
|
||||
}, [isSuccess, key, defaultValue, queryClient]);
|
||||
|
||||
@@ -43,10 +46,47 @@ export function useDashboardState<T>(key: string, defaultValue: T) {
|
||||
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue });
|
||||
|
||||
clientRef.current.patch('/dashboards', { [key]: newValue }).catch(() => { });
|
||||
clientRef.current
|
||||
.patch('/dashboards', { [key]: newValue })
|
||||
.catch((err: unknown) =>
|
||||
revert(queryClient, key, newValue, key in currentState ? currentValue : undefined, err),
|
||||
);
|
||||
},
|
||||
[key, defaultValue, queryClient],
|
||||
);
|
||||
|
||||
return { key, value, setValue, isLoaded: isSuccess };
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo an optimistic write the server refused, and say so.
|
||||
*
|
||||
* The optimistic cache is what made these failures invisible: the UI stayed correct until the next
|
||||
* reload, at which point the change was simply gone. That is worst for the things this store holds that
|
||||
* are not merely cosmetic — a chat panel's `config.agentName` is the address a peer agent is delivered
|
||||
* to, so a silently-dropped write leaves a panel that answers to a name it will not have tomorrow.
|
||||
*
|
||||
* Reverts only if the cache still holds exactly what we wrote. Writes to one key overlap freely (a
|
||||
* window resize fires one per group), and rolling back over a later successful write would turn one
|
||||
* failure into two.
|
||||
*/
|
||||
function revert(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
key: string,
|
||||
attempted: unknown,
|
||||
previous: unknown,
|
||||
err: unknown,
|
||||
): void {
|
||||
const state = queryClient.getQueryData<UserState>(QUERY_KEY) ?? {};
|
||||
if (state[key] === attempted) {
|
||||
const next = { ...state };
|
||||
if (previous === undefined) delete next[key];
|
||||
else next[key] = previous;
|
||||
queryClient.setQueryData(QUERY_KEY, next);
|
||||
}
|
||||
|
||||
console.error(`[dashboard-state] failed to persist "${key}"`, err);
|
||||
toast.error('Could not save your layout', {
|
||||
description: `"${key}" was not written and has been rolled back. ${err instanceof Error ? err.message : String(err)}`,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user