From d5b44791347944e58fb331cfe17978e9d58ec046 Mon Sep 17 00:00:00 2001 From: lllleolin-max <244385774+lllleolin-max@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:55:23 +0800 Subject: [PATCH] fix(client): ignore retries from superseded page loads --- __tests__/e2e/router-race/fast.md | 1 + __tests__/e2e/router-race/index.md | 16 ++++ __tests__/e2e/router-race/router-race.test.ts | 95 +++++++++++++++++++ __tests__/e2e/router-race/slow.md | 1 + src/client/app/router.ts | 10 +- 5 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 __tests__/e2e/router-race/fast.md create mode 100644 __tests__/e2e/router-race/index.md create mode 100644 __tests__/e2e/router-race/router-race.test.ts create mode 100644 __tests__/e2e/router-race/slow.md diff --git a/__tests__/e2e/router-race/fast.md b/__tests__/e2e/router-race/fast.md new file mode 100644 index 000000000..a50ee5a75 --- /dev/null +++ b/__tests__/e2e/router-race/fast.md @@ -0,0 +1 @@ +# Fast Page diff --git a/__tests__/e2e/router-race/index.md b/__tests__/e2e/router-race/index.md new file mode 100644 index 000000000..0c68e3ae0 --- /dev/null +++ b/__tests__/e2e/router-race/index.md @@ -0,0 +1,16 @@ + + +# Navigation Race + + + diff --git a/__tests__/e2e/router-race/router-race.test.ts b/__tests__/e2e/router-race/router-race.test.ts new file mode 100644 index 000000000..8a085af99 --- /dev/null +++ b/__tests__/e2e/router-race/router-race.test.ts @@ -0,0 +1,95 @@ +import type { Route } from 'playwright-chromium' + +const slowModule = /(?:\/router-race\/slow\.md\?|\/router-race_slow\.md\.)/ + +describe.runIf(process.env.VITE_TEST_BUILD)('page load retries', () => { + let slowRequest: Route | undefined + + beforeEach(async () => { + slowRequest = undefined + await page.route(slowModule, (route) => { + if (slowRequest) return route.continue() + slowRequest = route + }) + await goto('/router-race/') + }) + + afterEach(async () => { + await page.unrouteAll({ behavior: 'ignoreErrors' }) + }) + + async function startSlowNavigation() { + await page.locator('#slow').click() + await expect.poll(() => !!slowRequest).toBe(true) + } + + async function navigateToFastPage() { + await page.locator('#fast').click() + await expect + .poll(() => page.locator('h1').textContent()) + .toContain('Fast Page') + } + + async function expectFastPageAfterSlowNavigation() { + await page.waitForFunction( + () => document.documentElement.dataset.slowNavigationFinished === 'true' + ) + expect(await page.locator('h1').textContent()).toContain('Fast Page') + expect(new URL(page.url()).pathname).toBe('/router-race/fast.html') + } + + test('ignores a failed page request after another navigation completes', async () => { + await startSlowNavigation() + await navigateToFastPage() + await slowRequest!.abort() + + await expectFastPageAfterSlowNavigation() + }) + + test('ignores a retry after another navigation completes during a hash map fetch', async () => { + let hashMapRequest: Route | undefined + await page.route('**/hashmap.json', (route) => { + hashMapRequest = route + }) + + await startSlowNavigation() + await slowRequest!.abort() + await expect.poll(() => !!hashMapRequest).toBe(true) + await navigateToFastPage() + await hashMapRequest!.continue() + + await expectFastPageAfterSlowNavigation() + }) + + test('retries the current page with its updated hash after a deployment', async () => { + await page.route('**/hashmap.json', async (route) => { + const response = await route.fetch() + const map = await response.json() + map['router-race_slow.md'] = 'retry' + await route.fulfill({ json: map }) + }) + + await startSlowNavigation() + const response = await slowRequest!.fetch() + await page.route('**/router-race_slow.md.retry.js', (route) => + route.fulfill({ response }) + ) + await slowRequest!.abort() + + await page.waitForFunction( + () => document.documentElement.dataset.slowNavigationFinished === 'true' + ) + expect(await page.locator('h1').textContent()).toContain('Slow Page') + expect(new URL(page.url()).pathname).toBe('/router-race/slow.html') + }) + + test('still shows the not-found page when the current retry fails', async () => { + await startSlowNavigation() + await slowRequest!.abort() + + await page.waitForFunction( + () => document.documentElement.dataset.slowNavigationFinished === 'true' + ) + expect(await page.locator('h1').textContent()).toBe('PAGE NOT FOUND') + }) +}) diff --git a/__tests__/e2e/router-race/slow.md b/__tests__/e2e/router-race/slow.md new file mode 100644 index 000000000..5cb769b2e --- /dev/null +++ b/__tests__/e2e/router-race/slow.md @@ -0,0 +1 @@ +# Slow Page diff --git a/src/client/app/router.ts b/src/client/app/router.ts index bb03338c6..6fad25582 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -106,12 +106,12 @@ export function createRouter( if (!page) throw new Error(`Page not found: ${pendingPath}`) if (latestPendingPath === pendingPath) { - latestPendingPath = null - const { default: comp, __pageData } = page if (!comp) throw new Error(`Invalid route component: ${comp}`) await router.onAfterPageLoad?.(href) + if (latestPendingPath !== pendingPath) return + latestPendingPath = null route.path = inBrowser ? pendingPath : withBase(pendingPath) route.component = markRaw(comp) @@ -151,10 +151,12 @@ export function createRouter( // retry on fetch fail: the page to hash map may have been invalidated // because a new deploy happened while the page is open. Try to fetch // the updated pageToHash map and fetch again. - if (!isRetry) { + if (!isRetry && latestPendingPath === pendingPath) { try { const res = await fetch(runtimeBase() + 'hashmap.json') - ;(window as any).__VP_HASH_MAP__ = await res.json() + const hashMap = await res.json() + if (latestPendingPath !== pendingPath) return + ;(window as any).__VP_HASH_MAP__ = hashMap await loadPage(href, { scrollPosition, isRetry: true, initialLoad }) return } catch (e) {}