diff --git a/Readme.md b/Readme.md index a1e0dce..7268e96 100644 --- a/Readme.md +++ b/Readme.md @@ -30,11 +30,14 @@ You can create your own action creators for this package, or you can use the one payload: { url, params - } + }, + meta: { timeout } } ``` -Where `url` and `params` are what you would pass as the first and second arguments to the native `fetch` API. If you want your action creators to support some async flow control, you should use [redux-effects](https://github.com/redux-effects/redux-effects)' `bind` function. If you do, your fetch action will return you an object with the following properties: +Where `url` and `params` are what you would pass as the first and second arguments to the native `fetch` API. Third argument is action meta: `timeout` - the numerical value (in ms) which is used for rejecting request promise if it takes longer time to complete than the value of this property. + +If you want your action creators to support some async flow control, you should use [redux-effects](https://github.com/redux-effects/redux-effects)' `bind` function. If you do, your fetch action will return you an object with the following properties: * `url` - The url of the endpoint you requested (as returned by the request) * `status` - The numerical status code of the response (e.g. 200) @@ -79,7 +82,7 @@ function signupUser (user) { bind(fetch(api + '/user', { method: 'POST', body: user - }), ({value}) => userDidLogin(value), ({value}) => setError(value)) + }, { timeout: 10000 }), ({value}) => userDidLogin(value), ({value}) => setError(value)) ] } diff --git a/package.json b/package.json index 855b089..fd18385 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "babel-preset-es2015": "^6.3.13", "babel-preset-stage-1": "^6.5.0", "babel-tape-runner": "^2.0.0", + "fetch-mock": "5.9.4", "lodash": "^4.8.1", "tap-spec": "^4.1.1", "tape": "^4.2.0" diff --git a/src/index.js b/src/index.js index 68d5c07..9a5d37e 100644 --- a/src/index.js +++ b/src/index.js @@ -16,14 +16,29 @@ const FETCH = 'EFFECT_FETCH' */ function fetchMiddleware ({dispatch, getState}) { - return next => action => + return next => action => action.type === FETCH - ? g().fetch(action.payload.url, action.payload.params) + ? getRequestPromise(action.payload, action.meta) .then(checkStatus) .then(createResponse, createErrorResponse) : next(action) } +function getRequestPromise ({ params, url }, meta) { + const fetchPromise = g().fetch(url, params) + + if (meta && typeof meta.timeout === 'number') { + const rejectOnTimeout = new Promise((_, reject) => { + const error = new Error(`Request to ${url} timed out`) + setTimeout(() => reject(error), meta.timeout) + }) + + return Promise.race([fetchPromise, rejectOnTimeout]) + } + + return fetchPromise +} + /** * g - Return the global object (in the browser or node) */ @@ -95,13 +110,14 @@ function checkStatus (res) { * Action creator */ -function fetchActionCreator (url = '', params = {}) { +function fetchActionCreator (url = '', params = {}, meta = {}) { return { type: FETCH, payload: { url, params - } + }, + meta } } diff --git a/test/index.js b/test/index.js index bec69db..e54edd5 100644 --- a/test/index.js +++ b/test/index.js @@ -3,8 +3,23 @@ */ import test from 'tape' +import fetchMock from 'fetch-mock' import fetchMw, {fetch} from '../src' +const timeoutUrl = 'http://localhost/timeout' + +const successUrl = 'http://localhost/200' + +const failureUrl = 'http://localhost/404' + +fetchMock.get(timeoutUrl, new Promise(res => setTimeout(res, 2000)).then(() => 200)); + +fetchMock.get(successUrl, { + headers: { 'Content-Type': ['text/html'] }, +}); + +fetchMock.get(failureUrl, { status: 404 }); + /** * Setup */ @@ -21,8 +36,8 @@ const run = fetchMw(api)(() => {}) */ test('should work', t => { - run(fetch('https://www.google.com')).then(({url, headers, value, status, statusText}) => { - t.equal(url, 'https://www.google.com') + run(fetch(successUrl)).then(({url, headers, value, status, statusText}) => { + t.equal(url, successUrl) t.equal(status, 200) t.equal(statusText, 'OK') t.ok(headers.get('content-type').indexOf('text/html') !== -1) @@ -30,7 +45,12 @@ test('should work', t => { }) }) +test('should reject on timeout', t => { + t.plan(1) + run(fetch(timeoutUrl, {}, { timeout: 1000 })).then(() => t.fail(), (res) => t.pass()) +}) + test('should reject on invalid response', t => { t.plan(1) - run(fetch('https://www.google.com/notAValidUrl')).then(() => t.fail(), (res) => t.pass()) + run(fetch(failureUrl)).then(() => t.fail(), (res) => t.pass()) })