fix(admin): resolve tunnel owner emails via one listUsers scan
The upstream admin gateway deterministically truncates the first per-row getUserById that immediately follows a request's auth check, yielding an empty body the per-call retry cannot clear — so a couple of tunnel rows showed owner_email '—' on every load regardless of retry or concurrency. Replace the per-row getUserById fan-out with a single bounded listUsers scan (same transient retry) that builds an id->email map and stops early once every owner on the page is resolved. A single listUsers read does not hit the truncation pattern, eliminating the dashes. Remove the now-unused mapWithConcurrency helper and OWNER_EMAIL_CONCURRENCY constant.
This commit is contained in:
+28
-30
@@ -1,6 +1,6 @@
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { getSupabaseAdmin } from '@/lib/supabase/admin';
|
||||
import { withAdminRetry, mapWithConcurrency } from '@/lib/admin/retry';
|
||||
import { withAdminRetry } from '@/lib/admin/retry';
|
||||
import {
|
||||
parseOrder,
|
||||
parseSort,
|
||||
@@ -77,16 +77,6 @@ type TunnelRow = TunnelJoinRow & {
|
||||
const USER_SCAN_MAX_PAGES = 50;
|
||||
const USER_SCAN_PER_PAGE = 1000;
|
||||
|
||||
// Resolve tunnel owner emails a couple at a time rather than all at once. The
|
||||
// upstream tolerates a small concurrent burst, but a large fan-out of
|
||||
// getUserById calls (one per row, all in flight) trips an upstream throttle that
|
||||
// truncates the tail of the burst into empty/partial bodies. Those truncations
|
||||
// arrive faster than the per-call retry window can clear them, so a few rows
|
||||
// were deterministically rendered as "—" on every list load. A low concurrency
|
||||
// keeps each lookup inside the throttle allowance; measured 0 failures at 2 and
|
||||
// consistent truncations at 4, so we stay conservative here.
|
||||
const OWNER_EMAIL_CONCURRENCY = 2;
|
||||
|
||||
function userSortValue(u: User, sort: UserSort): string | number {
|
||||
switch (sort) {
|
||||
case 'email':
|
||||
@@ -299,26 +289,34 @@ export async function getTunnelsList(opts: {
|
||||
total = count ?? rows.length;
|
||||
}
|
||||
|
||||
// Resolve owner emails (per-row getUserById; acceptable for current scale).
|
||||
// The user_id comes from an existing tunnel row, so an empty body here is a
|
||||
// transient burst flake rather than a genuine not-found — retry it, and bound
|
||||
// the concurrency so the enrichment doesn't self-throttle. The try/catch null
|
||||
// fallback remains as a last resort so one bad row can never 500 the whole
|
||||
// list (it surfaces as "—" only if every retry still fails).
|
||||
const emails = await mapWithConcurrency(
|
||||
rows,
|
||||
OWNER_EMAIL_CONCURRENCY,
|
||||
async (t) => {
|
||||
try {
|
||||
const { data: u } = await withAdminRetry(() =>
|
||||
admin.auth.admin.getUserById(t.user_id),
|
||||
);
|
||||
return u.user?.email ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
// Resolve owner emails via a single bounded listUsers scan rather than one
|
||||
// getUserById per row. The upstream admin gateway deterministically truncates
|
||||
// the first per-row lookup that immediately follows the request's auth check
|
||||
// (an empty body the per-call retry can't clear), which surfaced as "—" on
|
||||
// every load. A single listUsers read does not hit that pattern, so we scan
|
||||
// the directory once (bounded, with the same transient-retry) and build an
|
||||
// id→email map, stopping as soon as every owner on this page is resolved.
|
||||
const wanted = new Set(rows.map((t) => t.user_id));
|
||||
const emailById = new Map<string, string | null>();
|
||||
if (wanted.size > 0) {
|
||||
for (let p = 1; p <= USER_SCAN_MAX_PAGES; p++) {
|
||||
const { data, error } = await withAdminRetry(() =>
|
||||
admin.auth.admin.listUsers({ page: p, perPage: USER_SCAN_PER_PAGE }),
|
||||
);
|
||||
if (error) throw new Error(error.message);
|
||||
const us = data.users;
|
||||
if (us.length === 0) break;
|
||||
for (const u of us) {
|
||||
if (wanted.has(u.id)) emailById.set(u.id, u.email ?? null);
|
||||
}
|
||||
},
|
||||
);
|
||||
// Stop early once every owner on this tunnels page has been found.
|
||||
if (rows.every((t) => emailById.has(t.user_id))) break;
|
||||
if (us.length < USER_SCAN_PER_PAGE) break;
|
||||
}
|
||||
}
|
||||
// Any owner not found in the scan falls back to "—" (null) — graceful, and a
|
||||
// missing owner can never 500 the whole list.
|
||||
const emails = rows.map((t) => emailById.get(t.user_id) ?? null);
|
||||
|
||||
const tunnels: TunnelItem[] = rows.map((t, i) => ({
|
||||
user_id: t.user_id,
|
||||
|
||||
Reference in New Issue
Block a user