| |
| |
| |
|
|
| const GLOBAL_MAX_PER_MIN = 40; |
| const USER_MAX = 3; |
| const USER_WINDOW_MS = 3 * 60 * 60 * 1000; |
|
|
| let windowStart = Date.now(); |
| let countThisMinute = 0; |
| const userHits = new Map(); |
|
|
| |
| const queue = []; |
| let processing = false; |
|
|
| function resetWindowIfNeeded() { |
| const now = Date.now(); |
| if (now - windowStart >= 60_000) { |
| windowStart = now; |
| countThisMinute = 0; |
| } |
| } |
|
|
| export function checkUserQuota(clientId) { |
| const now = Date.now(); |
| const hits = (userHits.get(clientId) || []).filter(t => now - t < USER_WINDOW_MS); |
| userHits.set(clientId, hits); |
| if (hits.length >= USER_MAX) { |
| const oldest = hits[0]; |
| const retryMs = USER_WINDOW_MS - (now - oldest); |
| return { allowed: false, retryMs, remaining: 0 }; |
| } |
| return { allowed: true, remaining: USER_MAX - hits.length }; |
| } |
|
|
| export function recordUserHit(clientId) { |
| const hits = userHits.get(clientId) || []; |
| hits.push(Date.now()); |
| userHits.set(clientId, hits); |
| } |
|
|
| |
| export function enqueue(task) { |
| return new Promise((resolve, reject) => { |
| queue.push({ task, resolve, reject }); |
| drain(); |
| }); |
| } |
|
|
| async function drain() { |
| if (processing) return; |
| processing = true; |
| while (queue.length) { |
| resetWindowIfNeeded(); |
| if (countThisMinute >= GLOBAL_MAX_PER_MIN) { |
| const wait = 60_000 - (Date.now() - windowStart); |
| await new Promise(r => setTimeout(r, Math.max(wait, 250))); |
| continue; |
| } |
| const { task, resolve, reject } = queue.shift(); |
| countThisMinute++; |
| try { |
| resolve(await task()); |
| } catch (e) { |
| reject(e); |
| } |
| } |
| processing = false; |
| } |
|
|