1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /**
* Bounded-concurrency async map.
*
* The dashboards enriched their candidate lists with a plain
* `for (const c of candidates) { await enrich(c) }`, which serialises one
* round-trip per item — /issues issued up to 600 sequential queries to Neon
* per page load (2 per issue × 300 candidates) and /pulls up to 300. Latency
* was therefore N × RTT rather than N/limit × RTT.
*
* `Promise.all(items.map(...))` would fix the wall-clock but fire all N at
* once, which on a pooled managed Postgres just moves the queue into the
* connection pool (or exhausts it and starts erroring). This keeps at most
* `limit` in flight.
*
* Results are returned in input order regardless of completion order.
*/
export async function mapWithConcurrency<T, R>(
items: readonly T[],
limit: number,
fn: (item: T, index: number) => Promise<R>
): Promise<R[]> {
if (items.length === 0) return [];
const effective = Math.max(1, Math.min(limit, items.length));
const results = new Array<R>(items.length);
let next = 0;
async function worker(): Promise<void> {
for (;;) {
const i = next++;
if (i >= items.length) return;
results[i] = await fn(items[i], i);
}
}
await Promise.all(Array.from({ length: effective }, () => worker()));
return results;
}
/**
* Default in-flight cap for per-row enrichment against the primary database.
*
* Chosen to be comfortably below a managed-Postgres pool size while still
* collapsing a 300-item serial walk into ~25 sequential waves.
*/
export const DB_FANOUT_LIMIT = 12;
|