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