fix(web): key the rollback baseline per server, and pair it at completion

Review round 1 on #2091. A single record slot let a save on one server
discard the baseline a later failed write on another still needed — the
settings modal opens for any server, connected or not, so that sequence
reproduces #2089 through the very mechanism meant to prevent it. Records
are now kept per server id.

The record also paired with the `servers` entry captured when the write
was *issued*, so a list read landing while the write was in flight was
mistaken for one that happened after it and discarded a live record. It
now pairs with the entry as of completion, read through a ref (mirrored
in an effect — writing a ref during render is a `react-hooks/refs`
error). The residual — a read whose response predates the write but
arrives after it — needs a revision or ETag `useServers` does not
expose, and is documented on the hook.

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 11:18:34 -04:00
commit e359368892

View file

@ -1163,7 +1163,7 @@ function App() {
// What every settings write that landed on disk actually wrote, so a later
// failed write can be rolled back to that rather than to a `servers` entry
// frozen at the last successful list read (#2089).
const lastPersistedSettings = useLastPersistedSettings();
const lastPersistedSettings = useLastPersistedSettings(servers);
// The active server's persisted paginated setting drives the display mode.
// The sidebar toggle edits it (optimistically, below) and persists it.
const persistedPaginatedLists =
@ -3741,7 +3741,6 @@ function App() {
// to have landed while that is the fresher account (#2089).
const prevSettings = lastPersistedSettings.resolve(
activeServerId,
current,
current.settings ?? EMPTY_SETTINGS,
);
const next: InspectorServerSettings = {
@ -3779,7 +3778,7 @@ function App() {
// whatever is written next, since the `servers` entry it was derived
// from will keep describing the old value if the reload behind this
// write — or any later one — fails (#2089).
lastPersistedSettings.record(activeServerId, next, current);
lastPersistedSettings.record(activeServerId, next);
})
.catch((err: unknown) => {
// Persist failed: revert the optimistic override and roll the live
@ -4169,7 +4168,6 @@ function App() {
// outside the coverage gate, so wiring written here is tested by nothing
// (#1950 review r17).
onPersist: async (id: string, value: InspectorServerSettings) => {
const entry = servers.find((s) => s.id === id);
await refreshingPersist(updateServerSettings, refreshInitialConfig)(
id,
value,
@ -4177,12 +4175,10 @@ function App() {
// Recorded for the same reason the pagination toggle records its own
// write (#2089): this is what disk holds now, and the `servers` entry it
// was edited from will keep describing the previous value for as long as
// list reads keep failing. Both settings writers feed the one record, so
// a rollback in either takes the most recent write, not just the most
// recent write *of its own kind*. No entry means the modal was opened
// against a server the list doesn't have, and there is nothing for a
// later read to supersede.
if (entry) lastPersistedSettings.record(id, value, entry);
// list reads keep failing. Both settings writers feed the same per-server
// record, so a rollback in either takes the most recent write for that
// server, not just the most recent write *of its own kind*.
lastPersistedSettings.record(id, value);
},
// Surface failures via toast — the modal usually closes
// immediately on user dismiss, so a silent fail-on-flush would