fix(ui): recover newer builds after stale probes (#126628)

* fix(ui): recover newer builds after stale probes

* fix(ui): elect newest stale-build recovery target
This commit is contained in:
Peter Steinberger
2026-08-20 04:21:57 -07:00
committed by GitHub
parent b7b7eec5ee
commit c8f88a8b38
2 changed files with 127 additions and 29 deletions
+90 -8
View File
@@ -104,7 +104,7 @@ describe("scheduleStaleChunkReload", () => {
expect(storage.getItem(GUARD_KEY)).toBe("build-a");
});
it("never auto-reloads twice for the same build, but recovers on a newer build", async () => {
it("never lets a persisted build guard suppress recovery for a newer build", async () => {
const reload = vi.fn();
const storage = memoryStorage({ [GUARD_KEY]: "build-a" });
stubDocumentFetch(new Response(null, { status: 200 }));
@@ -119,7 +119,7 @@ describe("scheduleStaleChunkReload", () => {
expect(reload).not.toHaveBeenCalled();
await expect(
scheduleStaleChunkReload({
now: () => 7000,
now: () => 2000,
buildId: "build-b",
storage,
reload,
@@ -179,20 +179,102 @@ describe("scheduleStaleChunkReload", () => {
await expect(
scheduleStaleChunkReload({
now: () => 1000,
buildId: "build-a",
storage,
reload,
}),
).resolves.toBe(false);
await expect(scheduleStaleChunkReload({ now: () => 2000, storage, reload })).resolves.toBe(
false,
);
await expect(
scheduleStaleChunkReload({ now: () => 2000, buildId: "build-a", storage, reload }),
).resolves.toBe(false);
expect(fetchMock).toHaveBeenCalledTimes(1);
await expect(scheduleStaleChunkReload({ now: () => 7000, storage, reload })).resolves.toBe(
true,
);
await expect(
scheduleStaleChunkReload({ now: () => 7000, buildId: "build-a", storage, reload }),
).resolves.toBe(true);
expect(reload).toHaveBeenCalledTimes(1);
});
it("probes a newer build immediately after an older build was unreachable", async () => {
const reload = vi.fn();
const storage = memoryStorage();
const fetchMock = stubDocumentFetch(
new Response(null, { status: 503 }),
new Response(null, { status: 200 }),
);
await expect(
scheduleStaleChunkReload({ now: () => 1000, buildId: "build-a", storage, reload }),
).resolves.toBe(false);
await expect(
scheduleStaleChunkReload({ now: () => 2000, buildId: "build-b", storage, reload }),
).resolves.toBe(true);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(reload).toHaveBeenCalledTimes(1);
expect(storage.getItem(GUARD_KEY)).toBe("build-b");
});
it("reprobes a newer build after its joined older-build probe fails", async () => {
const olderProbe = deferred<Response>();
const fetchMock = vi
.fn<typeof fetch>()
.mockImplementationOnce(async () => olderProbe.promise)
.mockResolvedValueOnce(new Response(null, { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
const reload = vi.fn();
const storage = memoryStorage();
const olderBuild = scheduleStaleChunkReload({
now: () => 1000,
buildId: "build-a",
storage,
reload,
});
const newerBuild = scheduleStaleChunkReload({
now: () => 2000,
buildId: "build-b",
storage,
reload,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
olderProbe.resolve(new Response(null, { status: 503 }));
await expect(Promise.all([olderBuild, newerBuild])).resolves.toEqual([false, true]);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(reload).toHaveBeenCalledTimes(1);
expect(storage.getItem(GUARD_KEY)).toBe("build-b");
});
it("reloads only the newest build after a shared document probe succeeds", async () => {
const sharedProbe = deferred<Response>();
const fetchMock = vi.fn<typeof fetch>(async () => sharedProbe.promise);
vi.stubGlobal("fetch", fetchMock);
const reload = vi.fn();
const storage = memoryStorage();
const olderBuild = scheduleStaleChunkReload({
now: () => 1000,
buildId: "build-a",
storage,
reload,
});
const newerBuild = scheduleStaleChunkReload({
now: () => 2000,
buildId: "build-b",
storage,
reload,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
sharedProbe.resolve(new Response(null, { status: 200 }));
await expect(Promise.all([olderBuild, newerBuild])).resolves.toEqual([false, true]);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(reload).toHaveBeenCalledTimes(1);
expect(storage.getItem(GUARD_KEY)).toBe("build-b");
});
it("settles and aborts a hanging document probe after its deadline", async () => {
vi.useFakeTimers();
const reload = vi.fn();
+37 -21
View File
@@ -38,9 +38,12 @@ type MissingStylesheetRecoveryDeps = {
retry?: () => Promise<boolean>;
};
const lastAttemptAtByStorage = new WeakMap<object, number>();
let lastAttemptWithoutStorage: number | null = null;
let inFlightDocumentProbe: Promise<boolean> | null = null;
const recoveryByStorage = new WeakMap<
object,
{ attemptsByBuild: Map<string, number>; latestBuildId: string }
>();
const unavailableStorage = {};
let inFlightDocumentProbe: { buildId?: string; promise: Promise<boolean> } | null = null;
export function isStaleChunkImportError(error: unknown): boolean {
return (
@@ -65,9 +68,9 @@ function sessionStorageOrNull(): Pick<Storage, "getItem" | "setItem"> | null {
}
}
function probeControlUiDocument(): Promise<boolean> {
function probeControlUiDocument(buildId?: string): Promise<boolean> {
if (inFlightDocumentProbe) {
return inFlightDocumentProbe;
return inFlightDocumentProbe.promise;
}
const probe = (async () => {
const controller = new AbortController();
@@ -86,11 +89,11 @@ function probeControlUiDocument(): Promise<boolean> {
}
})();
const settledProbe = probe.finally(() => {
if (inFlightDocumentProbe === settledProbe) {
if (inFlightDocumentProbe?.promise === settledProbe) {
inFlightDocumentProbe = null;
}
});
inFlightDocumentProbe = settledProbe;
inFlightDocumentProbe = { buildId, promise: settledProbe };
return settledProbe;
}
@@ -125,19 +128,7 @@ function persistGuardBuildId(
* app webviews) instead of the recoverable panel error.
*/
export async function scheduleStaleChunkReload(deps: StaleChunkReloadDeps = {}): Promise<boolean> {
const now = deps.now?.() ?? Date.now();
const storage = deps.storage === undefined ? sessionStorageOrNull() : deps.storage;
const lastAttemptAt = storage
? (lastAttemptAtByStorage.get(storage) ?? null)
: lastAttemptWithoutStorage;
if (lastAttemptAt !== null && now - lastAttemptAt < ATTEMPT_COOLDOWN_MS) {
return false;
}
if (storage) {
lastAttemptAtByStorage.set(storage, now);
} else {
lastAttemptWithoutStorage = now;
}
const buildId = deps.buildId ?? CONTROL_UI_BUILD_INFO.buildId;
// One automatic reload per build id: if the reloaded document still fails
// with the same build, the build itself is broken and reloading cannot help.
@@ -145,13 +136,38 @@ export async function scheduleStaleChunkReload(deps: StaleChunkReloadDeps = {}):
if (readGuardBuildId(storage) === buildId) {
return false;
}
if (!(await probeControlUiDocument())) {
const now = deps.now?.() ?? Date.now();
const storageIdentity = storage ?? unavailableStorage;
const recovery = recoveryByStorage.get(storageIdentity) ?? {
attemptsByBuild: new Map<string, number>(),
latestBuildId: buildId,
};
const { attemptsByBuild } = recovery;
for (const [attemptedBuildId, attemptedAt] of attemptsByBuild) {
if (now - attemptedAt >= ATTEMPT_COOLDOWN_MS) {
attemptsByBuild.delete(attemptedBuildId);
}
}
if (attemptsByBuild.has(buildId)) {
return false;
}
attemptsByBuild.set(buildId, now);
recovery.latestBuildId = buildId;
recoveryByStorage.set(storageIdentity, recovery);
// A newer build cannot inherit the failed probe started for an older build.
const joinedOlderBuildProbe = Boolean(
inFlightDocumentProbe && inFlightDocumentProbe.buildId !== buildId,
);
if (
!(await probeControlUiDocument(buildId)) &&
(!joinedOlderBuildProbe || !(await probeControlUiDocument(buildId)))
) {
return false;
}
// A reload resets the in-memory state, so without a persisted guard a broken
// build would reload forever. When storage is unavailable or rejects the
// write, leave recovery to the manual Retry path instead of reloading.
if (!persistGuardBuildId(storage, buildId)) {
if (recovery.latestBuildId !== buildId || !persistGuardBuildId(storage, buildId)) {
return false;
}
(deps.reload ?? reloadControlUiDocument)();