diff --git a/src/lib/microcosm/hydrate.ts b/src/lib/microcosm/hydrate.ts index 868563e..053dab7 100644 --- a/src/lib/microcosm/hydrate.ts +++ b/src/lib/microcosm/hydrate.ts @@ -18,14 +18,11 @@ import { type AppBskyActorProfile, type AppBskyFeedDefs, type AppBskyFeedPost, + type AtpAgent, } from '@atproto/api' import {constellation} from '#/lib/microcosm' -import { - countRecords, - getRecordByUri, - resolveMiniDoc, -} from '#/lib/microcosm/slingshot' +import {getRecordByUri, resolveMiniDoc} from '#/lib/microcosm/slingshot' import {IMG_CDN_URL} from '#/env' /** @@ -198,19 +195,21 @@ async function hydrateEmbed( * Hydrate a `ProfileViewDetailed` for a DID or handle from microcosm. * * - `followersCount` comes from Constellation (backlinks of follows -> this DID). - * - `followsCount` / `postsCount` come from counting the DID's own - * `app.bsky.graph.follow` / `app.bsky.feed.post` records via listRecords - * (these aren't backlinks). Bounded by countRecords' page cap, so very busy - * accounts report a floor (e.g. "3000+") rather than an exact total. + * - `followsCount` / `postsCount` are PRE-COMPUTED server-side counts that + * microcosm cannot reproduce (Constellation is target-indexed; PDS pagination + * is non-viable for busy accounts). If an `agent` is provided, we fetch just + * those two numbers from the AppView's getProfile. Everything else stays + * microcosm-sourced; without an agent the counts are simply omitted. */ export async function hydrateProfile( identifier: string, signal?: AbortSignal, + agent?: AtpAgent, ): Promise { const mini = await resolveMiniDoc(identifier, signal) const {did, handle} = mini - const [recordRes, followers, follows, posts] = await Promise.all([ + const [recordRes, followers, counts] = await Promise.all([ getRecordByUri( `at://${did}/app.bsky.actor.profile/self`, undefined, @@ -219,16 +218,16 @@ export async function hydrateProfile( constellation .getBacklinksCount(did, constellation.Sources.followers, signal) .catch(() => undefined), - // Cap pagination so profile loads stay responsive: exact counts in the - // thousands don't matter for display (the AppView caps too). 5 pages = 500. - countRecords( - {repo: did, collection: 'app.bsky.graph.follow', maxPages: 5}, - signal, - ).catch(() => undefined), - countRecords( - {repo: did, collection: 'app.bsky.feed.post', maxPages: 5}, - signal, - ).catch(() => undefined), + // Pre-computed counts the AppView already has; only these fields are read. + agent + ? agent + .getProfile({actor: did}) + .then(r => ({ + postsCount: r.data.postsCount, + followsCount: r.data.followsCount, + })) + .catch(() => undefined) + : Promise.resolve(undefined), ]) const profile = recordRes?.value as AppBskyActorProfile.Record | undefined @@ -242,8 +241,8 @@ export async function hydrateProfile( avatar: cdnUrl('avatar', did, profile?.avatar), banner: cdnUrl('banner', did, profile?.banner), followersCount: followers?.total, - followsCount: follows?.count, - postsCount: posts?.count, + followsCount: counts?.followsCount, + postsCount: counts?.postsCount, labels: extractSelfLabels(did, profile), indexedAt: undefined, createdAt: profile?.createdAt, diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 1982cef..2bc8d02 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -82,7 +82,10 @@ export function useProfileQuery({ queryFn: async ({signal}) => { if (MICROCOSM_ENABLED) { try { - return await hydrateProfile(did ?? '', signal) + // agent is passed so the otherwise-unreproducible pre-computed counts + // (postsCount/followsCount) come from getProfile; everything else is + // microcosm-sourced. + return await hydrateProfile(did ?? '', signal, agent) } catch (e) { // Hydration is partial/best-effort; fall back to the AppView. logger.debug('microcosm hydrateProfile failed, falling back', {