interface RetryOptions {
maxRetries?: number;
initialDelay?: number;
maxDelay?: number;
}
class RateLimitError extends Error {
constructor(public retryAfter?: number) {
super("Rate limit exceeded");
this.name = "RateLimitError";
}
}
async function fetchWithRetry(url: string, options: RequestInit, retryOptions: RetryOptions = {}): Promise<Response> {
const { maxRetries = 3, initialDelay = 1000, maxDelay = 32000 } = retryOptions;
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
if (attempt === maxRetries) {
throw new RateLimitError();
}
// Calculate delay with exponential backoff and jitter
const baseDelay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
const jitter = Math.random() * 1000;
const delay = baseDelay + jitter;
console.log(`Rate limited. Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay)}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
return response;
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries) {
const delay = initialDelay * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError || new Error("Request failed after retries");
}
// Usage with a rate limit handler
async function apiRequest<T>(endpoint: string, options?: RequestInit): Promise<T> {
const response = await fetchWithRetry(`https://api.us.firstquadrant.ai/v5${endpoint}`, {
...options,
headers: {
Authorization: "Bearer YOUR_API_KEY",
"FirstQuadrant-Organization-ID": "org_YOUR_ORG_ID",
"Content-Type": "application/json",
...options?.headers,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}