replies timeline only, appview-less bluesky client
1import type { ActorIdentifier, CanonicalResourceUri, Cid, ResourceUri } from '@atcute/lexicons'; 2import { recordCache, type AtpClient } from './client'; 3import { err, ok, type Result } from '$lib/result'; 4import type { Backlinks } from './constellation'; 5import { AppBskyFeedPost } from '@atcute/bluesky'; 6 7export type PostWithUri = { uri: ResourceUri; cid: Cid | undefined; record: AppBskyFeedPost.Main }; 8export type PostWithBacklinks = PostWithUri & { 9 replies: Result<Backlinks, string>; 10}; 11export type PostsWithReplyBacklinks = PostWithBacklinks[]; 12 13export const fetchPostsWithBacklinks = async ( 14 client: AtpClient, 15 repo: ActorIdentifier, 16 cursor?: string, 17 limit?: number 18): Promise<Result<{ posts: PostsWithReplyBacklinks; cursor?: string }, string>> => { 19 const recordsList = await client.listRecords('app.bsky.feed.post', repo, cursor, limit); 20 if (!recordsList.ok) return err(`can't retrieve posts: ${recordsList.error}`); 21 cursor = recordsList.value.cursor; 22 const records = recordsList.value.records; 23 24 const allBacklinks = await Promise.all( 25 records.map(async (r) => { 26 recordCache.set(r.uri, r); 27 const res = await client.getBacklinksUri( 28 r.uri as CanonicalResourceUri, 29 'app.bsky.feed.post:reply.parent.uri' 30 ); 31 return { 32 uri: r.uri, 33 cid: r.cid, 34 record: r.value as AppBskyFeedPost.Main, 35 replies: res 36 }; 37 }) 38 ); 39 40 return ok({ posts: allBacklinks, cursor }); 41}; 42 43export const hydratePosts = async ( 44 client: AtpClient, 45 data: PostsWithReplyBacklinks 46): Promise<Map<ResourceUri, PostWithUri>> => { 47 const allPosts = await Promise.all( 48 data.map(async (post) => { 49 const result: Result<PostWithUri, string>[] = [ok(post)]; 50 if (post.replies.ok) { 51 const replies = await Promise.all( 52 post.replies.value.records.map((r) => 53 client.getRecord(AppBskyFeedPost.mainSchema, r.did, r.rkey) 54 ) 55 ); 56 result.push(...replies); 57 } 58 return result; 59 }) 60 ); 61 const posts = new Map( 62 allPosts 63 .flat() 64 .flatMap((res) => (res.ok ? [res.value] : [])) 65 .map((post) => [post.uri, post]) 66 ); 67 68 // hydrate posts 69 const missingPosts = await Promise.all( 70 Array.from(posts).map(async ([, post]) => { 71 let result: PostWithUri[] = [post]; 72 let parent = post.record.reply?.parent; 73 while (parent) { 74 if (posts.has(parent.uri as CanonicalResourceUri)) { 75 return result; 76 } 77 const p = await client.getRecordUri(AppBskyFeedPost.mainSchema, parent.uri); 78 if (p.ok) { 79 result = [p.value, ...result]; 80 parent = p.value.record.reply?.parent; 81 continue; 82 } 83 parent = undefined; 84 } 85 return result; 86 }) 87 ); 88 for (const post of missingPosts.flat()) { 89 posts.set(post.uri, post); 90 } 91 92 return posts; 93};