UNPKG

@aurelia/fetch-client

Version:

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![CircleCI](https://circleci.com/

174 lines (151 loc) • 6.12 kB
import { IPlatform, resolve } from '@aurelia/kernel'; import { HttpClient } from '../http-client'; import { IFetchInterceptor } from '../interfaces'; import { ErrorNames, createMappedError } from '../errors'; /** * The strategy to use when retrying requests. */ export const RetryStrategy = /*@__PURE__*/Object.freeze({ fixed: 0, incremental: 1, exponential: 2, random: 3 }); const defaultRetryConfig: IRetryConfiguration = { maxRetries: 3, interval: 1000, strategy: RetryStrategy.fixed }; /** * Interceptor that retries requests on error, based on a given RetryConfiguration. */ export class RetryInterceptor implements IFetchInterceptor { /** @internal */ private readonly p = resolve(IPlatform); public retryConfig: IRetryConfiguration; /** * Creates an instance of RetryInterceptor. */ public constructor(retryConfig?: IRetryConfiguration) { this.retryConfig = {...defaultRetryConfig, ...(retryConfig ?? {})}; if (this.retryConfig.strategy === RetryStrategy.exponential && (this.retryConfig.interval as number) <= 1000) { throw createMappedError(ErrorNames.retry_interceptor_invalid_exponential_interval, this.retryConfig.interval); } } /** * Called with the request before it is sent. It remembers the request so it can be retried on error. * * @param request - The request to be sent. * @returns The existing request, a new request or a response; or a Promise for any of these. */ public request(request: IRetryableRequest): IRetryableRequest { if (!request.retryConfig) { request.retryConfig = {...this.retryConfig}; request.retryConfig.counter = 0; } // do this on every request request.retryConfig.requestClone = request.clone(); return request; } /** * Called with the response after it is received. Clears the remembered request, as it was succesfull. * * @param response - The response. * @returns The response; or a Promise for one. */ public response(response: Response, request: IRetryableRequest): Response { // retry was successful, so clean up after ourselves delete request.retryConfig; return response; } /** * Handles fetch errors and errors generated by previous interceptors. This * function acts as a Promise rejection handler. It wil retry the remembered request based on the * configured RetryConfiguration. * * @param error - The rejection value from the fetch request or from a * previous interceptor. * @returns The response of the retry; or a Promise for one. */ public responseError(error: Response, request: IRetryableRequest, httpClient: HttpClient): Response | Promise<Response> { const { retryConfig } = request as { retryConfig: Required<IRetryConfiguration> }; const { requestClone } = retryConfig; return Promise.resolve().then(() => { if (retryConfig.counter < retryConfig.maxRetries) { const result = retryConfig.doRetry != null ? retryConfig.doRetry(error, request) : true; return Promise.resolve(result).then(doRetry => { if (doRetry) { retryConfig.counter++; const delay = calculateDelay(retryConfig); return new Promise(resolve => this.p.setTimeout(resolve, !isNaN(delay) ? delay : 0)) .then(() => { const newRequest = requestClone.clone(); if (typeof (retryConfig.beforeRetry) === 'function') { return retryConfig.beforeRetry(newRequest, httpClient); } return newRequest; }) .then(newRequest => { const retryableRequest: IRetryableRequest = {...newRequest, retryConfig }; return httpClient.fetch(retryableRequest); }); } // no more retries, so clean up delete request.retryConfig; throw error; }); } // no more retries, so clean up delete request.retryConfig; throw error; }); } } function calculateDelay(retryConfig: IRetryConfiguration): number { const { interval, strategy, minRandomInterval, maxRandomInterval, counter } = retryConfig as Required<IRetryConfiguration>; if (typeof (strategy) === 'function') { return (retryConfig.strategy as (retryCount: number) => number)(counter); } switch (strategy) { case (RetryStrategy.fixed): return retryStrategies[RetryStrategy.fixed](interval); case (RetryStrategy.incremental): return retryStrategies[RetryStrategy.incremental](counter, interval); case (RetryStrategy.exponential): return retryStrategies[RetryStrategy.exponential](counter, interval); case (RetryStrategy.random): return retryStrategies[RetryStrategy.random](counter, interval, minRandomInterval, maxRandomInterval); default: throw createMappedError(ErrorNames.retry_interceptor_invalid_strategy, strategy); } } const retryStrategies = [ // fixed interval => interval, // incremental (retryCount, interval) => interval * retryCount, // exponential (retryCount, interval) => retryCount === 1 ? interval : interval ** retryCount / 1000, // random (retryCount, interval, minRandomInterval = 0, maxRandomInterval = 60000) => { return Math.random() * (maxRandomInterval - minRandomInterval) + minRandomInterval; } ] as [ (interval: number) => number, (retryCount: number, interval: number) => number, (retryCount: number, interval: number) => number, (retryCount: number, interval: number, minRandomInterval?: number, maxRandomInterval?: number) => number ]; export type IRetryableRequest = Request & { retryConfig?: IRetryConfiguration }; export interface IRetryConfiguration { maxRetries: number; interval?: number; strategy?: number | ((retryCount: number) => number); minRandomInterval?: number; maxRandomInterval?: number; counter?: number; requestClone?: Request; doRetry?(response: Response, request: Request): boolean | Promise<boolean>; beforeRetry?(request: Request, client: HttpClient): Request | Promise<Request>; }