Browse Source

Use expires when cache-control is present without max-age. Use no-cache and no-store.

Update dependencies.

Add line break to end of .gitignore
pull/33/head
John Wehr 4 years ago
parent
commit
bbdeabd972
  1. 1
      .gitignore
  2. 18
      package.json
  3. 106
      src/index.js
  4. 35
      test/expired.in.js
  5. 19
      test/expired.js
  6. 79
      test/expired.on.js
  7. 39
      test/headers.js

1
.gitignore

@ -70,3 +70,4 @@ Icon
Network Trash Folder Network Trash Folder
Temporary Items Temporary Items
.apdisk .apdisk
yarn.lock

18
package.json

@ -1,11 +1,11 @@
{ {
"name": "expired", "name": "expired",
"version": "1.3.12", "version": "1.4.0",
"description": "Calculate when HTTP responses expire from the cache headers", "description": "Calculate when HTTP responses expire from the cache headers",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"test": "nyc ava", "test": "nyc ava",
"lint": "xo", "lint": "xo --fix",
"coverage": "nyc report --reporter=text-lcov | coveralls" "coverage": "nyc report --reporter=text-lcov | coveralls"
}, },
"repository": { "repository": {
@ -30,15 +30,13 @@
}, },
"homepage": "https://github.com/lukechilds/expired", "homepage": "https://github.com/lukechilds/expired",
"dependencies": { "dependencies": {
"date-fns": "1.28.5", "date-fns": "2.19.0",
"parse-headers": "2.0.1" "parse-headers": "2.0.3"
}, },
"devDependencies": { "devDependencies": {
"ava": "^0.25.0", "ava": "0.25.0",
"coveralls": "^3.0.0", "coveralls": "3.1.0",
"date-fns": "^1.28.3", "nyc": "15.1.0",
"nyc": "^11.0.1", "xo": "0.38.2"
"timekeeper": "^2.0.0",
"xo": "^0.19.0"
} }
} }

106
src/index.js

@ -1,55 +1,95 @@
'use strict'; 'use strict';
const isBefore = require('date-fns/is_before'); const isBefore = require('date-fns/isBefore');
const differenceInMilliseconds = require('date-fns/difference_in_milliseconds'); const differenceInMilliseconds = require('date-fns/differenceInMilliseconds');
const addSeconds = require('date-fns/add_seconds'); const addSeconds = require('date-fns/addSeconds');
const parse = require('parse-headers'); const parse = require('parse-headers');
// Returns boolean for whether or not the cache has expired const maxAgeRegex = /max-age=(\d+)/;
const expired = (headers, date) => isBefore(expired.on(headers), (date || new Date())); const noCacheRegex = /(no-cache)|(no-store)/;
// Return ms until cache expires
expired.in = (headers, date) => differenceInMilliseconds(expired.on(headers), (date || new Date()));
// Returns date when cache will expire const getExpirationDate = headers => {
expired.on = headers => {
// Check we have headers // Check we have headers
if (!headers) { if (!headers) {
throw new Error('Headers argument is missing'); throw new Error('Missing required argument "headers"');
}
const {
date: dateHeader,
age: ageHeader,
'cache-control': cacheControlHeader,
expires: expiresHeader
} = typeof headers === 'string' ? parse(headers) : headers;
// Confirm we have date header
if (typeof dateHeader !== 'string') {
throw new TypeError('Missing required header "Date"');
} }
// Parse headers if we got a raw string if (typeof cacheControlHeader === 'string') {
headers = (typeof headers === 'string') ? parse(headers) : headers; // Prioritize no-cache and no-store
const noCacheMatches = cacheControlHeader.match(noCacheRegex);
if (noCacheMatches) {
return new Date(dateHeader);
}
// Prioritize Cache-Control with max-age header
const maxAgeMatches = cacheControlHeader.match(maxAgeRegex);
if (maxAgeMatches) {
const maxAge = Number.parseInt(maxAgeMatches ? maxAgeMatches[1] : 0, 10);
if (typeof ageHeader === 'number') {
return addSeconds(new Date(dateHeader), maxAge - ageHeader);
}
// Check we have date header return addSeconds(new Date(dateHeader), maxAge);
if (!headers.date) { }
throw new Error('Date header is missing');
} }
// Default to Date header if (expiresHeader) {
let expiredOn = new Date(headers.date); return new Date(expiresHeader);
}
// Return expiry dateHeader
return new Date(dateHeader);
};
// Returns boolean for whether or not the cache has expired
const expired = (headers, date) => {
if (date) {
if (date instanceof Date) {
return isBefore(expired.on(headers), date);
}
if (typeof date === 'string') {
return isBefore(expired.on(headers), new Date(date));
}
throw new Error(`Optional argument "date" must be a string or Date object, found ${typeof date}`);
}
// Prefer Cache-Control return isBefore(expired.on(headers), new Date());
if (headers['cache-control']) { };
// Get max age ms
let maxAge = headers['cache-control'].match(/max-age=(\d+)/);
maxAge = parseInt(maxAge ? maxAge[1] : 0, 10);
// Take current age into account // Return ms until cache expires
if (headers.age) { expired.in = (headers, date) => {
maxAge -= headers.age; if (date) {
if (date instanceof Date) {
return differenceInMilliseconds(expired.on(headers), date);
} }
// Calculate expiry date if (typeof date === 'string') {
expiredOn = addSeconds(expiredOn, maxAge); return differenceInMilliseconds(expired.on(headers), new Date(date));
}
// Fall back to Expires if it exists throw new Error(`Optional argument "date" must be a string or Date object, found ${typeof date}`);
} else if (headers.expires) {
expiredOn = new Date(headers.expires);
} }
// Return expiry date return differenceInMilliseconds(expired.on(headers), new Date());
return expiredOn; };
// Returns date when cache will expire
expired.on = headers => {
return getExpirationDate(headers);
}; };
module.exports = expired; module.exports = expired;

