/** * Handle-based actor search for the DID lookup form. * * Primary service is waow.tech's typeahead (https://typeahead.waow.tech/docs), * which mirrors bluesky's `app.bsky.actor.searchActorsTypeahead` endpoint. * Bluesky's public API is the fallback when waow looks like it's having * server trouble (a 5xx status, or unreachable entirely). */ export type TypeaheadActor = { did: string; handle: string; displayName?: string; }; const ENDPOINT = '/xrpc/app.bsky.actor.searchActorsTypeahead'; const WAOW_API = 'https://typeahead.waow.tech'; const BSKY_API = 'https://public.api.bsky.app'; const LIMIT = 8; function endpointUrl(base: string, q: string): string { const params = new URLSearchParams({ q, limit: String(LIMIT) }); return `${base}${ENDPOINT}?${params}`; } function parseActors(body: unknown): TypeaheadActor[] { if (typeof body !== 'object' || body === null) return []; const actors = (body as { actors?: unknown }).actors; if (!Array.isArray(actors)) return []; return actors.flatMap((actor: unknown) => { if (typeof actor !== 'object' || actor === null) return []; const { did, handle, displayName } = actor as Record; if (typeof did !== 'string' || typeof handle !== 'string') return []; return [ { did, handle, displayName: typeof displayName === 'string' ? displayName : undefined, }, ]; }); } /** * Search actors by (partial) handle or display name. * * Makes at most one request to each service per call — no retry loops. The * caller is expected to abort superseded queries via `signal`; an abort * throws, every other failure resolves to an empty list. */ export async function searchActorsTypeahead( q: string, signal: AbortSignal, ): Promise { let res: Response | undefined; try { res = await fetch(endpointUrl(WAOW_API, q), { signal, // their docs ask clients to identify themselves (header is in their // CORS allow-list, verified 2026-08) headers: { 'X-Client': 'web.hubble.microcosm.blue' }, }); } catch (err) { if (signal.aborted) throw err; // network-level failure: fall through to the bluesky attempt } // a 4xx means waow rejected this query; don't shop it around if (res && !res.ok && res.status < 500) return []; if (!res || !res.ok) { res = await fetch(endpointUrl(BSKY_API, q), { signal }); if (!res.ok) return []; } return parseActors(await res.json()); }