resilient-http-client
Version:
This is an augmented [Angular HttpClient][angular-http-client] which overrides the get method to enrich resilience behaviour.
145 lines (138 loc) • 6.63 kB
JavaScript
import { Subject, retry, of, concatMap, tap, delay, take, map, timer, catchError, throwError, finalize } from 'rxjs';
import * as i1 from '@angular/common/http';
import { HttpStatusCode } from '@angular/common/http';
import * as i0 from '@angular/core';
import { Injectable } from '@angular/core';
import { v4 } from 'uuid';
const getTopicSpecificBooleanOrDefault = (config, propertyName) => {
const topicConfig = config.topicToConfigDict[config.topic];
const isDefinedByTopicConfig = topicConfig && topicConfig[propertyName] !== undefined;
return isDefinedByTopicConfig ? topicConfig[propertyName] : config[propertyName];
};
const isRetryDisabled = (config) => getTopicSpecificBooleanOrDefault(config, 'disableRetry');
const waitForUserDecision = (config) => getTopicSpecificBooleanOrDefault(config, 'waitForUserDecision');
const shouldLogResult = (config) => getTopicSpecificBooleanOrDefault(config, 'logResult');
const shouldTrace = (config) => getTopicSpecificBooleanOrDefault(config, 'trace');
const applyResilience = (config, uuid) => {
const userRetryOrCancelSubject = new Subject();
let retryCount = 0;
return retry({
delay: (error) => of(error).pipe(concatMap((err) => {
if (isRetryDisabled(config) || !config.retryOnStatusCodeList.includes(err.status)) {
const topicSpecificMessageOrEmpty = config.topicToConfigDict[config.topic]?.failMessage || '';
config.onFail(config.topic, uuid, topicSpecificMessageOrEmpty);
throw err;
}
retryCount = retryCount + 1;
const retries = retryCount;
if (retryCount <= config.retryIntervalInMillisList.length) {
return of(err).pipe(tap(() => config.onRequestRetry(config.topic, uuid, retries, config.retryIntervalInMillisList[retries - 1], err.status)), delay(config.retryIntervalInMillisList[retryCount - 1]));
}
else {
retryCount = 0;
if (waitForUserDecision(config)) {
config.onWaitingForUserDecision(config.topic, uuid, retries, err.status, userRetryOrCancelSubject);
return userRetryOrCancelSubject.asObservable().pipe(take(1), map((shouldRetry) => {
if (shouldRetry) {
config.onRequestRetry(config.topic, uuid, retries, config.retryIntervalInMillisList[retries - 1], err.status);
return of(err);
}
const topicSpecificMessageOrEmpty = config.topicToConfigDict[config.topic]?.failMessage || '';
config.onFail(config.topic, uuid, topicSpecificMessageOrEmpty);
throw err;
}));
}
else {
const topicSpecificMessageOrEmpty = config.topicToConfigDict[config.topic]?.failMessage || '';
config.onFail(config.topic, uuid, topicSpecificMessageOrEmpty);
throw err;
}
}
})),
});
};
const DEFAULT_RESILIENCE_CONFIG = {
isDelayedAfterMs: 3000,
disableRetry: false,
logResult: false,
trace: false,
waitForUserDecision: false,
retryOnStatusCodeList: [
HttpStatusCode.RequestTimeout,
HttpStatusCode.TooManyRequests,
HttpStatusCode.InternalServerError,
HttpStatusCode.BadGateway,
HttpStatusCode.ServiceUnavailable,
HttpStatusCode.GatewayTimeout,
],
retryIntervalInMillisList: [0, 200, 500, 1000, 1000],
topicToConfigDict: {},
onWaitingForUserDecision: () => { },
onFail: () => { },
onRequestDelayed: () => { },
onRequestRetry: () => { },
onRequestStart: () => { },
onRequestFinalize: () => { },
};
class ResilientHttpClientService {
constructor(httpClient) {
this.httpClient = httpClient;
}
get(url, resilienceConfig, options) {
const finalConfig = {
...DEFAULT_RESILIENCE_CONFIG,
...resilienceConfig,
};
const uuid = v4();
const isDelayedSubscription = timer(finalConfig.isDelayedAfterMs)
.pipe(take(1))
.subscribe(() => {
finalConfig.onRequestDelayed(finalConfig.topic, uuid);
});
const timeStart = Date.now();
finalConfig.onRequestStart(finalConfig.topic, uuid);
return this.httpClient
.get(url, {
...options,
observe: 'response',
})
.pipe(applyResilience(finalConfig, uuid), map((res) => res.body), tap((res) => {
if (shouldLogResult(finalConfig)) {
// eslint-disable-next-line no-console
console.log(finalConfig.topic, res);
}
}), catchError((err) => {
const topicRelatedConfig = finalConfig.topicToConfigDict[finalConfig.topic];
if (topicRelatedConfig) {
if (topicRelatedConfig.failoverResponse) {
return of(topicRelatedConfig.failoverResponse);
}
}
return throwError(() => err);
}), finalize(() => {
if (shouldTrace(finalConfig)) {
const timeEnd = Date.now();
// eslint-disable-next-line no-console
console.log(`Fetch time in ms of call-topic ${finalConfig.topic}:`, finalConfig.topic, uuid, timeEnd - timeStart);
}
finalConfig.onRequestFinalize(finalConfig.topic, uuid);
isDelayedSubscription.unsubscribe();
}));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: ResilientHttpClientService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: ResilientHttpClientService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.3", ngImport: i0, type: ResilientHttpClientService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: i1.HttpClient }] });
/*
* Public API Surface of resilient-http-client
*/
/**
* Generated bundle index. Do not edit.
*/
export { DEFAULT_RESILIENCE_CONFIG, ResilientHttpClientService, applyResilience, getTopicSpecificBooleanOrDefault, shouldLogResult, shouldTrace };
//# sourceMappingURL=resilient-http-client.mjs.map