my website at ewancroft.uk
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(engagement): implement engagement fetching from Constellation and integrate with post data refactor(cache): enhance cache logging for better debugging refactor(fetch): improve profile and status fetching with detailed logging

+166 -13
+31 -6
src/lib/services/atproto/agents.ts
··· 1 1 import { AtpAgent } from '@atproto/api'; 2 2 import type { ResolvedIdentity } from './types'; 3 3 4 + // Primary Microcosm Constellation endpoint 5 + export const constellationAgent = new AtpAgent({ service: 'https://constellation.microcosm.blue' }); 6 + 4 7 // Default fallback agent for public Bluesky API calls 5 8 export const defaultAgent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 6 9 ··· 12 15 * Resolves a DID to find its PDS endpoint using Slingshot. 13 16 */ 14 17 export async function resolveIdentity(did: string): Promise<ResolvedIdentity> { 18 + console.info(`[Identity] Resolving DID: ${did}`); 19 + 15 20 const response = await fetch( 16 21 `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}` 17 22 ); 18 23 19 24 if (!response.ok) { 20 - throw new Error(`Failed to resolve identifier: ${response.status} ${response.statusText}`); 25 + console.error(`[Identity] Resolution failed: ${response.status} ${response.statusText}`); 26 + throw new Error(`Failed to resolve identifier via Slingshot: ${response.status} ${response.statusText}`); 21 27 } 22 28 29 + console.debug(`[Identity] Raw response:`, await response.clone().text()); 23 30 const data = await response.json(); 24 31 25 32 if (!data.did || !data.pds) { ··· 33 40 * Gets or creates an agent for the public Bluesky API with PDS fallback 34 41 */ 35 42 export async function getPublicAgent(did: string): Promise<AtpAgent> { 36 - if (resolvedAgent) return resolvedAgent; 43 + console.info(`[Agent] Getting public agent for DID: ${did}`); 44 + if (resolvedAgent) { 45 + console.debug('[Agent] Using cached agent'); 46 + return resolvedAgent; 47 + } 37 48 38 49 try { 50 + // Try Constellation first 51 + try { 52 + console.info('[Agent] Attempting Constellation endpoint'); 53 + const response = await constellationAgent.getProfile({ actor: did }); 54 + if (response.success) { 55 + console.info('[Agent] Successfully connected to Constellation'); 56 + resolvedAgent = constellationAgent; 57 + return resolvedAgent; 58 + } 59 + } catch (constellationErr) { 60 + console.warn('[Agent] Constellation endpoint unreachable:', constellationErr); 61 + } 62 + 63 + // Then try Slingshot for PDS resolution 64 + console.info('[Agent] Attempting Slingshot resolution'); 39 65 const resolved = await resolveIdentity(did); 66 + console.info(`[Agent] Resolved PDS endpoint: ${resolved.pds}`); 40 67 resolvedAgent = new AtpAgent({ service: resolved.pds }); 41 68 return resolvedAgent; 42 69 } catch (err) { 43 - console.error('Failed to resolve DID via Slingshot, falling back:', err); 70 + console.error('[Agent] All Microcosm endpoints failed, falling back to Bluesky:', err); 44 71 resolvedAgent = defaultAgent; 45 72 return resolvedAgent; 46 73 } 47 - } 48 - 49 - /** 74 + }/** 50 75 * Gets or creates a PDS-specific agent 51 76 */ 52 77 export async function getPDSAgent(did: string): Promise<AtpAgent> {
+9 -1
src/lib/services/atproto/cache.ts
··· 8 8 private readonly TTL = 5 * 60 * 1000; // 5 minutes 9 9 10 10 get<T>(key: string): T | null { 11 + console.debug(`[Cache] Getting key: ${key}`); 11 12 const entry = this.cache.get(key); 12 - if (!entry) return null; 13 + if (!entry) { 14 + console.debug(`[Cache] Cache miss for key: ${key}`); 15 + return null; 16 + } 13 17 14 18 if (Date.now() - entry.timestamp > this.TTL) { 19 + console.debug(`[Cache] Entry expired for key: ${key}`); 15 20 this.cache.delete(key); 16 21 return null; 17 22 } 18 23 24 + console.debug(`[Cache] Cache hit for key: ${key}`); 19 25 return entry.data; 20 26 } 21 27 22 28 set<T>(key: string, data: T): void { 29 + console.debug(`[Cache] Setting key: ${key}`, data); 23 30 this.cache.set(key, { 24 31 data, 25 32 timestamp: Date.now() ··· 27 34 } 28 35 29 36 delete(key: string): void { 37 + console.debug(`[Cache] Deleting key: ${key}`); 30 38 this.cache.delete(key); 31 39 } 32 40
+85
src/lib/services/atproto/engagement.ts
··· 1 + import { cache } from './cache'; 2 + 3 + export type EngagementType = 'app.bsky.feed.like' | 'app.bsky.feed.repost'; 4 + 5 + interface EngagementResponse { 6 + dids: string[]; 7 + cursor?: string; 8 + } 9 + 10 + /** 11 + * Fetches engagement data (likes/reposts) for a post from Constellation as a fallback 12 + */ 13 + export async function fetchEngagementFromConstellation( 14 + uri: string, 15 + type: EngagementType, 16 + cursor?: string 17 + ): Promise<EngagementResponse> { 18 + console.info(`[Constellation] Fetching ${type} data for ${uri}`); 19 + 20 + const cacheKey = `engagement:${type}:${uri}:${cursor || 'initial'}`; 21 + const cached = cache.get<EngagementResponse>(cacheKey); 22 + if (cached) { 23 + console.debug('[Constellation] Returning cached engagement data'); 24 + return cached; 25 + } 26 + 27 + try { 28 + const url = new URL('https://constellation.microcosm.blue/links/distinct-dids'); 29 + url.searchParams.append('target', uri); 30 + url.searchParams.append('collection', type); 31 + url.searchParams.append('path', ''); 32 + url.searchParams.append('limit', '100'); 33 + if (cursor) { 34 + url.searchParams.append('cursor', cursor); 35 + } 36 + 37 + console.debug(`[Constellation] Requesting: ${url.toString()}`); 38 + const response = await fetch(url); 39 + 40 + if (!response.ok) { 41 + throw new Error(`Constellation HTTP error! Status: ${response.status}`); 42 + } 43 + 44 + const data = await response.json(); 45 + console.debug('[Constellation] Response received:', data); 46 + 47 + const result: EngagementResponse = { 48 + dids: data.dids || [], 49 + cursor: data.cursor 50 + }; 51 + 52 + // Cache the results 53 + cache.set(cacheKey, result); 54 + return result; 55 + } catch (error) { 56 + console.error('[Constellation] Failed to fetch engagement data:', error); 57 + throw error; 58 + } 59 + } 60 + 61 + /** 62 + * Fetches all engagement data by paginating through results 63 + */ 64 + export async function fetchAllEngagement( 65 + uri: string, 66 + type: EngagementType 67 + ): Promise<string[]> { 68 + console.info(`[Constellation] Fetching all ${type} data for ${uri}`); 69 + 70 + const allDids: Set<string> = new Set(); 71 + let cursor: string | undefined = undefined; 72 + 73 + try { 74 + do { 75 + const response = await fetchEngagementFromConstellation(uri, type, cursor); 76 + response.dids.forEach(did => allDids.add(did)); 77 + cursor = response.cursor; 78 + } while (cursor); 79 + 80 + return Array.from(allDids); 81 + } catch (error) { 82 + console.error('[Constellation] Failed to fetch all engagement:', error); 83 + return Array.from(allDids); // Return what we have so far 84 + } 85 + }
+16 -3
src/lib/services/atproto/fetch.ts
··· 7 7 * Fetches user profile from AT Protocol 8 8 */ 9 9 export async function fetchProfile(): Promise<ProfileData> { 10 + console.info('[Profile] Fetching profile data'); 10 11 const cacheKey = `profile:${PUBLIC_ATPROTO_DID}`; 11 12 const cached = cache.get<ProfileData>(cacheKey); 12 - if (cached) return cached; 13 + if (cached) { 14 + console.debug('[Profile] Returning cached profile data'); 15 + return cached; 16 + } 13 17 14 18 try { 19 + console.info('[Profile] Cache miss, fetching from network'); 15 20 // Profile data is public, try Bluesky API first, then PDS 16 21 const profile = await withFallback(PUBLIC_ATPROTO_DID, async (agent) => { 22 + console.debug('[Profile] Attempting profile fetch with agent:', agent.service.toString()); 17 23 const response = await agent.getProfile({ actor: PUBLIC_ATPROTO_DID }); 18 24 return response.data; 19 25 }); ··· 30 36 postsCount: profile.postsCount 31 37 }; 32 38 39 + console.info('[Profile] Successfully fetched profile data'); 40 + console.debug('[Profile] Profile data:', data); 33 41 cache.set(cacheKey, data); 34 42 return data; 35 43 } catch (error) { 36 - console.error('Failed to fetch profile from all sources:', error); 44 + console.error('[Profile] Failed to fetch profile from all sources:', error); 37 45 throw error; 38 46 } 39 47 } ··· 42 50 * Fetches user status from custom lexicon 43 51 */ 44 52 export async function fetchStatus(): Promise<StatusData | null> { 53 + console.info('[Status] Fetching status data'); 45 54 const cacheKey = `status:${PUBLIC_ATPROTO_DID}`; 46 55 const cached = cache.get<StatusData>(cacheKey); 47 - if (cached) return cached; 56 + if (cached) { 57 + console.debug('[Status] Returning cached status data'); 58 + return cached; 59 + } 48 60 49 61 try { 62 + console.info('[Status] Cache miss, fetching from network'); 50 63 // Custom collection, prefer PDS first 51 64 const records = await withFallback( 52 65 PUBLIC_ATPROTO_DID,
+25 -3
src/lib/services/atproto/posts.ts
··· 3 3 import { withFallback, defaultAgent } from './agents'; 4 4 import { resolveIdentity } from './agents'; 5 5 import { buildPdsBlobUrl } from './media'; 6 + import { fetchAllEngagement } from './engagement'; 6 7 import type { 7 8 BlogPost, 8 9 BlogPostsData, ··· 17 18 * Fetches all Leaflet publications for a user 18 19 */ 19 20 export async function fetchLeafletPublications(): Promise<LeafletPublicationsData> { 21 + console.info('[Leaflet] Fetching publications'); 20 22 const cacheKey = `leaflet:publications:${PUBLIC_ATPROTO_DID}`; 21 23 const cached = cache.get<LeafletPublicationsData>(cacheKey); 22 - if (cached) return cached; 24 + if (cached) { 25 + console.debug('[Leaflet] Returning cached publications'); 26 + return cached; 27 + } 23 28 24 29 const publications: LeafletPublication[] = []; 30 + console.info('[Leaflet] Cache miss, fetching from network'); 25 31 26 32 try { 33 + console.debug('[Leaflet] Querying publications records'); 27 34 const publicationsRecords = await withFallback( 28 35 PUBLIC_ATPROTO_DID, 29 36 async (agent) => { ··· 470 477 } 471 478 } 472 479 480 + // Get engagement data from Constellation as a fallback 481 + let finalLikeCount = postData.likeCount; 482 + let finalRepostCount = postData.repostCount; 483 + 484 + try { 485 + const [likers, reposters] = await Promise.all([ 486 + fetchAllEngagement(postData.uri, 'app.bsky.feed.like'), 487 + fetchAllEngagement(postData.uri, 'app.bsky.feed.repost') 488 + ]); 489 + finalLikeCount = Math.max(postData.likeCount || 0, likers.length); 490 + finalRepostCount = Math.max(postData.repostCount || 0, reposters.length); 491 + } catch (error: unknown) { 492 + console.warn('[fetchPostFromUri] Failed to fetch engagement from Constellation:', error); 493 + } 494 + 473 495 const post: BlueskyPost = { 474 496 text: value.text, 475 497 createdAt: value.createdAt, 476 498 uri: postData.uri, 477 499 author, 478 - likeCount: postData.likeCount, 479 - repostCount: postData.repostCount, 500 + likeCount: finalLikeCount, 501 + repostCount: finalRepostCount, 480 502 replyCount: postData.replyCount, 481 503 hasImages, 482 504 imageUrls,