Make overridden TTL trump all headers

This commit is contained in:
Martijn Walraven 2018-07-17 21:11:59 -07:00
parent c31b02972a
commit 3308b897d4
3 changed files with 93 additions and 54 deletions

View file

@ -37,13 +37,16 @@ export class HTTPCache {
);
}
const { policy: policyRaw, body } = JSON.parse(entry);
const { policy: policyRaw, ttlOverride, body } = JSON.parse(entry);
const policy = CachePolicy.fromObject(policyRaw);
// Remove url from the policy, because otherwise it would never match a request with a custom cache key
policy._url = undefined;
if (policy.satisfiesWithoutRevalidation(policyRequestFrom(request))) {
if (
(ttlOverride && policy.age() < ttlOverride) ||
policy.satisfiesWithoutRevalidation(policyRequestFrom(request))
) {
const headers = policy.responseHeaders();
return new Response(body, {
url: policy._url,
@ -65,13 +68,11 @@ export class HTTPCache {
);
return this.storeResponseAndReturnClone(
modified
? revalidationResponse
: new Response(body, {
url: revalidatedPolicy._url,
status: revalidatedPolicy._status,
headers: revalidatedPolicy.responseHeaders(),
}),
new Response(modified ? await revalidationResponse.text() : body, {
url: revalidatedPolicy._url,
status: revalidatedPolicy._status,
headers: revalidatedPolicy.responseHeaders(),
}),
request,
revalidatedPolicy,
cacheKey,
@ -93,17 +94,12 @@ export class HTTPCache {
cacheOptions = cacheOptions(response, request);
}
let ttl = cacheOptions && cacheOptions.ttl;
let ttlOverride = cacheOptions && cacheOptions.ttl;
if (ttl) {
policy._rescc = { 'max-age': ttl };
}
if (!ttlOverride && !policy.storable()) return response;
if (!policy.storable()) return response;
if (!ttl) {
ttl = Math.round(policy.timeToLive() / 1000);
}
let ttl = ttlOverride || Math.round(policy.timeToLive() / 1000);
if (ttl <= 0) return response;
// If a response can be revalidated, we don't want to remove it from the cache right after it expires.
// We may be able to use better heuristics here, but for now we'll take the max-age times 2.
@ -111,11 +107,10 @@ export class HTTPCache {
ttl *= 2;
}
if (ttl <= 0) return response;
const body = await response.text();
const entry = JSON.stringify({
policy: policy.toObject(),
ttlOverride,
body,
});
@ -131,7 +126,7 @@ export class HTTPCache {
url: response.url,
status: response.status,
statusText: response.statusText,
headers: policy.responseHeaders(),
headers: response.headers,
});
}
}

View file

@ -86,50 +86,93 @@ describe('HTTPCache', () => {
expect(response.headers.get('Age')).toEqual('0');
});
it('allows overriding the TTL', async () => {
fetch.mockJSONResponseOnce(
{ name: 'Ada Lovelace' },
{ 'Cache-Control': 'private, no-cache' },
);
describe('overriding TTL', () => {
it('returns a cached response when the overridden TTL is not expired', async () => {
fetch.mockJSONResponseOnce(
{ name: 'Ada Lovelace' },
{
'Cache-Control': 'private, no-cache',
'Set-Cookie': 'foo',
},
);
await httpCache.fetch(new Request('https://api.example.com/people/1'), {
cacheOptions: {
ttl: 30,
},
await httpCache.fetch(new Request('https://api.example.com/people/1'), {
cacheOptions: {
ttl: 30,
},
});
advanceTimeBy(10000);
const response = await httpCache.fetch(
new Request('https://api.example.com/people/1'),
);
expect(fetch.mock.calls.length).toEqual(1);
expect(await response.json()).toEqual({ name: 'Ada Lovelace' });
expect(response.headers.get('Age')).toEqual('10');
});
advanceTimeBy(10000);
it('fetches a fresh response from the origin when the overridden TTL expired', async () => {
fetch.mockJSONResponseOnce(
{ name: 'Ada Lovelace' },
{
'Cache-Control': 'private, no-cache',
'Set-Cookie': 'foo',
},
);
const response = await httpCache.fetch(
new Request('https://api.example.com/people/1'),
);
await httpCache.fetch(new Request('https://api.example.com/people/1'), {
cacheOptions: {
ttl: 30,
},
});
expect(fetch.mock.calls.length).toEqual(1);
expect(await response.json()).toEqual({ name: 'Ada Lovelace' });
expect(response.headers.get('Age')).toEqual('10');
});
advanceTimeBy(30000);
it('allows overriding the TTL dynamically', async () => {
fetch.mockJSONResponseOnce(
{ name: 'Ada Lovelace' },
{ 'Cache-Control': 'private, no-cache' },
);
fetch.mockJSONResponseOnce(
{ name: 'Alan Turing' },
{
'Cache-Control': 'private, no-cache',
'Set-Cookie': 'foo',
},
);
await httpCache.fetch(new Request('https://api.example.com/people/1'), {
cacheOptions: (response: Response, request: Request) => ({
ttl: 30,
}),
const response = await httpCache.fetch(
new Request('https://api.example.com/people/1'),
);
expect(fetch.mock.calls.length).toEqual(2);
expect(await response.json()).toEqual({ name: 'Alan Turing' });
expect(response.headers.get('Age')).toEqual('0');
});
advanceTimeBy(10000);
it('allows overriding the TTL dynamically', async () => {
fetch.mockJSONResponseOnce(
{ name: 'Ada Lovelace' },
{
'Cache-Control': 'private, no-cache',
'Set-Cookie': 'foo',
},
);
const response = await httpCache.fetch(
new Request('https://api.example.com/people/1'),
);
await httpCache.fetch(new Request('https://api.example.com/people/1'), {
cacheOptions: (response: Response, request: Request) => ({
ttl: 30,
}),
});
expect(fetch.mock.calls.length).toEqual(1);
expect(await response.json()).toEqual({ name: 'Ada Lovelace' });
expect(response.headers.get('Age')).toEqual('10');
advanceTimeBy(10000);
const response = await httpCache.fetch(
new Request('https://api.example.com/people/1'),
);
expect(fetch.mock.calls.length).toEqual(1);
expect(await response.json()).toEqual({ name: 'Ada Lovelace' });
expect(response.headers.get('Age')).toEqual('10');
});
});
it('allows specifying a custom cache key', async () => {

View file

@ -20,6 +20,7 @@ declare module 'http-cache-semantics' {
satisfiesWithoutRevalidation(request: Request): boolean;
responseHeaders(): Headers;
age(): number;
timeToLive(): number;
revalidationHeaders(request: Request): Headers;