import { expireCachedProfile, hasCachedProfile, isDidBanned, listWikiOwnerDids, } from "../server/db/queries/index.ts"; import { resolveProfile } from "./profile.ts"; // Injectable so tests exercise the sweep, not the network. resolveProfile's own // `fetchFn` hook is not usable here: every setCachedProfile call inside it is // guarded on `fetchFn === fetch`, so injecting one resolves without ever writing. export type ProfileResolver = (did: string) => Promise; // Two at a time: this is maintenance, and each resolve is two upstream requests // (plc.directory, then the Bluesky appview). No reason to burst 45 of them. const SWEEP_CONCURRENCY = 2; // Listing queries read profile_cache with no freshness predicate, and nothing on a // listing path resolves. So a renamed owner's handle goes stale and stays stale — // the profile page that would refresh it sits behind the dead link it produces. // This is the only thing that reliably breaks that loop; identity events are the // fast path, not the guarantee. // // No forced expiry here: resolveProfile short-circuits inside profileCacheHours, // so the TTL decides what actually costs a request. A sweep of 45 owners where // none are stale makes zero network calls. export async function sweepOwnerHandles( resolve: ProfileResolver = resolveProfile, ): Promise { const dids = listWikiOwnerDids().filter((did) => !isDidBanned(did)); let swept = 0; for (let i = 0; i < dids.length; i += SWEEP_CONCURRENCY) { const batch = dids.slice(i, i + SWEEP_CONCURRENCY); await Promise.all( batch.map(async (did) => { try { await resolve(did); swept++; } catch { // resolveProfile logs and caches its own failures; one bad owner // must not abandon the rest of the sweep. } }), ); } return swept; } // An identity event means the handle changed now, so the TTL has to be bypassed — // otherwise this is a no-op until the row ages out on its own. // // UPDATE-only by way of hasCachedProfile: identity events arrive for every DID on // the network, not just ours, so an unknown DID must cost nothing beyond the lookup. export function refreshTrackedProfile( did: string, resolve: ProfileResolver = resolveProfile, ): void { if (isDidBanned(did) || !hasCachedProfile(did)) return; expireCachedProfile(did); void resolve(did).catch(() => { // Already logged in resolveProfile; ingestion must not stop for one profile. }); }