@aurelia/fetch-client
Version:
[](https://opensource.org/licenses/MIT) [](http://www.typescriptlang.org/) [ • 6.12 kB
text/typescript
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>;
}