fix(web): resolve the rollback baseline at failure time, not at issue
Review round 3 on #2091. The baseline was still captured when the write went out, so overlapping toggles reproduced the original bug through the new machinery: toggle 1 PUTs `true` while its reload is pending, toggle 2 reads the baseline and sees `false`, toggle 1 lands `true`, toggle 2 fails — and its catch pushed the `false` it had closed over, contradicting both disk and the hook. `resolve` now reads the record and the list live and takes no fallback, returning `undefined` when neither has anything to say, so calling it in the failure handler answers for the state at failure time. Both the value being written and the rollback go through it, each at the moment it is needed. The hook says explicitly not to hoist the result. The regression test drives the exact order — issue 1, issue 2, land 1, fail 2 — with deferred promises rather than timing, and fails on the issue-time capture. 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
c67ea8a002
commit
ce29d8273d
1 changed files with 117 additions and 55 deletions
|
|
@ -2465,6 +2465,62 @@ describe("App paginated list pagination toggle (#1721)", () => {
|
|||
};
|
||||
expect(lastPush?.paginatedLists).toBe(true);
|
||||
});
|
||||
|
||||
it("rolls back to a write that landed while this one was in flight (#2089)", async () => {
|
||||
// Overlapping toggles. The baseline cannot be captured when a write is
|
||||
// issued: toggle 1 is still in flight when toggle 2 reads it, so toggle 2
|
||||
// would close over `false` — and by the time toggle 2 fails, toggle 1 has
|
||||
// landed `true` on disk. Rolling back to the captured value contradicts
|
||||
// disk exactly as the stale-list case does; the baseline has to be resolved
|
||||
// at failure time. The two writes are driven by deferred promises so the
|
||||
// order — issue 1, issue 2, land 1, fail 2 — is exact rather than timed.
|
||||
const user = userEvent.setup();
|
||||
let landFirst: (() => void) | undefined;
|
||||
let failSecond: ((err: Error) => void) | undefined;
|
||||
const first = new Promise<void>((resolve) => {
|
||||
landFirst = resolve;
|
||||
});
|
||||
const second = new Promise<void>((_resolve, reject) => {
|
||||
failSecond = (err) => {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
updateServerSettingsSpy
|
||||
.mockImplementationOnce(() => first)
|
||||
.mockImplementationOnce(() => second);
|
||||
|
||||
renderWithMantine(<App />);
|
||||
await user.click(screen.getByText("connect"));
|
||||
await waitFor(() => expect(clientInstances).toHaveLength(1));
|
||||
|
||||
await user.click(screen.getByText("paginated-on"));
|
||||
await user.click(screen.getByText("paginated-off"));
|
||||
await waitFor(() =>
|
||||
expect(updateServerSettingsSpy).toHaveBeenCalledTimes(2),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
landFirst?.();
|
||||
await first;
|
||||
});
|
||||
await act(async () => {
|
||||
failSecond?.(new Error("disk full"));
|
||||
await second.catch(() => undefined);
|
||||
});
|
||||
|
||||
// `true` is what the landed write put on disk — not the `false` the second
|
||||
// toggle read before it got there.
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("tools-paginated")).toHaveTextContent("true"),
|
||||
);
|
||||
const client = clientInstances[0] as EventTarget & {
|
||||
setServerSettings: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
const lastPush = client.setServerSettings.mock.calls.at(-1)?.[0] as {
|
||||
paginatedLists?: boolean;
|
||||
};
|
||||
expect(lastPush?.paginatedLists).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// A background command runs through `runCommandInBackground`, which owns the
|
||||
|
|
|
|||
Loading…
Reference in a new issue