UNPKG

revenuecat-api

Version:

Type-safe RevenueCat API client using fetch with automatic rate limiting

161 lines (160 loc) 6.3 kB
"use strict"; // https://www.revenuecat.com/docs/api-v2#tag/Rate-Limit Object.defineProperty(exports, "__esModule", { value: true }); exports.createRateLimitMiddleware = void 0; class RateLimitManager { async delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } endpointStates = new Map(); maxRetries = 3; maxQueueSize = 100; getEndpointKey(request) { const url = new URL(request.url); return `${request.method}:${url.pathname}`; } getRetryAfterTime(response) { const retryAfter = response.headers.get("Retry-After"); if (retryAfter) { const seconds = parseInt(retryAfter, 10); if (!isNaN(seconds)) { return seconds; } } // Fallback to 1 second if no valid Retry-After header return 1; } async isRetryable(response) { try { // Clone the response to avoid consuming the body const clonedResponse = response.clone(); const body = await clonedResponse.json(); // Check if retryable field exists and is false if (typeof body === "object" && body !== null && "retryable" in body) { return body.retryable !== false; } // Default to retryable if no retryable field is present return true; } catch { // If we can't parse the JSON, default to retryable return true; } } async waitForThrottle(request) { const endpointKey = this.getEndpointKey(request); // Initialize endpoint state if it doesn't exist if (!this.endpointStates.has(endpointKey)) { this.endpointStates.set(endpointKey, { isThrottled: false, retryAfter: 0, queue: [], lastRetryTime: 0, processing: false, }); } const state = this.endpointStates.get(endpointKey); // If throttled, wait for the retry-after time if (state.isThrottled) { const now = Date.now(); const waitTime = state.lastRetryTime + state.retryAfter * 1000 - now; if (waitTime > 0) { await this.delay(waitTime); } state.isThrottled = false; } } async handleResponse(request, response) { if (response.status !== 429) { return response; } // Check if the response indicates it's not retryable const shouldRetry = await this.isRetryable(response); if (!shouldRetry) { // If not retryable, return the original response immediately return response; } const endpointKey = this.getEndpointKey(request); // Initialize endpoint state if it doesn't exist if (!this.endpointStates.has(endpointKey)) { this.endpointStates.set(endpointKey, { isThrottled: false, retryAfter: 0, queue: [], lastRetryTime: 0, processing: false, }); } const state = this.endpointStates.get(endpointKey); let retryAfter = this.getRetryAfterTime(response); state.isThrottled = true; state.retryAfter = retryAfter; state.lastRetryTime = Date.now(); let lastResponse = response; for (let attempt = 0; attempt < this.maxRetries; attempt++) { // Wait for the retry-after time await this.delay(retryAfter * 1000); try { const retryResponse = await fetch(request, {}); if (retryResponse.status !== 429) { // Success, clear throttled state and return state.isThrottled = false; return retryResponse; } // Check if the retry response is also not retryable const shouldRetryAgain = await this.isRetryable(retryResponse); if (!shouldRetryAgain) { // If not retryable, return the response immediately state.isThrottled = false; return retryResponse; } // If still 429, update retryAfter for next attempt retryAfter = this.getRetryAfterTime(retryResponse); state.retryAfter = retryAfter; state.lastRetryTime = Date.now(); lastResponse = retryResponse; } catch (error) { // If fetch fails, throw the error state.isThrottled = false; throw error; } } // All retries exhausted, return the last 429 response state.isThrottled = false; return lastResponse; } getQueueSize(request) { const endpointKey = this.getEndpointKey(request); const state = this.endpointStates.get(endpointKey); return state?.queue.length || 0; } warnIfQueueTooLarge(request) { const queueSize = this.getQueueSize(request); if (queueSize >= this.maxQueueSize) { const endpointKey = this.getEndpointKey(request); console.warn(`[RevenueCat API] Rate limit queue for ${endpointKey} has reached maximum size of ${this.maxQueueSize}. Consider implementing additional throttling.`); } } } const createRateLimitMiddleware = () => { const rateLimitManager = new RateLimitManager(); return { async onRequest({ request }) { // Wait if the endpoint is currently throttled await rateLimitManager.waitForThrottle(request); // Warn if queue is getting too large rateLimitManager.warnIfQueueTooLarge(request); // Don't return anything - let the request proceed normally return undefined; }, async onResponse({ request, response }) { // Handle 429 responses with retry logic if (response.status === 429) { return await rateLimitManager.handleResponse(request, response); } return undefined; }, }; }; exports.createRateLimitMiddleware = createRateLimitMiddleware;