Skip to content

Commit

Permalink
@uppy/utils: add fetcher (#5073)
Browse files Browse the repository at this point in the history
  • Loading branch information
Murderlon authored Apr 16, 2024
1 parent cc9df76 commit 377c3be
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/@uppy/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
"./lib/CompanionClientProvider": "./lib/CompanionClientProvider.js",
"./lib/FileProgress": "./lib/FileProgress.js",
"./src/microtip.scss": "./src/microtip.scss",
"./lib/UserFacingApiError": "./lib/UserFacingApiError.js"
"./lib/UserFacingApiError": "./lib/UserFacingApiError.js",
"./lib/fetcher": "./lib/fetcher.js"
},
"dependencies": {
"lodash": "^4.17.21",
Expand Down
145 changes: 145 additions & 0 deletions packages/@uppy/utils/src/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import NetworkError from './NetworkError.ts'
import ProgressTimeout from './ProgressTimeout.ts'

const noop = (): void => {}

export type FetcherOptions = {
/** The HTTP method to use for the request. Default is 'GET'. */
method?: string

/** The request payload, if any. Default is null. */
body?: Document | XMLHttpRequestBodyInit | null

/** Milliseconds between XMLHttpRequest upload progress events before the request is aborted. Default is 30000 ms. */
timeout?: number

/** Sets the withCredentials property of the XMLHttpRequest object. Default is false. */
withCredentials?: boolean

/** Sets the responseType property of the XMLHttpRequest object. Default is an empty string. */
responseType?: XMLHttpRequestResponseType

/** An object representing any headers to send with the request. */
headers?: Record<string, string>

/** The number of retry attempts to make if the request fails. Default is 3. */
retries?: number

/** Called before the request is made. */
onBeforeRequest?: (
xhr: XMLHttpRequest,
retryCount: number,
) => void | Promise<void>

/** Function for tracking upload progress. */
onUploadProgress?: (event: ProgressEvent) => void

/** A function to determine whether to retry the request. */
shouldRetry?: (xhr: XMLHttpRequest) => boolean

/** Called after the response has succeeded or failed but before the promise is resolved. */
onAfterRequest?: (
xhr: XMLHttpRequest,
retryCount: number,
) => void | Promise<void>

/** Called when no XMLHttpRequest upload progress events have been received for `timeout` ms. */
onTimeout?: () => void

/** Signal to abort the upload. */
signal?: AbortSignal
}

/**
* Fetches data from a specified URL using XMLHttpRequest, with optional retry functionality and progress tracking.
*
* @param url The URL to send the request to.
* @param options Optional settings for the fetch operation.
*/
export function fetcher(
url: string,
options: FetcherOptions = {},
): Promise<XMLHttpRequest> {
const {
body = null,
headers = {},
method = 'GET',
onBeforeRequest = noop,
onUploadProgress = noop,
shouldRetry = () => true,
onAfterRequest = noop,
onTimeout = noop,
responseType,
retries = 3,
signal = null,
timeout = 30_000,
withCredentials = false,
} = options

// 300 ms, 600 ms, 1200 ms, 2400 ms, 4800 ms
const delay = (attempt: number): number => 0.3 * 2 ** (attempt - 1) * 1000
const timer = new ProgressTimeout(timeout, onTimeout)

function requestWithRetry(retryCount = 0): Promise<XMLHttpRequest> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
const xhr = new XMLHttpRequest()

xhr.open(method, url, true)
xhr.withCredentials = withCredentials
if (responseType) {
xhr.responseType = responseType
}

signal?.addEventListener('abort', () => {
xhr.abort()
// Using DOMException for abort errors aligns with
// the convention established by the Fetch API.
reject(new DOMException('Aborted', 'AbortError'))
})

xhr.onload = async () => {
await onAfterRequest(xhr, retryCount)

if (xhr.status >= 200 && xhr.status < 300) {
timer.done()
resolve(xhr)
} else if (shouldRetry(xhr) && retryCount < retries) {
setTimeout(() => {
requestWithRetry(retryCount + 1).then(resolve, reject)
}, delay(retryCount))
} else {
timer.done()
reject(new NetworkError(xhr.statusText, xhr))
}
}

xhr.onerror = () => {
if (shouldRetry(xhr) && retryCount < retries) {
setTimeout(() => {
requestWithRetry(retryCount + 1).then(resolve, reject)
}, delay(retryCount))
} else {
timer.done()
reject(new NetworkError(xhr.statusText, xhr))
}
}

xhr.upload.onprogress = (event: ProgressEvent) => {
timer.progress()
onUploadProgress(event)
}

if (headers) {
Object.keys(headers).forEach((key) => {
xhr.setRequestHeader(key, headers[key])
})
}

await onBeforeRequest(xhr, retryCount)
xhr.send(body)
})
}

return requestWithRetry()
}

0 comments on commit 377c3be

Please sign in to comment.