UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

131 lines (130 loc) 3.72 kB
/** * Retry logic with exponential backoff */ import { NetworkError, RateLimitError } from '../errors/index.js'; const DEFAULT_OPTIONS = { maxRetries: 3, initialDelay: 1000, maxDelay: 30000, backoffFactor: 2, retryableStatuses: [429, 500, 502, 503, 504], }; /** * Sleep for specified milliseconds */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Calculate delay with exponential backoff */ function calculateDelay(attempt, options) { const delay = Math.min(options.initialDelay * Math.pow(options.backoffFactor, attempt - 1), options.maxDelay); // Add jitter to prevent thundering herd const jitter = Math.random() * 0.3 * delay; return Math.floor(delay + jitter); } /** * Determine if error is retryable */ function isRetryableError(error, options) { if (error instanceof RateLimitError) { return true; } if (error instanceof NetworkError) { return true; } // Check axios error response if (typeof error === 'object' && error !== null && 'response' in error && typeof error.response?.status === 'number') { return options.retryableStatuses.includes(error.response.status); } return false; } /** * Execute function with retry logic */ export async function withRetry(fn, options) { const opts = { ...DEFAULT_OPTIONS, ...options }; let lastError; for (let attempt = 1; attempt <= opts.maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Check if we should retry if (attempt === opts.maxRetries || !isRetryableError(error, opts)) { throw error; } // Calculate delay let delay = calculateDelay(attempt, opts); // Use retry-after header if available if (error instanceof RateLimitError && error.retryAfter) { delay = error.retryAfter * 1000; } else if (typeof error === 'object' && error !== null && 'response' in error && error.response?.headers?.['retry-after']) { const retryAfter = parseInt(error.response.headers['retry-after']); if (!isNaN(retryAfter)) { delay = retryAfter * 1000; } } // Wait before retrying await sleep(delay); } } throw lastError; } /** * Circuit breaker pattern for API failures */ export class CircuitBreaker { threshold; timeout; failures = 0; lastFailureTime = 0; state = 'closed'; constructor(threshold = 5, timeout = 60000 // 1 minute ) { this.threshold = threshold; this.timeout = timeout; } async execute(fn) { if (this.state === 'open') { if (Date.now() - this.lastFailureTime > this.timeout) { this.state = 'half-open'; } else { throw new NetworkError('Circuit breaker is open'); } } try { const result = await fn(); if (this.state === 'half-open') { this.reset(); } return result; } catch (error) { this.recordFailure(); throw error; } } recordFailure() { this.failures++; this.lastFailureTime = Date.now(); if (this.failures >= this.threshold) { this.state = 'open'; } } reset() { this.failures = 0; this.lastFailureTime = 0; this.state = 'closed'; } }