diff --git a/src/lib/microcosm/hydrate.ts b/src/lib/microcosm/hydrate.ts index 053dab7..4fee298 100644 --- a/src/lib/microcosm/hydrate.ts +++ b/src/lib/microcosm/hydrate.ts @@ -194,12 +194,14 @@ async function hydrateEmbed( /** * Hydrate a `ProfileViewDetailed` for a DID or handle from microcosm. * - * - `followersCount` comes from Constellation (backlinks of follows -> this DID). - * - `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. + * 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. */ export async function hydrateProfile( identifier: string, @@ -209,15 +211,12 @@ export async function hydrateProfile( const mini = await resolveMiniDoc(identifier, signal) const {did, handle} = mini - const [recordRes, followers, counts] = await Promise.all([ + const [recordRes, counts] = await Promise.all([ getRecordByUri( `at://${did}/app.bsky.actor.profile/self`, undefined, signal, ).catch(() => undefined), - constellation - .getBacklinksCount(did, constellation.Sources.followers, signal) - .catch(() => undefined), // Pre-computed counts the AppView already has; only these fields are read. agent ? agent @@ -225,11 +224,22 @@ export async function hydrateProfile( .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 + const profile = recordRes?.value as AppBskyActorProfile.Record | undefined return { @@ -240,7 +250,7 @@ export async function hydrateProfile( description: profile?.description, avatar: cdnUrl('avatar', did, profile?.avatar), banner: cdnUrl('banner', did, profile?.banner), - followersCount: followers?.total, + followersCount: counts?.followersCount ?? followersFallback, followsCount: counts?.followsCount, postsCount: counts?.postsCount, labels: extractSelfLabels(did, profile),