- 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>
72 lines
1.7 KiB
JavaScript
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;
|