7 changed files with 177 additions and 120 deletions
@ -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; |
||||
|
@ -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))); |
||||
}); |
}); |
||||
|
@ -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…
Reference in new issue