UNPKG

@roadiehq/backstage-plugin-github-pull-requests

Version:
118 lines (116 loc) 4.25 kB
class SecondaryRateLimitHandler { static instance; requestQueue = []; isProcessing = false; lastRequestTime = 0; minDelay = 1e3; backoffMultiplier = 1; maxBackoff = 6e4; // 1 minute static getInstance() { if (!this.instance) { this.instance = new SecondaryRateLimitHandler(); } return this.instance; } async executeWithBackoff(request, maxRetries = 5) { return new Promise((resolve, reject) => { this.requestQueue.push(async () => { let lastError = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const timeSinceLastRequest = Date.now() - this.lastRequestTime; const currentDelay = this.minDelay * this.backoffMultiplier; const waitTime = Math.max(0, currentDelay - timeSinceLastRequest); if (waitTime > 0) { await new Promise((res) => setTimeout(res, waitTime)); } this.lastRequestTime = Date.now(); const result = await request(); this.backoffMultiplier = 1; resolve(result); return; } catch (error) { lastError = error; if (this.isSecondaryRateLimit(error)) { console.warn( `Secondary rate limit hit (attempt ${attempt + 1}/${maxRetries + 1})` ); const retryAfter = this.extractRetryAfter(error); const backoffDelay = retryAfter || Math.pow(2, attempt) * 1e3; this.backoffMultiplier = Math.min( this.backoffMultiplier * 2, this.maxBackoff / this.minDelay ); if (attempt < maxRetries) { await new Promise((res) => setTimeout(res, backoffDelay)); continue; } } else if (this.isPrimaryRateLimit(error)) { const resetTime = this.extractRateLimitReset(error); if (resetTime && attempt < maxRetries) { const waitTime = resetTime - Date.now() + 1e3; console.warn( `Primary rate limit hit, waiting ${waitTime}ms until reset` ); await new Promise((res) => setTimeout(res, waitTime)); continue; } } break; } } reject( lastError || new Error("Unknown error during request execution") ); }); this.processQueue(); }); } isSecondaryRateLimit(error) { if (!error?.response) return false; const status = error.response.status; const message = error.message?.toLowerCase() || ""; const body = error.response.data?.message?.toLowerCase() || ""; return status === 403 && (message.includes("secondary rate limit") || body.includes("secondary rate limit") || body.includes("abuse detection") || body.includes("too many requests") || error.response?.headers?.["x-ratelimit-remaining"] !== "0"); } isPrimaryRateLimit(error) { if (!error?.response) return false; const status = error.response.status; const remaining = error.response?.headers?.["x-ratelimit-remaining"]; return status === 403 && remaining === "0"; } extractRetryAfter(error) { const retryAfter = error.response?.headers?.["retry-after"]; if (retryAfter) { const seconds = parseInt(retryAfter, 10); return isNaN(seconds) ? null : seconds * 1e3; } const message = error.response?.data?.documentation_url; if (message?.includes("secondary-rate-limits")) { return 6e4; } return null; } extractRateLimitReset(error) { const reset = error.response?.headers?.["x-ratelimit-reset"]; if (reset) { const resetTime = parseInt(reset, 10) * 1e3; return isNaN(resetTime) ? null : resetTime; } return null; } async processQueue() { if (this.isProcessing || this.requestQueue.length === 0) { return; } this.isProcessing = true; while (this.requestQueue.length > 0) { const request = this.requestQueue.shift(); await request(); } this.isProcessing = false; } } export { SecondaryRateLimitHandler }; //# sourceMappingURL=SecondaryRateLimitHandler.esm.js.map