80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
/**
|
||
* CW API Concurrency Limiter
|
||
*
|
||
* Limits the number of simultaneous in-flight requests to the ConnectWise
|
||
* API. CW responds significantly slower under high concurrency (observed
|
||
* ~3× slower at 9 concurrent vs 5–6 concurrent), so bounding the
|
||
* parallelism actually reduces total wall-clock time.
|
||
*
|
||
* Implemented as an Axios request interceptor that gates on a simple
|
||
* counting semaphore. When the limit is reached, new requests queue and
|
||
* resolve in FIFO order as earlier requests complete.
|
||
*/
|
||
|
||
import type { AxiosInstance, InternalAxiosRequestConfig } from "axios";
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Semaphore
|
||
// ---------------------------------------------------------------------------
|
||
|
||
class Semaphore {
|
||
private _current = 0;
|
||
private _queue: (() => void)[] = [];
|
||
|
||
constructor(private _max: number) {}
|
||
|
||
/** Acquire a slot — resolves immediately if under the limit, else waits. */
|
||
acquire(): Promise<void> {
|
||
if (this._current < this._max) {
|
||
this._current++;
|
||
return Promise.resolve();
|
||
}
|
||
return new Promise<void>((resolve) => {
|
||
this._queue.push(resolve);
|
||
});
|
||
}
|
||
|
||
/** Release a slot — wakes the next queued caller, if any. */
|
||
release(): void {
|
||
const next = this._queue.shift();
|
||
if (next) {
|
||
// Hand the slot directly to the next waiter (don't decrement)
|
||
next();
|
||
} else {
|
||
this._current--;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Interceptor attachment
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* Attach a concurrency-limiting interceptor to an Axios instance.
|
||
*
|
||
* @param api - The Axios instance to limit.
|
||
* @param max - Maximum concurrent in-flight requests (default: 6).
|
||
*/
|
||
export function attachCwConcurrencyLimiter(api: AxiosInstance, max = 6): void {
|
||
const sem = new Semaphore(max);
|
||
|
||
// Request interceptor: wait for a slot before the request fires
|
||
api.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
||
await sem.acquire();
|
||
return config;
|
||
});
|
||
|
||
// Response interceptor: release the slot on success or failure
|
||
api.interceptors.response.use(
|
||
(response) => {
|
||
sem.release();
|
||
return response;
|
||
},
|
||
(error) => {
|
||
sem.release();
|
||
return Promise.reject(error);
|
||
},
|
||
);
|
||
}
|