You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.js 4.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. 'use strict';
  2. const EventEmitter = require('events');
  3. const urlLib = require('url');
  4. const normalizeUrl = require('normalize-url');
  5. const getStream = require('get-stream');
  6. const CachePolicy = require('http-cache-semantics');
  7. const Response = require('responselike');
  8. const lowercaseKeys = require('lowercase-keys');
  9. const cloneResponse = require('clone-response');
  10. const Keyv = require('keyv');
  11. class CacheableRequest {
  12. constructor(request, cacheAdapter) {
  13. if (typeof request !== 'function') {
  14. throw new TypeError('Parameter `request` must be a function');
  15. }
  16. this.cache = new Keyv({
  17. uri: typeof cacheAdapter === 'string' && cacheAdapter,
  18. store: typeof cacheAdapter !== 'string' && cacheAdapter,
  19. namespace: 'cacheable-request'
  20. });
  21. return this.createCacheableRequest(request);
  22. }
  23. createCacheableRequest(request) {
  24. return (opts, cb) => {
  25. if (typeof opts === 'string') {
  26. opts = urlLib.parse(opts);
  27. }
  28. opts = Object.assign({
  29. headers: {},
  30. method: 'GET',
  31. cache: true,
  32. strictTtl: false,
  33. automaticFailover: false
  34. }, opts);
  35. opts.headers = lowercaseKeys(opts.headers);
  36. const ee = new EventEmitter();
  37. const url = normalizeUrl(urlLib.format(opts));
  38. const key = `${opts.method}:${url}`;
  39. let revalidate = false;
  40. let madeRequest = false;
  41. const makeRequest = opts => {
  42. madeRequest = true;
  43. const handler = response => {
  44. if (revalidate) {
  45. const revalidatedPolicy = CachePolicy.fromObject(revalidate.cachePolicy).revalidatedPolicy(opts, response);
  46. if (!revalidatedPolicy.modified) {
  47. const headers = revalidatedPolicy.policy.responseHeaders();
  48. response = new Response(response.statusCode, headers, revalidate.body, revalidate.url);
  49. response.cachePolicy = revalidatedPolicy.policy;
  50. response.fromCache = true;
  51. }
  52. }
  53. if (!response.fromCache) {
  54. response.cachePolicy = new CachePolicy(opts, response);
  55. response.fromCache = false;
  56. }
  57. let clonedResponse;
  58. if (opts.cache && response.cachePolicy.storable()) {
  59. clonedResponse = cloneResponse(response);
  60. getStream.buffer(response)
  61. .then(body => {
  62. const value = {
  63. cachePolicy: response.cachePolicy.toObject(),
  64. url: response.url,
  65. statusCode: response.fromCache ? revalidate.statusCode : response.statusCode,
  66. body
  67. };
  68. const ttl = opts.strictTtl ? response.cachePolicy.timeToLive() : undefined;
  69. return this.cache.set(key, value, ttl);
  70. })
  71. .catch(err => ee.emit('error', new CacheableRequest.CacheError(err)));
  72. } else if (opts.cache && revalidate) {
  73. this.cache.delete(key)
  74. .catch(err => ee.emit('error', new CacheableRequest.CacheError(err)));
  75. }
  76. ee.emit('response', clonedResponse || response);
  77. if (typeof cb === 'function') {
  78. cb(clonedResponse || response);
  79. }
  80. };
  81. try {
  82. const req = request(opts, handler);
  83. ee.emit('request', req);
  84. } catch (err) {
  85. ee.emit('error', new CacheableRequest.RequestError(err));
  86. }
  87. };
  88. const get = opts => Promise.resolve()
  89. .then(() => opts.cache ? this.cache.get(key) : undefined)
  90. .then(cacheEntry => {
  91. if (typeof cacheEntry === 'undefined') {
  92. return makeRequest(opts);
  93. }
  94. const policy = CachePolicy.fromObject(cacheEntry.cachePolicy);
  95. if (policy.satisfiesWithoutRevalidation(opts)) {
  96. const headers = policy.responseHeaders();
  97. const response = new Response(cacheEntry.statusCode, headers, cacheEntry.body, cacheEntry.url);
  98. response.cachePolicy = policy;
  99. response.fromCache = true;
  100. ee.emit('response', response);
  101. if (typeof cb === 'function') {
  102. cb(response);
  103. }
  104. } else {
  105. revalidate = cacheEntry;
  106. opts.headers = policy.revalidationHeaders(opts);
  107. makeRequest(opts);
  108. }
  109. });
  110. this.cache.on('error', err => ee.emit('error', new CacheableRequest.CacheError(err)));
  111. get(opts).catch(err => {
  112. if (opts.automaticFailover && !madeRequest) {
  113. makeRequest(opts);
  114. }
  115. ee.emit('error', new CacheableRequest.CacheError(err));
  116. });
  117. return ee;
  118. };
  119. }
  120. }
  121. CacheableRequest.RequestError = class extends Error {
  122. constructor(err) {
  123. super(err.message);
  124. this.name = 'RequestError';
  125. Object.assign(this, err);
  126. }
  127. };
  128. CacheableRequest.CacheError = class extends Error {
  129. constructor(err) {
  130. super(err.message);
  131. this.name = 'CacheError';
  132. Object.assign(this, err);
  133. }
  134. };
  135. module.exports = CacheableRequest;