UNPKG

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
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