/** * 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 { if (this._current < this._max) { this._current++; return Promise.resolve(); } return new Promise((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); }, ); }