UNPKG

qp-es-ui

Version:

Query Park UI Components for React

212 lines (190 loc) 6.69 kB
/** * Handles success for `FetchWithTimeout`. * * @name successHandler * @function * @param {...any} [values] Values to pass resolve * @returns {Promise} */ /** * Handles error for `FetchWithTimeout`. * * @name errorHandler * @function * @param {Error} [error] Error to pass reject * @returns {Promise} */ /** * Fetch... With a timeout :O. * * @name FetchWithTimeout * @function * @param {String} url * @param {Object} options fetch options * @param {Number} ms Time to wait in milliseconds * @returns {Promise} */ /** An Error representing a timeout. */ class TimeoutError extends Error { /** * Creates a new instance of `TimeoutError`. * * @param {Object} options * @param {string} [options.message] Classic `error.message`. It can be * auto generated by providing `ms` and/or `url` while omitting `message` * @param {Number} [options.ms] Timeout duration in milliseconds * @param {String} [options.url] Url for which the timeout occured * @returns {TimeoutError} */ constructor (options = {}) { const message = options.message || (options.ms ? `Waited ${options.ms} milliseconds ` : '') + (options.url ? `for ${options.url}` : '') super(message) this.name = this.constructor.name this.url = options.url this.ms = options.ms } } /** * Uses setTimeout to reject the returned promise after `ms` milliseconds. * * @async * @param {String} [url=''] * @param {Number} [ms=5000] Time to wait in milliseconds * @returns {Promise} */ export function promiseTimeout (url = '', ms = 5000) { return new Promise((resolve, reject) => setTimeout(() => reject(new TimeoutError({ url, ms })), ms)) } /** * Give a timeout to fetch via `setTimeout`. A request queue of ongoing requests * are tracked allowing for mutliple uses of an instance of `TimeoutFetch`. * Requests are removed from that queue when both the request and timeout have * completed or when the timeout has completed and the queue is larger than * `options.maxLength`. * * @param {window.fetch} _fetch * @param {Object} [_options={}] Options to configure TimeoutFallback * @param {Number} [options.maxLength=20] Max length of the internal request * tracker before it starts removing timedout but unresolved requests. * @returns {FetchWithTimeout} */ export function TimeoutFallback (_fetch, _options = {}) { let _requests = [] let _maxLength = _options.maxLength || 20 /** * Using `Array.filter` remove requests where both `request` and `timeout` * have completed or where `timeout` has completed and `requests.length` is * larger than `_maxLength` or where `request` has completed and * `requests.length` is larger than `_maxLength`. */ const _prune = () => { _requests = _requests.filter((request, _, requests) => (request.request.promise === null && request.timeout.promise === null) || (request.timeout.promise === null && requests.length > _maxLength) || (request.request.promise === null && requests.length > _maxLength)) } /** * A higher order functin that provides reference to `self`. The * `successHandler` will mark `self` as complete, prune the `requests` queue * and resolve. * * @param {Object} self The errorer * @returns {successHandler} */ const _handleSuccess = (self) => (...values) => { self.promise = null _prune() return Promise.resolve(...values) } /** * A higher order function that provides reference to `self` and * `competition`. The `errorHandler` will mark `self` as complete, prune the * `requests` queue and only reject if `competition` has not already completed. * * @param {Object} self The errorer * @param {Object} competition Another potential errorer * @returns {errorHandler} */ const _handleError = (self, competition) => (error) => { self.promise = null _prune() return (competition.promise !== null) ? Promise.reject(error) : Promise.resolve() } /** See FetchWithTimeout */ const fetch = (url, options, ms) => { // ? Objects to keep reference when passed around (_handlers) const request = { promise: _fetch(url, options) } const timeout = { promise: promiseTimeout(url, ms) } _requests.push({ url, options, ms, request, timeout }) return Promise.race([ request.promise.then( _handleSuccess(request, timeout), _handleError(request, timeout) ), timeout.promise.catch(_handleError(timeout, request)) ]) } return fetch } /** * Give fetch a timeout using `AbortController`. This method is perfered over * the fallback since it cancels the fetch request entirely instead of allowing * it to hang. * * @param {window.fetch} fetch * @returns {FetchWithTimeout} */ export function Aborter (fetch) { /** * A higher order function that provides a reference to a `timeoutId`. The * `successHandler` will clear the `timeoutId` and resolve. * * @param {Number} timeoutId Identifier returned by `setTimeout` * @returns {successHandler} */ const _handleSuccess = (timeoutId) => (...values) => { clearTimeout(timeoutId) return Promise.resolve(...values) } /** * A higher order function that provides a reference to a `url` and `ms`. The * `errorHandler` will convert any non descriptive`AbortError` to a * `TimeoutError` before rejecting. * * @param {String} url * @param {Number} ms Milliseconds * @returns {errorHandler} */ const _handleError = (url, ms) => (error) => /The operation was aborted\./.test(error.message) ? Promise.reject(new TimeoutError({ url, ms })) : Promise.reject(error) return (url, options = {}, ms = 5000) => { /* eslint-disable-next-line no-undef */ const controller = new AbortController() const signal = controller.signal const timeoutId = setTimeout(() => controller.abort(), ms) return fetch(url, { ...options, signal }) .then(_handleSuccess(timeoutId), _handleError(url, ms)) } } /** * Get fetch with a timeout. This will use the AbortController api if available. * The fallback is a timeout solution. * * @param {window.fetch|any} fetch Any fetch implementation supporting promises * @returns {Aborter|TimeoutFallback} */ function FetchWithTimeout (fetch) { if (typeof AbortController === 'function') { return Aborter(fetch) } else { return TimeoutFallback(fetch) } } export default FetchWithTimeout