Skip to main content
Glama
cameronsjo

MCP Server Template

by cameronsjo
retry.ts5.2 kB
/** * Retry handler with exponential backoff and jitter * * Provides resilient execution of async operations with configurable * retry policies, exponential backoff, and jitter to prevent thundering herd. */ import { createLogger } from './logger.js'; const logger = createLogger('retry'); /** * Retry configuration options */ export interface RetryOptions { /** Maximum number of retry attempts (default: 3) */ maxAttempts?: number; /** Initial delay in milliseconds (default: 1000) */ initialDelayMs?: number; /** Maximum delay in milliseconds (default: 30000) */ maxDelayMs?: number; /** Backoff multiplier (default: 2) */ backoffMultiplier?: number; /** Add random jitter to prevent thundering herd (default: true) */ jitter?: boolean; /** Jitter factor 0-1, portion of delay to randomize (default: 0.25) */ jitterFactor?: number; /** Function to determine if error is retryable (default: all errors) */ isRetryable?: (error: unknown) => boolean; /** Callback on each retry attempt */ onRetry?: (error: unknown, attempt: number, delayMs: number) => void; } const DEFAULT_OPTIONS: Required<RetryOptions> = { maxAttempts: 3, initialDelayMs: 1000, maxDelayMs: 30000, backoffMultiplier: 2, jitter: true, jitterFactor: 0.25, isRetryable: () => true, onRetry: () => undefined, }; /** * Calculate delay with exponential backoff and optional jitter */ function calculateDelay( attempt: number, options: Required<RetryOptions> ): number { // Exponential backoff: initialDelay * multiplier^attempt const exponentialDelay = options.initialDelayMs * Math.pow(options.backoffMultiplier, attempt); // Cap at max delay const cappedDelay = Math.min(exponentialDelay, options.maxDelayMs); // Add jitter if enabled if (options.jitter) { const jitterRange = cappedDelay * options.jitterFactor; const jitter = Math.random() * jitterRange * 2 - jitterRange; return Math.max(0, Math.round(cappedDelay + jitter)); } return Math.round(cappedDelay); } /** * Sleep for specified milliseconds */ function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Execute an async function with retry logic * * @example * ```typescript * const result = await withRetry( * () => fetchExternalApi(), * { * maxAttempts: 5, * isRetryable: (err) => err instanceof NetworkError, * onRetry: (err, attempt) => logger.warn('Retrying', { attempt }), * } * ); * ``` */ export async function withRetry<T>( fn: () => Promise<T>, options?: RetryOptions ): Promise<T> { const opts: Required<RetryOptions> = { ...DEFAULT_OPTIONS, ...options }; let lastError: unknown; for (let attempt = 0; attempt < opts.maxAttempts; attempt++) { try { return await fn(); } catch (error) { lastError = error; // Check if we should retry const isLastAttempt = attempt === opts.maxAttempts - 1; const shouldRetry = !isLastAttempt && opts.isRetryable(error); if (!shouldRetry) { throw error; } // Calculate delay and wait const delayMs = calculateDelay(attempt, opts); logger.debug('Retry scheduled', { attempt: attempt + 1, maxAttempts: opts.maxAttempts, delayMs, error: error instanceof Error ? error.message : String(error), }); opts.onRetry(error, attempt + 1, delayMs); await sleep(delayMs); } } // Should not reach here, but TypeScript needs this throw lastError; } /** * Create a retry wrapper with preset options * * @example * ```typescript * const apiRetry = createRetryHandler({ * maxAttempts: 5, * isRetryable: isNetworkError, * }); * * const result = await apiRetry(() => callApi()); * ``` */ export function createRetryHandler( defaultOptions: RetryOptions ): <T>(fn: () => Promise<T>, options?: RetryOptions) => Promise<T> { return <T>(fn: () => Promise<T>, options?: RetryOptions) => withRetry(fn, { ...defaultOptions, ...options }); } /** * Common retryable error checkers */ export const RetryableErrors = { /** Network/connection errors */ isNetworkError: (error: unknown): boolean => { if (error instanceof Error) { const networkCodes = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND']; return networkCodes.some((code) => error.message.includes(code)); } return false; }, /** HTTP 5xx server errors */ isServerError: (error: unknown): boolean => { if (error instanceof Error && 'status' in error) { const status = (error as { status: number }).status; return status >= 500 && status < 600; } return false; }, /** HTTP 429 rate limit errors */ isRateLimitError: (error: unknown): boolean => { if (error instanceof Error && 'status' in error) { return (error as { status: number }).status === 429; } return false; }, /** Combined: network, server, or rate limit */ isTransientError: (error: unknown): boolean => { return ( RetryableErrors.isNetworkError(error) || RetryableErrors.isServerError(error) || RetryableErrors.isRateLimitError(error) ); }, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cameronsjo/mcp-server-template'

If you have feedback or need assistance with the MCP directory API, please join our Discord server