@kitstack/nest-powertools
Version:
A comprehensive collection of NestJS powertools, decorators, and utilities to supercharge your backend development
268 lines • 10.4 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var CircuitBreaker_1, ResilientHttpService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResilientHttpService = exports.CircuitBreaker = void 0;
const common_1 = require("@nestjs/common");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const rxjs_2 = require("rxjs");
let CircuitBreaker = CircuitBreaker_1 = class CircuitBreaker {
constructor(config) {
this.config = config;
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
this.logger = new common_1.Logger(CircuitBreaker_1.name);
this.config = {
failureThreshold: 5,
resetTimeout: 60000,
monitoringPeriod: 30000,
...config,
};
}
async execute(operation) {
if (this.state === 'OPEN') {
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
this.logger.log('Circuit breaker moving to HALF_OPEN state');
}
else {
return this.handleOpenCircuit();
}
}
return operation().pipe((0, operators_1.tap)(() => this.onSuccess()), (0, operators_1.catchError)((error) => {
this.onFailure();
return (0, rxjs_1.throwError)(() => error);
}));
}
shouldAttemptReset() {
if (!this.nextAttemptTime) {
return false;
}
return Date.now() >= this.nextAttemptTime.getTime();
}
handleOpenCircuit() {
const error = new Error('Circuit breaker is OPEN');
if (this.config.fallbackHandler) {
try {
const fallbackResult = this.config.fallbackHandler(error);
return (0, rxjs_2.of)(fallbackResult);
}
catch (fallbackError) {
return (0, rxjs_1.throwError)(() => fallbackError);
}
}
return (0, rxjs_1.throwError)(() => error);
}
onSuccess() {
this.successCount++;
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
this.failureCount = 0;
this.logger.log('Circuit breaker reset to CLOSED state');
}
}
onFailure() {
this.failureCount++;
this.lastFailureTime = new Date();
if (this.state === 'HALF_OPEN') {
this.state = 'OPEN';
this.nextAttemptTime = new Date(Date.now() + this.config.resetTimeout);
this.logger.warn('Circuit breaker opened from HALF_OPEN state');
}
else if (this.failureCount >= this.config.failureThreshold) {
this.state = 'OPEN';
this.nextAttemptTime = new Date(Date.now() + this.config.resetTimeout);
this.logger.warn(`Circuit breaker opened after ${this.failureCount} failures`);
}
}
getStats() {
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount,
lastFailureTime: this.lastFailureTime,
nextAttemptTime: this.nextAttemptTime,
};
}
reset() {
this.state = 'CLOSED';
this.failureCount = 0;
this.successCount = 0;
this.lastFailureTime = undefined;
this.nextAttemptTime = undefined;
this.logger.log('Circuit breaker manually reset');
}
};
exports.CircuitBreaker = CircuitBreaker;
exports.CircuitBreaker = CircuitBreaker = CircuitBreaker_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [Object])
], CircuitBreaker);
let ResilientHttpService = ResilientHttpService_1 = class ResilientHttpService {
constructor(httpService) {
this.httpService = httpService;
this.logger = new common_1.Logger(ResilientHttpService_1.name);
this.circuitBreakers = new Map();
this.metrics = [];
}
get(url, config) {
return this.makeRequest('GET', url, undefined, config);
}
post(url, data, config) {
return this.makeRequest('POST', url, data, config);
}
put(url, data, config) {
return this.makeRequest('PUT', url, data, config);
}
delete(url, config) {
return this.makeRequest('DELETE', url, undefined, config);
}
patch(url, data, config) {
return this.makeRequest('PATCH', url, data, config);
}
makeRequest(method, url, data, config) {
const startTime = Date.now();
const resilientConfig = this.extractResilientConfig(config);
const axiosConfig = this.extractAxiosConfig(config);
let request;
switch (method.toUpperCase()) {
case 'GET':
request = this.httpService.get(url, axiosConfig);
break;
case 'POST':
request = this.httpService.post(url, data, axiosConfig);
break;
case 'PUT':
request = this.httpService.put(url, data, axiosConfig);
break;
case 'DELETE':
request = this.httpService.delete(url, axiosConfig);
break;
case 'PATCH':
request = this.httpService.patch(url, data, axiosConfig);
break;
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
if (resilientConfig.timeout) {
request = request.pipe((0, operators_1.timeout)(resilientConfig.timeout));
}
if (resilientConfig.retry) {
request = this.applyRetryLogic(request, resilientConfig.retry);
}
if (resilientConfig.circuitBreaker) {
const circuitBreaker = this.getOrCreateCircuitBreaker(url, resilientConfig.circuitBreaker);
request = circuitBreaker.execute(() => request);
}
return request.pipe((0, operators_1.tap)((response) => {
const duration = Date.now() - startTime;
this.recordMetrics(url, method, duration, true, response.status);
if (resilientConfig.enableLogging) {
this.logger.log(`${method} ${url} - ${response.status} (${duration}ms)`);
}
}), (0, operators_1.catchError)((error) => {
const duration = Date.now() - startTime;
this.recordMetrics(url, method, duration, false, error.response?.status, error.message);
if (resilientConfig.enableLogging) {
this.logger.error(`${method} ${url} - Error: ${error.message} (${duration}ms)`);
}
return (0, rxjs_1.throwError)(() => error);
}));
}
applyRetryLogic(request, retryConfig) {
const config = {
maxAttempts: 3,
delay: 1000,
exponentialBackoff: true,
retryCondition: (error) => error.response?.status >= 500 || error.code === 'ECONNRESET',
...retryConfig,
};
return request.pipe((0, operators_1.retryWhen)((errors) => errors.pipe((0, operators_1.scan)((retryCount, error) => {
if (retryCount >= config.maxAttempts ||
!config.retryCondition(error)) {
throw error;
}
return retryCount + 1;
}, 0), (0, operators_1.delay)(config.delay))));
}
getOrCreateCircuitBreaker(url, config) {
const key = this.getCircuitBreakerKey(url);
if (!this.circuitBreakers.has(key)) {
this.circuitBreakers.set(key, new CircuitBreaker(config));
}
return this.circuitBreakers.get(key);
}
getCircuitBreakerKey(url) {
try {
const urlObj = new URL(url);
return `${urlObj.protocol}//${urlObj.host}`;
}
catch {
return url;
}
}
extractResilientConfig(config) {
if (!config)
return {};
const { timeout, retry, circuitBreaker, enableLogging, ...axiosConfig } = config;
return { timeout, retry, circuitBreaker, enableLogging };
}
extractAxiosConfig(config) {
if (!config)
return {};
const { timeout, retry, circuitBreaker, enableLogging, ...axiosConfig } = config;
return axiosConfig;
}
recordMetrics(url, method, duration, success, statusCode, error) {
const metric = {
url,
method,
duration,
success,
statusCode,
error,
timestamp: new Date(),
};
this.metrics.push(metric);
if (this.metrics.length > 1000) {
this.metrics = this.metrics.slice(-1000);
}
}
getCircuitBreakerStats(url) {
if (url) {
const key = this.getCircuitBreakerKey(url);
const circuitBreaker = this.circuitBreakers.get(key);
return circuitBreaker ? [circuitBreaker.getStats()] : [];
}
return Array.from(this.circuitBreakers.values()).map((cb) => cb.getStats());
}
getMetrics(limit = 100) {
return this.metrics.slice(-limit);
}
resetCircuitBreaker(url) {
const key = this.getCircuitBreakerKey(url);
const circuitBreaker = this.circuitBreakers.get(key);
if (circuitBreaker) {
circuitBreaker.reset();
}
}
clearMetrics() {
this.metrics = [];
}
};
exports.ResilientHttpService = ResilientHttpService;
exports.ResilientHttpService = ResilientHttpService = ResilientHttpService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [Function])
], ResilientHttpService);
//# sourceMappingURL=resilient-http.hook.js.map