Skip to content

Instantly share code, notes, and snippets.

@skanne
Created July 7, 2021 07:42
Show Gist options
  • Save skanne/3a38926f5e2114e7b9ddefb86b5910c8 to your computer and use it in GitHub Desktop.
Save skanne/3a38926f5e2114e7b9ddefb86b5910c8 to your computer and use it in GitHub Desktop.
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