/** * FIFO Semaphore for per-hoster concurrency control. * acquire() blocks until a slot is available, release() frees it. */ class Semaphore { constructor(limit) { this.limit = Math.max(1, limit || 1); this.active = 0; this.queue = []; } acquire() { return new Promise((resolve) => { if (this.active < this.limit) { this.active++; resolve(); } else { this.queue.push(resolve); } }); } release() { if (this.queue.length > 0) { // Don't decrement active — hand slot directly to next waiter const next = this.queue.shift(); next(); } else { this.active = Math.max(0, this.active - 1); } } updateLimit(newLimit) { this.limit = Math.max(1, newLimit || 1); // If new limit is higher, wake up waiting tasks while (this.active < this.limit && this.queue.length > 0) { this.active++; const next = this.queue.shift(); next(); } } get pending() { return this.queue.length; } } module.exports = Semaphore;