/** * Rate limiting utilities for batch operations */ /** * Semaphore for controlling concurrent operations */ export class Semaphore { private permits: number; private waiting: Array<() => void> = []; constructor(maxConcurrent: number) { this.permits = maxConcurrent; } async acquire(): Promise { if (this.permits > 0) { this.permits--; return; } return new Promise((resolve) => { this.waiting.push(resolve); }); } release(): void { if (this.waiting.length > 0) { const next = this.waiting.shift(); if (next) next(); } else { this.permits++; } } /** * Execute a function with semaphore protection */ async run(fn: () => Promise): Promise { await this.acquire(); try { return await fn(); } finally { this.release(); } } } /** * Rate limiter that enforces a minimum delay between operations */ export class RateLimiter { private lastOperation: number = 0; constructor(private minDelayMs: number) {} async wait(): Promise { const now = Date.now(); const elapsed = now - this.lastOperation; if (elapsed < this.minDelayMs) { await new Promise((resolve) => setTimeout(resolve, this.minDelayMs - elapsed) ); } this.lastOperation = Date.now(); } } export interface BatchItemResult { success: boolean; result?: R; error?: Error; } export interface ProcessBatchOptions { /** Maximum concurrent operations (default: 5) */ concurrency?: number; /** Minimum delay between operations in ms (default: 0) */ minDelay?: number; /** Progress callback */ onProgress?: (completed: number, total: number) => void; /** Error handler - return true to continue, false to stop */ onError?: (error: Error, item: T) => boolean; /** Optional result transformer */ transform?: (result: R) => R; } /** * Process items in batches with rate limiting */ export async function processBatch( items: T[], processor: (item: T) => Promise, options: ProcessBatchOptions = {} ): Promise[]> { const { concurrency: rawConcurrency = 5, minDelay = 0, onProgress, onError, } = options; // A concurrency of 0 would deadlock the semaphore (it never grants permits). // Coerce nonsense values to 1 so callers can't accidentally hang the app. const concurrency = Number.isFinite(rawConcurrency) && rawConcurrency >= 1 ? Math.floor(rawConcurrency) : 1; const semaphore = new Semaphore(concurrency); const rateLimiter = minDelay > 0 ? new RateLimiter(minDelay) : null; let completed = 0; const processItem = async (item: T): Promise> => { return semaphore.run(async () => { if (rateLimiter) { await rateLimiter.wait(); } try { const result = await processor(item); completed++; onProgress?.(completed, items.length); return { success: true, result }; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); if (onError && !onError(err, item)) { throw err; // Stop processing } completed++; onProgress?.(completed, items.length); return { success: false, error: err }; } }); }; const promises = items.map((item) => processItem(item)); return await Promise.all(promises); }