fetch-dedupe
Advanced tools
Comparing version 4.0.0-beta.1 to 4.0.0-beta.2
# Changelog | ||
### v4.0.0 (2018/7/14) | ||
**Breaking Changes** | ||
- Response caching is now built in, and enabled by default. | ||
### v3.0.0 (2018/4/12) | ||
@@ -16,3 +22,3 @@ | ||
* Resolves a problem where you were unable to use the signature `fetch(input, init)`. | ||
- Resolves a problem where you were unable to use the signature `fetch(input, init)`. | ||
@@ -23,3 +29,3 @@ ### v2.1.0 (2018/2/7) | ||
* `responseType` can now be specified as a function. This is useful for backends that don't | ||
- `responseType` can now be specified as a function. This is useful for backends that don't | ||
respect the `Accept` header. "Enterprisey" backends frequently return text stack traces | ||
@@ -32,3 +38,3 @@ for errors, as an example. | ||
* `dedupeOptions` is now optional. The `responseType` is `"json"` by default, unless the | ||
- `dedupeOptions` is now optional. The `responseType` is `"json"` by default, unless the | ||
status code is 204, in which case it will be `"text"`. | ||
@@ -40,4 +46,4 @@ | ||
* `init` is now optional | ||
* A `requestKey` will be generated for you if it is omitted | ||
- `init` is now optional | ||
- A `requestKey` will be generated for you if it is omitted | ||
@@ -44,0 +50,0 @@ ### v0.1.0 (2018/2/4) |
@@ -7,4 +7,4 @@ (function (global, factory) { | ||
var requestCache = {}; | ||
var responseCache = {}; | ||
var activeRequests = {}; | ||
var responseCacheStore = {}; | ||
@@ -28,17 +28,27 @@ function getRequestKey() { | ||
function isRequestInFlight(requestKey) { | ||
return Boolean(requestCache[requestKey]); | ||
return Boolean(activeRequests[requestKey]); | ||
} | ||
function isResponseCached(requestKey) { | ||
return Boolean(responseCache[requestKey]); | ||
function clearActiveRequests() { | ||
activeRequests = {}; | ||
} | ||
function clearRequestCache() { | ||
requestCache = {}; | ||
} | ||
var responseCache = { | ||
get: function get(requestKey) { | ||
return responseCacheStore[requestKey]; | ||
}, | ||
set: function set(requestKey, res) { | ||
responseCacheStore[requestKey] = res; | ||
return responseCacheStore[requestKey]; | ||
}, | ||
has: function has(requestKey) { | ||
// `undefined` is not a valid JSON key, so we can reliably use | ||
// it to determine if the value exists or not.dfs | ||
return typeof responseCacheStore[requestKey] !== 'undefined'; | ||
}, | ||
clear: function clear() { | ||
responseCacheStore = {}; | ||
} | ||
}; | ||
function clearResponseCache() { | ||
responseCache = {}; | ||
} | ||
// This loops through all of the handlers for the request and either | ||
@@ -51,9 +61,7 @@ // resolves or rejects them. | ||
var handlers = requestCache[requestKey] || []; | ||
var handlers = activeRequests[requestKey] || []; | ||
handlers.forEach(function (handler) { | ||
if (res) { | ||
var resToSend = new Response(res.body, res); | ||
resToSend.data = res.data; | ||
handler.resolve(resToSend); | ||
handler.resolve(res); | ||
} else { | ||
@@ -66,3 +74,3 @@ handler.reject(err); | ||
// clear the handlers for the next request. | ||
requestCache[requestKey] = null; | ||
activeRequests[requestKey] = null; | ||
} | ||
@@ -134,9 +142,6 @@ | ||
if (appliedCachePolicy !== 'network-only') { | ||
cachedResponse = responseCache[requestKeyToUse]; | ||
cachedResponse = responseCacheStore[requestKeyToUse]; | ||
if (cachedResponse) { | ||
var resp = new Response(cachedResponse.body, cachedResponse); | ||
resp.data = cachedResponse.data; | ||
resp.fromCache = true; | ||
return Promise.resolve(resp); | ||
return Promise.resolve(cachedResponse); | ||
} else if (cachePolicy === 'cache-only') { | ||
@@ -150,7 +155,7 @@ var cacheError = new CacheMissError('Response for fetch request not found in cache.'); | ||
if (dedupe) { | ||
if (!requestCache[requestKeyToUse]) { | ||
requestCache[requestKeyToUse] = []; | ||
if (!activeRequests[requestKeyToUse]) { | ||
activeRequests[requestKeyToUse] = []; | ||
} | ||
var handlers = requestCache[requestKeyToUse]; | ||
var handlers = activeRequests[requestKeyToUse]; | ||
var requestInFlight = Boolean(handlers.length); | ||
@@ -186,3 +191,3 @@ var requestHandler = {}; | ||
res.data = data; | ||
responseCache[requestKeyToUse] = res; | ||
responseCacheStore[requestKeyToUse] = res; | ||
@@ -220,5 +225,4 @@ if (dedupe) { | ||
exports.isRequestInFlight = isRequestInFlight; | ||
exports.isResponseCached = isResponseCached; | ||
exports.clearRequestCache = clearRequestCache; | ||
exports.clearResponseCache = clearResponseCache; | ||
exports.clearActiveRequests = clearActiveRequests; | ||
exports.responseCache = responseCache; | ||
exports.fetchDedupe = fetchDedupe; | ||
@@ -225,0 +229,0 @@ |
@@ -1,1 +0,1 @@ | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.FetchDedupe={})}(this,function(e){"use strict";var P={},R={};function w(){var e=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{},t=e.url,r=e.method,o=e.responseType,n=void 0===o?"":o,i=e.body,s=void 0===i?"":i;return[void 0===t?"":t,(void 0===r?"":r).toUpperCase(),n,s].join("||")}function C(e){var t=e.requestKey,r=e.res,o=e.err;(P[t]||[]).forEach(function(e){if(r){var t=new Response(r.body,r);t.data=r.data,e.resolve(t)}else e.reject(o)}),P[t]=null}function K(){var e=Error.apply(this,arguments);e.name=this.name="CacheMissError",this.message=e.message,this.stack=e.stack}K.prototype=Object.create(Error.prototype,{constructor:{value:K,writable:!0,configurable:!0}}),e.getRequestKey=w,e.isRequestInFlight=function(e){return!!P[e]},e.isResponseCached=function(e){return!!R[e]},e.clearRequestCache=function(){P={}},e.clearResponseCache=function(){R={}},e.fetchDedupe=function(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=arguments[2],o=void 0,n=void 0;r?(o=r,n=t):t&&(t.responseType||t.dedupe||t.cachePolicy||t.requestKey)?(o=t,n={}):(o={},n=t);var i=o.requestKey,s=o.responseType,u=void 0===s?"":s,c=o.dedupe,a=void 0===c||c,d=o.cachePolicy,f=(n.method||e.method||"").toUpperCase(),h=void 0;h=d||("GET"===f||"OPTIONS"===f||"HEAD"===f||""===f?"cache-first":"network-only");var p=i||w({url:e.url||e,method:n.method||e.method||"",body:n.body||e.body||""}),v=void 0;if("network-only"!==h){if(v=R[p]){var l=new Response(v.body,v);return l.data=v.data,l.fromCache=!0,Promise.resolve(l)}if("cache-only"===d){var y=new K("Response for fetch request not found in cache.");return Promise.reject(y)}}var m=void 0;if(a){P[p]||(P[p]=[]);var b=P[p],q=!!b.length,j={};if(m=new Promise(function(e,t){j.resolve=e,j.reject=t}),b.push(j),q)return m}var g=fetch(e,n).then(function(t){var e=void 0;return e=u instanceof Function?u(t):u||(204===t.status?"text":"json"),t[e]().then(function(e){if(t.data=e,R[p]=t,!a)return t;C({requestKey:p,res:t})},function(){if(t.data=null,!a)return t;C({requestKey:p,res:t})})},function(e){if(!a)return Promise.reject(e);C({requestKey:p,err:e})});return a?m:g},Object.defineProperty(e,"__esModule",{value:!0})}); | ||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t(e.FetchDedupe={})}(this,function(e){"use strict";var j={},P={};function K(){var e=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{},t=e.url,r=e.method,o=e.responseType,n=void 0===o?"":o,i=e.body,u=void 0===i?"":i;return[void 0===t?"":t,(void 0===r?"":r).toUpperCase(),n,u].join("||")}var t={get:function(e){return P[e]},set:function(e,t){return P[e]=t,P[e]},has:function(e){return void 0!==P[e]},clear:function(){P={}}};function E(e){var t=e.requestKey,r=e.res,o=e.err;(j[t]||[]).forEach(function(e){r?e.resolve(r):e.reject(o)}),j[t]=null}function w(){var e=Error.apply(this,arguments);e.name=this.name="CacheMissError",this.message=e.message,this.stack=e.stack}w.prototype=Object.create(Error.prototype,{constructor:{value:w,writable:!0,configurable:!0}}),e.getRequestKey=K,e.isRequestInFlight=function(e){return!!j[e]},e.clearActiveRequests=function(){j={}},e.responseCache=t,e.fetchDedupe=function(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=arguments[2],o=void 0,n=void 0;n=r?(o=r,t):t&&(t.responseType||t.dedupe||t.cachePolicy||t.requestKey)?(o=t,{}):(o={},t);var i=o.requestKey,u=o.responseType,s=void 0===u?"":u,c=o.dedupe,a=void 0===c||c,f=o.cachePolicy,d=(n.method||e.method||"").toUpperCase(),v=void 0;v=f||("GET"==d||"OPTIONS"==d||"HEAD"==d||""==d?"cache-first":"network-only");var h=i||K({url:e.url||e,method:n.method||e.method||"",body:n.body||e.body||""}),p=void 0;if("network-only"!==v){if(p=P[h])return Promise.resolve(p);if("cache-only"===f){var l=new w("Response for fetch request not found in cache.");return Promise.reject(l)}}var y=void 0;if(a){j[h]||(j[h]=[]);var m=j[h],q=!!m.length,b={};if(y=new Promise(function(e,t){b.resolve=e,b.reject=t}),m.push(b),q)return y}var g=fetch(e,n).then(function(t){var e=void 0;return e=s instanceof Function?s(t):s||(204===t.status?"text":"json"),t[e]().then(function(e){if(t.data=e,P[h]=t,!a)return t;E({requestKey:h,res:t})},function(){if(t.data=null,!a)return t;E({requestKey:h,res:t})})},function(e){if(!a)return Promise.reject(e);E({requestKey:h,err:e})});return a?y:g},Object.defineProperty(e,"__esModule",{value:!0})}); |
@@ -1,3 +0,3 @@ | ||
var requestCache = {}; | ||
var responseCache = {}; | ||
var activeRequests = {}; | ||
var responseCacheStore = {}; | ||
@@ -21,17 +21,27 @@ export function getRequestKey() { | ||
export function isRequestInFlight(requestKey) { | ||
return Boolean(requestCache[requestKey]); | ||
return Boolean(activeRequests[requestKey]); | ||
} | ||
export function isResponseCached(requestKey) { | ||
return Boolean(responseCache[requestKey]); | ||
export function clearActiveRequests() { | ||
activeRequests = {}; | ||
} | ||
export function clearRequestCache() { | ||
requestCache = {}; | ||
} | ||
export var responseCache = { | ||
get: function get(requestKey) { | ||
return responseCacheStore[requestKey]; | ||
}, | ||
set: function set(requestKey, res) { | ||
responseCacheStore[requestKey] = res; | ||
return responseCacheStore[requestKey]; | ||
}, | ||
has: function has(requestKey) { | ||
// `undefined` is not a valid JSON key, so we can reliably use | ||
// it to determine if the value exists or not.dfs | ||
return typeof responseCacheStore[requestKey] !== 'undefined'; | ||
}, | ||
clear: function clear() { | ||
responseCacheStore = {}; | ||
} | ||
}; | ||
export function clearResponseCache() { | ||
responseCache = {}; | ||
} | ||
// This loops through all of the handlers for the request and either | ||
@@ -44,9 +54,7 @@ // resolves or rejects them. | ||
var handlers = requestCache[requestKey] || []; | ||
var handlers = activeRequests[requestKey] || []; | ||
handlers.forEach(function (handler) { | ||
if (res) { | ||
var resToSend = new Response(res.body, res); | ||
resToSend.data = res.data; | ||
handler.resolve(resToSend); | ||
handler.resolve(res); | ||
} else { | ||
@@ -59,3 +67,3 @@ handler.reject(err); | ||
// clear the handlers for the next request. | ||
requestCache[requestKey] = null; | ||
activeRequests[requestKey] = null; | ||
} | ||
@@ -127,9 +135,6 @@ | ||
if (appliedCachePolicy !== 'network-only') { | ||
cachedResponse = responseCache[requestKeyToUse]; | ||
cachedResponse = responseCacheStore[requestKeyToUse]; | ||
if (cachedResponse) { | ||
var resp = new Response(cachedResponse.body, cachedResponse); | ||
resp.data = cachedResponse.data; | ||
resp.fromCache = true; | ||
return Promise.resolve(resp); | ||
return Promise.resolve(cachedResponse); | ||
} else if (cachePolicy === 'cache-only') { | ||
@@ -143,7 +148,7 @@ var cacheError = new CacheMissError('Response for fetch request not found in cache.'); | ||
if (dedupe) { | ||
if (!requestCache[requestKeyToUse]) { | ||
requestCache[requestKeyToUse] = []; | ||
if (!activeRequests[requestKeyToUse]) { | ||
activeRequests[requestKeyToUse] = []; | ||
} | ||
var handlers = requestCache[requestKeyToUse]; | ||
var handlers = activeRequests[requestKeyToUse]; | ||
var requestInFlight = Boolean(handlers.length); | ||
@@ -179,3 +184,3 @@ var requestHandler = {}; | ||
res.data = data; | ||
responseCache[requestKeyToUse] = res; | ||
responseCacheStore[requestKeyToUse] = res; | ||
@@ -182,0 +187,0 @@ if (dedupe) { |
@@ -6,8 +6,6 @@ 'use strict'; | ||
exports.isRequestInFlight = isRequestInFlight; | ||
exports.isResponseCached = isResponseCached; | ||
exports.clearRequestCache = clearRequestCache; | ||
exports.clearResponseCache = clearResponseCache; | ||
exports.clearActiveRequests = clearActiveRequests; | ||
exports.fetchDedupe = fetchDedupe; | ||
let requestCache = {}; | ||
let responseCache = {}; | ||
let activeRequests = {}; | ||
let responseCacheStore = {}; | ||
@@ -26,27 +24,38 @@ function getRequestKey({ | ||
function isRequestInFlight(requestKey) { | ||
return Boolean(requestCache[requestKey]); | ||
return Boolean(activeRequests[requestKey]); | ||
} | ||
function isResponseCached(requestKey) { | ||
return Boolean(responseCache[requestKey]); | ||
function clearActiveRequests() { | ||
activeRequests = {}; | ||
} | ||
function clearRequestCache() { | ||
requestCache = {}; | ||
} | ||
const responseCache = exports.responseCache = { | ||
get(requestKey) { | ||
return responseCacheStore[requestKey]; | ||
}, | ||
function clearResponseCache() { | ||
responseCache = {}; | ||
} | ||
set(requestKey, res) { | ||
responseCacheStore[requestKey] = res; | ||
return responseCacheStore[requestKey]; | ||
}, | ||
has(requestKey) { | ||
// `undefined` is not a valid JSON key, so we can reliably use | ||
// it to determine if the value exists or not.dfs | ||
return typeof responseCacheStore[requestKey] !== 'undefined'; | ||
}, | ||
clear() { | ||
responseCacheStore = {}; | ||
} | ||
}; | ||
// This loops through all of the handlers for the request and either | ||
// resolves or rejects them. | ||
function resolveRequest({ requestKey, res, err }) { | ||
const handlers = requestCache[requestKey] || []; | ||
const handlers = activeRequests[requestKey] || []; | ||
handlers.forEach(handler => { | ||
if (res) { | ||
const resToSend = new Response(res.body, res); | ||
resToSend.data = res.data; | ||
handler.resolve(resToSend); | ||
handler.resolve(res); | ||
} else { | ||
@@ -59,3 +68,3 @@ handler.reject(err); | ||
// clear the handlers for the next request. | ||
requestCache[requestKey] = null; | ||
activeRequests[requestKey] = null; | ||
} | ||
@@ -116,9 +125,6 @@ | ||
if (appliedCachePolicy !== 'network-only') { | ||
cachedResponse = responseCache[requestKeyToUse]; | ||
cachedResponse = responseCacheStore[requestKeyToUse]; | ||
if (cachedResponse) { | ||
var resp = new Response(cachedResponse.body, cachedResponse); | ||
resp.data = cachedResponse.data; | ||
resp.fromCache = true; | ||
return Promise.resolve(resp); | ||
return Promise.resolve(cachedResponse); | ||
} else if (cachePolicy === 'cache-only') { | ||
@@ -132,7 +138,7 @@ const cacheError = new CacheMissError(`Response for fetch request not found in cache.`); | ||
if (dedupe) { | ||
if (!requestCache[requestKeyToUse]) { | ||
requestCache[requestKeyToUse] = []; | ||
if (!activeRequests[requestKeyToUse]) { | ||
activeRequests[requestKeyToUse] = []; | ||
} | ||
const handlers = requestCache[requestKeyToUse]; | ||
const handlers = activeRequests[requestKeyToUse]; | ||
const requestInFlight = Boolean(handlers.length); | ||
@@ -168,3 +174,3 @@ const requestHandler = {}; | ||
res.data = data; | ||
responseCache[requestKeyToUse] = res; | ||
responseCacheStore[requestKeyToUse] = res; | ||
@@ -171,0 +177,0 @@ if (dedupe) { |
{ | ||
"name": "fetch-dedupe", | ||
"version": "4.0.0-beta.1", | ||
"version": "4.0.0-beta.2", | ||
"description": "A thin wrapper around fetch that prevents duplicate requests.", | ||
@@ -61,3 +61,3 @@ "main": "lib/index.js", | ||
"isomorphic-fetch": "^2.2.1", | ||
"jest": "^22.1.4", | ||
"jest": "^23.6.0", | ||
"rimraf": "^2.6.2", | ||
@@ -64,0 +64,0 @@ "rollup": "^0.45.1", |
@@ -5,14 +5,21 @@ # Fetch Dedupe | ||
[](https://www.npmjs.com/package/fetch-dedupe) | ||
[](https://codeclimate.com/github/jmeas/fetch-dedupe) | ||
[](https://codeclimate.com/github/jamesplease/fetch-dedupe) | ||
[](https://unpkg.com/fetch-dedupe/dist/fetch-dedupe.min.js) | ||
A (very) thin wrapper around | ||
A thin wrapper around | ||
[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) | ||
that prevents duplicate requests. | ||
that implements request deduplication and response caching. | ||
### Motivation | ||
A common feature of libraries or frameworks that build abstractions around HTTP requests is that | ||
they deduplicate requests that are exactly the same. This library extracts that functionality. | ||
Making a single HTTP request is not difficult to do in JavaScript. However, complex web applications often make many | ||
requests as the user navigates through the app. | ||
Features such as request deduplication and response caching can often save the developer of apps like these from headache and | ||
bugs. | ||
`fetch-dedupe` is a wrapper around fetch that includes request deduplication and response caching for you, and it's a delight | ||
to use. | ||
### Installation | ||
@@ -88,4 +95,9 @@ | ||
- `getRequestKey()` | ||
- `responseCache` | ||
- `.get()` | ||
- `.set()` | ||
- `.has()` | ||
- `.clear()` | ||
- `isRequestInFlight()` | ||
- `clearRequestCache()` | ||
- `clearActiveRequests()` | ||
@@ -190,2 +202,22 @@ ##### `fetchDedupe( input [, init] [, dedupeOptions] )` | ||
##### `responseCache.get( requestKey )` | ||
Returns the cached response for `requestKey`. If the response does not exist, then `undefined` | ||
will be returned instead. | ||
##### `responseCache.set( requestKey, res )` | ||
Call this to manually update the cached value of `requestKey` with `res`. | ||
> Note: this is an advanced method, and you generally do not need to manually update the store. | ||
##### `responseCache.has( 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. | ||
##### `responseCache.clear()` | ||
Remove all responses from the cache. | ||
##### `isRequestInFlight( requestKey )` | ||
@@ -213,23 +245,6 @@ | ||
##### `isResponseCached( requestKey )` | ||
##### `clearActiveRequests()` | ||
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. | ||
Removes all of the tracked in-flight requests. | ||
> 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()` | ||
Wipe the cache of in-flight requests. | ||
> Warning: this is **not** safe to use in application code. It is mostly useful for testing. | ||
##### `clearResponseCache()` | ||
Wipe the cache of responses. | ||
> Warning: this is **not** safe to use in application code. It is mostly useful for testing. | ||
### Guides | ||
@@ -239,5 +254,4 @@ | ||
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. | ||
Any time tbat 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. | ||
@@ -269,9 +283,9 @@ Interactions with the cache can be controlled with the `cachePolicy` option. There are three possible | ||
If the response cannot be interpreted as the `responseType`, then it will be set as `null`. | ||
If the response cannot be parsed as the `responseType`, then it will be set as `null`. | ||
There are two common situations for this: | ||
- The backend returns an empty string when you specify `responseType: 'json'` | ||
- The response body is an empty string when you specify `responseType: 'json'` | ||
- The backend returns a raw text string when you specify `responseType: 'json'` | ||
- The response body is a raw text string when you specify `responseType: 'json'` (i.e.; invalid JSON) | ||
@@ -306,9 +320,9 @@ You can use the `responseType` option to have fine-grained control over the parsing of the | ||
##### `res.bodyUsed` is `false` when the body has already been used | ||
### Requirements | ||
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`. | ||
- a global [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) method. If your browser does not support it, then we | ||
recommend GitHub's [fetch polyfill](https://github.com/github/fetch). | ||
As a workaround, when using `fetch-dedupe`, the body will always be used by the time you receive | ||
the Response. | ||
> Note: Node users can try and use [node-fetch](https://github.com/bitinn/node-fetch), although we aren't currently targeting Node support with this | ||
> library. | ||
@@ -319,3 +333,3 @@ ### Implementors | ||
- [React Request](https://github.com/jmeas/react-request) | ||
- [React Request](https://github.com/jamesplease/react-request) | ||
@@ -322,0 +336,0 @@ Are you using it on a project? Add it to this list by opening a Pull Request |
35741
549
333