'use strict'; const EventEmitter = require('events'); const urlLib = require('url'); const normalizeUrl = require('normalize-url'); const getStream = require('get-stream'); const CachePolicy = require('http-cache-semantics'); const Response = require('responselike'); const lowercaseKeys = require('lowercase-keys'); const cloneResponse = require('clone-response'); const Keyv = require('keyv'); class CacheableRequest { constructor(request, cacheAdapter) { if (typeof request !== 'function') { throw new TypeError('Parameter `request` must be a function'); } this.cache = new Keyv({ uri: typeof cacheAdapter === 'string' && cacheAdapter, store: typeof cacheAdapter !== 'string' && cacheAdapter, namespace: 'cacheable-request' }); return this.createCacheableRequest(request); } createCacheableRequest(request) { return (opts, cb) => { if (typeof opts === 'string') { opts = urlLib.parse(opts); } opts = Object.assign({ headers: {}, method: 'GET', cache: true, strictTtl: false, automaticFailover: false }, opts); opts.headers = lowercaseKeys(opts.headers); const ee = new EventEmitter(); const url = normalizeUrl(urlLib.format(opts)); const key = `${opts.method}:${url}`; let revalidate = false; let madeRequest = false; const makeRequest = opts => { madeRequest = true; const handler = response => { if (revalidate) { const revalidatedPolicy = CachePolicy.fromObject(revalidate.cachePolicy).revalidatedPolicy(opts, response); if (!revalidatedPolicy.modified) { const headers = revalidatedPolicy.policy.responseHeaders(); response = new Response(response.statusCode, headers, revalidate.body, revalidate.url); response.cachePolicy = revalidatedPolicy.policy; response.fromCache = true; } } if (!response.fromCache) { response.cachePolicy = new CachePolicy(opts, response); response.fromCache = false; } let clonedResponse; if (opts.cache && response.cachePolicy.storable()) { clonedResponse = cloneResponse(response); getStream.buffer(response) .then(body => { const value = { cachePolicy: response.cachePolicy.toObject(), url: response.url, statusCode: response.fromCache ? revalidate.statusCode : response.statusCode, body }; const ttl = opts.strictTtl ? response.cachePolicy.timeToLive() : undefined; return this.cache.set(key, value, ttl); }) .catch(err => ee.emit('error', new CacheableRequest.CacheError(err))); } else if (opts.cache && revalidate) { this.cache.delete(key) .catch(err => ee.emit('error', new CacheableRequest.CacheError(err))); } ee.emit('response', clonedResponse || response); if (typeof cb === 'function') { cb(clonedResponse || response); } }; try { const req = request(opts, handler); ee.emit('request', req); } catch (err) { ee.emit('error', new CacheableRequest.RequestError(err)); } }; const get = opts => Promise.resolve() .then(() => opts.cache ? this.cache.get(key) : undefined) .then(cacheEntry => { if (typeof cacheEntry === 'undefined') { return makeRequest(opts); } const policy = CachePolicy.fromObject(cacheEntry.cachePolicy); if (policy.satisfiesWithoutRevalidation(opts)) { const headers = policy.responseHeaders(); const response = new Response(cacheEntry.statusCode, headers, cacheEntry.body, cacheEntry.url); response.cachePolicy = policy; response.fromCache = true; ee.emit('response', response); if (typeof cb === 'function') { cb(response); } } else { revalidate = cacheEntry; opts.headers = policy.revalidationHeaders(opts); makeRequest(opts); } }); this.cache.on('error', err => ee.emit('error', new CacheableRequest.CacheError(err))); get(opts).catch(err => { if (opts.automaticFailover && !madeRequest) { makeRequest(opts); } ee.emit('error', new CacheableRequest.CacheError(err)); }); return ee; }; } } CacheableRequest.RequestError = class extends Error { constructor(err) { super(err.message); this.name = 'RequestError'; Object.assign(this, err); } }; CacheableRequest.CacheError = class extends Error { constructor(err) { super(err.message); this.name = 'CacheError'; Object.assign(this, err); } }; module.exports = CacheableRequest;