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:
cliffhall 2026-08-23 09:39:47 -04:00
commit fd21d0d528

View file

@ -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 },
);