policy.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. const CacheSemantics = require('http-cache-semantics')
  2. const Negotiator = require('negotiator')
  3. const ssri = require('ssri')
  4. // HACK: negotiator lazy loads several of its own modules
  5. // as a micro optimization. we need to be sure that they're
  6. // in memory as soon as possible at startup so that we do
  7. // not try to lazy load them after the directory has been
  8. // retired during a self update of the npm CLI, we do this
  9. // by calling all of the methods that trigger a lazy load
  10. // on a fake instance.
  11. const preloadNegotiator = new Negotiator({ headers: {} })
  12. preloadNegotiator.charsets()
  13. preloadNegotiator.encodings()
  14. preloadNegotiator.languages()
  15. preloadNegotiator.mediaTypes()
  16. // options passed to http-cache-semantics constructor
  17. const policyOptions = {
  18. shared: false,
  19. ignoreCargoCult: true,
  20. }
  21. // a fake empty response, used when only testing the
  22. // request for storability
  23. const emptyResponse = { status: 200, headers: {} }
  24. // returns a plain object representation of the Request
  25. const requestObject = (request) => {
  26. const _obj = {
  27. method: request.method,
  28. url: request.url,
  29. headers: {},
  30. }
  31. request.headers.forEach((value, key) => {
  32. _obj.headers[key] = value
  33. })
  34. return _obj
  35. }
  36. // returns a plain object representation of the Response
  37. const responseObject = (response) => {
  38. const _obj = {
  39. status: response.status,
  40. headers: {},
  41. }
  42. response.headers.forEach((value, key) => {
  43. _obj.headers[key] = value
  44. })
  45. return _obj
  46. }
  47. class CachePolicy {
  48. constructor ({ entry, request, response, options }) {
  49. this.entry = entry
  50. this.request = requestObject(request)
  51. this.response = responseObject(response)
  52. this.options = options
  53. this.policy = new CacheSemantics(this.request, this.response, policyOptions)
  54. if (this.entry) {
  55. // if we have an entry, copy the timestamp to the _responseTime
  56. // this is necessary because the CacheSemantics constructor forces
  57. // the value to Date.now() which means a policy created from a
  58. // cache entry is likely to always identify itself as stale
  59. this.policy._responseTime = this.entry.metadata.time
  60. }
  61. }
  62. // static method to quickly determine if a request alone is storable
  63. static storable (request, options) {
  64. // no cachePath means no caching
  65. if (!options.cachePath)
  66. return false
  67. // user explicitly asked not to cache
  68. if (options.cache === 'no-store')
  69. return false
  70. // we only cache GET and HEAD requests
  71. if (!['GET', 'HEAD'].includes(request.method))
  72. return false
  73. // otherwise, let http-cache-semantics make the decision
  74. // based on the request's headers
  75. const policy = new CacheSemantics(requestObject(request), emptyResponse, policyOptions)
  76. return policy.storable()
  77. }
  78. // returns true if the policy satisfies the request
  79. satisfies (request) {
  80. const _req = requestObject(request)
  81. if (this.request.headers.host !== _req.headers.host)
  82. return false
  83. const negotiatorA = new Negotiator(this.request)
  84. const negotiatorB = new Negotiator(_req)
  85. if (JSON.stringify(negotiatorA.mediaTypes()) !== JSON.stringify(negotiatorB.mediaTypes()))
  86. return false
  87. if (JSON.stringify(negotiatorA.languages()) !== JSON.stringify(negotiatorB.languages()))
  88. return false
  89. if (JSON.stringify(negotiatorA.encodings()) !== JSON.stringify(negotiatorB.encodings()))
  90. return false
  91. if (this.options.integrity)
  92. return ssri.parse(this.options.integrity).match(this.entry.integrity)
  93. return true
  94. }
  95. // returns true if the request and response allow caching
  96. storable () {
  97. return this.policy.storable()
  98. }
  99. // NOTE: this is a hack to avoid parsing the cache-control
  100. // header ourselves, it returns true if the response's
  101. // cache-control contains must-revalidate
  102. get mustRevalidate () {
  103. return !!this.policy._rescc['must-revalidate']
  104. }
  105. // returns true if the cached response requires revalidation
  106. // for the given request
  107. needsRevalidation (request) {
  108. const _req = requestObject(request)
  109. // force method to GET because we only cache GETs
  110. // but can serve a HEAD from a cached GET
  111. _req.method = 'GET'
  112. return !this.policy.satisfiesWithoutRevalidation(_req)
  113. }
  114. responseHeaders () {
  115. return this.policy.responseHeaders()
  116. }
  117. // returns a new object containing the appropriate headers
  118. // to send a revalidation request
  119. revalidationHeaders (request) {
  120. const _req = requestObject(request)
  121. return this.policy.revalidationHeaders(_req)
  122. }
  123. // returns true if the request/response was revalidated
  124. // successfully. returns false if a new response was received
  125. revalidated (request, response) {
  126. const _req = requestObject(request)
  127. const _res = responseObject(response)
  128. const policy = this.policy.revalidatedPolicy(_req, _res)
  129. return !policy.modified
  130. }
  131. }
  132. module.exports = CachePolicy