fix: profile shows Cospan/Tangled follow counts, not Bluesky social graph

Author: Aaron Steven White
Commit 6b7c95c704c796388b098afbbac815e7992c6dca
Parent: 25a0b27b70
Structural diff unavailable

These commits were pushed via plain git push, so no pre-parsed schemas are available. Install git-remote-cospan and re-push via panproto:// to see scope-level changes, breaking change detection, and semantic diffs.

brew install panproto/tap/git-remote-cospan
1 file changed +39 -16
@@ -1,8 +1,9 @@
11 import type { PageServerLoad } from './$types';
22 import { getProfile } from '$lib/api/actor.js';
33 import { listRepos } from '$lib/api/repo.js';
4+import { xrpcQuery } from '$lib/api/client.js';
45 
5-async function fetchBlueskyProfile(did: string) {
6+async function fetchBlueskyIdentity(did: string) {
67 	try {
78 		const resp = await fetch(
89 			`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`
@@ -10,43 +11,65 @@ async function fetchBlueskyProfile(did: string) {
1011 		if (resp.ok) {
1112 			const data = await resp.json();
1213 			return {
13-				did,
1414 				displayName: data.displayName ?? null,
1515 				handle: data.handle ?? did,
1616 				description: data.description ?? null,
1717 				avatarUrl: data.avatar ?? null,
18-				followerCount: data.followersCount ?? 0,
19-				followingCount: data.followsCount ?? 0,
20-				repoCount: 0,
2118 			};
2219 		}
2320 	} catch {}
2421 	return null;
2522 }
2623 
24+async function fetchCospanFollowCounts(did: string) {
25+	try {
26+		const [followers, following] = await Promise.all([
27+			xrpcQuery<{ follows: any[] }>('dev.cospan.graph.follow.list', { did, direction: 'followers', limit: 1 }),
28+			xrpcQuery<{ follows: any[] }>('dev.cospan.graph.follow.list', { did, direction: 'following', limit: 1 }),
29+		]);
30+		// The API doesn't return total counts, just paginated lists.
31+		// For now, fetch a larger batch and count. TODO: add count endpoint.
32+		const [followersFull, followingFull] = await Promise.all([
33+			xrpcQuery<{ follows: any[] }>('dev.cospan.graph.follow.list', { did, direction: 'followers', limit: 1000 }),
34+			xrpcQuery<{ follows: any[] }>('dev.cospan.graph.follow.list', { did, direction: 'following', limit: 1000 }),
35+		]);
36+		return {
37+			followerCount: followersFull.follows?.length ?? 0,
38+			followingCount: followingFull.follows?.length ?? 0,
39+		};
40+	} catch {
41+		return { followerCount: 0, followingCount: 0 };
42+	}
43+}
44+
2745 export const load: PageServerLoad = async ({ params }) => {
28-	// Always fetch Bluesky profile for avatar and social stats
29-	const bskyProfile = await fetchBlueskyProfile(params.did);
46+	// Fetch identity from Bluesky (avatar, handle, description only)
47+	const bskyIdentity = await fetchBlueskyIdentity(params.did);
3048 
31-	// Try Cospan profile for any Cospan-specific fields
49+	// Fetch Cospan-specific profile
3250 	let cospanProfile = null;
3351 	try {
3452 		cospanProfile = await getProfile({ did: params.did });
3553 	} catch {}
3654 
37-	// Merge: Bluesky provides avatar/stats, Cospan overrides if it has data
38-	const profile = bskyProfile
39-		? {
40-				...bskyProfile,
41-				...(cospanProfile?.displayName ? { displayName: cospanProfile.displayName } : {}),
42-				...(cospanProfile?.description ? { description: cospanProfile.description } : {}),
43-			}
44-		: cospanProfile;
55+	// Fetch follow counts from Cospan database (includes Tangled follows)
56+	const followCounts = await fetchCospanFollowCounts(params.did);
4557 
4658 	let repos = { items: [] as any[], cursor: null as string | null };
4759 	try {
4860 		repos = await listRepos({ did: params.did, limit: 30 });
4961 	} catch {}
5062 
63+	const profile = {
64+		did: params.did,
65+		displayName: cospanProfile?.displayName ?? bskyIdentity?.displayName ?? null,
66+		handle: bskyIdentity?.handle ?? params.did,
67+		description: cospanProfile?.description ?? bskyIdentity?.description ?? null,
68+		avatarUrl: bskyIdentity?.avatarUrl ?? null,
69+		followerCount: followCounts.followerCount,
70+		followingCount: followCounts.followingCount,
71+		repoCount: repos.items.length,
72+	};
73+
5174 	return { profile, repos, did: params.did };
5275 };
cospan · schematic version control on atproto built on AT Protocol