fix(web): apply the whole live surface, and track write outcomes per server
Review round 9 on #2091, all three findings. Every live-apply now goes through one `applyLiveServerSettings`: the settings object, the Network log size, and the roots diff together. A close with a pending debounce applies the draft to all three, so reconciling only `setServerSettings` when that save rejected left the server advertising roots that were never persisted and the log sized for them. The "this server's draft is unpersisted" flag moves from a single id in `App.tsx` into the tracker, which already orders writes by issue and keeps state per server. One id could be overwritten by a failure for a different server, or cleared by a stale success, either of which made a close apply a rejected draft. And when nothing has ever landed for a server, close falls back to the empty settings the toggle rollback uses rather than to the draft — the old fallback re-applied the very values that had just failed. 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
ba633fc8d2
commit
db84823262
2 changed files with 159 additions and 56 deletions
|
|
@ -191,6 +191,31 @@ describe("useLastPersistedSettings", () => {
|
|||
expect(api.resolve("A")).toBe(fresh);
|
||||
});
|
||||
|
||||
it("reports a failed last write per server, ordered by issue", () => {
|
||||
// The settings modal's draft survives a rejected save, so its owner has to
|
||||
// know whether what the user last tried to save is on disk. A failure on B
|
||||
// says nothing about A, and a straggling failure from an older write does
|
||||
// not overrule a newer write that succeeded.
|
||||
const { api } = harness([entry("A", settings()), entry("B", settings())]);
|
||||
expect(api.lastWriteFailed("A")).toBe(false);
|
||||
|
||||
const firstA = api.begin("A");
|
||||
const secondA = api.begin("A");
|
||||
secondA.landed(settings({ paginatedLists: true }));
|
||||
firstA.failed();
|
||||
expect(api.lastWriteFailed("A")).toBe(false);
|
||||
|
||||
api.begin("B").failed();
|
||||
expect(api.lastWriteFailed("B")).toBe(true);
|
||||
expect(api.lastWriteFailed("A")).toBe(false);
|
||||
|
||||
// …and a newer failure on A does count, until a newer write lands.
|
||||
api.begin("A").failed();
|
||||
expect(api.lastWriteFailed("A")).toBe(true);
|
||||
api.begin("A").landed(settings());
|
||||
expect(api.lastWriteFailed("A")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns nothing when the server has left the list entirely", () => {
|
||||
const { api, setServers } = harness([entry("A", settings())]);
|
||||
api.begin("A").landed(settings({ paginatedLists: true }));
|
||||
|
|
|
|||
|
|
@ -81,6 +81,19 @@ export interface LastPersistedSettings {
|
|||
* same class of bug as reading the baseline from a stale `servers` entry.
|
||||
*/
|
||||
resolve: (serverId: string) => InspectorServerSettings | undefined;
|
||||
/**
|
||||
* Whether the most recently **issued** write that has reported for this
|
||||
* server ended in failure — i.e. whatever the user last tried to save is not
|
||||
* what disk holds. A caller holding an edit buffer for that server (the
|
||||
* settings modal's draft) uses it to know its buffer is unpersisted, and to
|
||||
* apply `resolve` instead of the buffer.
|
||||
*
|
||||
* Ordered by issue, not by arrival, for the same reason the record is: a
|
||||
* straggling failure from an older write does not overwrite the outcome of a
|
||||
* newer one, and outcomes are kept per server so a failure on B says nothing
|
||||
* about A.
|
||||
*/
|
||||
lastWriteFailed: (serverId: string) => boolean;
|
||||
}
|
||||
|
||||
export interface SettingsWrite {
|
||||
|
|
@ -126,6 +139,12 @@ export function useLastPersistedSettings(
|
|||
// whether it is the last word or whether something later will still report.
|
||||
// A write leaves this set through `landed` or `failed` alike.
|
||||
const pendingRef = useRef<Map<string, Set<number>>>(new Map());
|
||||
// Outcome of the latest-issued write that has reported, per server. Kept
|
||||
// apart from the record because a *failed* write leaves no record but is
|
||||
// still the answer to "is the user's last attempt on disk?".
|
||||
const outcomesRef = useRef<Map<string, { sequence: number; ok: boolean }>>(
|
||||
new Map(),
|
||||
);
|
||||
// Read the list through a ref so a write pairs with the entry as it stands
|
||||
// when it finishes, not the one captured when it was issued. Mirrored in an
|
||||
// effect rather than assigned during render: writing a ref mid-render is an
|
||||
|
|
@ -141,12 +160,18 @@ export function useLastPersistedSettings(
|
|||
const pending = pendingRef.current.get(serverId) ?? new Set<number>();
|
||||
pending.add(sequence);
|
||||
pendingRef.current.set(serverId, pending);
|
||||
const settle = () => {
|
||||
const settle = (ok: boolean) => {
|
||||
pending.delete(sequence);
|
||||
// Keep the outcome of the latest-issued write that has reported, so an
|
||||
// older straggler cannot overwrite a newer one's verdict.
|
||||
const previous = outcomesRef.current.get(serverId);
|
||||
if (!previous || previous.sequence <= sequence) {
|
||||
outcomesRef.current.set(serverId, { sequence, ok });
|
||||
}
|
||||
};
|
||||
return {
|
||||
landed: (written: InspectorServerSettings) => {
|
||||
settle();
|
||||
settle(true);
|
||||
const confirmed = confirmedRef.current.get(serverId) ?? 0;
|
||||
// A write issued after this one already reported; it describes disk
|
||||
// more recently, whichever of the two happened to finish first.
|
||||
|
|
@ -160,10 +185,15 @@ export function useLastPersistedSettings(
|
|||
for (const other of pending) if (other > sequence) return false;
|
||||
return true;
|
||||
},
|
||||
failed: settle,
|
||||
failed: () => settle(false),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const lastWriteFailed = useCallback(
|
||||
(serverId: string) => outcomesRef.current.get(serverId)?.ok === false,
|
||||
[],
|
||||
);
|
||||
|
||||
const resolve = useCallback(
|
||||
(serverId: string): InspectorServerSettings | undefined => {
|
||||
const entry = serversRef.current.find((s) => s.id === serverId);
|
||||
|
|
@ -182,5 +212,5 @@ export function useLastPersistedSettings(
|
|||
[],
|
||||
);
|
||||
|
||||
return { begin, resolve };
|
||||
return { begin, resolve, lastWriteFailed };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue