UNPKG

@kitstack/nest-powertools

Version:

A comprehensive collection of NestJS powertools, decorators, and utilities to supercharge your backend development

268 lines 10.4 kB
"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