/** * FIFO Semaphore for per-hoster concurrency control. * acquire(signal?) blocks until a slot is available or the signal aborts. * release() frees a slot. */ class Semaphore { constructor(limit) { this.limit = Math.max(1, limit || 1); this.active = 0; this.queue = []; // { resolve, reject, onAbort? } } acquire(signal) { return new Promise((resolve, reject) => { if (signal && signal.aborted) { reject(new Error('Aborted')); return; } if (this.active < this.limit) { this.active++; resolve(); return; } const entry = { resolve, reject }; if (signal) { entry.onAbort = () => { // Remove from queue without granting a slot const idx = this.queue.indexOf(entry); if (idx !== -1) this.queue.splice(idx, 1); reject(new Error('Aborted')); }; signal.addEventListener('abort', entry.onAbort, { once: true }); } this.queue.push(entry); }); } release() { if (this.queue.length > 0) { // Don't decrement active — hand slot directly to next waiter const entry = this.queue.shift(); // Clean up abort listener if (entry.onAbort) { // Entry was granted a slot; no need for abort listener anymore } entry.resolve(); } 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 entry = this.queue.shift(); entry.resolve(); } } get pending() { return this.queue.length; } } module.exports = Semaphore;