text: '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) => {
  			let url;
  			if (typeof opts === 'string') {
  				url = normalizeUrlObject(urlLib.parse(opts));
  				opts = {};
  			} else if (opts instanceof urlLib.URL) {
  				url = normalizeUrlObject(urlLib.parse(opts.toString()));
  				opts = {};
  			} else {
  				const [pathname, ...searchParts] = (opts.path || '').split('?');
  				const search = searchParts.length > 0 ?
  					`?${searchParts.join('?')}` :
  					'';
  				url = normalizeUrlObject({ ...opts, pathname, search });
  			}
  			opts = {
  				headers: {},
  				method: 'GET',
  				cache: true,
  				strictTtl: false,
  				automaticFailover: false,
  				...opts,
  				...urlObjectToRequestOptions(url)
  			};
  			opts.headers = lowercaseKeys(opts.headers);
  
  			const ee = new EventEmitter();
  			const normalizedUrlString = normalizeUrl(
  				urlLib.format(url),
  				{
  					stripWWW: false,
  					removeTrailingSlash: false
  				}
  			);
  			const key = `${opts.method}:${normalizedUrlString}`;
  			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);
  
  						(async () => {
  							try {
  								const body = await getStream.buffer(response);
  
  								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;
  								await this.cache.set(key, value, ttl);
  							} catch (err) {
  								ee.emit('error', new CacheableRequest.CacheError(err));
  							}
  						})();
  					} else if (opts.cache && revalidate) {
  						(async () => {
  							try {
  								await 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));
  				}
  			};
  
  			(async () => {
  				const get = async opts => {
  					await Promise.resolve();
  
  					const cacheEntry = opts.cache ? await this.cache.get(key) : undefined;
  					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)));
  
  				try {
  					await get(opts);
  				} catch (err) {
  					if (opts.automaticFailover && !madeRequest) {
  						makeRequest(opts);
  					}
  					ee.emit('error', new CacheableRequest.CacheError(err));
  				}
  			})();
  
  			return ee;
  		};
  	}
  }
  
  function urlObjectToRequestOptions(url) {
  	const options = { ...url };
  	options.path = `${url.pathname || '/'}${url.search || ''}`;
  	delete options.pathname;
  	delete options.search;
  	return options;
  }
  
  function normalizeUrlObject(url) {
  	// If url was parsed by url.parse or new URL:
  	// - hostname will be set
  	// - host will be hostname[:port]
  	// - port will be set if it was explicit in the parsed string
  	// Otherwise, url was from request options:
  	// - hostname or host may be set
  	// - host shall not have port encoded
  	return {
  		protocol: url.protocol,
  		auth: url.auth,
  		hostname: url.hostname || url.host || 'localhost',
  		port: url.port,
  		pathname: url.pathname,
  		search: url.search
  	};
  }
  
  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;
  