35
test/expired.in.js

@ -1,43 +1,36 @@
import test from 'ava'; const test = require('ava');
import tk from 'timekeeper'; const addSeconds = require('date-fns/addSeconds');
import addSeconds from 'date-fns/add_seconds'; const expired = require('..');
import expired from '../';
test('expired.in is a function', t => { test('expired.in is a function', t => {
t.is(typeof expired.in, 'function'); t.is(typeof expired.in, 'function');
}); });
test('expired.in returns positive ms for valid cache', t => { test('expired.in returns positive ms for valid cache', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const maxAge = 300; const maxAge = 300;
const headers = { const headers = {
date, date: date.toUTCString(),
age: 0, age: 0,
'cache-control': `public, max-age=${maxAge}` 'cache-control': `public, max-age=${maxAge}`
}; };
const expiredIn = maxAge * 1000; const expiredIn = maxAge * 1000;
t.is(expired.in(headers, date), expiredIn);
tk.freeze(date);
t.is(expired.in(headers), expiredIn);
tk.reset();
}); });
test('expired.in returns zero ms for instantly stale cache', t => { test('expired.in returns zero ms for instantly stale cache', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const headers = { const headers = {
date, date: date.toUTCString(),
age: 0, age: 0,
'cache-control': `public, max-age=0` 'cache-control': 'public, max-age=0'
}; };
const expiredIn = 0; const expiredIn = 0;
t.is(expired.in(headers, date), expiredIn);
tk.freeze(date);
t.is(expired.in(headers), expiredIn);
tk.reset();
}); });
test('expired.in returns negative ms for stale cache', t => { test('expired.in returns negative ms for stale cache', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const dateOffset = -600; const dateOffset = -600;
const maxAge = 300; const maxAge = 300;
const headers = { const headers = {
@ -46,10 +39,7 @@ test('expired.in returns negative ms for stale cache', t => {
'cache-control': `public, max-age=${maxAge}` 'cache-control': `public, max-age=${maxAge}`
}; };
const expiredIn = (maxAge + dateOffset) * 1000; const expiredIn = (maxAge + dateOffset) * 1000;
t.is(expired.in(headers, date), expiredIn);
tk.freeze(date);
t.is(expired.in(headers), expiredIn);
tk.reset();
}); });
test('expired.in accepts currentDate argument', t => { test('expired.in accepts currentDate argument', t => {
@ -59,7 +49,6 @@ test('expired.in accepts currentDate argument', t => {
age: 0, age: 0,
'cache-control': 'public, max-age=300' 'cache-control': 'public, max-age=300'
}; };
t.is(expired.in(headers, date), 300000); t.is(expired.in(headers, date), 300000);
t.is(expired.in(headers, addSeconds(date, 500)), -200000); t.is(expired.in(headers, addSeconds(date, 500)), -200000);
}); });

19
test/expired.js

@ -1,40 +1,39 @@
import test from 'ava'; const test = require('ava');
import subSeconds from 'date-fns/sub_seconds'; const subSeconds = require('date-fns/subSeconds');
import addSeconds from 'date-fns/add_seconds'; const addSeconds = require('date-fns/addSeconds');
import expired from '../'; const expired = require('..');
test('expired is a function', t => { test('expired is a function', t => {
t.is(typeof expired, 'function'); t.is(typeof expired, 'function');
}); });
test('expired returns false for valid cache', t => { test('expired returns false for valid cache', t => {
const date = new Date(new Date().toUTCString());
const headers = { const headers = {
date: new Date().toUTCString(), date: date.toUTCString(),
age: 0, age: 0,
'cache-control': 'public, max-age=300' 'cache-control': 'public, max-age=300'
}; };
t.false(expired(headers)); t.false(expired(headers));
}); });
test('expired returns true for stale cache', t => { test('expired returns true for stale cache', t => {
const date = new Date(new Date().toUTCString());
const headers = { const headers = {
date: subSeconds(new Date(), 500).toUTCString(), date: subSeconds(new Date(), 500).toUTCString(),
age: 0, age: 0,
'cache-control': 'public, max-age=300' 'cache-control': 'public, max-age=300'
}; };
t.true(expired(headers, date));
t.true(expired(headers));
}); });
test('expired accepts currentDate argument', t => { test('expired accepts currentDate argument', t => {
const date = new Date(); const date = new Date(new Date().toUTCString());
const headers = { const headers = {
date: date.toUTCString(), date: date.toUTCString(),
age: 0, age: 0,
'cache-control': 'public, max-age=300' 'cache-control': 'public, max-age=300'
}; };
t.false(expired(headers, date)); t.false(expired(headers, date));
t.true(expired(headers, addSeconds(date, 500))); t.true(expired(headers, addSeconds(date, 500)));
}); });

79
test/expired.on.js

@ -1,17 +1,17 @@
import test from 'ava'; const test = require('ava');
import addSeconds from 'date-fns/add_seconds'; const addSeconds = require('date-fns/addSeconds');
import isEqual from 'date-fns/is_equal'; const isEqual = require('date-fns/isEqual');
import expired from '../'; const expired = require('..');
test('expired.on is a function', t => { test('expired.on is a function', t => {
t.is(typeof expired.in, 'function'); t.is(typeof expired.in, 'function');
}); });
test('expired.on returns correct expirey date for valid cache', t => { test('expired.on returns correct expirey date for valid cache', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const maxAge = 300; const maxAge = 300;
const headers = { const headers = {
date, date: date.toUTCString(),
age: 0, age: 0,
'cache-control': `public, max-age=${maxAge}` 'cache-control': `public, max-age=${maxAge}`
}; };
@ -21,18 +21,18 @@ test('expired.on returns correct expirey date for valid cache', t => {
}); });
test('expired.on returns correct expirey date for instantly stale cache', t => { test('expired.on returns correct expirey date for instantly stale cache', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const headers = { const headers = {
date, date: date.toUTCString(),
age: 0, age: 0,
'cache-control': `public, max-age=0` 'cache-control': 'public, max-age=0'
}; };
t.true(isEqual(expired.on(headers), date)); t.true(isEqual(expired.on(headers), date));
}); });
test('expired.on returns correct expirey date for stale cache', t => { test('expired.on returns correct expirey date for stale cache', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const dateOffset = -600; const dateOffset = -600;
const maxAge = 300; const maxAge = 300;
const headers = { const headers = {
@ -46,11 +46,11 @@ test('expired.on returns correct expirey date for stale cache', t => {
}); });
test('expired.on takes age into account', t => { test('expired.on takes age into account', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const age = 150; const age = 150;
const maxAge = 300; const maxAge = 300;
const headers = { const headers = {
date, date: date.toUTCString(),
age, age,
'cache-control': `public, max-age=${maxAge}` 'cache-control': `public, max-age=${maxAge}`
}; };
@ -60,26 +60,67 @@ test('expired.on takes age into account', t => {
}); });
test('expired.on uses Expires header', t => { test('expired.on uses Expires header', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const headers = { const headers = {
date: addSeconds(date, 300), date: addSeconds(date, 300).toUTCString(),
expires: date expires: date.toUTCString()
}; };
t.true(isEqual(expired.on(headers), date)); t.true(isEqual(expired.on(headers), date));
}); });
test('expired.on prefers Cache-Control over Expires header', t => { test('expired.on prefers Cache-Control with max-age over Expires header', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const expires = new Date(date.getTime() + (100 * 1000));
const age = 150; const age = 150;
const maxAge = 300; const maxAge = 300;
const headers = { const headers = {
date, date: date.toUTCString(),
age, age,
'cache-control': `public, max-age=${maxAge}`, 'cache-control': `public, max-age=${maxAge}`,
expires: date expires: expires.toUTCString()
}; };
const expiredOn = addSeconds(date, (maxAge - age)); const expiredOn = addSeconds(date, (maxAge - age));
t.true(isEqual(expired.on(headers), expiredOn)); t.true(isEqual(expired.on(headers), expiredOn));
}); });
test('expired.on prefers Cache-Control with no-cache over Expires header', t => {
const date = new Date(new Date().toUTCString());
const expires = new Date(date.getTime() + (100 * 1000));
const age = 150;
const headers = {
date: date.toUTCString(),
age,
'cache-control': 'no-cache',
expires: expires.toUTCString()
};
t.true(isEqual(expired.on(headers), date));
});
test('expired.on prefers Cache-Control with no-store over Expires header', t => {
const date = new Date(new Date().toUTCString());
const expires = new Date(date.getTime() + (100 * 1000));
const age = 150;
const headers = {
date: date.toUTCString(),
age,
'cache-control': 'no-store',
expires: expires.toUTCString()
};
t.true(isEqual(expired.on(headers), date));
});
test('expired.on uses Expires header when max-age is not set in Cache-Control', t => {
const date = new Date(new Date().toUTCString());
const expires = new Date(date.getTime() + (100 * 1000));
const age = 150;
const headers = {
date: date.toUTCString(),
age,
'cache-control': 'public',
expires: expires.toUTCString()
};
t.true(isEqual(expired.on(headers), expires));
});

39
test/headers.js

@ -1,54 +1,43 @@
import test from 'ava'; const test = require('ava');
import tk from 'timekeeper'; const isEqual = require('date-fns/isEqual');
import isEqual from 'date-fns/is_equal'; const expired = require('..');
import expired from '../';
test('throw error if header argument is missing', t => { test('throw error if header argument is missing', t => {
const err = t.throws(() => expired()); const error = t.throws(() => expired());
t.is(err.message, 'Headers argument is missing'); t.is(error.message, 'Missing required argument "headers"');
}); });
test('throw error if Date header is missing', t => { test('throw error if Date header is missing', t => {
const headers = {}; const headers = {};
const error = t.throws(() => expired(headers));
const err = t.throws(() => expired(headers)); t.is(error.message, 'Missing required header "Date"');
t.is(err.message, 'Date header is missing');
}); });
test('headers can be passed in as an object', t => { test('headers can be passed in as an object', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const headers = { const headers = {
date, date: date.toUTCString(),
age: 0, age: 0,
'cache-control': `public, max-age=0` 'cache-control': 'public, max-age=0'
}; };
tk.freeze(date);
t.true(isEqual(expired.on(headers), date)); t.true(isEqual(expired.on(headers), date));
tk.reset();
}); });
test('headers can be passed in as a string', t => { test('headers can be passed in as a string', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const headers = ` const headers = `
Date: ${date} Date: ${date.toUTCString()}
Age: 0 Age: 0
Cache-Control public, max-age=0`; Cache-Control public, max-age=0`;
tk.freeze(date);
t.true(isEqual(expired.on(headers), date)); t.true(isEqual(expired.on(headers), date));
tk.reset();
}); });
test('headers can contain status code', t => { test('headers can contain status code', t => {
const date = new Date().toUTCString(); const date = new Date(new Date().toUTCString());
const headers = ` const headers = `
HTTP/1.1 200 OK HTTP/1.1 200 OK
Date: ${date} Date: ${date.toUTCString()}
Age: 0 Age: 0
Cache-Control public, max-age=0`; Cache-Control public, max-age=0`;
tk.freeze(date);
t.true(isEqual(expired.on(headers), date)); t.true(isEqual(expired.on(headers), date));
tk.reset();
}); });

Loading…
Cancel
Save