policy.js 4.79 KB
const CacheSemantics = require('http-cache-semantics')
const Negotiator = require('negotiator')
const ssri = require('ssri')

// HACK: negotiator lazy loads several of its own modules
// as a micro optimization. we need to be sure that they're
// in memory as soon as possible at startup so that we do
// not try to lazy load them after the directory has been
// retired during a self update of the npm CLI, we do this
// by calling all of the methods that trigger a lazy load
// on a fake instance.
const preloadNegotiator = new Negotiator({ headers: {} })
preloadNegotiator.charsets()
preloadNegotiator.encodings()
preloadNegotiator.languages()
preloadNegotiator.mediaTypes()

// options passed to http-cache-semantics constructor
const policyOptions = {
  shared: false,
  ignoreCargoCult: true,
}

// a fake empty response, used when only testing the
// request for storability
const emptyResponse = { status: 200, headers: {} }

// returns a plain object representation of the Request
const requestObject = (request) => {
  const _obj = {
    method: request.method,
    url: request.url,
    headers: {},
  }

  request.headers.forEach((value, key) => {
    _obj.headers[key] = value
  })

  return _obj
}

// returns a plain object representation of the Response
const responseObject = (response) => {
  const _obj = {
    status: response.status,
    headers: {},
  }

  response.headers.forEach((value, key) => {
    _obj.headers[key] = value
  })

  return _obj
}

class CachePolicy {
  constructor ({ entry, request, response, options }) {
    this.entry = entry
    this.request = requestObject(request)
    this.response = responseObject(response)
    this.options = options
    this.policy = new CacheSemantics(this.request, this.response, policyOptions)

    if (this.entry) {
      // if we have an entry, copy the timestamp to the _responseTime
      // this is necessary because the CacheSemantics constructor forces
      // the value to Date.now() which means a policy created from a
      // cache entry is likely to always identify itself as stale
      this.policy._responseTime = this.entry.metadata.time
    }
  }

  // static method to quickly determine if a request alone is storable
  static storable (request, options) {
    // no cachePath means no caching
    if (!options.cachePath)
      return false

    // user explicitly asked not to cache
    if (options.cache === 'no-store')
      return false

    // we only cache GET and HEAD requests
    if (!['GET', 'HEAD'].includes(request.method))
      return false

    // otherwise, let http-cache-semantics make the decision
    // based on the request's headers
    const policy = new CacheSemantics(requestObject(request), emptyResponse, policyOptions)
    return policy.storable()
  }

  // returns true if the policy satisfies the request
  satisfies (request) {
    const _req = requestObject(request)
    if (this.request.headers.host !== _req.headers.host)
      return false

    const negotiatorA = new Negotiator(this.request)
    const negotiatorB = new Negotiator(_req)

    if (JSON.stringify(negotiatorA.mediaTypes()) !== JSON.stringify(negotiatorB.mediaTypes()))
      return false

    if (JSON.stringify(negotiatorA.languages()) !== JSON.stringify(negotiatorB.languages()))
      return false

    if (JSON.stringify(negotiatorA.encodings()) !== JSON.stringify(negotiatorB.encodings()))
      return false

    if (this.options.integrity)
      return ssri.parse(this.options.integrity).match(this.entry.integrity)

    return true
  }

  // returns true if the request and response allow caching
  storable () {
    return this.policy.storable()
  }

  // NOTE: this is a hack to avoid parsing the cache-control
  // header ourselves, it returns true if the response's
  // cache-control contains must-revalidate
  get mustRevalidate () {
    return !!this.policy._rescc['must-revalidate']
  }

  // returns true if the cached response requires revalidation
  // for the given request
  needsRevalidation (request) {
    const _req = requestObject(request)
    // force method to GET because we only cache GETs
    // but can serve a HEAD from a cached GET
    _req.method = 'GET'
    return !this.policy.satisfiesWithoutRevalidation(_req)
  }

  responseHeaders () {
    return this.policy.responseHeaders()
  }

  // returns a new object containing the appropriate headers
  // to send a revalidation request
  revalidationHeaders (request) {
    const _req = requestObject(request)
    return this.policy.revalidationHeaders(_req)
  }

  // returns true if the request/response was revalidated
  // successfully. returns false if a new response was received
  revalidated (request, response) {
    const _req = requestObject(request)
    const _res = responseObject(response)
    const policy = this.policy.revalidatedPolicy(_req, _res)
    return !policy.modified
  }
}

module.exports = CachePolicy