import { isProKey } from "../services/keys.js"; // Per-key rate limits (requests per minute) const FREE_RATE_LIMIT = 10; const PRO_RATE_LIMIT = 30; const RATE_WINDOW_MS = 60_000; // 1 minute // Concurrency limits const MAX_CONCURRENT_PDFS = 3; const MAX_QUEUE_SIZE = 10; const rateLimitStore = new Map(); let activePdfCount = 0; const pdfQueue = []; function cleanupExpiredEntries() { const now = Date.now(); for (const [key, entry] of rateLimitStore.entries()) { if (now >= entry.resetTime) { rateLimitStore.delete(key); } } } function getRateLimit(apiKey) { return isProKey(apiKey) ? PRO_RATE_LIMIT : FREE_RATE_LIMIT; } function checkRateLimit(apiKey) { cleanupExpiredEntries(); const now = Date.now(); const limit = getRateLimit(apiKey); const entry = rateLimitStore.get(apiKey); if (!entry || now >= entry.resetTime) { // Create new window rateLimitStore.set(apiKey, { count: 1, resetTime: now + RATE_WINDOW_MS }); return true; } if (entry.count >= limit) { return false; } entry.count++; return true; } async function acquireConcurrencySlot() { if (activePdfCount < MAX_CONCURRENT_PDFS) { activePdfCount++; return; } if (pdfQueue.length >= MAX_QUEUE_SIZE) { throw new Error("QUEUE_FULL"); } return new Promise((resolve, reject) => { pdfQueue.push({ resolve, reject }); }); } function releaseConcurrencySlot() { activePdfCount--; const waiter = pdfQueue.shift(); if (waiter) { activePdfCount++; waiter.resolve(); } } export function pdfRateLimitMiddleware(req, res, next) { const keyInfo = req.apiKeyInfo; const apiKey = keyInfo?.key || "unknown"; // Check rate limit first if (!checkRateLimit(apiKey)) { const limit = getRateLimit(apiKey); const tier = isProKey(apiKey) ? "pro" : "free"; res.status(429).json({ error: "Rate limit exceeded", limit: `${limit} PDFs per minute`, tier, retryAfter: "60 seconds" }); return; } // Add concurrency control to the request req.acquirePdfSlot = acquireConcurrencySlot; req.releasePdfSlot = releaseConcurrencySlot; next(); } export function getConcurrencyStats() { return { activePdfCount, queueSize: pdfQueue.length, maxConcurrent: MAX_CONCURRENT_PDFS, maxQueue: MAX_QUEUE_SIZE }; } // Proactive cleanup every 60s setInterval(cleanupExpiredEntries, 60_000);