import { parseCanonicalResourceUri, type CanonicalResourceUri, type Cid, type ResourceUri } from '@atcute/lexicons'; import { type AtpClient } from './client.svelte'; import { err, expect, ok, type Ok, type Result } from '$lib/result'; import type { Backlinks } from './constellation'; import { AppBskyFeedPost } from '@atcute/bluesky'; import type { Did, RecordKey } from '@atcute/lexicons/syntax'; import { replySource, toCanonicalUri } from '$lib'; export type PostWithUri = { uri: ResourceUri; cid: Cid | undefined; record: AppBskyFeedPost.Main }; export type PostWithBacklinks = PostWithUri & { replies?: Backlinks; }; export const fetchPosts = async ( subject: Did, client: AtpClient, cursor?: string, limit?: number, withBacklinks: boolean = true ): Promise> => { const recordsList = await client.listRecords(subject, 'app.bsky.feed.post', cursor, limit); if (!recordsList.ok) return err(`can't retrieve posts: ${recordsList.error}`); cursor = recordsList.value.cursor; const records = recordsList.value.records; if (!withBacklinks) { return ok({ posts: records.map((r) => ({ uri: r.uri, cid: r.cid, record: r.value as AppBskyFeedPost.Main })), cursor }); } try { const allBacklinks = await Promise.all( records.map(async (r): Promise => { const result = await client.getBacklinks(r.uri, replySource); if (!result.ok) throw `cant fetch replies: ${result.error}`; const replies = result.value; return { uri: r.uri, cid: r.cid, record: r.value as AppBskyFeedPost.Main, replies }; }) ); return ok({ posts: allBacklinks, cursor }); } catch (error) { return err(`cant fetch posts backlinks: ${error}`); } }; export const hydratePosts = async ( client: AtpClient, repo: Did, data: PostWithBacklinks[], cacheFn: (did: Did, rkey: RecordKey) => Ok | undefined ): Promise, string>> => { let posts: Map = new Map(); try { const allPosts = await Promise.all( data.map(async (post) => { const result: PostWithUri[] = [post]; if (post.replies) { const replies = await Promise.all( post.replies.records.map(async (r) => { const reply = cacheFn(r.did, r.rkey) ?? (await client.getRecord(AppBskyFeedPost.mainSchema, r.did, r.rkey)); if (!reply.ok) throw `cant fetch reply: ${reply.error}`; return reply.value; }) ); result.push(...replies); } return result; }) ); posts = new Map(allPosts.flat().map((post) => [post.uri, post])); } catch (error) { return err(`cant hydrate immediate replies: ${error}`); } const fetchUpwardsChain = async (post: PostWithUri) => { let parent = post.record.reply?.parent; while (parent) { const parentUri = parent.uri as CanonicalResourceUri; // if we already have this parent, then we already fetched this chain / are fetching it if (posts.has(parentUri)) return; const parsedParentUri = expect(parseCanonicalResourceUri(parentUri)); const p = cacheFn(parsedParentUri.repo, parsedParentUri.rkey) ?? (await client.getRecord( AppBskyFeedPost.mainSchema, parsedParentUri.repo, parsedParentUri.rkey )); if (p.ok) { posts.set(p.value.uri, p.value); parent = p.value.record.reply?.parent; continue; } // TODO: handle deleted parent posts parent = undefined; } }; await Promise.all(posts.values().map(fetchUpwardsChain)); try { const fetchDownwardsChain = async (post: PostWithUri) => { const { repo: postRepo } = expect(parseCanonicalResourceUri(post.uri)); if (repo === postRepo) return; // get chains that are the same author until we exhaust them const backlinks = await client.getBacklinks(post.uri, replySource); if (!backlinks.ok) return; const promises = []; for (const reply of backlinks.value.records) { if (reply.did !== postRepo) continue; // if we already have this reply, then we already fetched this chain / are fetching it if (posts.has(toCanonicalUri(reply))) continue; const record = cacheFn(reply.did, reply.rkey) ?? (await client.getRecord(AppBskyFeedPost.mainSchema, reply.did, reply.rkey)); if (!record.ok) break; // TODO: this doesnt handle deleted posts in between posts.set(record.value.uri, record.value); promises.push(fetchDownwardsChain(record.value)); } await Promise.all(promises); }; await Promise.all(posts.values().map(fetchDownwardsChain)); } catch (error) { return err(`cant fetch post reply chain: ${error}`); } return ok(posts); };