diff --git a/src/lib/microcosm/hydrate.ts b/src/lib/microcosm/hydrate.ts index e6bc0ed..06441f9 100644 --- a/src/lib/microcosm/hydrate.ts +++ b/src/lib/microcosm/hydrate.ts @@ -23,11 +23,14 @@ import { type AppBskyGraphList, type AppBskyLabelerDefs, type AppBskyLabelerService, - type AtpAgent, } from '@atproto/api' import {constellation} from '#/lib/microcosm' -import {getRecordByUri, resolveMiniDoc} from '#/lib/microcosm/slingshot' +import { + countRecords, + getRecordByUri, + resolveMiniDoc, +} from '#/lib/microcosm/slingshot' import {IMG_CDN_URL} from '#/env' /** @@ -467,60 +470,49 @@ async function hydrateVerificationState( } /** - * Hydrate a `ProfileViewDetailed` for a DID or handle from microcosm. + * Hydrate a `ProfileViewDetailed` for a DID or handle, fully from microcosm. * - * Counts: `followersCount` / `followsCount` / `postsCount` are pre-computed - * server-side. `postsCount` and `followsCount` are not reproducible from - * microcosm at all (Constellation is target-indexed; PDS pagination is - * non-viable for busy accounts), and exact follower counts vary by index, so - * for consistency all three come from the AppView's getProfile when an `agent` - * is provided. Without an agent (logged out), `followersCount` falls back to - * Constellation and the other two are omitted. Identity, profile record, and - * avatar are always microcosm-sourced. + * Counts: `followersCount` from Constellation backlinks, `followsCount` by + * counting the actor's own `app.bsky.graph.follow` records (bounded PDS + * pagination). `postsCount` is intentionally omitted - counting a repo's own + * posts needs full PDS pagination, which is non-viable for busy accounts, so we + * don't show a number we can't compute reliably. */ export async function hydrateProfile( identifier: string, signal?: AbortSignal, - agent?: AtpAgent, ): Promise { const mini = await resolveMiniDoc(identifier, signal) const {did, handle} = mini - const [recordRes, counts] = await Promise.all([ - getRecordByUri( - `at://${did}/app.bsky.actor.profile/self`, - undefined, - 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, - followersCount: r.data.followersCount, - })) - .catch(() => undefined) - : Promise.resolve(undefined), - ]) - - // Without an agent (e.g. logged out), followers is still reconstructable from - // Constellation; posts/follows are not. - const followersFallback = - counts == null - ? await constellation - .getBacklinksCount(did, constellation.Sources.followers, signal) - .then(r => r.total) - .catch(() => undefined) - : undefined + // All counts come from microcosm now: + // followersCount = Constellation backlinks (who follows this DID) + // followsCount = count of the actor's own app.bsky.graph.follow records + // postsCount is omitted: counting a repo's own posts requires paginating the + // whole PDS collection, which is non-viable for busy accounts, so we don't + // show a number we can't compute reliably. + const [recordRes, followersCount, followsCount, verification] = + await Promise.all([ + getRecordByUri( + `at://${did}/app.bsky.actor.profile/self`, + undefined, + signal, + ).catch(() => undefined), + constellation + .getBacklinksCount(did, constellation.Sources.followers, signal) + .then(r => r.total) + .catch(() => undefined), + countRecords( + {repo: did, collection: 'app.bsky.graph.follow'}, + signal, + ) + .then(r => r.count) + .catch(() => undefined), + hydrateVerificationState(did, handle, signal).catch(() => undefined), + ]) const profile = recordRes?.value as AppBskyActorProfile.Record | undefined - const verification = await hydrateVerificationState(did, handle, signal).catch( - () => undefined, - ) - return { $type: 'app.bsky.actor.defs#profileViewDetailed', did, @@ -529,9 +521,8 @@ export async function hydrateProfile( description: profile?.description, avatar: cdnUrl('avatar', did, profile?.avatar), banner: cdnUrl('banner', did, profile?.banner), - followersCount: counts?.followersCount ?? followersFallback, - followsCount: counts?.followsCount, - postsCount: counts?.postsCount, + followersCount, + followsCount, labels: extractSelfLabels(did, profile), indexedAt: undefined, createdAt: profile?.createdAt, diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 2bc8d02..0640ff0 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -82,10 +82,10 @@ export function useProfileQuery({ queryFn: async ({signal}) => { if (MICROCOSM_ENABLED) { try { - // 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) + // Fully microcosm-sourced: followers/follows counts from Constellation + // + PDS record counting. postsCount is omitted (not reliably + // computable). + return await hydrateProfile(did ?? '', signal) } catch (e) { // Hydration is partial/best-effort; fall back to the AppView. logger.debug('microcosm hydrateProfile failed, falling back', {