'use strict' ;
function parseCacheControl ( header ) {
const cc = { } ;
if ( ! header ) return cc ;
// TODO: When there is more than one value present for a given directive (e.g., two Expires header fields, multiple Cache-Control: max-age directives),
// the directive's value is considered invalid. Caches are encouraged to consider responses that have invalid freshness information to be stale
const parts = header . split ( /\s*,\s*/ ) ; // TODO: lame parsing
for ( const part of parts ) {
const [ k , v ] = part . split ( /\s*=\s*/ ) ;
cc [ k ] = ( v === undefined ) ? true : v . replace ( /^"|"$/g , '' ) ; // TODO: lame unquoting
}
// The s-maxage directive also implies the semantics of the proxy-revalidate response directive.
if ( 's-maxage' in cc ) {
cc [ 'proxy-revalidate' ] = true ;
}
return cc ;
}
function CachePolicy ( req , res , { shared } = { } ) {
if ( ! res || ! res . headers ) {
throw Error ( "Response headers missing" ) ;
}
if ( ! req || ! req . headers ) {
throw Error ( "Request headers missing" ) ;
}
this . _ responseTime = this . now ( ) ;
this . _ isShared = shared !== false ;
this . _ res = res ;
this . _ rescc = parseCacheControl ( res . headers [ 'cache-control' ] ) ;
this . _ req = req ;
this . _ reqcc = parseCacheControl ( req . headers [ 'cache-control' ] ) ;
// When the Cache-Control header field is not present in a request, caches MUST consider the no-cache request pragma-directive
// as having the same effect as if "Cache-Control: no-cache" were present (see Section 5.2.1).
if ( ! res . headers [ 'cache-control' ] && /no-cache/ . test ( res . headers . pragma ) ) {
this . _ rescc [ 'no-cache' ] = true ;
}
}
CachePolicy . prototype = {
now ( ) {
return Date . now ( ) ;
} ,
/ * *
* Value of the Date response header or current time if Date was demed invalid
* @ return timestamp
* /
date ( ) {
const dateValue = Date . parse ( this . _ res . headers . date )
const maxClockDrift = 8 * 3600 * 1000 ;
if ( Number . isNaN ( dateValue ) || dateValue < this . _ responseTime - maxClockDrift || dateValue > this . _ responseTime + maxClockDrift ) {
return this . _ responseTime ;
}
return dateValue ;
} ,
/ * *
* Value of the Age header , in seconds , updated for the current time
* @ return Number
* /
age ( ) {
let age = Math . max ( 0 , ( this . _ responseTime - this . date ( ) ) / 1000 ) ;
if ( this . _ res . headers . age ) {
let ageValue = parseInt ( this . _ res . headers . age ) ;
if ( isFinite ( ageValue ) ) {
if ( ageValue > age ) age = ageValue ;
}
}
const residentTime = ( this . now ( ) - this . _ responseTime ) / 1000 ;
return age + residentTime ;
} ,
maxAge ( ) {
if ( this . _ rescc [ 'no-cache' ] || this . _ rescc [ 'no-store' ] ) {
return 0 ;
}
// Shared responses with cookies are cacheable according to the RFC, but IMHO it'd be unwise to do so by default
// so this implementation requires explicit opt-in via public header
if ( this . _ isShared && ( this . _ rescc [ 'private' ] || ( this . _ res . headers [ 'set-cookie' ] && ! this . _ rescc [ 'public' ] ) ) ) {
return 0 ;
}
// TODO: vary is not supported yet
if ( this . _ res . headers [ 'vary' ] ) {
return 0 ;
}
if ( this . _ isShared ) {
// if a response includes the s-maxage directive, a shared cache recipient MUST ignore the Expires field.
if ( this . _ rescc [ 's-maxage' ] ) {
return parseInt ( this . _ rescc [ 's-maxage' ] , 10 ) ;
}
}
// If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field.
if ( this . _ rescc [ 'max-age' ] ) {
return parseInt ( this . _ rescc [ 'max-age' ] , 10 ) ;
}
const dateValue = this . date ( ) ;
if ( this . _ res . headers [ 'expires' ] ) {
const expires = Date . parse ( this . _ res . headers [ 'expires' ] ) ;
// A cache recipient MUST interpret invalid date formats, especially the value "0", as representing a time in the past (i.e., "already expired").
if ( Number . isNaN ( expires ) || expires < dateValue ) {
return 0 ;
}
return ( expires - dateValue ) / 1000 ;
}
if ( this . _ res . headers [ 'last-modified' ] ) {
const lastModified = Date . parse ( this . _ res . headers [ 'last-modified' ] ) ;
if ( isFinite ( lastModified ) && dateValue > lastModified ) {
return ( dateValue - lastModified ) * 0.00001 ; // In absence of other information cache for 1% of item's age
}
}
return 0 ;
} ,
isFresh ( ) {
return this . maxAge ( ) > this . age ( ) ;
} ,
} ;
module . exports = CachePolicy ;