Skip to content

Commit

Permalink
Bump version 1.0.3
Browse files Browse the repository at this point in the history
improved `retryMiddleware` with thunk retries delays and force fetch
  • Loading branch information
nodkz committed May 4, 2016
1 parent 39fab84 commit d60c9a1
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 28 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## master

## 1.0.3 (May 4, 2016)

* feat: improved `retryMiddleware` with thunk retries delays and force fetch

## 1.0.2 (May 4, 2016)

* feat: New `retryMiddleware` for request retry if the initial request fails (thanks to @mario-jerkovic)
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ Available middlewares:
- **retry** - for request retry if the initial request fails. Options:
* `fetchTimeout` - number in milliseconds that defines in how much time will request timeout after it has been sent to the server (default: `15000`).
* `retryDelays` - array of millisecond that defines the values on which retries are based on (default: `[1000, 3000]`).
* `statusCodes` - array of XMLHttpRequest status codes which will fire up retryMiddleware (default: `status < 200 or status > 300`).
* `statusCodes` - array of response status codes which will fire up retryMiddleware (default: `status < 200 or status > 300`).
* `allowMutations` - by default retries disabled for mutations, you may allow process retries for them passing `true` (default: `false`)
* `forceRetry` - function(cb, delay), when request is delayed for next retry, middleware will call this function and pass to it a callback and delay time. When you call this callback, middleware will proceed request immediately (default: `false`).
- **auth** - for adding auth token, and refreshing it if gets 401 response from server. Options:
* `token` - string or function(req) which returns token. If function is provided, then it will be called for every request (so you may change tokens on fly).
* `tokenRefreshPromise`: - function(req, err) which must return promise with new token, called only if server returns 401 status code and this function is provided.
Expand Down Expand Up @@ -116,8 +118,9 @@ Relay.injectNetworkLayer(new RelayNetworkLayer([
}),
retryMiddleware({
fetchTimeout: 15000,
retryDelays: [1000, 3000],
statusCodes: [404, 503, 504]
retryDelays: (attempt) => Math.pow(2, attempt + 4) * 100, // or simple array [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
forceRetry: (cb, delay) => { window.forceRelayRetry = cb; console.log('call `forceRelayRetry()` for immediately retry! Or wait ' + delay + ' ms.'); },
statusCodes: [500, 503, 504]
})
], { disableBatchQuery: true }));
```
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-relay-network-layer",
"version": "1.0.2",
"description": "Network Layer for React Relay and Express (Batch Queries, AuthToken, Logging)",
"version": "1.0.3",
"description": "Network Layer for React Relay and Express (Batch Queries, AuthToken, Logging, Retry)",
"files": [
"es",
"lib"
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function authMiddleware(opts = {}) {
if (err.name === 'WrongTokenError') {
return tokenRefreshPromise(req, err.res)
.then(newToken => {
req.header['Authorization'] = `${prefix}${newToken}`;
req.headers['Authorization'] = `${prefix}${newToken}`;
return next(req); // re-run query with new token
});
}
Expand Down
127 changes: 105 additions & 22 deletions src/middleware/retry.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,115 @@
/* eslint-disable no-console */
import fetchWithRetries from 'fbjs/lib/fetchWithRetries';

function fetchOnError(request) {
return fetchWithRetries(request.url, request).then((response) => response);
const timeoutError = new Error("fetch timeout");

function isFunction(value) {
return !!(value && value.constructor && value.call && value.apply);
}

function promiseTimeoutDelay(promise, timeoutMS, delayMS = 0, forceRetryWhenDelay) {
return new Promise((resolve, reject) => {
const timeoutPromise = () => {
const timeoutId = setTimeout(() => {
reject(timeoutError);
}, timeoutMS);

promise.then(
(res) => {
clearTimeout(timeoutId);
resolve(res);
},
(err) => {
clearTimeout(timeoutId);
reject(err);
}
);
};

if (delayMS > 0) {
let delayInProgress = true;
const delayId = setTimeout(() => {
delayInProgress = false;
timeoutPromise();
}, delayMS);

if (forceRetryWhenDelay) {
forceRetryWhenDelay(() => {
if (delayInProgress) {
clearTimeout(delayId);
timeoutPromise();
}
}, delayMS);
}
} else {
timeoutPromise();
}
});
}

export default function retryMiddleware(opts = {}) {
const fetchTimeout = opts.fetchTimeout || false;
const retryDelays = opts.retryDelays || false;
const fetchTimeout = opts.fetchTimeout || 15000;
const retryDelays = opts.retryDelays || [1000, 3000];
const statusCodes = opts.statusCodes || false;
const logger = opts.logger || console.log.bind(console, '[RELAY-NETWORK]');
const allowMutations = opts.allowMutations || false;
const forceRetry = opts.forceRetry || false;

return next => req => (
next(req).then(res => {
if (res.status < 200 || res.status > 300 || !res.status.ok) {
const request = Object.assign({}, req);

if (fetchTimeout || retryDelays) {
request.fetchTimeout = fetchTimeout;
request.retryDelays = retryDelays;
}
if (statusCodes && statusCodes.indexOf(res.status) !== -1) {
return fetchOnError(request);
} else if (!statusCodes) {
return fetchOnError(request);
let retryAfterMs = () => false;
if (retryDelays) {
if (Array.isArray(retryDelays)) {
retryAfterMs = (attempt) => {
if (retryDelays.length >= attempt) {
return retryDelays[attempt - 1];
}
}

return false;
};
} else if (isFunction(retryDelays)) {
retryAfterMs = retryDelays;
}
}


return next => req => {
if (req.relayReqType === 'mutation' && !allowMutations) {
return next(req);
})
);
}

let attempt = 0;

const sendTimedRequest = (timeout, delay = 0) => {
attempt++;
return promiseTimeoutDelay(next(req), timeout, delay, forceRetry)
.then(res => {
let statusError = false;
if (statusCodes) {
statusError = statusCodes.indexOf(res.status) !== -1;
} else {
statusError = res.status < 200 || res.status > 300;
}

if (statusError) {
const retryDelayMS = retryAfterMs(attempt);
if (retryDelayMS) {
logger(`response status ${res.status}, retrying after ${retryDelayMS} ms`);
return sendTimedRequest(timeout, retryDelayMS);
}
}

return res;
})
.catch(err => {
if (err === timeoutError) {
const retryDelayMS = retryAfterMs(attempt);
if (retryDelayMS) {
logger(`response timeout, retrying after ${retryDelayMS} ms`);
return sendTimedRequest(timeout, retryDelayMS);
}
}

return new Promise((resolve, reject) => reject(err));
});
};

return sendTimedRequest(fetchTimeout, 0);
};
}

0 comments on commit d60c9a1

Please sign in to comment.