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};