diff --git a/packages/void/src/pages/navigation-engine.ts b/packages/void/src/pages/navigation-engine.ts index 6e4f49873..843e0c31f 100644 --- a/packages/void/src/pages/navigation-engine.ts +++ b/packages/void/src/pages/navigation-engine.ts @@ -651,6 +651,8 @@ export async function createPageNavigationEngine( return r.json(); }); }); + // Drop the stale entry so the revalidated data lands (set() won't overwrite a usable one). + _prefetchCache.flush(fetchUrl, method); _prefetchCache.set(fetchUrl, method, bgFetch, PREFETCH_DEFAULT_CACHE_FOR); } } else { diff --git a/packages/void/test/unit/pages-client.test.ts b/packages/void/test/unit/pages-client.test.ts index 393aefec1..d2aa9f17d 100644 --- a/packages/void/test/unit/pages-client.test.ts +++ b/packages/void/test/unit/pages-client.test.ts @@ -155,6 +155,51 @@ describe('createVoidRouter prefetch', () => { expect(fetchMock).toHaveBeenCalledTimes(1); }); + it('serves revalidated data after a stale-while-revalidate refresh', async () => { + stubClientGlobals(); + let n = 0; + vi.stubGlobal( + 'fetch', + vi.fn(() => { + n += 1; + return Promise.resolve( + Response.json( + { component: 'users', props: { v: n } }, + { headers: { 'X-VoidPages': 'true' } }, + ), + ); + }), + ); + + const { router } = await createVoidRouter({ + adapter: { + applyUpdate: vi.fn(), + createDeferred: vi.fn(() => ({})), + isLoading: vi.fn(() => false), + rejectDeferred: vi.fn((_ref, error) => error), + resolveDeferred: vi.fn((_ref, value) => value), + }, + components: { + home: vi.fn(async () => ({ default: {} })), + users: vi.fn(async () => ({ default: {} })), + }, + layoutTree: {}, + matchRoute: vi.fn((url) => (url === '/users' ? 'users' : 'home')), + prefetchDefaultCacheFor: parseCacheFor(['5ms', '10s']), + prefetchHoverDelay: 75, + routeMeta: { users: {} }, + viewTransitions: false, + }); + + await expect(router.prefetch('/users')).resolves.toMatchObject({ props: { v: 1 } }); + await new Promise((resolve) => setTimeout(resolve, 30)); + // Visit while stale: serves v:1 and fires the background revalidate to v:2. + await router.visit('/users'); + await new Promise((resolve) => setTimeout(resolve, 30)); + // The next read must return the revalidated v:2, not the discarded-refresh stale v:1. + await expect(router.prefetch('/users')).resolves.toMatchObject({ props: { v: 2 } }); + }); + it('does not resolve disposed initial deferred props into a later page', async () => { stubClientGlobals(); const initialPayload = {