fix(admin): bound owner-email enrichment concurrency to avoid self-throttle

A large concurrent burst of getUserById calls during the tunnels-list
email enrichment self-inflicts an upstream throttle (truncated/empty
bodies) that even the per-call retry can't fully escape, intermittently
rendering owner_email as '—'. Add mapWithConcurrency and resolve owner
emails at most a few at a time so each lookup stays inside the throttle
allowance; retry + null fallback preserved.
This commit is contained in:
Gerhard Scheikl
2026-05-31 15:57:54 +02:00
parent 17fe642168
commit b0dba8ec0e
2 changed files with 46 additions and 7 deletions
+30
View File
@@ -126,3 +126,33 @@ export async function withAdminRetry<R extends { error: MaybeError }>(
if (threw) throw lastThrown;
return lastResult as R;
}
/**
* Map over `items` running at most `concurrency` async tasks at a time, while
* preserving result order.
*
* Firing every GoTrue admin lookup at once (e.g. one `getUserById` per tunnel
* row) can self-inflict an upstream throttle: the proxy truncates the tail of a
* large concurrent burst, producing the very empty-body responses we retry on —
* and because the retries fire back into the same saturated window, a few rows
* can still fail. Bounding the concurrency keeps each lookup inside the
* throttle's allowance so {@link withAdminRetry} reliably resolves every row.
*/
export async function mapWithConcurrency<T, R>(
items: readonly T[],
concurrency: number,
task: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
const results = new Array<R>(items.length);
const limit = Math.max(1, Math.min(concurrency, items.length || 1));
let next = 0;
async function worker(): Promise<void> {
for (;;) {
const i = next++;
if (i >= items.length) return;
results[i] = await task(items[i], i);
}
}
await Promise.all(Array.from({ length: limit }, () => worker()));
return results;
}