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