pax_global_header 0000666 0000000 0000000 00000000064 14060121503 0014502 g ustar 00root root 0000000 0000000 52 comment=635f3698f9eaa3fe93e46d82f398efd970852de7
cacheable-request-7.0.2/ 0000775 0000000 0000000 00000000000 14060121503 0015065 5 ustar 00root root 0000000 0000000 cacheable-request-7.0.2/.gitattributes 0000664 0000000 0000000 00000000023 14060121503 0017753 0 ustar 00root root 0000000 0000000 * text=auto eol=lf
cacheable-request-7.0.2/.github/ 0000775 0000000 0000000 00000000000 14060121503 0016425 5 ustar 00root root 0000000 0000000 cacheable-request-7.0.2/.github/FUNDING.yml 0000664 0000000 0000000 00000000465 14060121503 0020247 0 ustar 00root root 0000000 0000000 github:
- 'lukechilds'
custom:
- 'https://blockstream.info/address/1LukeQU5jwebXbMLDVydeH4vFSobRV9rkj'
- 'https://blockstream.info/address/3Luke2qRn5iLj4NiFrvLBu2jaEj7JeMR6w'
- 'https://blockstream.info/address/bc1qlukeyq0c69v97uss68fet26kjkcsrymd2kv6d4'
- 'https://tippin.me/@lukechilds'
cacheable-request-7.0.2/.gitignore 0000664 0000000 0000000 00000002130 14060121503 0017051 0 ustar 00root root 0000000 0000000 test/testdb.sqlite
## Node
# Don't commit lockfiles
package-lock.json
yarn.lock
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
## OS X
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
cacheable-request-7.0.2/.npmrc 0000664 0000000 0000000 00000000023 14060121503 0016200 0 ustar 00root root 0000000 0000000 package-lock=false
cacheable-request-7.0.2/.travis.yml 0000664 0000000 0000000 00000000224 14060121503 0017174 0 ustar 00root root 0000000 0000000 language: node_js
node_js:
- '12'
- '10'
- '8'
script: npm test
after_success: npm run coverage
notifications:
email:
on_success: never
cacheable-request-7.0.2/LICENSE 0000664 0000000 0000000 00000002054 14060121503 0016073 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2017 Luke Childs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
cacheable-request-7.0.2/README.md 0000664 0000000 0000000 00000016642 14060121503 0016355 0 ustar 00root root 0000000 0000000 # cacheable-request
> Wrap native HTTP requests with RFC compliant cache support
[](https://travis-ci.org/lukechilds/cacheable-request)
[](https://coveralls.io/github/lukechilds/cacheable-request?branch=master)
[](https://www.npmjs.com/package/cacheable-request)
[](https://www.npmjs.com/package/cacheable-request)
[RFC 7234](http://httpwg.org/specs/rfc7234.html) compliant HTTP caching for native Node.js HTTP/HTTPS requests. Caching works out of the box in memory or is easily pluggable with a wide range of storage adapters.
**Note:** This is a low level wrapper around the core HTTP modules, it's not a high level request library.
## Features
- Only stores cacheable responses as defined by RFC 7234
- Fresh cache entries are served directly from cache
- Stale cache entries are revalidated with `If-None-Match`/`If-Modified-Since` headers
- 304 responses from revalidation requests use cached body
- Updates `Age` header on cached responses
- Can completely bypass cache on a per request basis
- In memory cache by default
- Official support for Redis, MongoDB, SQLite, PostgreSQL and MySQL storage adapters
- Easily plug in your own or third-party storage adapters
- If DB connection fails, cache is automatically bypassed ([disabled by default](#optsautomaticfailover))
- Adds cache support to any existing HTTP code with minimal changes
- Uses [http-cache-semantics](https://github.com/pornel/http-cache-semantics) internally for HTTP RFC 7234 compliance
## Install
```shell
npm install cacheable-request
```
## Usage
```js
const http = require('http');
const CacheableRequest = require('cacheable-request');
// Then instead of
const req = http.request('http://example.com', cb);
req.end();
// You can do
const cacheableRequest = new CacheableRequest(http.request);
const cacheReq = cacheableRequest('http://example.com', cb);
cacheReq.on('request', req => req.end());
// Future requests to 'example.com' will be returned from cache if still valid
// You pass in any other http.request API compatible method to be wrapped with cache support:
const cacheableRequest = new CacheableRequest(https.request);
const cacheableRequest = new CacheableRequest(electron.net);
```
## Storage Adapters
`cacheable-request` uses [Keyv](https://github.com/lukechilds/keyv) to support a wide range of storage adapters.
For example, to use Redis as a cache backend, you just need to install the official Redis Keyv storage adapter:
```
npm install @keyv/redis
```
And then you can pass `CacheableRequest` your connection string:
```js
const cacheableRequest = new CacheableRequest(http.request, 'redis://user:pass@localhost:6379');
```
[View all official Keyv storage adapters.](https://github.com/lukechilds/keyv#official-storage-adapters)
Keyv also supports anything that follows the Map API so it's easy to write your own storage adapter or use a third-party solution.
e.g The following are all valid storage adapters
```js
const storageAdapter = new Map();
// or
const storageAdapter = require('./my-storage-adapter');
// or
const QuickLRU = require('quick-lru');
const storageAdapter = new QuickLRU({ maxSize: 1000 });
const cacheableRequest = new CacheableRequest(http.request, storageAdapter);
```
View the [Keyv docs](https://github.com/lukechilds/keyv) for more information on how to use storage adapters.
## API
### new cacheableRequest(request, [storageAdapter])
Returns the provided request function wrapped with cache support.
#### request
Type: `function`
Request function to wrap with cache support. Should be [`http.request`](https://nodejs.org/api/http.html#http_http_request_options_callback) or a similar API compatible request function.
#### storageAdapter
Type: `Keyv storage adapter`
Default: `new Map()`
A [Keyv](https://github.com/lukechilds/keyv) storage adapter instance, or connection string if using with an official Keyv storage adapter.
### Instance
#### cacheableRequest(opts, [cb])
Returns an event emitter.
##### opts
Type: `object`, `string`
- Any of the default request functions options.
- Any [`http-cache-semantics`](https://github.com/kornelski/http-cache-semantics#constructor-options) options.
- Any of the following:
###### opts.cache
Type: `boolean`
Default: `true`
If the cache should be used. Setting this to false will completely bypass the cache for the current request.
###### opts.strictTtl
Type: `boolean`
Default: `false`
If set to `true` once a cached resource has expired it is deleted and will have to be re-requested.
If set to `false` (default), after a cached resource's TTL expires it is kept in the cache and will be revalidated on the next request with `If-None-Match`/`If-Modified-Since` headers.
###### opts.maxTtl
Type: `number`
Default: `undefined`
Limits TTL. The `number` represents milliseconds.
###### opts.automaticFailover
Type: `boolean`
Default: `false`
When set to `true`, if the DB connection fails we will automatically fallback to a network request. DB errors will still be emitted to notify you of the problem even though the request callback may succeed.
###### opts.forceRefresh
Type: `boolean`
Default: `false`
Forces refreshing the cache. If the response could be retrieved from the cache, it will perform a new request and override the cache instead.
##### cb
Type: `function`
The callback function which will receive the response as an argument.
The response can be either a [Node.js HTTP response stream](https://nodejs.org/api/http.html#http_class_http_incomingmessage) or a [responselike object](https://github.com/lukechilds/responselike). The response will also have a `fromCache` property set with a boolean value.
##### .on('request', request)
`request` event to get the request object of the request.
**Note:** This event will only fire if an HTTP request is actually made, not when a response is retrieved from cache. However, you should always handle the `request` event to end the request and handle any potential request errors.
##### .on('response', response)
`response` event to get the response object from the HTTP request or cache.
##### .on('error', error)
`error` event emitted in case of an error with the cache.
Errors emitted here will be an instance of `CacheableRequest.RequestError` or `CacheableRequest.CacheError`. You will only ever receive a `RequestError` if the request function throws (normally caused by invalid user input). Normal request errors should be handled inside the `request` event.
To properly handle all error scenarios you should use the following pattern:
```js
cacheableRequest('example.com', cb)
.on('error', err => {
if (err instanceof CacheableRequest.CacheError) {
handleCacheError(err); // Cache error
} else if (err instanceof CacheableRequest.RequestError) {
handleRequestError(err); // Request function thrown
}
})
.on('request', req => {
req.on('error', handleRequestError); // Request error emitted
req.end();
});
```
**Note:** Database connection errors are emitted here, however `cacheable-request` will attempt to re-request the resource and bypass the cache on a connection error. Therefore a database connection error doesn't necessarily mean the request won't be fulfilled.
## License
MIT © Luke Childs
cacheable-request-7.0.2/package.json 0000664 0000000 0000000 00000002210 14060121503 0017346 0 ustar 00root root 0000000 0000000 {
"name": "cacheable-request",
"version": "7.0.2",
"description": "Wrap native HTTP requests with RFC compliant cache support",
"license": "MIT",
"repository": "lukechilds/cacheable-request",
"author": "Luke Childs (http://lukechilds.co.uk)",
"main": "src/index.js",
"engines": {
"node": ">=8"
},
"scripts": {
"test": "xo && nyc ava",
"coverage": "nyc report --reporter=text-lcov | coveralls"
},
"files": [
"src"
],
"keywords": [
"HTTP",
"HTTPS",
"cache",
"caching",
"layer",
"cacheable",
"RFC 7234",
"RFC",
"7234",
"compliant"
],
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
},
"devDependencies": {
"@keyv/sqlite": "^2.0.0",
"ava": "^1.1.0",
"coveralls": "^3.0.0",
"create-test-server": "3.0.0",
"delay": "^4.0.0",
"eslint-config-xo-lukechilds": "^1.0.0",
"nyc": "^14.1.1",
"pify": "^4.0.0",
"sqlite3": "^4.0.2",
"this": "^1.0.2",
"xo": "^0.23.0"
},
"xo": {
"extends": "xo-lukechilds"
}
}
cacheable-request-7.0.2/src/ 0000775 0000000 0000000 00000000000 14060121503 0015654 5 ustar 00root root 0000000 0000000 cacheable-request-7.0.2/src/index.js 0000664 0000000 0000000 00000015430 14060121503 0017324 0 ustar 00root root 0000000 0000000 '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,
stripAuthentication: false
}
);
const key = `${opts.method}:${normalizedUrlString}`;
let revalidate = false;
let madeRequest = false;
const makeRequest = opts => {
madeRequest = true;
let requestErrored = false;
let requestErrorCallback;
const requestErrorPromise = new Promise(resolve => {
requestErrorCallback = () => {
if (!requestErrored) {
requestErrored = true;
resolve();
}
};
});
const handler = response => {
if (revalidate && !opts.forceRefresh) {
response.status = response.statusCode;
const revalidatedPolicy = CachePolicy.fromObject(revalidate.cachePolicy).revalidatedPolicy(opts, response);
if (!revalidatedPolicy.modified) {
const headers = revalidatedPolicy.policy.responseHeaders();
response = new Response(revalidate.statusCode, headers, revalidate.body, revalidate.url);
response.cachePolicy = revalidatedPolicy.policy;
response.fromCache = true;
}
}
if (!response.fromCache) {
response.cachePolicy = new CachePolicy(opts, response, opts);
response.fromCache = false;
}
let clonedResponse;
if (opts.cache && response.cachePolicy.storable()) {
clonedResponse = cloneResponse(response);
(async () => {
try {
const bodyPromise = getStream.buffer(response);
await Promise.race([
requestErrorPromise,
new Promise(resolve => response.once('end', resolve))
]);
if (requestErrored) {
return;
}
const body = await bodyPromise;
const value = {
cachePolicy: response.cachePolicy.toObject(),
url: response.url,
statusCode: response.fromCache ? revalidate.statusCode : response.statusCode,
body
};
let ttl = opts.strictTtl ? response.cachePolicy.timeToLive() : undefined;
if (opts.maxTtl) {
ttl = ttl ? Math.min(ttl, opts.maxTtl) : opts.maxTtl;
}
await this.cache.set(key, value, ttl);
} catch (error) {
ee.emit('error', new CacheableRequest.CacheError(error));
}
})();
} else if (opts.cache && revalidate) {
(async () => {
try {
await this.cache.delete(key);
} catch (error) {
ee.emit('error', new CacheableRequest.CacheError(error));
}
})();
}
ee.emit('response', clonedResponse || response);
if (typeof cb === 'function') {
cb(clonedResponse || response);
}
};
try {
const req = request(opts, handler);
req.once('error', requestErrorCallback);
req.once('abort', requestErrorCallback);
ee.emit('request', req);
} catch (error) {
ee.emit('error', new CacheableRequest.RequestError(error));
}
};
(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) && !opts.forceRefresh) {
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);
}
};
const errorHandler = error => ee.emit('error', new CacheableRequest.CacheError(error));
this.cache.once('error', errorHandler);
ee.on('response', () => this.cache.removeListener('error', errorHandler));
try {
await get(opts);
} catch (error) {
if (opts.automaticFailover && !madeRequest) {
makeRequest(opts);
}
ee.emit('error', new CacheableRequest.CacheError(error));
}
})();
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(error) {
super(error.message);
this.name = 'RequestError';
Object.assign(this, error);
}
};
CacheableRequest.CacheError = class extends Error {
constructor(error) {
super(error.message);
this.name = 'CacheError';
Object.assign(this, error);
}
};
module.exports = CacheableRequest;
cacheable-request-7.0.2/test/ 0000775 0000000 0000000 00000000000 14060121503 0016044 5 ustar 00root root 0000000 0000000 cacheable-request-7.0.2/test/cache.js 0000664 0000000 0000000 00000040207 14060121503 0017450 0 ustar 00root root 0000000 0000000 import { request } from 'http';
import url from 'url';
import util from 'util';
import test from 'ava';
import getStream from 'get-stream';
import createTestServer from 'create-test-server';
import delay from 'delay';
import sqlite3 from 'sqlite3';
import pify from 'pify';
import CacheableRequest from 'this';
let s;
// Promisify cacheableRequest
const promisify = cacheableRequest => opts => new Promise((resolve, reject) => {
cacheableRequest(opts, async response => {
const body = await getStream(response);
response.body = body;
// Give the cache time to update
await delay(100);
resolve(response);
})
.on('request', req => req.end())
.once('error', reject);
});
test.before('setup', async () => {
s = await createTestServer();
let noStoreIndex = 0;
s.get('/no-store', (req, res) => {
noStoreIndex++;
res.setHeader('Cache-Control', 'public, no-cache, no-store');
res.end(noStoreIndex.toString());
});
let cacheIndex = 0;
s.get('/cache', (req, res) => {
cacheIndex++;
res.setHeader('Cache-Control', 'public, max-age=60');
res.end(cacheIndex.toString());
});
s.get('/last-modified', (req, res) => {
res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('Last-Modified', 'Wed, 21 Oct 2015 07:28:00 GMT');
let responseBody = 'last-modified';
if (req.headers['if-modified-since'] === 'Wed, 21 Oct 2015 07:28:00 GMT') {
res.statusCode = 304;
responseBody = null;
}
res.end(responseBody);
});
let calledFirstError = false;
s.get('/first-error', (req, res) => {
if (calledFirstError) {
res.end('ok');
return;
}
calledFirstError = true;
res.statusCode = 502;
res.end('received 502');
});
s.get('/etag', (req, res) => {
res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('ETag', '33a64df551425fcc55e4d42a148795d9f25f89d4');
let responseBody = 'etag';
if (req.headers['if-none-match'] === '33a64df551425fcc55e4d42a148795d9f25f89d4') {
res.statusCode = 304;
responseBody = null;
}
res.end(responseBody);
});
s.get('/revalidate-modified', (req, res) => {
res.setHeader('Cache-Control', 'public, max-age=0');
res.setHeader('ETag', '33a64df551425fcc55e4d42a148795d9f25f89d4');
let responseBody = 'revalidate-modified';
if (req.headers['if-none-match'] === '33a64df551425fcc55e4d42a148795d9f25f89d4') {
res.setHeader('ETag', '0000000000000000000000000000000000');
responseBody = 'new-body';
}
res.end(responseBody);
});
let cacheThenNoStoreIndex = 0;
s.get('/cache-then-no-store-on-revalidate', (req, res) => {
const cc = cacheThenNoStoreIndex === 0 ? 'public, max-age=0' : 'public, no-cache, no-store';
cacheThenNoStoreIndex++;
res.setHeader('Cache-Control', cc);
res.end('cache-then-no-store-on-revalidate');
});
s.get('/echo', (req, res) => {
const { headers, query, path, originalUrl, body } = req;
res.json({
headers,
query,
path,
originalUrl,
body
});
});
});
test('Non cacheable responses are not cached', async t => {
const endpoint = '/no-store';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const firstResponseInt = Number((await cacheableRequestHelper(s.url + endpoint)).body);
const secondResponseInt = Number((await cacheableRequestHelper(s.url + endpoint)).body);
t.is(cache.size, 0);
t.true(firstResponseInt < secondResponseInt);
});
test('Cacheable responses are cached', async t => {
const endpoint = '/cache';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const firstResponse = await cacheableRequestHelper(s.url + endpoint);
const secondResponse = await cacheableRequestHelper(s.url + endpoint);
t.is(cache.size, 1);
t.is(firstResponse.body, secondResponse.body);
});
test('Cacheable responses have unique cache key', async t => {
const endpoint = '/cache';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const firstResponse = await cacheableRequestHelper(s.url + endpoint + '?foo');
const secondResponse = await cacheableRequestHelper(s.url + endpoint + '?bar');
t.is(cache.size, 2);
t.not(firstResponse.body, secondResponse.body);
});
async function testCacheKey(t, input, expected) {
const expectKey = `cacheable-request:${expected}`;
const okMessage = `OK ${expectKey}`;
const cache = {
get(key) {
t.is(key, expectKey);
throw new Error(okMessage);
}
};
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
await t.throwsAsync(
cacheableRequestHelper(input),
CacheableRequest.CacheError,
okMessage
);
}
testCacheKey.title = (providedTitle, input) => util.format(
'Cache key is http.request compatible for arg %s(%j)%s',
input.constructor.name,
input,
providedTitle ? ` (${providedTitle})` : ''
);
test(
testCacheKey,
'http://www.example.com',
'GET:http://www.example.com'
);
test(
'strips default path',
testCacheKey,
'http://www.example.com/',
'GET:http://www.example.com'
);
test(
'keeps trailing /',
testCacheKey,
'http://www.example.com/test/',
'GET:http://www.example.com/test/'
);
test(
testCacheKey,
new url.URL('http://www.example.com'),
'GET:http://www.example.com'
);
test(
'no requried properties',
testCacheKey,
{},
'GET:http://localhost'
);
test(
testCacheKey,
{
protocol: 'http:',
host: 'www.example.com',
port: 80,
path: '/'
},
'GET:http://www.example.com'
);
test(
testCacheKey,
{
hostname: 'www.example.com',
port: 80,
path: '/'
},
'GET:http://www.example.com'
);
test(
testCacheKey,
{
hostname: 'www.example.com',
port: 8080,
path: '/'
},
'GET:http://www.example.com:8080'
);
test(
testCacheKey,
{ host: 'www.example.com' },
'GET:http://www.example.com'
);
test(
'hostname over host',
testCacheKey,
{
host: 'www.example.com',
hostname: 'xyz.example.com'
},
'GET:http://xyz.example.com'
);
test(
'hostname defaults to localhost',
testCacheKey,
{ path: '/' },
'GET:http://localhost'
);
test(
'ignores pathname',
testCacheKey,
{
path: '/foo',
pathname: '/bar'
},
'GET:http://localhost/foo'
);
test(
'ignores search',
testCacheKey,
{
path: '/?foo=bar',
search: '?bar=baz'
},
'GET:http://localhost/?foo=bar'
);
test(
'ignores query',
testCacheKey,
{
path: '/?foo=bar',
query: { bar: 'baz' }
},
'GET:http://localhost/?foo=bar'
);
test(
testCacheKey,
{ auth: 'user:pass' },
'GET:http://user:pass@localhost'
);
test(
testCacheKey,
{ method: 'POST' },
'POST:http://localhost'
);
test('request options path query is passed through', async t => {
const cacheableRequest = new CacheableRequest(request);
const cacheableRequestHelper = promisify(cacheableRequest);
const argString = `${s.url}/echo?foo=bar`;
const argURL = new url.URL(argString);
const urlObject = url.parse(argString);
const argOptions = {
hostname: urlObject.hostname,
port: urlObject.port,
path: urlObject.path
};
const inputs = [argString, argURL, argOptions];
for (const input of inputs) {
// eslint-disable-next-line no-await-in-loop
const response = await cacheableRequestHelper(input);
const body = JSON.parse(response.body);
const message = util.format(
'when request arg is %s(%j)',
input.constructor.name,
input
);
t.is(body.query.foo, 'bar', message);
}
});
test('Setting opts.cache to false bypasses cache for a single request', async t => {
const endpoint = '/cache';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const opts = url.parse(s.url + endpoint);
const optsNoCache = { cache: false, ...opts };
const firstResponse = await cacheableRequestHelper(opts);
const secondResponse = await cacheableRequestHelper(opts);
const thirdResponse = await cacheableRequestHelper(optsNoCache);
const fourthResponse = await cacheableRequestHelper(opts);
t.false(firstResponse.fromCache);
t.true(secondResponse.fromCache);
t.false(thirdResponse.fromCache);
t.true(fourthResponse.fromCache);
});
test('TTL is passed to cache', async t => {
const endpoint = '/cache';
const store = new Map();
const cache = {
get: store.get.bind(store),
set: (key, value, ttl) => {
t.is(typeof ttl, 'number');
t.true(ttl > 0);
return store.set(key, value, ttl);
},
delete: store.delete.bind(store)
};
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const opts = { strictTtl: true, ...url.parse(s.url + endpoint) };
t.plan(2);
await cacheableRequestHelper(opts);
});
test('TTL is not passed to cache if strictTtl is false', async t => {
const endpoint = '/cache';
const store = new Map();
const cache = {
get: store.get.bind(store),
set: (key, value, ttl) => {
t.true(typeof ttl === 'undefined');
return store.set(key, value, ttl);
},
delete: store.delete.bind(store)
};
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const opts = { strictTtl: false, ...url.parse(s.url + endpoint) };
t.plan(1);
await cacheableRequestHelper(opts);
});
test('Setting opts.maxTtl will limit the TTL', async t => {
const endpoint = '/cache';
const store = new Map();
const cache = {
get: store.get.bind(store),
set: (key, value, ttl) => {
t.is(ttl, 1000);
return store.set(key, value, ttl);
},
delete: store.delete.bind(store)
};
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const opts = {
...url.parse(s.url + endpoint),
maxTtl: 1000
};
t.plan(1);
await cacheableRequestHelper(opts);
});
test('Setting opts.maxTtl when opts.strictTtl is true will use opts.maxTtl if it\'s smaller', async t => {
const endpoint = '/cache';
const store = new Map();
const cache = {
get: store.get.bind(store),
set: (key, value, ttl) => {
t.true(ttl === 1000);
return store.set(key, value, ttl);
},
delete: store.delete.bind(store)
};
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const opts = {
...url.parse(s.url + endpoint),
strictTtl: true,
maxTtl: 1000
};
t.plan(1);
await cacheableRequestHelper(opts);
});
test('Setting opts.maxTtl when opts.strictTtl is true will use remote TTL if it\'s smaller', async t => {
const endpoint = '/cache';
const store = new Map();
const cache = {
get: store.get.bind(store),
set: (key, value, ttl) => {
t.true(ttl < 100000);
return store.set(key, value, ttl);
},
delete: store.delete.bind(store)
};
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const opts = {
...url.parse(s.url + endpoint),
strictTtl: true,
maxTtl: 100000
};
t.plan(1);
await cacheableRequestHelper(opts);
});
test('Stale cache entries with Last-Modified headers are revalidated', async t => {
const endpoint = '/last-modified';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const firstResponse = await cacheableRequestHelper(s.url + endpoint);
const secondResponse = await cacheableRequestHelper(s.url + endpoint);
t.is(cache.size, 1);
t.is(firstResponse.statusCode, 200);
t.is(secondResponse.statusCode, 200);
t.false(firstResponse.fromCache);
t.true(secondResponse.fromCache);
t.is(firstResponse.body, 'last-modified');
t.is(firstResponse.body, secondResponse.body);
});
test('Stale cache entries with ETag headers are revalidated', async t => {
const endpoint = '/etag';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const firstResponse = await cacheableRequestHelper(s.url + endpoint);
const secondResponse = await cacheableRequestHelper(s.url + endpoint);
t.is(cache.size, 1);
t.is(firstResponse.statusCode, 200);
t.is(secondResponse.statusCode, 200);
t.false(firstResponse.fromCache);
t.true(secondResponse.fromCache);
t.is(firstResponse.body, 'etag');
t.is(firstResponse.body, secondResponse.body);
});
test('Stale cache entries that can\'t be revalidate are deleted from cache', async t => {
const endpoint = '/cache-then-no-store-on-revalidate';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const firstResponse = await cacheableRequestHelper(s.url + endpoint);
t.is(cache.size, 1);
const secondResponse = await cacheableRequestHelper(s.url + endpoint);
t.is(cache.size, 0);
t.is(firstResponse.statusCode, 200);
t.is(secondResponse.statusCode, 200);
t.is(firstResponse.body, 'cache-then-no-store-on-revalidate');
t.is(firstResponse.body, secondResponse.body);
});
test('Response objects have fromCache property set correctly', async t => {
const endpoint = '/cache';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const response = await cacheableRequestHelper(s.url + endpoint);
const cachedResponse = await cacheableRequestHelper(s.url + endpoint);
t.false(response.fromCache);
t.true(cachedResponse.fromCache);
});
test('Revalidated responses that are modified are passed through', async t => {
const endpoint = '/revalidate-modified';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const firstResponse = await cacheableRequestHelper(s.url + endpoint);
const secondResponse = await cacheableRequestHelper(s.url + endpoint);
t.is(firstResponse.statusCode, 200);
t.is(secondResponse.statusCode, 200);
t.is(firstResponse.body, 'revalidate-modified');
t.is(secondResponse.body, 'new-body');
});
test('Undefined callback parameter inside cache logic is handled', async t => {
const endpoint = '/cache';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
await cacheableRequestHelper(s.url + endpoint);
cacheableRequest(s.url + endpoint);
await delay(500);
t.pass();
});
test('Keyv cache adapters load via connection uri', async t => {
const endpoint = '/cache';
const cacheableRequest = new CacheableRequest(request, 'sqlite://test/testdb.sqlite');
const cacheableRequestHelper = promisify(cacheableRequest);
const db = new sqlite3.Database('test/testdb.sqlite');
const query = pify(db.all.bind(db));
const firstResponse = await cacheableRequestHelper(s.url + endpoint);
await delay(1000);
const secondResponse = await cacheableRequestHelper(s.url + endpoint);
const cacheResult = await query(`SELECT * FROM keyv WHERE "key" = "cacheable-request:GET:${s.url + endpoint}"`);
t.false(firstResponse.fromCache);
t.true(secondResponse.fromCache);
t.is(cacheResult.length, 1);
await query('DELETE FROM keyv');
});
test('ability to force refresh', async t => {
const endpoint = '/cache';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const opts = url.parse(s.url + endpoint);
const firstResponse = await cacheableRequestHelper(opts);
const secondResponse = await cacheableRequestHelper({ ...opts, forceRefresh: true });
const thirdResponse = await cacheableRequestHelper(opts);
t.not(firstResponse.body, secondResponse.body);
t.is(secondResponse.body, thirdResponse.body);
});
test('checks status codes when comparing cache & response', async t => {
const endpoint = '/first-error';
const cache = new Map();
const cacheableRequest = new CacheableRequest(request, cache);
const cacheableRequestHelper = promisify(cacheableRequest);
const opts = url.parse(s.url + endpoint);
const firstResponse = await cacheableRequestHelper(opts);
const secondResponse = await cacheableRequestHelper(opts);
t.is(firstResponse.body, 'received 502');
t.is(secondResponse.body, 'ok');
});
test.after('cleanup', async () => {
await s.close();
});
cacheable-request-7.0.2/test/cacheable-request-class.js 0000664 0000000 0000000 00000001176 14060121503 0023067 0 ustar 00root root 0000000 0000000 import { request } from 'http';
import test from 'ava';
import CacheableRequest from 'this';
test('CacheableRequest is a function', t => {
t.is(typeof CacheableRequest, 'function');
});
test('CacheableRequest cannot be invoked without \'new\'', t => {
t.throws(() => CacheableRequest(request)); // eslint-disable-line new-cap
t.notThrows(() => new CacheableRequest(request));
});
test('CacheableRequest throws TypeError if request fn isn\'t passed in', t => {
const error = t.throws(() => {
new CacheableRequest(); // eslint-disable-line no-new
}, TypeError);
t.is(error.message, 'Parameter `request` must be a function');
});
cacheable-request-7.0.2/test/cacheable-request-instance.js 0000664 0000000 0000000 00000021042 14060121503 0023560 0 ustar 00root root 0000000 0000000 import EventEmitter from 'events';
import { request } from 'http';
import url from 'url';
import test from 'ava';
import createTestServer from 'create-test-server';
import getStream from 'get-stream';
import CacheableRequest from 'this';
let s;
test.before('setup', async () => {
s = await createTestServer();
s.get('/', (req, res) => {
res.setHeader('cache-control', 'max-age=60');
res.end('hi');
});
});
test('cacheableRequest is a function', t => {
const cacheableRequest = new CacheableRequest(request);
t.is(typeof cacheableRequest, 'function');
});
test.cb('cacheableRequest returns an event emitter', t => {
const cacheableRequest = new CacheableRequest(request);
const returnValue = cacheableRequest(url.parse(s.url), () => t.end()).on('request', req => req.end());
t.true(returnValue instanceof EventEmitter);
});
test.cb('cacheableRequest passes requests through if no cache option is set', t => {
const cacheableRequest = new CacheableRequest(request);
cacheableRequest(url.parse(s.url), async response => {
const body = await getStream(response);
t.is(body, 'hi');
t.end();
}).on('request', req => req.end());
});
test.cb('cacheableRequest accepts url as string', t => {
const cacheableRequest = new CacheableRequest(request);
cacheableRequest(s.url, async response => {
const body = await getStream(response);
t.is(body, 'hi');
t.end();
}).on('request', req => req.end());
});
test.cb('cacheableRequest accepts url as URL', t => {
const cacheableRequest = new CacheableRequest(request);
cacheableRequest(new url.URL(s.url), async response => {
const body = await getStream(response);
t.is(body, 'hi');
t.end();
}).on('request', req => req.end());
});
test.cb('cacheableRequest handles no callback parameter', t => {
const cacheableRequest = new CacheableRequest(request);
cacheableRequest(url.parse(s.url)).on('request', req => {
req.end();
req.on('response', response => {
t.is(response.statusCode, 200);
t.end();
});
});
});
test.cb('cacheableRequest emits response event for network responses', t => {
const cacheableRequest = new CacheableRequest(request);
cacheableRequest(url.parse(s.url))
.on('request', req => req.end())
.on('response', response => {
t.false(response.fromCache);
t.end();
});
});
test.cb('cacheableRequest emits response event for cached responses', t => {
const cacheableRequest = new CacheableRequest(request);
const cache = new Map();
const opts = Object.assign(url.parse(s.url), { cache });
cacheableRequest(opts, () => {
// This needs to happen in next tick so cache entry has time to be stored
setImmediate(() => {
cacheableRequest(opts)
.on('request', req => req.end())
.on('response', response => {
t.true(response.fromCache);
t.end();
});
});
}).on('request', req => req.end());
});
test.cb('cacheableRequest emits CacheError if cache adapter connection errors', t => {
const cacheableRequest = new CacheableRequest(request, 'sqlite://non/existent/database.sqlite');
cacheableRequest(url.parse(s.url))
.on('error', err => {
t.true(err instanceof CacheableRequest.CacheError);
t.is(err.code, 'SQLITE_CANTOPEN');
t.end();
})
.on('request', req => req.end());
});
test.cb('cacheableRequest emits CacheError if cache.get errors', t => {
const errMessage = 'Fail';
const store = new Map();
const cache = {
get: () => {
throw new Error(errMessage);
},
set: store.set.bind(store),
delete: store.delete.bind(store)
};
const cacheableRequest = new CacheableRequest(request, cache);
cacheableRequest(url.parse(s.url))
.on('error', err => {
t.true(err instanceof CacheableRequest.CacheError);
t.is(err.message, errMessage);
t.end();
})
.on('request', req => req.end());
});
test.cb('cacheableRequest emits CacheError if cache.set errors', t => {
const errMessage = 'Fail';
const store = new Map();
const cache = {
get: store.get.bind(store),
set: () => {
throw new Error(errMessage);
},
delete: store.delete.bind(store)
};
const cacheableRequest = new CacheableRequest(request, cache);
cacheableRequest(url.parse(s.url))
.on('error', err => {
t.true(err instanceof CacheableRequest.CacheError);
t.is(err.message, errMessage);
t.end();
})
.on('request', req => req.end());
});
test.cb('cacheableRequest emits CacheError if cache.delete errors', t => {
const errMessage = 'Fail';
const store = new Map();
const cache = {
get: store.get.bind(store),
set: store.set.bind(store),
delete: () => {
throw new Error(errMessage);
}
};
const cacheableRequest = new CacheableRequest(request, cache);
(async () => {
let i = 0;
const s = await createTestServer();
s.get('/', (req, res) => {
const cc = i === 0 ? 'public, max-age=0' : 'public, no-cache, no-store';
i++;
res.setHeader('Cache-Control', cc);
res.end('hi');
});
cacheableRequest(s.url, () => {
// This needs to happen in next tick so cache entry has time to be stored
setImmediate(() => {
cacheableRequest(s.url)
.on('error', async err => {
t.true(err instanceof CacheableRequest.CacheError);
t.is(err.message, errMessage);
await s.close();
t.end();
})
.on('request', req => req.end());
});
}).on('request', req => req.end());
})();
});
test.cb('cacheableRequest emits RequestError if request function throws', t => {
const cacheableRequest = new CacheableRequest(request);
const opts = url.parse(s.url);
opts.headers = { invalid: '💣' };
cacheableRequest(opts)
.on('error', err => {
t.true(err instanceof CacheableRequest.RequestError);
t.end();
})
.on('request', req => req.end());
});
test.cb('cacheableRequest does not cache response if request is aborted before receiving first byte of response', t => {
/* eslint-disable max-nested-callbacks */
// eslint-disable-next-line promise/prefer-await-to-then
createTestServer().then(s => {
s.get('/delay-start', (req, res) => {
setTimeout(() => {
res.setHeader('cache-control', 'max-age=60');
res.end('hi');
}, 50);
});
const cacheableRequest = new CacheableRequest(request);
const opts = url.parse(s.url);
opts.path = '/delay-start';
cacheableRequest(opts)
.on('request', req => {
req.end();
setTimeout(() => {
req.abort();
}, 20);
setTimeout(() => {
cacheableRequest(opts, async response => {
t.is(response.fromCache, false);
const body = await getStream(response);
t.is(body, 'hi');
t.end();
}).on('request', req => req.end());
}, 100);
});
});
/* eslint-enable max-nested-callbacks */
});
test.cb('cacheableRequest does not cache response if request is aborted after receiving part of the response', t => {
/* eslint-disable max-nested-callbacks */
// eslint-disable-next-line promise/prefer-await-to-then
createTestServer().then(s => {
s.get('/delay-partial', (req, res) => {
res.setHeader('cache-control', 'max-age=60');
res.write('h');
setTimeout(() => {
res.end('i');
}, 50);
});
const cacheableRequest = new CacheableRequest(request);
const opts = url.parse(s.url);
opts.path = '/delay-partial';
cacheableRequest(opts)
.on('request', req => {
req.end();
setTimeout(() => {
req.abort();
}, 20);
setTimeout(() => {
cacheableRequest(opts, async response => {
t.is(response.fromCache, false);
const body = await getStream(response);
t.is(body, 'hi');
t.end();
}).on('request', req => req.end());
}, 100);
});
});
/* eslint-enable max-nested-callbacks */
});
test.cb('cacheableRequest makes request even if initial DB connection fails (when opts.automaticFailover is enabled)', t => {
const cacheableRequest = new CacheableRequest(request, 'sqlite://non/existent/database.sqlite');
const opts = url.parse(s.url);
opts.automaticFailover = true;
cacheableRequest(opts, res => {
t.is(res.statusCode, 200);
t.end();
})
.on('error', () => {})
.on('request', req => req.end());
});
test.cb('cacheableRequest makes request even if current DB connection fails (when opts.automaticFailover is enabled)', t => {
/* eslint-disable unicorn/error-message */
const cache = {
get: () => {
throw new Error();
},
set: () => {
throw new Error();
},
delete: () => {
throw new Error();
}
};
/* eslint-enable unicorn/error-message */
const cacheableRequest = new CacheableRequest(request, cache);
const opts = url.parse(s.url);
opts.automaticFailover = true;
cacheableRequest(opts, res => {
t.is(res.statusCode, 200);
t.end();
})
.on('error', () => {})
.on('request', req => req.end());
});
test.after('cleanup', async () => {
await s.close();
});