|
const RETRY_INTERVALS = [0, 4, 8, 16, 32, 64] // seconds to wait for retry the request |
|
const DEFAULT_TIMEOUT_MS = 15000 |
|
|
|
const fetchAPI = ({ |
|
// DOCS https://gist.github.com/marianopaulin02/5d988a04aa1a54023bd1bc68bd95dd7c |
|
endpoint = '/', // Your URL |
|
uri = '', // Your URI, or Can be included in endpoint |
|
maxIntents = 1, // How many retry intents |
|
timeout = DEFAULT_TIMEOUT_MS, // Max waiting time for a request before abort |
|
rawResponse = false, // Return raw fetch response, else process as json |
|
rawBody = false, // by dafault JSON.stringify is applied to body |
|
useAuthorization = null, // async function to get value for Authorization header |
|
...extraFetchOptions // Additional options for fetch() |
|
} = {}) => { |
|
const config = { |
|
method: 'GET', |
|
uri: '', |
|
endpoint, |
|
query: [], |
|
headers: {}, |
|
maxIntents: maxIntents > 0 ? maxIntents : 1, |
|
timeout, |
|
useAuthorization, |
|
} |
|
let intents = 0 |
|
const doRequest = () => { |
|
let abortRequest = null |
|
let intentTimeout = null |
|
let intentIntervalCount = 0 |
|
let continueIntent = true |
|
const abort = () => { |
|
// When request is cancelled, promise keeps pending |
|
abortRequest && abortRequest(false) |
|
continueIntent = false |
|
} |
|
const prom = new Promise((resolve, reject) => { |
|
const exec = async () => { |
|
intents++ |
|
const controller = new AbortController() |
|
const timeout = setTimeout(controller.abort, config.timeout || DEFAULT_TIMEOUT_MS) |
|
abortRequest = (retry = true) => { |
|
clearTimeout(timeout) |
|
clearTimeout(intentTimeout) |
|
controller.abort() |
|
if (retry) { |
|
exec() |
|
} |
|
} |
|
const { endpoint, uri, body, query, headers, method } = config |
|
const options = { |
|
method, |
|
headers, |
|
body: rawBody ? body : JSON.stringify(body), |
|
signal: controller.signal, |
|
...extraFetchOptions, |
|
} |
|
if (config.useAuthorization) { |
|
const authString = await config.useAuthorization() |
|
options.headers.Authorization = authString |
|
} |
|
try { |
|
const endpointEndSlash = endpoint.endsWith('/') |
|
const url = `${endpoint}${(uri && endpointEndSlash) || endpointEndSlash ? '' : '/'}${ |
|
uri.startsWith('/') ? uri.substr(1) : uri |
|
}${query.length ? '?' : ''}${encodeURI(query.join('&'))}` |
|
const result = await fetch(url, options) |
|
if (rawResponse) { |
|
resolve(result) |
|
} else { |
|
if (result.ok) { |
|
try { |
|
result.json().then(resolve) |
|
} catch (e) { |
|
resolve(result) |
|
} |
|
} else { |
|
try { |
|
result.json().then(reject) |
|
} catch (e) { |
|
reject(result) |
|
} |
|
} |
|
} |
|
} catch (e) { |
|
if (config.maxIntents === 1) { |
|
return reject(e) |
|
} else if (intents > config.maxIntents) { |
|
const error = new Error('Max failed intents') |
|
return reject(error) |
|
} |
|
if (intentIntervalCount === RETRY_INTERVALS.length) { |
|
intentIntervalCount = 0 |
|
} else { |
|
intentIntervalCount++ |
|
} |
|
if (continueIntent) { |
|
intentTimeout = setTimeout(exec, RETRY_INTERVALS[intentIntervalCount] * 1000) |
|
} |
|
} finally { |
|
clearTimeout(timeout) |
|
} |
|
} |
|
exec() |
|
}) |
|
prom.abort = abort |
|
return prom |
|
} |
|
const obj = { |
|
uri(uri = '/') { |
|
config.uri = uri |
|
return this |
|
}, |
|
endpoint(endpoint) { |
|
config.endpoint = endpoint |
|
return this |
|
}, |
|
params(params = {}) { |
|
if (Array.isArray(params)) { |
|
// ['a=1','b=2'] |
|
config.query = [...config.query, ...params] |
|
} else { |
|
// {a:1, b:2} |
|
Object.keys(params).forEach(param => { |
|
config.query.push(`${param}=${params[param]}`) |
|
}) |
|
} |
|
return this |
|
}, |
|
useAuthorization(useAuthorization) { |
|
// Specially used when many retrys are performed, token may expire |
|
// expected response "Bearer <token>" |
|
config.useAuthorization = useAuthorization |
|
return this |
|
}, |
|
maxIntents(maxIntents = 1) { |
|
config.maxIntents = maxIntents > 0 ? maxIntents : 1 |
|
return this |
|
}, |
|
headers(headers = {}) { |
|
config.headers = { ...config.headers, ...headers } |
|
return this |
|
}, |
|
get() { |
|
return doRequest() |
|
}, |
|
post(body) { |
|
config.method = 'POST' |
|
config.body = body |
|
return doRequest() |
|
}, |
|
patch(body) { |
|
config.method = 'PATCH' |
|
config.body = body |
|
return doRequest() |
|
}, |
|
delete() { |
|
config.method = 'DELETE' |
|
return doRequest() |
|
}, |
|
} |
|
|
|
return obj |
|
} |
|
|
|
export default fetchAPI |
Amazing!