UNPKG

@mrboombastic/mailerlite-api-v2-node

Version:

[MailerLite API v2](https://developers.mailerlite.com/docs/getting-started-with-mailerlite-api) [Node.js](https://nodejs.org/en/) SDK. It is mostly a thin wrapper on [axios](https://github.com/axios/axios) that provides [authentication](https://developers

148 lines (147 loc) 5.72 kB
export class RateLimitHandler { constructor(options = {}) { this.rateLimitRetryAttempts = options.rateLimitRetryAttempts ?? 3; this.rateLimitRetryDelay = options.rateLimitRetryDelay ?? 1000; this.onRateLimitHit = options.onRateLimitHit; this.onRateLimitRetry = options.onRateLimitRetry; } /** * Parse rate limit headers from API response */ parseRateLimitHeaders(response) { const headers = response.headers; const limit = headers["x-ratelimit-limit"]; const remaining = headers["x-ratelimit-remaining"]; const reset = headers["x-ratelimit-reset"]; const retryAfter = headers["x-ratelimit-retry-after"]; // If any required header is missing, return null if (!limit || !remaining || !reset || !retryAfter) { return null; } return { limit: parseInt(limit, 10), remaining: parseInt(remaining, 10), reset: new Date(reset), retryAfter: parseInt(retryAfter, 10), }; } /** * Check if the error is a rate limit error (429 status) */ isRateLimitError(error) { return error.response?.status === 429; } /** * Create a RateLimitError from an AxiosError */ createRateLimitError(error) { const rateLimitHeaders = error.response ? this.parseRateLimitHeaders(error.response) : null; const rateLimitError = new Error(`Rate limit exceeded. ${rateLimitHeaders ? `Retry after ${rateLimitHeaders.retryAfter} seconds.` : "Please try again later."}`); rateLimitError.name = "RateLimitError"; rateLimitError.isRateLimitError = true; rateLimitError.rateLimitHeaders = rateLimitHeaders || { limit: 60, remaining: 0, reset: new Date(Date.now() + 60000), retryAfter: 60, }; return rateLimitError; } /** * Handle rate limit by waiting and retrying */ async handleRateLimit(error, retryFn, attempt = 0) { if (!this.isRateLimitError(error) || attempt >= this.rateLimitRetryAttempts) { throw this.createRateLimitError(error); } const rateLimitHeaders = error.response ? this.parseRateLimitHeaders(error.response) : null; if (rateLimitHeaders) { // Trigger the rate limit hit callback if (this.onRateLimitHit && attempt === 0) { this.onRateLimitHit(rateLimitHeaders); } // Trigger the retry callback if (this.onRateLimitRetry) { this.onRateLimitRetry(attempt + 1, rateLimitHeaders); } // Calculate delay: use retryAfter from headers, but add some buffer const delayMs = (rateLimitHeaders.retryAfter * 1000) + this.rateLimitRetryDelay; await this.sleep(delayMs); try { return await retryFn(); } catch (retryError) { if (retryError instanceof Error && "response" in retryError) { return await this.handleRateLimit(retryError, retryFn, attempt + 1); } throw retryError; } } else { // Fallback delay if headers are not available const fallbackDelay = Math.pow(2, attempt) * this.rateLimitRetryDelay; await this.sleep(fallbackDelay); try { return await retryFn(); } catch (retryError) { if (retryError instanceof Error && "response" in retryError) { return await this.handleRateLimit(retryError, retryFn, attempt + 1); } throw retryError; } } } /** * Check if we're approaching rate limit and should slow down */ shouldThrottle(response) { const rateLimitHeaders = this.parseRateLimitHeaders(response); if (!rateLimitHeaders) return false; // Throttle if we have less than 10% of requests remaining const throttleThreshold = Math.max(1, Math.floor(rateLimitHeaders.limit * 0.1)); return rateLimitHeaders.remaining <= throttleThreshold; } /** * Calculate suggested delay to avoid hitting rate limits */ getThrottleDelay(response) { const rateLimitHeaders = this.parseRateLimitHeaders(response); if (!rateLimitHeaders) return 0; // Calculate time until reset const timeUntilReset = rateLimitHeaders.reset.getTime() - Date.now(); // If reset time has passed, no delay needed if (timeUntilReset <= 0) return 0; // Calculate delay to spread remaining requests evenly const delayBetweenRequests = timeUntilReset / Math.max(1, rateLimitHeaders.remaining); // Cap the delay at a reasonable maximum (30 seconds) return Math.min(delayBetweenRequests, 30000); } /** * Utility function to sleep for a given number of milliseconds */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Log rate limit information for debugging */ logRateLimitInfo(response) { const rateLimitHeaders = this.parseRateLimitHeaders(response); if (rateLimitHeaders) { console.log("Rate Limit Info:", { limit: rateLimitHeaders.limit, remaining: rateLimitHeaders.remaining, resetTime: rateLimitHeaders.reset.toISOString(), retryAfter: rateLimitHeaders.retryAfter, }); } } }