@roadiehq/backstage-plugin-github-pull-requests
Version:
118 lines (116 loc) • 4.25 kB
JavaScript
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