fetch-dedupe
Advanced tools
Comparing version 2.1.0 to 3.0.0-beta1
# Changelog | ||
### v2.1.1 (2018/3/24) | ||
**Bug Fixes** | ||
* Resolves a problem where you were unable to use the signature `fetch(input, init)`. | ||
### v2.1.0 (2018/2/7) | ||
@@ -4,0 +10,0 @@ |
@@ -7,6 +7,4 @@ (function (global, factory) { | ||
// This is a cache of in-flight requests. Each request key maps to an | ||
// array of Promises. When the request resolves, each promise in the | ||
// array is pushed to. | ||
var requests = {}; | ||
var requestCache = {}; | ||
var responseCache = {}; | ||
@@ -30,9 +28,17 @@ function getRequestKey() { | ||
function isRequestInFlight(requestKey) { | ||
return Boolean(requests[requestKey]); | ||
return Boolean(requestCache[requestKey]); | ||
} | ||
function isResponseCached(requestKey) { | ||
return Boolean(responseCache[requestKey]); | ||
} | ||
function clearRequestCache() { | ||
requests = {}; | ||
requestCache = {}; | ||
} | ||
function clearResponseCache() { | ||
responseCache = {}; | ||
} | ||
// This loops through all of the handlers for the request and either | ||
@@ -45,7 +51,9 @@ // resolves or rejects them. | ||
var handlers = requests[requestKey] || []; | ||
var handlers = requestCache[requestKey] || []; | ||
handlers.forEach(function (handler) { | ||
if (res) { | ||
handler.resolve(res); | ||
var resToSend = new Response(res.body, res); | ||
resToSend.data = res.data; | ||
handler.resolve(resToSend); | ||
} else { | ||
@@ -58,6 +66,24 @@ handler.reject(err); | ||
// clear the handlers for the next request. | ||
requests[requestKey] = null; | ||
requestCache[requestKey] = null; | ||
} | ||
function fetchDedupe(input, init, dedupeOptions) { | ||
function CacheMissError() { | ||
var err = Error.apply(this, arguments); | ||
err.name = this.name = 'CacheMissError'; | ||
this.message = err.message; | ||
this.stack = err.stack; | ||
} | ||
CacheMissError.prototype = Object.create(Error.prototype, { | ||
constructor: { | ||
value: CacheMissError, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
function fetchDedupe(input) { | ||
var init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var dedupeOptions = arguments[2]; | ||
var opts = void 0, | ||
@@ -68,3 +94,3 @@ initToUse = void 0; | ||
initToUse = init; | ||
} else if (init && init.responseType) { | ||
} else if (init && (init.responseType || init.dedupe || init.cachePolicy || init.requestKey)) { | ||
opts = init; | ||
@@ -74,3 +100,3 @@ initToUse = {}; | ||
opts = {}; | ||
initToUse = {}; | ||
initToUse = init; | ||
} | ||
@@ -83,6 +109,18 @@ | ||
_opts$dedupe = _opts.dedupe, | ||
dedupe = _opts$dedupe === undefined ? true : _opts$dedupe; | ||
dedupe = _opts$dedupe === undefined ? true : _opts$dedupe, | ||
cachePolicy = _opts.cachePolicy; | ||
var method = initToUse.method || input.method || ''; | ||
var upperCaseMethod = method.toUpperCase(); | ||
var appliedCachePolicy = void 0; | ||
if (cachePolicy) { | ||
appliedCachePolicy = cachePolicy; | ||
} else { | ||
var isReadRequest = upperCaseMethod === 'GET' || upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD' || upperCaseMethod === ''; | ||
appliedCachePolicy = isReadRequest ? 'cache-first' : 'network-only'; | ||
} | ||
// Build the default request key if one is not passed | ||
var requestKeyToUse = requestKey || getRequestKey({ | ||
@@ -97,9 +135,24 @@ // If `input` is a request, then we use that URL | ||
var cachedResponse = void 0; | ||
if (appliedCachePolicy !== 'network-only') { | ||
cachedResponse = responseCache[requestKeyToUse]; | ||
if (cachedResponse) { | ||
var resp = new Response(cachedResponse.body, cachedResponse); | ||
resp.data = cachedResponse.data; | ||
resp.fromCache = true; | ||
return Promise.resolve(resp); | ||
} else if (cachePolicy === 'cache-only') { | ||
var cacheError = new CacheMissError('Response for fetch request not found in cache.'); | ||
return Promise.reject(cacheError); | ||
} | ||
} | ||
var proxyReq = void 0; | ||
if (dedupe) { | ||
if (!requests[requestKeyToUse]) { | ||
requests[requestKeyToUse] = []; | ||
if (!requestCache[requestKeyToUse]) { | ||
requestCache[requestKeyToUse] = []; | ||
} | ||
var handlers = requests[requestKeyToUse]; | ||
var handlers = requestCache[requestKeyToUse]; | ||
var requestInFlight = Boolean(handlers.length); | ||
@@ -135,2 +188,3 @@ var requestHandler = {}; | ||
res.data = data; | ||
responseCache[requestKeyToUse] = res; | ||
@@ -160,3 +214,5 @@ if (dedupe) { | ||
exports.isRequestInFlight = isRequestInFlight; | ||
exports.isResponseCached = isResponseCached; | ||
exports.clearRequestCache = clearRequestCache; | ||
exports.clearResponseCache = clearResponseCache; | ||
exports.fetchDedupe = fetchDedupe; | ||
@@ -163,0 +219,0 @@ |
@@ -1,5 +0,3 @@ | ||
// This is a cache of in-flight requests. Each request key maps to an | ||
// array of Promises. When the request resolves, each promise in the | ||
// array is pushed to. | ||
var requests = {}; | ||
var requestCache = {}; | ||
var responseCache = {}; | ||
@@ -23,9 +21,17 @@ export function getRequestKey() { | ||
export function isRequestInFlight(requestKey) { | ||
return Boolean(requests[requestKey]); | ||
return Boolean(requestCache[requestKey]); | ||
} | ||
export function isResponseCached(requestKey) { | ||
return Boolean(responseCache[requestKey]); | ||
} | ||
export function clearRequestCache() { | ||
requests = {}; | ||
requestCache = {}; | ||
} | ||
export function clearResponseCache() { | ||
responseCache = {}; | ||
} | ||
// This loops through all of the handlers for the request and either | ||
@@ -38,7 +44,9 @@ // resolves or rejects them. | ||
var handlers = requests[requestKey] || []; | ||
var handlers = requestCache[requestKey] || []; | ||
handlers.forEach(function (handler) { | ||
if (res) { | ||
handler.resolve(res); | ||
var resToSend = new Response(res.body, res); | ||
resToSend.data = res.data; | ||
handler.resolve(resToSend); | ||
} else { | ||
@@ -51,6 +59,24 @@ handler.reject(err); | ||
// clear the handlers for the next request. | ||
requests[requestKey] = null; | ||
requestCache[requestKey] = null; | ||
} | ||
export function fetchDedupe(input, init, dedupeOptions) { | ||
function CacheMissError() { | ||
var err = Error.apply(this, arguments); | ||
err.name = this.name = 'CacheMissError'; | ||
this.message = err.message; | ||
this.stack = err.stack; | ||
} | ||
CacheMissError.prototype = Object.create(Error.prototype, { | ||
constructor: { | ||
value: CacheMissError, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
export function fetchDedupe(input) { | ||
var init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var dedupeOptions = arguments[2]; | ||
var opts = void 0, | ||
@@ -61,3 +87,3 @@ initToUse = void 0; | ||
initToUse = init; | ||
} else if (init && init.responseType) { | ||
} else if (init && (init.responseType || init.dedupe || init.cachePolicy || init.requestKey)) { | ||
opts = init; | ||
@@ -67,3 +93,3 @@ initToUse = {}; | ||
opts = {}; | ||
initToUse = {}; | ||
initToUse = init; | ||
} | ||
@@ -76,6 +102,18 @@ | ||
_opts$dedupe = _opts.dedupe, | ||
dedupe = _opts$dedupe === undefined ? true : _opts$dedupe; | ||
dedupe = _opts$dedupe === undefined ? true : _opts$dedupe, | ||
cachePolicy = _opts.cachePolicy; | ||
var method = initToUse.method || input.method || ''; | ||
var upperCaseMethod = method.toUpperCase(); | ||
var appliedCachePolicy = void 0; | ||
if (cachePolicy) { | ||
appliedCachePolicy = cachePolicy; | ||
} else { | ||
var isReadRequest = upperCaseMethod === 'GET' || upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD' || upperCaseMethod === ''; | ||
appliedCachePolicy = isReadRequest ? 'cache-first' : 'network-only'; | ||
} | ||
// Build the default request key if one is not passed | ||
var requestKeyToUse = requestKey || getRequestKey({ | ||
@@ -90,9 +128,24 @@ // If `input` is a request, then we use that URL | ||
var cachedResponse = void 0; | ||
if (appliedCachePolicy !== 'network-only') { | ||
cachedResponse = responseCache[requestKeyToUse]; | ||
if (cachedResponse) { | ||
var resp = new Response(cachedResponse.body, cachedResponse); | ||
resp.data = cachedResponse.data; | ||
resp.fromCache = true; | ||
return Promise.resolve(resp); | ||
} else if (cachePolicy === 'cache-only') { | ||
var cacheError = new CacheMissError('Response for fetch request not found in cache.'); | ||
return Promise.reject(cacheError); | ||
} | ||
} | ||
var proxyReq = void 0; | ||
if (dedupe) { | ||
if (!requests[requestKeyToUse]) { | ||
requests[requestKeyToUse] = []; | ||
if (!requestCache[requestKeyToUse]) { | ||
requestCache[requestKeyToUse] = []; | ||
} | ||
var handlers = requests[requestKeyToUse]; | ||
var handlers = requestCache[requestKeyToUse]; | ||
var requestInFlight = Boolean(handlers.length); | ||
@@ -128,2 +181,3 @@ var requestHandler = {}; | ||
res.data = data; | ||
responseCache[requestKeyToUse] = res; | ||
@@ -130,0 +184,0 @@ if (dedupe) { |
@@ -6,8 +6,8 @@ 'use strict'; | ||
exports.isRequestInFlight = isRequestInFlight; | ||
exports.isResponseCached = isResponseCached; | ||
exports.clearRequestCache = clearRequestCache; | ||
exports.clearResponseCache = clearResponseCache; | ||
exports.fetchDedupe = fetchDedupe; | ||
// This is a cache of in-flight requests. Each request key maps to an | ||
// array of Promises. When the request resolves, each promise in the | ||
// array is pushed to. | ||
let requests = {}; | ||
let requestCache = {}; | ||
let responseCache = {}; | ||
@@ -26,17 +26,27 @@ function getRequestKey({ | ||
function isRequestInFlight(requestKey) { | ||
return Boolean(requests[requestKey]); | ||
return Boolean(requestCache[requestKey]); | ||
} | ||
function isResponseCached(requestKey) { | ||
return Boolean(responseCache[requestKey]); | ||
} | ||
function clearRequestCache() { | ||
requests = {}; | ||
requestCache = {}; | ||
} | ||
function clearResponseCache() { | ||
responseCache = {}; | ||
} | ||
// This loops through all of the handlers for the request and either | ||
// resolves or rejects them. | ||
function resolveRequest({ requestKey, res, err }) { | ||
const handlers = requests[requestKey] || []; | ||
const handlers = requestCache[requestKey] || []; | ||
handlers.forEach(handler => { | ||
if (res) { | ||
handler.resolve(res); | ||
const resToSend = new Response(res.body, res); | ||
resToSend.data = res.data; | ||
handler.resolve(resToSend); | ||
} else { | ||
@@ -49,6 +59,21 @@ handler.reject(err); | ||
// clear the handlers for the next request. | ||
requests[requestKey] = null; | ||
requestCache[requestKey] = null; | ||
} | ||
function fetchDedupe(input, init, dedupeOptions) { | ||
function CacheMissError() { | ||
var err = Error.apply(this, arguments); | ||
err.name = this.name = 'CacheMissError'; | ||
this.message = err.message; | ||
this.stack = err.stack; | ||
} | ||
CacheMissError.prototype = Object.create(Error.prototype, { | ||
constructor: { | ||
value: CacheMissError, | ||
writable: true, | ||
configurable: true | ||
} | ||
}); | ||
function fetchDedupe(input, init = {}, dedupeOptions) { | ||
let opts, initToUse; | ||
@@ -58,3 +83,3 @@ if (dedupeOptions) { | ||
initToUse = init; | ||
} else if (init && init.responseType) { | ||
} else if (init && (init.responseType || init.dedupe || init.cachePolicy || init.requestKey)) { | ||
opts = init; | ||
@@ -64,7 +89,18 @@ initToUse = {}; | ||
opts = {}; | ||
initToUse = {}; | ||
initToUse = init; | ||
} | ||
const { requestKey, responseType = '', dedupe = true } = opts; | ||
const { requestKey, responseType = '', dedupe = true, cachePolicy } = opts; | ||
const method = initToUse.method || input.method || ''; | ||
const upperCaseMethod = method.toUpperCase(); | ||
let appliedCachePolicy; | ||
if (cachePolicy) { | ||
appliedCachePolicy = cachePolicy; | ||
} else { | ||
const isReadRequest = upperCaseMethod === 'GET' || upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD' || upperCaseMethod === ''; | ||
appliedCachePolicy = isReadRequest ? 'cache-first' : 'network-only'; | ||
} | ||
// Build the default request key if one is not passed | ||
@@ -80,9 +116,24 @@ let requestKeyToUse = requestKey || getRequestKey({ | ||
let cachedResponse; | ||
if (appliedCachePolicy !== 'network-only') { | ||
cachedResponse = responseCache[requestKeyToUse]; | ||
if (cachedResponse) { | ||
var resp = new Response(cachedResponse.body, cachedResponse); | ||
resp.data = cachedResponse.data; | ||
resp.fromCache = true; | ||
return Promise.resolve(resp); | ||
} else if (cachePolicy === 'cache-only') { | ||
const cacheError = new CacheMissError(`Response for fetch request not found in cache.`); | ||
return Promise.reject(cacheError); | ||
} | ||
} | ||
let proxyReq; | ||
if (dedupe) { | ||
if (!requests[requestKeyToUse]) { | ||
requests[requestKeyToUse] = []; | ||
if (!requestCache[requestKeyToUse]) { | ||
requestCache[requestKeyToUse] = []; | ||
} | ||
const handlers = requests[requestKeyToUse]; | ||
const handlers = requestCache[requestKeyToUse]; | ||
const requestInFlight = Boolean(handlers.length); | ||
@@ -118,2 +169,3 @@ const requestHandler = {}; | ||
res.data = data; | ||
responseCache[requestKeyToUse] = res; | ||
@@ -120,0 +172,0 @@ if (dedupe) { |
{ | ||
"name": "fetch-dedupe", | ||
"version": "2.1.0", | ||
"version": "3.0.0-beta1", | ||
"description": "A thin wrapper around fetch that prevents duplicate requests.", | ||
@@ -10,2 +10,3 @@ "main": "lib/index.js", | ||
"test": "jest", | ||
"test:watch": "jest --watch", | ||
"prepublish": "in-publish && npm run build || not-in-publish", | ||
@@ -12,0 +13,0 @@ "build": "npm run clean && npm run build:umd && npm run build:umd:min && npm run build:es && npm run build:commonjs", |
# Fetch Dedupe | ||
[![Travis build status](http://img.shields.io/travis/jmeas/fetch-dedupe.svg?style=flat)](https://travis-ci.org/jmeas/fetch-dedupe) | ||
[![Travis build status](http://img.shields.io/travis/jamesplease/fetch-dedupe.svg?style=flat)](https://travis-ci.org/jamesplease/fetch-dedupe) | ||
[![npm version](https://img.shields.io/npm/v/fetch-dedupe.svg)](https://www.npmjs.com/package/fetch-dedupe) | ||
@@ -115,2 +115,5 @@ [![Test Coverage](https://codeclimate.com/github/jmeas/fetch-dedupe/badges/coverage.svg)](https://codeclimate.com/github/jmeas/fetch-dedupe) | ||
* `cachePolicy` *(String)*: Determines interactions with the cache. Valid options are `"cache-first"`, `"cache-only"`, | ||
and `"network-only"`. For more, refer to the section on Caching. | ||
Given the two possible value types of `input`, optional second argument, there are a way few ways that you can | ||
@@ -209,2 +212,11 @@ call `fetchDedupe`. Let's run through valid calls to `fetchDedupe`: | ||
##### `isResponseCached( requestKey )` | ||
Pass in a `requestKey` to see if there is a cache entry for the request. This can be used | ||
to determine if a call to `fetchDedupe` will hit the cache or not. | ||
> Now: We **strongly** recommend that you manually pass in `requestKey` to `fetchDedupe` | ||
if you intend to use this method. In other words, _do not_ rely on being able to | ||
reliably reproduce the request key that is created when a `requestKey` is not passed in. | ||
##### `clearRequestCache()` | ||
@@ -216,2 +228,37 @@ | ||
##### `clearResponseCache()` | ||
Wipe the cache of responses. | ||
> Warning: this is **not** safe to use in application code. It is mostly useful for testing. | ||
### Guides | ||
##### Caching | ||
The way the cache works is like this: any time a response from the server is received, it will be | ||
cached using the request's request key. Subsequent requests are matched with existing cached server | ||
responses using their request key. | ||
Interactions with the cache can be controlled with the `cachePolicy` option. There are three possible | ||
values: | ||
**`cache-first`** | ||
This is the default behavior. | ||
Requests will first look at the cache to see if a response for the same request key exists. If a response is | ||
found, then it will be returned, and no network request will be made. | ||
If no response exists in the cache, then a network request will be made. | ||
**`network-only`** | ||
The cache is ignored, and a network request is always made. | ||
**`cache-only`** | ||
If a response exists in the cache, then it will be returned. If no response | ||
exists in the cache, then an error will be passed into the render prop function. | ||
### FAQ & Troubleshooting | ||
@@ -236,3 +283,3 @@ | ||
If your API returns empty bodies with other codes, then you have two options. The first is to | ||
If your API returns empty bodies in other situations, then you have two options. The first is to | ||
pass a function as `responseType`. This lets you specify the `responseType` based on the `response`. | ||
@@ -248,3 +295,3 @@ | ||
// >= 400 status codes = stack traces, so also treat them as text | ||
return (response.ok || response.status !== 204) ? 'json' : 'text'; | ||
return (response.ok && response.status !== 204) ? 'json' : 'text'; | ||
} | ||
@@ -273,2 +320,19 @@ } | ||
##### Is the data duplicated? | ||
Although you receive a new `Response` object with every call to `fetch-dedupe`, the body will be read, | ||
so the response's body stream will be empty. In addition, the `data` property between every | ||
`response` is shared. Accordingly, the data returned by the server is never duplicated. | ||
This is an optimization that allows `fetch-dedupe` to be used in applications that fetch | ||
large payloads. | ||
##### `res.bodyUsed` is `false` when the body has already been used | ||
As of Feb 2018, there is a bug in several browsers and `node-fetch`, where the value of `bodyUsed` | ||
will be `false` when it should, in fact, be `true`. | ||
As a workaround, when using `fetch-dedupe`, the body will always be used by the time you receive | ||
the Response. | ||
### Implementors | ||
@@ -275,0 +339,0 @@ |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
32681
504
343
3
7
2