text: /**
   * 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
   **/
  
  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;
    }
    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
   **/
  function nowPlusSeconds(seconds){
    const now = new Date();
    now.setTime(now.getTime() + seconds * 1000);
    return now;
  }
  
  module.exports.getCacheExpiry = getCacheExpiry;
  module.exports.nowPlusSeconds = nowPlusSeconds;
  