Created
July 7, 2021 07:42
-
-
Save skanne/3a38926f5e2114e7b9ddefb86b5910c8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fx = effect => props => [effect, props] | |
const noop = () => {} | |
const _abortControllers = {} | |
export const request = fx( | |
( | |
dispatch, | |
{ | |
// The request URL. | |
url, | |
// The request options. | |
options, | |
// A string handle for aborting any previous request. | |
// If not present, then the URL's origin + pathName will be taken. | |
handle, | |
// The time after which the request times out. 60 sec = 1 min as default. | |
timeout = 60000, | |
// The name of the method to access the response body: text, json, formData, blob, arrayBuffer. | |
// See https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#body | |
expect = 'json', | |
// The action to be dispatched on successful completion. | |
onsuccess = noop, | |
// The action to be dispatched in case of an error (not AbortError). | |
onerror = noop, | |
// The action to be dispatched when the request times out. | |
ontimeout = noop, | |
} | |
) => { | |
// If no abort handle was passed, create one based on the origin and pathname | |
// of the request URL, assuming that related requests only differ in the query string | |
// (e.g. when URL points to a search and the search term is passed via GET parameter). | |
if (!handle) { | |
const { origin, pathname } = new URL(url, location) | |
handle = origin + pathname | |
} | |
// If a previous request with the same handle is already running, abort it. | |
_abortControllers[handle]?.abort() | |
// The AbortController is needed in order to 'stop' the request if it times out, or | |
// if a previous request needs to be aborted. | |
_abortControllers[handle] = new AbortController() | |
const { signal } = _abortControllers[handle] | |
// After a specified amount of time the request is aborted, which | |
// causes the ontimeout action to be dispatched. | |
const timeoutId = setTimeout(() => { | |
_abortControllers[handle]?.abort() | |
dispatch(ontimeout, `Request timed out after ${timeout} ms`) | |
}, timeout) | |
// Send the request with options (if any, except for the signal which is always added). | |
fetch(url, { ...options, signal }) | |
// Check if the response is okay. | |
.then(response => { | |
if (!response.ok) { | |
throw new Error(response.status + ' ' + response.statusText) | |
} | |
return response | |
}) | |
// Extract the expected data from the response. | |
.then(response => response[expect]()) | |
// Dispatch the onsuccess action with the result data as payload. | |
.then(result => dispatch(onsuccess, result)) | |
// Catch any error (with the exception of AbortError) and dispatch the onerror action. | |
.catch(error => error.name !== 'AbortError' && dispatch(onerror, error)) | |
// In all cases, clear the timeout that would abort the request, | |
// and remove the controller itself. | |
.finally(() => { | |
clearTimeout(timeoutId) | |
delete _abortControllers[handle] | |
}) | |
} | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment