cache_expiry_parser.js
/**
* Get the expiry time of an XMLHttpRequest response
* returns a Date object for when the request expires
* returns null if there is valid data that says not to cache
* returns undefined if there is no caching information
**/
export function getCacheExpiry(xhr){
const expiresHeader = xhr.getResponseHeader('Expires');
const cacheControlHeader = xhr.getResponseHeader('Cache-Control');
let expiry = parseCacheControlHeader(cacheControlHeader);
if(expiry === undefined) {
expiry = parseExpiresHeader(expiresHeader);
}
expiry = nullifyInvalidExpiration(expiry);
return expiry;
}
/**
* Parse the data in the Cache-Control header for information
* returns Date object if the Cache-Control header is valid
* returns null if there is a valid Cache-Control header that says not to cache
* returns undefined if there is no valid Cache-Control header
**/
function parseCacheControlHeader(cacheControlHeader){
if(cacheControlHeader === null || cacheControlHeader === undefined){
return undefined;
}
const headerData = cacheControlHeader.split(',');
let expiry, keyword;
for(let i=0; i<headerData.length; i++){
keyword = headerData[i].trim();
if(keyword.includes('max-age')){
expiry = parseCacheControlAge(keyword, expiry);
}else{
expiry = parseCacheControlKeyword(keyword, expiry);
}
}
return expiry;
}
/**
* Parse the max-age value from the Cache-Control header data
* returns Date object if the max-age value is valid
* returns undefined if there is no valid data
**/
function parseCacheControlAge(maxAge, expiry){
if(expiry === null){
return expiry;
}
let seconds = maxAge.split('=')[1].trim();
seconds = parseInt(seconds, 10);
if(isNaN(seconds)){
return undefined;
}else{
return nowPlusSeconds(seconds);
}
}
/**
* Parse non-max-age keywords in the Cache-Control header
* returns expiry Date, undefined, or null depending on the keyword behavior
**/
function parseCacheControlKeyword(keyword, expiry){
if(keyword.includes('public') || keyword.includes('private')){
return expiry;
}
if(keyword.includes('no-cache') || keyword.includes('no-store')){
return null;
}
if(keyword.includes('must-revalidate') || keyword.includes('proxy-revalidate')){
return expiry; // Noop
}
if(keyword.includes('s-maxage')){
return expiry; // Noop
}
return expiry; // Unknown keyword, Noop
}
/**
* Parse the data in the Expires header for information
* returns Date object if the Expires header is valid
* returns null if there is a valid Expires header that says not to cache
* returns undefined if there is no valid Cache-Control header
**/
function parseExpiresHeader(expiresHeader){
if(expiresHeader === null || expiresHeader === undefined) {
return undefined;
}
const expires = new Date(expiresHeader);
if(expires.toString() === 'Invalid Date'){
return null;
}
return expires;
}
/**
* Nullify any invalid expiration Date objects (if they're in the past)
**/
function nullifyInvalidExpiration(expiration){
if(expiration instanceof Date && expiration < new Date()){
return null;
}
return expiration;
}
/**
* Returns the current time plus the given number of seconds into the future
**/
export function nowPlusSeconds(seconds){
const now = new Date();
now.setTime(now.getTime() + seconds * 1000);
return now;
}