import type { Platform } from '../types/index'; // ── Types ── export interface RateLimiterConfig { minIntervalMs: number; } export interface PlatformRateLimitState { lastCallTime: number | null; errorCount: number; effectiveIntervalMs: number; } // ── Constants ── /** Maximum backoff interval — 60 seconds. */ const MAX_BACKOFF_MS = 60_000; // ── Rate Limiter ── /** * Per-platform rate limiter with exponential backoff. * * `acquire(platform)` waits until the minimum interval has elapsed since the * last call for that platform. `reportError` doubles the effective interval * (up to MAX_BACKOFF_MS). `reportSuccess` resets to the configured minimum. */ export class RateLimiter { private readonly config: Record; private readonly state = new Map(); constructor(config: Partial>) { this.config = config as Record; // Initialize state for each configured platform for (const [platform, cfg] of Object.entries(this.config)) { this.state.set(platform, { lastCallTime: null, errorCount: 0, effectiveIntervalMs: cfg.minIntervalMs, }); } } /** * Wait until enough time has elapsed since the last call for this platform. * Updates lastCallTime after the wait completes. */ async acquire(platform: Platform): Promise { const state = this.getOrCreateState(platform); const now = Date.now(); if (state.lastCallTime !== null) { const elapsed = now - state.lastCallTime; const remaining = state.effectiveIntervalMs - elapsed; if (remaining > 0) { console.log( `[rate-limiter] ${platform}: waiting ${remaining}ms (effective interval: ${state.effectiveIntervalMs}ms)` ); await delay(remaining); } } state.lastCallTime = Date.now(); } /** * Report a rate-limit or transient error for a platform. * Doubles the effective interval (exponential backoff) up to MAX_BACKOFF_MS. */ reportError(platform: Platform): void { const state = this.getOrCreateState(platform); state.errorCount++; const cfg = this.config[platform]; const baseInterval = cfg?.minIntervalMs ?? 1000; state.effectiveIntervalMs = Math.min( baseInterval * Math.pow(2, state.errorCount), MAX_BACKOFF_MS ); console.log( `[rate-limiter] ${platform}: backoff — errorCount=${state.errorCount}, effectiveInterval=${state.effectiveIntervalMs}ms` ); } /** * Report a successful call for a platform. * Resets error count and effective interval to the configured minimum. */ reportSuccess(platform: Platform): void { const state = this.getOrCreateState(platform); const cfg = this.config[platform]; const baseInterval = cfg?.minIntervalMs ?? 1000; state.errorCount = 0; state.effectiveIntervalMs = baseInterval; } /** * Get the current state of all platforms for diagnostic inspection. */ getState(): Record { const result: Record = {}; for (const [platform, state] of this.state) { result[platform] = { ...state }; } return result; } // ── Internal ── private getOrCreateState(platform: Platform): PlatformRateLimitState { let state = this.state.get(platform); if (!state) { const cfg = this.config[platform]; const minInterval = cfg?.minIntervalMs ?? 1000; state = { lastCallTime: null, errorCount: 0, effectiveIntervalMs: minInterval, }; this.state.set(platform, state); } return state; } } // ── Helpers ── function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); }