chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Simple FIFO semaphore used to cap the number of in-flight requests sent
|
||||
* through a fetch dispatcher.
|
||||
*/
|
||||
class Semaphore {
|
||||
private active = 0
|
||||
private readonly queue: Array<() => void> = []
|
||||
|
||||
constructor(private readonly max: number) {}
|
||||
|
||||
async acquire(signal?: AbortSignal): Promise<() => void> {
|
||||
if (signal?.aborted) throw abortReason(signal)
|
||||
if (this.active < this.max) {
|
||||
this.active++
|
||||
return () => this.release()
|
||||
}
|
||||
|
||||
return new Promise<() => void>((resolve, reject) => {
|
||||
const onAcquire = () => {
|
||||
signal?.removeEventListener('abort', onAbort)
|
||||
this.active++
|
||||
resolve(() => this.release())
|
||||
}
|
||||
const onAbort = () => {
|
||||
const i = this.queue.indexOf(onAcquire)
|
||||
if (i >= 0) this.queue.splice(i, 1)
|
||||
reject(abortReason(signal))
|
||||
}
|
||||
this.queue.push(onAcquire)
|
||||
signal?.addEventListener('abort', onAbort, { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
private release() {
|
||||
this.active--
|
||||
const next = this.queue.shift()
|
||||
if (next) next()
|
||||
}
|
||||
}
|
||||
|
||||
function abortReason(signal: AbortSignal | undefined): unknown {
|
||||
return signal?.reason ?? new DOMException('Aborted', 'AbortError')
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap `fetcher` so at most `max` requests are in-flight at any time.
|
||||
* Subsequent requests are FIFO-queued inside the SDK process and dispatched
|
||||
* as earlier requests settle.
|
||||
*
|
||||
* NOTE: the slot is released as soon as `fetcher` resolves with the response
|
||||
* headers, not when the response body is fully consumed. This means the
|
||||
* effective concurrency can be higher than `max` while bodies are
|
||||
* still streaming.
|
||||
*
|
||||
* TODO: release on body end (consume/cancel/error) so the
|
||||
* SDK-level cap aligns with the dispatcher's connection accounting
|
||||
*/
|
||||
export function limitConcurrency(
|
||||
fetcher: typeof fetch,
|
||||
max: number
|
||||
): typeof fetch {
|
||||
if (!Number.isFinite(max) || max <= 0) {
|
||||
return fetcher
|
||||
}
|
||||
|
||||
const sem = new Semaphore(max)
|
||||
|
||||
return (async (input, init) => {
|
||||
const signal =
|
||||
init?.signal ?? (input instanceof Request ? input.signal : undefined)
|
||||
const release = await sem.acquire(signal)
|
||||
try {
|
||||
return await fetcher(input, init)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}) as typeof fetch
|
||||
}
|
||||
Reference in New Issue
Block a user