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