import { atprotoClient } from '../api/client'; import type { Label, ResolvedLabel } from '../api/types'; import { rewriteBlobUrl } from '../api/blobRewrite'; interface CachedLabeler { name: string; avatar?: string; labelNames: Map; } const cache = new Map(); let fetchPromise: Promise | null = null; const pendingDids = new Set(); export async function prefetchLabelerDids(dids: string[]): Promise { if (!dids || dids.length === 0) return; await fetchLabelerServices(dids); } async function fetchLabelerServices(srcDids: string[]): Promise { const uncached = srcDids.filter((did) => !cache.has(did) && !pendingDids.has(did)); if (uncached.length === 0 && !fetchPromise) return; for (const did of uncached) { pendingDids.add(did); } if (fetchPromise) { await fetchPromise; const stillUncached = srcDids.filter((did) => !cache.has(did)); if (stillUncached.length === 0) return; return fetchLabelerServices(stillUncached); } const didsToFetch = [...pendingDids]; pendingDids.clear(); fetchPromise = (async () => { try { const agent = atprotoClient.api; if (!agent) { console.error('Cannot fetch labeler services: not logged in'); return; } const CHUNK_SIZE = 25; for (let i = 0; i < didsToFetch.length; i += CHUNK_SIZE) { const chunk = didsToFetch.slice(i, i + CHUNK_SIZE); const resp = await agent.app.bsky.labeler.getServices({ dids: chunk, detailed: true, }); for (const view of resp.data.views) { if (!('creator' in view)) continue; const creator = view.creator as { did: string; displayName?: string; avatar?: string; }; const policies = 'policies' in view ? view.policies : undefined; const labelNames = new Map(); if (policies?.labelValueDefinitions) { for (const def of policies.labelValueDefinitions) { const enLocale = def.locales.find((l) => l.lang === 'en'); const name = enLocale?.name ?? def.locales[0]?.name ?? def.identifier; labelNames.set(def.identifier, name); } } cache.set(creator.did, { name: creator.displayName ?? 'Labeler', avatar: rewriteBlobUrl(creator.avatar), labelNames, }); } } } catch (err) { console.error('Error fetching labeler services:', err); } finally { fetchPromise = null; } })(); await fetchPromise; } export async function resolveLabels(labels: Label[]): Promise { if (!labels || labels.length === 0) return []; const srcDids = [...new Set(labels.map((l) => l.src))]; await fetchLabelerServices(srcDids); return labels.map((label) => { const cached = cache.get(label.src); const displayName = cached?.labelNames.get(label.val) ?? label.val; return { val: label.val, displayName, labelerName: cached?.name ?? 'Labeler', labelerAvatar: cached?.avatar, }; }); } export function resolveLabelsSync(labels: Label[]): ResolvedLabel[] { if (!labels || labels.length === 0) return []; return labels.map((label) => { const cached = cache.get(label.src); const displayName = cached?.labelNames.get(label.val) ?? label.val; return { val: label.val, displayName, labelerName: cached?.name ?? 'Labeler', labelerAvatar: cached?.avatar, }; }); }