Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions packages/void/src/runtime/routing-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type ManifestLoader = (
) => Promise<ClientManifest | null>;
export function createManifestLoader(): ManifestLoader {
let cached: ClientManifest | null = null;
let pending: Promise<ClientManifest | null> | null = null;
return async (request, env) => {
if (cached) {
return cached;
Expand All @@ -68,14 +69,21 @@ export function createManifestLoader(): ManifestLoader {
if (!assets || typeof assets.fetch !== 'function') {
return null;
}
const response = await assets.fetch(
new Request(new URL('/.vite/manifest.json', request.url).toString()),
);
if (!response.ok) {
return null;
}
cached = (await response.json()) as ClientManifest;
return cached;
// Concurrent cold requests share one fetch and parse. A missing or failed
// manifest is not cached, so a later request retries.
pending ??= (async () => {
const response = await assets.fetch(
new Request(new URL('/.vite/manifest.json', request.url).toString()),
);
if (!response.ok) {
return null;
}
cached = (await response.json()) as ClientManifest;
return cached;
})().finally(() => {
pending = null;
});
return pending;
};
}
export type ClientAssetOptions = {
Expand Down
36 changes: 36 additions & 0 deletions packages/void/test/unit/routing-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,42 @@ describe('typed client asset tags', () => {
expect(islandTags.preloads).not.toContain('/md.js');
expect(binding).toHaveBeenCalledTimes(1);
});

it('shares one manifest fetch between concurrent cold loads', async () => {
const binding = vi.fn(async () => Response.json({ page: { file: 'page.js' } }));
const loader = createManifestLoader();
const env = { ASSETS: { fetch: binding } };
const request = new Request('https://app.test/page');
const manifests = await Promise.all(Array.from({ length: 8 }, () => loader(request, env)));
expect(binding).toHaveBeenCalledTimes(1);
expect(new Set(manifests).size).toBe(1);
expect(manifests[0]).toEqual({ page: { file: 'page.js' } });
expect(await loader(request, env)).toBe(manifests[0]);
expect(binding).toHaveBeenCalledTimes(1);
});

it('retries the manifest after a missing response or a rejected fetch', async () => {
const manifest = { page: { file: 'page.js' } };
const binding = vi
.fn<() => Promise<Response>>()
.mockResolvedValueOnce(new Response('missing', { status: 404 }))
.mockRejectedValueOnce(new Error('assets unavailable'))
.mockImplementation(async () => Response.json(manifest));
const loader = createManifestLoader();
const env = { ASSETS: { fetch: binding } };
const request = new Request('https://app.test/page');
expect(await loader(request, env)).toBeNull();
expect(binding).toHaveBeenCalledTimes(1);
const rejected = [loader(request, env), loader(request, env)];
await expect(rejected[0]).rejects.toThrow('assets unavailable');
await expect(rejected[1]).rejects.toThrow('assets unavailable');
expect(binding).toHaveBeenCalledTimes(2);
expect(await loader(request, env)).toEqual(manifest);
expect(binding).toHaveBeenCalledTimes(3);
await loader(request, env);
expect(binding).toHaveBeenCalledTimes(3);
});

it('loads development CSS with active roots and omits client tags when production has no manifest', async () => {
const devCss = vi.fn(
async (
Expand Down
Loading