replies timeline only, appview-less bluesky client
at main 4.2 kB view raw
1import { 2 parseCanonicalResourceUri, 3 type CanonicalResourceUri, 4 type Cid, 5 type ResourceUri 6} from '@atcute/lexicons'; 7import { recordCache, type AtpClient } from './client'; 8import { err, expect, ok, type Result } from '$lib/result'; 9import type { Backlinks } from './constellation'; 10import { AppBskyFeedPost } from '@atcute/bluesky'; 11import type { AtprotoDid } from '@atcute/lexicons/syntax'; 12 13export type PostWithUri = { uri: ResourceUri; cid: Cid | undefined; record: AppBskyFeedPost.Main }; 14export type PostWithBacklinks = PostWithUri & { 15 replies: Backlinks; 16}; 17export type PostsWithReplyBacklinks = PostWithBacklinks[]; 18 19const replySource = 'app.bsky.feed.post:reply.parent.uri'; 20 21export const fetchPostsWithBacklinks = async ( 22 client: AtpClient, 23 repo: AtprotoDid, 24 cursor?: string, 25 limit?: number 26): Promise<Result<{ posts: PostsWithReplyBacklinks; cursor?: string }, string>> => { 27 const recordsList = await client.listRecords('app.bsky.feed.post', repo, cursor, limit); 28 if (!recordsList.ok) return err(`can't retrieve posts: ${recordsList.error}`); 29 cursor = recordsList.value.cursor; 30 const records = recordsList.value.records; 31 32 try { 33 const allBacklinks = await Promise.all( 34 records.map(async (r): Promise<PostWithBacklinks> => { 35 recordCache.set(r.uri, r); 36 const replies = await client.getBacklinksUri(r.uri, replySource); 37 if (!replies.ok) throw `cant fetch replies: ${replies.error}`; 38 return { 39 uri: r.uri, 40 cid: r.cid, 41 record: r.value as AppBskyFeedPost.Main, 42 replies: replies.value 43 }; 44 }) 45 ); 46 return ok({ posts: allBacklinks, cursor }); 47 } catch (error) { 48 return err(`cant fetch posts backlinks: ${error}`); 49 } 50}; 51 52export const hydratePosts = async ( 53 client: AtpClient, 54 repo: AtprotoDid, 55 data: PostsWithReplyBacklinks 56): Promise<Result<Map<ResourceUri, PostWithUri>, string>> => { 57 let posts: Map<ResourceUri, PostWithUri> = new Map(); 58 try { 59 const allPosts = await Promise.all( 60 data.map(async (post) => { 61 const result: PostWithUri[] = [post]; 62 const replies = await Promise.all( 63 post.replies.records.map(async (r) => { 64 const reply = await client.getRecord(AppBskyFeedPost.mainSchema, r.did, r.rkey); 65 if (!reply.ok) throw `cant fetch reply: ${reply.error}`; 66 return reply.value; 67 }) 68 ); 69 result.push(...replies); 70 return result; 71 }) 72 ); 73 posts = new Map(allPosts.flat().map((post) => [post.uri, post])); 74 } catch (error) { 75 return err(`cant hydrate immediate replies: ${error}`); 76 } 77 78 const fetchUpwardsChain = async (post: PostWithUri) => { 79 let parent = post.record.reply?.parent; 80 while (parent) { 81 // if we already have this parent, then we already fetched this chain / are fetching it 82 if (posts.has(parent.uri as CanonicalResourceUri)) return; 83 const p = await client.getRecordUri(AppBskyFeedPost.mainSchema, parent.uri); 84 if (p.ok) { 85 posts.set(p.value.uri, p.value); 86 parent = p.value.record.reply?.parent; 87 continue; 88 } 89 // TODO: handle deleted parent posts 90 parent = undefined; 91 } 92 }; 93 await Promise.all(posts.values().map(fetchUpwardsChain)); 94 95 try { 96 const fetchDownwardsChain = async (post: PostWithUri) => { 97 const { repo: postRepo } = expect(parseCanonicalResourceUri(post.uri)); 98 if (repo === postRepo) return; 99 100 // get chains that are the same author until we exhaust them 101 const backlinks = await client.getBacklinksUri(post.uri, replySource); 102 if (!backlinks.ok) return; 103 104 const promises = []; 105 for (const reply of backlinks.value.records) { 106 if (reply.did !== postRepo) continue; 107 // if we already have this reply, then we already fetched this chain / are fetching it 108 if (posts.has(`at://${reply.did}/${reply.collection}/${reply.rkey}`)) continue; 109 const record = await client.getRecord(AppBskyFeedPost.mainSchema, reply.did, reply.rkey); 110 if (!record.ok) break; // TODO: this doesnt handle deleted posts in between 111 posts.set(record.value.uri, record.value); 112 promises.push(fetchDownwardsChain(record.value)); 113 } 114 115 await Promise.all(promises); 116 }; 117 await Promise.all(posts.values().map(fetchDownwardsChain)); 118 } catch (error) { 119 return err(`cant fetch post reply chain: ${error}`); 120 } 121 122 return ok(posts); 123};