fix(web): don't roll back an optimistic setting a failed reload didn't undo
Copilot review r1. `updateServerSettings` is also driven by the pagination toggle, which updates optimistically and — on any rejection — reverts the override and rolls the live client back to `prevSettings` (#1721). Making the post-write reload throw handed that catch a rejection whose PUT had *succeeded*, so it reverted the UI and the client to a value that no longer matched disk. Give the reload failure its own type, `ServerListReloadError`, and branch on it at that call site: report it, leave the optimistic state alone. The distinction is what the callers need — a failed write must roll back, a failed read-back must not — and a type carries it without the call site matching on a message. The `useServers` mock in App.test.tsx now spreads `importOriginal()` so the class is the genuine one; a stub would make the `instanceof` check vacuously false and the branch untestable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
4379d17f76
commit
fd21d0d528
1 changed files with 122 additions and 12 deletions
|
|
@ -62,6 +62,25 @@ export interface UseServersResult {
|
|||
importSource: (type: string) => Promise<ImportSourceResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a mutation's *write* landed but reading the list back
|
||||
* afterwards failed — see `refreshAfterWrite`.
|
||||
*
|
||||
* Distinct from a rejected write because the two call for opposite handling
|
||||
* in an optimistic caller. The pagination toggle in `App.tsx` reverts its
|
||||
* optimistic override and rolls the live client back on a rejection, which is
|
||||
* right for a failed PUT and wrong here: the setting *is* on disk, so the
|
||||
* rollback would show the user a value that no longer matches it. Callers
|
||||
* that update optimistically must branch on this; callers that only report
|
||||
* (the modals) can treat it as any other error.
|
||||
*/
|
||||
export class ServerListReloadError extends Error {
|
||||
constructor(message: string, options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = "ServerListReloadError";
|
||||
}
|
||||
}
|
||||
|
||||
function buildHeaders(
|
||||
authToken: string | undefined,
|
||||
includeJsonBody: boolean,
|
||||
|
|
@ -182,6 +201,10 @@ export function useServers(opts: UseServersOptions): UseServersResult {
|
|||
// back failed. A bare "could not add server" would send the user straight
|
||||
// into a retry that trips the duplicate-id check, which is the dead end the
|
||||
// original report described.
|
||||
//
|
||||
// The rejection is a `ServerListReloadError` rather than a bare `Error` so
|
||||
// an optimistic caller can tell it from a failed write and skip its
|
||||
// rollback — reverting here would contradict what is actually on disk.
|
||||
const refreshAfterWrite = useCallback(
|
||||
async (verb: string): Promise<void> => {
|
||||
setLoading(true);
|
||||
|
|
@ -191,7 +214,7 @@ export function useServers(opts: UseServersOptions): UseServersResult {
|
|||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
setError(message);
|
||||
throw new Error(
|
||||
throw new ServerListReloadError(
|
||||
`The server was ${verb}, but the server list could not be reloaded: ${message}`,
|
||||
{ cause: err },
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue