Skip to content

Commit 6a3504d

Browse files
committed
[breaking] respect cache-control max-age on the client
...for initially fetched responses Closes #4625 The proposed etag logic is not implemented because it sounds too complex and would add needless code for 99% of users This is a breaking change insofar that people could have relied on the old behavior somehow
1 parent cbbd9ab commit 6a3504d

File tree

7 files changed

+92
-3
lines changed

7 files changed

+92
-3
lines changed

.changeset/serious-bottles-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[breaking] respect cache-control max-age on the client for initially fetched responses

packages/kit/src/runtime/client/client.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { onMount, tick } from 'svelte';
22
import { normalize_error } from '../../utils/error.js';
33
import { make_trackable, decode_params, normalize_path } from '../../utils/url.js';
44
import { find_anchor, get_base_uri, scroll_state } from './utils.js';
5-
import { lock_fetch, unlock_fetch, initial_fetch, native_fetch } from './fetcher.js';
5+
import { lock_fetch, unlock_fetch, initial_fetch, subsequent_fetch } from './fetcher.js';
66
import { parse } from './parse.js';
77
import { error } from '../../exports/index.js';
88

@@ -574,7 +574,9 @@ export function create_client({ target, base, trailing_slash }) {
574574
depends(normalized);
575575

576576
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be normalized
577-
return started ? native_fetch(normalized, init) : initial_fetch(requested, init);
577+
return started
578+
? subsequent_fetch(requested, normalized, init)
579+
: initial_fetch(requested, init);
578580
},
579581
setHeaders: () => {}, // noop
580582
depends,

packages/kit/src/runtime/client/fetcher.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { hash } from '../hash.js';
22

33
let loading = 0;
44

5-
export const native_fetch = window.fetch;
5+
const native_fetch = window.fetch;
66

77
export function lock_fetch() {
88
loading += 1;
@@ -37,7 +37,12 @@ if (import.meta.env.DEV) {
3737
};
3838
}
3939

40+
const start = new Date().getTime();
41+
const cache = new Map();
42+
4043
/**
44+
* Should be called on the initial run of load functions that hydrate the page.
45+
* Saves any requests with cache-control max-age to the cache.
4146
* @param {RequestInfo} resource
4247
* @param {RequestInit} [opts]
4348
*/
@@ -53,8 +58,39 @@ export function initial_fetch(resource, opts) {
5358
const script = document.querySelector(selector);
5459
if (script && script.textContent) {
5560
const { body, ...init } = JSON.parse(script.textContent);
61+
const match = /** @type {string | undefined } */ (init?.headers?.['cache-control'])?.match(
62+
/max-age=(\d+)/
63+
);
64+
if (match) {
65+
const age = Number(init?.headers?.age ?? '0');
66+
const cache_time = Number(match[1]) - age;
67+
cache.set(url, { body, init, cache_time });
68+
}
5669
return Promise.resolve(new Response(body, init));
5770
}
5871

5972
return native_fetch(resource, opts);
6073
}
74+
75+
/**
76+
* Tries to get the response from the cache, if max-age allows it, else does a fetch.
77+
* @param {RequestInfo} original
78+
* @param {RequestInfo} resource
79+
* @param {RequestInit} [opts]
80+
*/
81+
export function subsequent_fetch(original, resource, opts) {
82+
if (cache.size) {
83+
const url = JSON.stringify(typeof original === 'string' ? original : original.url);
84+
const cached = cache.get(url);
85+
if (cached) {
86+
const { body, init, cache_time } = cached;
87+
const now = new Date().getTime();
88+
if ((now - start) / 1000 < cache_time) {
89+
return Promise.resolve(new Response(body, init));
90+
}
91+
cache.delete(url);
92+
}
93+
}
94+
95+
return native_fetch(resource, opts);
96+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* @type {import('./$types').PageLoad}
3+
*/
4+
export async function load({ fetch, depends }) {
5+
depends('cache:control');
6+
const resp = await fetch('./cache-control/called');
7+
return resp.json();
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script>
2+
import { invalidate } from '$app/navigation';
3+
/**
4+
* @type {import('./$types').PageData}
5+
*/
6+
export let data;
7+
async function fetch_again() {
8+
await fetch('./cache-control/called', {method: 'POST'})
9+
invalidate('cache:control');
10+
}
11+
</script>
12+
13+
<p>Count is {data.count}</p>
14+
<button on:click={fetch_again}>Fetch again</button>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { json } from '@sveltejs/kit';
2+
3+
let count = 0;
4+
5+
export function GET({ setHeaders }) {
6+
setHeaders({ 'cache-control': 'public, max-age=4', age: '2' });
7+
return json({ count });
8+
}
9+
10+
export function POST() {
11+
count++;
12+
}

packages/kit/test/apps/basics/test/client.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,18 @@ test.describe('Load', () => {
381381
expect(await page.textContent('pre')).toBe(JSON.stringify({ foo: { bar: 'Custom layout' } }));
382382
});
383383

384+
test('load does not call fetch is max-age allows it', async ({ page }) => {
385+
await page.goto('/load/cache-control');
386+
expect(await page.textContent('p')).toBe('Count is 0');
387+
await page.waitForTimeout(500);
388+
await page.click('button');
389+
await page.waitForTimeout(500);
390+
expect(await page.textContent('p')).toBe('Count is 0');
391+
await page.waitForTimeout(2000);
392+
await page.click('button');
393+
await expect(page.locator('p')).toHaveText('Count is 2');
394+
});
395+
384396
if (process.env.DEV) {
385397
test('using window.fetch causes a warning', async ({ page }) => {
386398
const port = 5173;

0 commit comments

Comments
 (0)