Skip to main content
Glama
retry-handler.ts3.35 kB
import { Logger } from '../utils/logger.js' export type JitterMode = 'none' | 'full' export interface RetryPolicy { maxRetries: number baseDelayMs: number maxDelayMs: number backoffFactor: number // multiplier per attempt jitter: JitterMode timeoutMs?: number // overall timeout budget (optional) retryOn?: { networkErrors?: boolean httpStatuses?: number[] httpStatusClasses?: Array<4 | 5> // 4=4xx,5=5xx } } export interface RetryContext { attempt: number lastError?: unknown lastStatus?: number } function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)) } function withJitter(base: number, mode: JitterMode): number { if (mode === 'none') return base // Full jitter: random between 0 and base return Math.floor(Math.random() * base) } function isRetryable(policy: RetryPolicy, _err: unknown, status?: number): boolean { if (status !== undefined) { if (policy.retryOn?.httpStatuses?.includes(status)) return true const klass = Math.floor(status / 100) if (klass === 5 && policy.retryOn?.httpStatusClasses?.includes(5)) return true if (klass === 4 && policy.retryOn?.httpStatusClasses?.includes(4)) return status === 408 || status === 429 return false } // No status means a network error or thrown exception from fetch return Boolean(policy.retryOn?.networkErrors ?? true) } export class RetryHandler { private readonly policy: RetryPolicy constructor(policy?: Partial<RetryPolicy>) { this.policy = { maxRetries: policy?.maxRetries ?? 3, baseDelayMs: policy?.baseDelayMs ?? 200, maxDelayMs: policy?.maxDelayMs ?? 5_000, backoffFactor: policy?.backoffFactor ?? 2, jitter: policy?.jitter ?? 'full', timeoutMs: policy?.timeoutMs, retryOn: policy?.retryOn ?? { networkErrors: true, httpStatusClasses: [5], httpStatuses: [408, 429] }, } } async execute<T>(op: () => Promise<T>, onRetry?: (ctx: RetryContext) => void): Promise<T> { const start = Date.now() let delay = this.policy.baseDelayMs let lastError: unknown for (let attempt = 0; attempt <= this.policy.maxRetries; attempt++) { try { const result = await op() return result } catch (err: any) { // If this looks like a fetch Response-like error, extract status let status: number | undefined if (err && typeof err === 'object' && 'status' in err && typeof (err as any).status === 'number') { status = (err as any).status } if (attempt >= this.policy.maxRetries || !isRetryable(this.policy, err, status)) { throw err } lastError = err const ctx: RetryContext = { attempt, lastError, lastStatus: status } try { onRetry?.(ctx) } catch { /* ignore */ } if (this.policy.timeoutMs && Date.now() - start + delay > this.policy.timeoutMs) { Logger.warn('Retry timeout budget exceeded') throw err } const wait = Math.min(this.policy.maxDelayMs, withJitter(delay, this.policy.jitter)) await sleep(wait) delay = Math.min(this.policy.maxDelayMs, Math.floor(delay * this.policy.backoffFactor)) } } // Should be unreachable throw lastError ?? new Error('RetryHandler failed without 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/Jakedismo/master-mcp-server'

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