|
@@ -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()
|
|
|
+}
|