fix: guard old publish polls and recovered failures

This commit is contained in:
Oleg Isonen committed 2026-10-21 02:00:46 +01:00
1 parent 62f42bff9d
commit 51573cd848
2 files changed
+130 -12

No files matched your search

@@ -12,6 +12,7 @@ import { PublishValidationResults } from "./publish-validation-results";
const {
usePublishProgress,
waitForPublishStatus,
waitForOwnedPublishStatus,
refreshUntilSuccess,
useManualValidationFindings,
getRefreshedProject,
@@ -357,6 +358,45 @@ test("publish polling stops after navigating to another project", async () => {
expect($project.get()).toBe(newProject);
});
test("an old poll cannot update a newly mounted copy of the same project", async () => {
const firstProject = { id: "project" } as Project;
$project.set(firstProject);
let progress: ReturnType<typeof usePublishProgress> | undefined;
const Harness = () => {
progress = usePublishProgress("project");
return null;
};
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
act(() => root?.render(<Harness />));
let finishQuery: (result: {
success: true;
project: Project;
}) => void = () => {};
const query = new Promise<{ success: true; project: Project }>((resolve) => {
finishQuery = resolve;
});
const polling = waitForOwnedPublishStatus({
projectId: "project",
isCurrentProject: progress!.isCurrentProject,
query: () => query,
});
act(() => root?.unmount());
$project.set({ id: "another-project" } as Project);
const reopenedProject = { id: "project", domain: "new" } as Project;
$project.set(reopenedProject);
finishQuery({
success: true,
project: { id: "project", domain: "old" } as Project,
});
await expect(polling).resolves.toBeUndefined();
expect($project.get()).toBe(reopenedProject);
});
test("publish completion does not show a toast while the Publish panel is open", () => {
const successToast = vi.spyOn(toast, "success").mockImplementation(() => "");
const errorToast = vi.spyOn(toast, "error").mockImplementation(() => "");
@@ -790,6 +830,59 @@ test("a newer build clears a recovered transport error", async () => {
expect(container.querySelector("output")?.textContent).toBe("");
});
test("a recovered failed build replaces a lost response error", async () => {
$project.set({
id: "project",
latestBuildVirtual: { buildId: "old" },
} as Project);
$publishDialog.set("publish");
const Harness = () => {
const progress = usePublishProgress("project");
return (
<>
<button
onClick={() =>
progress.start(() =>
Promise.reject(new Error("Network unavailable"))
)
}
>
Publish
</button>
<button
onClick={() =>
progress.reconcileRefreshedProject({
latestBuildVirtual: {
buildId: "new",
createdAt: new Date().toISOString(),
publishStatus: "FAILED",
},
} as Project)
}
>
Refresh
</button>
<output>{progress.publishError ?? progress.requestError}</output>
</>
);
};
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
act(() => root?.render(<Harness />));
const [publish, refresh] = container.querySelectorAll("button");
await act(async () => publish.click());
expect(container.querySelector("output")?.textContent).toBe(
"Network unavailable"
);
act(() => refresh.click());
expect(container.querySelector("output")?.textContent).toContain(
"Publish failed"
);
});
test("publish diagnostics remain visible after the panel remounts", () => {
$project.set({ id: "project" } as Project);
@@ -590,6 +590,7 @@ const usePublishProgress = (projectId: Project["id"]) => {
const [now, setNow] = useState(Date.now);
const isMounted = useRef(false);
const requestBaselineBuildId = useRef<string>();
const hasStartedPublishRequest = useRef(false);
const refreshGeneration = useRef(0);
const recoveredPoll = useRef<{
generation: number;
@@ -648,17 +649,24 @@ const usePublishProgress = (projectId: Project["id"]) => {
}, []);
const reconcileRefreshedProject = useCallback((project: Project) => {
if (
project.latestBuildVirtual != null &&
getPublishStatusAndText(project.latestBuildVirtual).status !== "FAILED"
) {
const build = project.latestBuildVirtual;
if (build == null) {
return;
}
const { status, statusText } = getPublishStatusAndText(build);
const isNewBuild =
build.buildId !== undefined &&
build.buildId !== requestBaselineBuildId.current;
if (status === "FAILED") {
if (hasStartedPublishRequest.current && isNewBuild) {
setRequestError(undefined);
setPublishErrorState({ value: statusText, source: "status" });
}
} else {
setPublishErrorState((current) =>
current?.source === "status" ? undefined : current
);
if (
project.latestBuildVirtual.buildId !== undefined &&
project.latestBuildVirtual.buildId !== requestBaselineBuildId.current
) {
if (isNewBuild) {
setRequestError(undefined);
}
}
@@ -713,6 +721,7 @@ const usePublishProgress = (projectId: Project["id"]) => {
const start = (publish: () => Promise<void>) => {
const startedAt = Date.now();
hasStartedPublishRequest.current = true;
requestBaselineBuildId.current =
$project.get()?.latestBuildVirtual?.buildId;
setStartedAt(startedAt);
@@ -796,6 +805,21 @@ const waitForPublishStatus = async (
}
};
const waitForOwnedPublishStatus = ({
projectId,
isCurrentProject,
query,
}: {
projectId: Project["id"];
isCurrentProject: () => boolean;
query?: () => ReturnType<typeof nativeClient.domain.project.query>;
}) =>
waitForPublishStatus(
() =>
getRefreshedProject({ projectId, isCurrent: isCurrentProject, query }),
isCurrentProject
);
const refreshUntilSuccess = async (
refresh: (showError: boolean) => Promise<Project | undefined>,
isOpen: () => boolean,
@@ -1100,10 +1124,10 @@ const Publish = ({
return;
}
const result = await waitForPublishStatus(
() => getRefreshedProject({ projectId: project.id }),
() => $project.get()?.id === project.id
);
const result = await waitForOwnedPublishStatus({
projectId: project.id,
isCurrentProject,
});
reportPublishResult({
result,
timesLeft,
@@ -2111,6 +2135,7 @@ export const __testing__ = {
useManualValidationFindings,
restoreValidatedDomains,
waitForPublishStatus,
waitForOwnedPublishStatus,
refreshUntilSuccess,
getRefreshedProject,
reportPublishResult,