UNPKG

breathe-api

Version:

Model Context Protocol server for Breathe HR APIs with Swagger/OpenAPI support - also works with custom APIs

190 lines 6.23 kB
import { BaseError, ApiError } from './errors.js'; const DEFAULT_RETRY_OPTIONS = { maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 30000, backoffMultiplier: 2, jitter: true, retryCondition: (error) => { if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') { return true; } if (error instanceof BaseError) { return error.isRetryable; } if (error instanceof ApiError && error.statusCode) { return [408, 429, 500, 502, 503, 504].includes(error.statusCode); } return false; }, }; function calculateDelay(attempt, initialDelayMs, maxDelayMs, backoffMultiplier, jitter) { let delay = initialDelayMs * Math.pow(backoffMultiplier, attempt - 1); delay = Math.min(delay, maxDelayMs); if (jitter) { const jitterAmount = delay * 0.25 * Math.random(); delay = delay + jitterAmount; } return Math.round(delay); } function sleep(ms, signal) { return new Promise((resolve, reject) => { if (signal?.aborted) { reject(new Error('Operation aborted')); return; } const timeout = setTimeout(resolve, ms); if (signal) { signal.addEventListener('abort', () => { clearTimeout(timeout); reject(new Error('Operation aborted')); }, { once: true }); } }); } export async function retry(fn, options = {}) { const config = { ...DEFAULT_RETRY_OPTIONS, ...options }; let lastError; for (let attempt = 1; attempt <= config.maxRetries + 1; attempt++) { try { if (options.signal?.aborted) { throw new Error('Operation aborted'); } if (options.progressTracker && config.maxRetries > 0) { const progress = (attempt - 1) / (config.maxRetries + 1); await options.progressTracker.update(progress, attempt === 1 ? 'Starting operation...' : `Retry attempt ${attempt - 1} of ${config.maxRetries}`); } return await fn(); } catch (error) { lastError = error; if (attempt > config.maxRetries) { break; } const shouldRetry = config.retryCondition(error, attempt); if (!shouldRetry) { break; } const delayMs = calculateDelay(attempt, config.initialDelayMs, config.maxDelayMs, config.backoffMultiplier, config.jitter); if (config.onRetry) { config.onRetry(error, attempt, delayMs); } if (options.progressTracker) { await options.progressTracker.update(attempt / (config.maxRetries + 1), `Retrying in ${Math.round(delayMs / 1000)}s... (attempt ${attempt} failed)`); } await sleep(delayMs, options.signal); } } throw lastError; } export function withRetry(options = {}) { return function (_target, _propertyKey, descriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args) { return retry(() => originalMethod.apply(this, args), options); }; return descriptor; }; } export class CircuitBreaker { name; threshold; timeout; successThreshold; failures = 0; lastFailureTime = 0; state = 'closed'; successCount = 0; constructor(name, threshold = 5, timeout = 60000, successThreshold = 2) { this.name = name; this.threshold = threshold; this.timeout = timeout; this.successThreshold = successThreshold; } async execute(fn) { if (this.state === 'open') { if (Date.now() - this.lastFailureTime < this.timeout) { throw new CircuitBreakerError(`Circuit breaker is open for ${this.name}`, this.name, this.failures); } this.state = 'half-open'; this.successCount = 0; } try { const result = await fn(); this.onSuccess(); return result; } catch (error) { this.onFailure(); throw error; } } onSuccess() { this.failures = 0; if (this.state === 'half-open') { this.successCount++; if (this.successCount >= this.successThreshold) { this.state = 'closed'; this.successCount = 0; } } } onFailure() { this.failures++; this.lastFailureTime = Date.now(); if (this.state === 'half-open') { this.state = 'open'; this.successCount = 0; } else if (this.failures >= this.threshold) { this.state = 'open'; } } getState() { return { state: this.state, failures: this.failures, lastFailureTime: this.lastFailureTime, }; } reset() { this.failures = 0; this.lastFailureTime = 0; this.state = 'closed'; this.successCount = 0; } } export class CircuitBreakerManager { breakers = new Map(); getBreaker(name, threshold, timeout, successThreshold) { if (!this.breakers.has(name)) { this.breakers.set(name, new CircuitBreaker(name, threshold, timeout, successThreshold)); } return this.breakers.get(name); } async execute(serviceName, fn, threshold, timeout) { const breaker = this.getBreaker(serviceName, threshold, timeout); return breaker.execute(fn); } getAllStates() { const states = {}; for (const [name, breaker] of this.breakers) { states[name] = breaker.getState(); } return states; } reset(name) { const breaker = this.breakers.get(name); if (breaker) { breaker.reset(); } } resetAll() { for (const breaker of this.breakers.values()) { breaker.reset(); } } } export const circuitBreakerManager = new CircuitBreakerManager(); import { CircuitBreakerError } from './errors.js'; //# sourceMappingURL=retry.js.map