Multi-Hoster-Upload/lib/semaphore.js
Administrator 3d759eb8a6 fix: semaphore abort support, progress clamp, and additional bug fixes
- Semaphore.acquire() now accepts AbortSignal — waiting jobs are properly
  removed from queue on abort, preventing startBatch from hanging forever
- Clamp upload progress to 0-100% in both upload-manager and renderer
- Upload-manager handles semaphore abort rejection gracefully

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 06:00:38 +01:00

72 lines
1.7 KiB
JavaScript

/**
* 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;