this repo has no description

At a glance, the fetch mechanism works

ari.express 6ca0a971 26aff55d

verified
Changed files
+185 -43
src
+1 -1
config.ts
··· 18 * Maximum number of posts to show in the feed (across all users) 19 * @default 100 20 */ 21 - static readonly MAX_POSTS: number = 100; 22 23 /** 24 * Footer text for the dashboard
··· 18 * Maximum number of posts to show in the feed (across all users) 19 * @default 100 20 */ 21 + static readonly MAX_POSTS: number = 5; 22 23 /** 24 * Footer text for the dashboard
+6 -2
src/App.svelte
··· 1 <script lang="ts"> 2 import PostComponent from "./lib/PostComponent.svelte"; 3 import AccountComponent from "./lib/AccountComponent.svelte"; 4 - import { fetchAllPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch"; 5 import { Config } from "../config"; 6 - const postsPromise = fetchAllPosts(); 7 const accountsPromise = getAllMetadataFromPds(); 8 </script> 9 ··· 12 {#await accountsPromise} 13 <p>Loading...</p> 14 {:then accountsData} 15 <div id="Account"> 16 <h1 id="Header">ATProto PDS</h1> 17 <p>Home to {accountsData.length} accounts</p> ··· 29 {#await postsPromise} 30 <p>Loading...</p> 31 {:then postsData} 32 <div id="Feed"> 33 <div id="spacer"></div> 34 {#each postsData as postObject}
··· 1 <script lang="ts"> 2 import PostComponent from "./lib/PostComponent.svelte"; 3 import AccountComponent from "./lib/AccountComponent.svelte"; 4 + import { getNextPosts, Post, getAllMetadataFromPds } from "./lib/pdsfetch"; 5 import { Config } from "../config"; 6 + const postsPromise = getNextPosts(); 7 const accountsPromise = getAllMetadataFromPds(); 8 </script> 9 ··· 12 {#await accountsPromise} 13 <p>Loading...</p> 14 {:then accountsData} 15 + 16 <div id="Account"> 17 <h1 id="Header">ATProto PDS</h1> 18 <p>Home to {accountsData.length} accounts</p> ··· 30 {#await postsPromise} 31 <p>Loading...</p> 32 {:then postsData} 33 + <button on:click={getNextPosts}> 34 + Load more posts 35 + </button> 36 <div id="Feed"> 37 <div id="spacer"></div> 38 {#each postsData as postObject}
+178 -40
src/lib/pdsfetch.ts
··· 18 // import { AppBskyActorDefs } from "@atcute/client/lexicons"; 19 20 interface AccountMetadata { 21 - did: string; 22 displayName: string; 23 handle: string; 24 avatarCid: string | null; 25 } 26 interface atUriObject { 27 repo: string; 28 collection: string; ··· 45 46 constructor( 47 record: ComAtprotoRepoListRecords.Record, 48 - account: AccountMetadata 49 ) { 50 this.postCid = record.cid; 51 this.recordName = processAtUri(record.uri).rkey; ··· 68 switch (post.embed?.$type) { 69 case "app.bsky.embed.images": 70 this.imagesCid = post.embed.images.map( 71 - (imageRecord: any) => imageRecord.image.ref.$link 72 ); 73 break; 74 case "app.bsky.embed.video": ··· 82 switch (post.embed.media.$type) { 83 case "app.bsky.embed.images": 84 this.imagesCid = post.embed.media.images.map( 85 - (imageRecord) => imageRecord.image.ref.$link 86 ); 87 88 break; ··· 118 return data.repos.map((repo: any) => repo.did) as At.Did[]; 119 }; 120 const getAccountMetadata = async ( 121 - did: `did:${string}:${string}` 122 - ): Promise<AccountMetadata> => { 123 // gonna assume self exists in the app.bsky.actor.profile 124 try { 125 const { data } = await rpc.get("com.atproto.repo.getRecord", { ··· 143 return account; 144 } catch (e) { 145 console.error(`Error fetching metadata for ${did}:`, e); 146 - return { 147 - did: "error", 148 - displayName: "", 149 - avatarCid: null, 150 - handle: "error", 151 - }; 152 } 153 }; 154 ··· 157 const metadata = await Promise.all( 158 dids.map(async (repo: `did:${string}:${string}`) => { 159 return await getAccountMetadata(repo); 160 - }) 161 ); 162 - return metadata.filter((account) => account.did !== "error"); 163 }; 164 165 const fetchPosts = async (did: string) => { 166 try { 167 const { data } = await rpc.get("com.atproto.repo.listRecords", { ··· 196 197 if (did.startsWith("did:plc:") || did.startsWith("did:web:")) { 198 const doc = await resolver.resolve( 199 - did as `did:plc:${string}` | `did:web:${string}` 200 ); 201 return doc; 202 } else { ··· 219 } 220 }; 221 222 - const fetchAllPosts = async () => { 223 - const users: AccountMetadata[] = await getAllMetadataFromPds(); 224 - const postRecords = await Promise.all( 225 - users.map( 226 - async (metadata: AccountMetadata) => await fetchPosts(metadata.did) 227 - ) 228 - ); 229 - const validPostRecords = postRecords.filter((record) => !record.error); 230 - const posts: Post[] = validPostRecords.flatMap((userFetch) => 231 - userFetch.records.map((record) => { 232 - const user = users.find( 233 - (user: AccountMetadata) => user.did == userFetch.did 234 ); 235 - if (!user) { 236 - throw new Error(`User with DID ${userFetch.did} not found`); 237 } 238 - return new Post(record, user); 239 - }) 240 ); 241 - 242 - posts.sort((a, b) => b.timestamp - a.timestamp); 243 - 244 - if(!Config.SHOW_FUTURE_POSTS) { 245 - // Filter out posts that are in the future 246 const now = Date.now(); 247 - const filteredPosts = posts.filter((post) => post.timestamp <= now); 248 - return filteredPosts.slice(0, Config.MAX_POSTS); 249 } 250 251 - return posts.slice(0, Config.MAX_POSTS); 252 }; 253 - export { fetchAllPosts, getAllMetadataFromPds, Post }; 254 export type { AccountMetadata };
··· 18 // import { AppBskyActorDefs } from "@atcute/client/lexicons"; 19 20 interface AccountMetadata { 21 + did: At.Did; 22 displayName: string; 23 handle: string; 24 avatarCid: string | null; 25 + currentCursor?: string; 26 } 27 + 28 + let accountsMetadata: AccountMetadata[] = []; 29 + // a chronologically sorted list of posts for all users, that will be shown by svelte 30 + // getNextPosts will populate this list with additional posts as needed 31 + let posts: Post[] = []; 32 interface atUriObject { 33 repo: string; 34 collection: string; ··· 51 52 constructor( 53 record: ComAtprotoRepoListRecords.Record, 54 + account: AccountMetadata, 55 ) { 56 this.postCid = record.cid; 57 this.recordName = processAtUri(record.uri).rkey; ··· 74 switch (post.embed?.$type) { 75 case "app.bsky.embed.images": 76 this.imagesCid = post.embed.images.map( 77 + (imageRecord: any) => imageRecord.image.ref.$link, 78 ); 79 break; 80 case "app.bsky.embed.video": ··· 88 switch (post.embed.media.$type) { 89 case "app.bsky.embed.images": 90 this.imagesCid = post.embed.media.images.map( 91 + (imageRecord) => imageRecord.image.ref.$link, 92 ); 93 94 break; ··· 124 return data.repos.map((repo: any) => repo.did) as At.Did[]; 125 }; 126 const getAccountMetadata = async ( 127 + did: `did:${string}:${string}`, 128 + ) => { 129 // gonna assume self exists in the app.bsky.actor.profile 130 try { 131 const { data } = await rpc.get("com.atproto.repo.getRecord", { ··· 149 return account; 150 } catch (e) { 151 console.error(`Error fetching metadata for ${did}:`, e); 152 + return null; 153 } 154 }; 155 ··· 158 const metadata = await Promise.all( 159 dids.map(async (repo: `did:${string}:${string}`) => { 160 return await getAccountMetadata(repo); 161 + }), 162 ); 163 + return metadata.filter((account) => account !== null) as AccountMetadata[]; 164 }; 165 166 + // OLD 167 const fetchPosts = async (did: string) => { 168 try { 169 const { data } = await rpc.get("com.atproto.repo.listRecords", { ··· 198 199 if (did.startsWith("did:plc:") || did.startsWith("did:web:")) { 200 const doc = await resolver.resolve( 201 + did as `did:plc:${string}` | `did:web:${string}`, 202 ); 203 return doc; 204 } else { ··· 221 } 222 }; 223 224 + interface PostsAcc { 225 + posts: ComAtprotoRepoListRecords.Record[]; 226 + account: AccountMetadata; 227 + } 228 + const getCutoffDate = (postAccounts: PostsAcc[]) => { 229 + const now = Date.now(); 230 + let cutoffDate: Date | null = null; 231 + postAccounts.forEach((postAcc) => { 232 + const latestPost = new Date( 233 + (postAcc.posts[postAcc.posts.length - 1].value as AppBskyFeedPost.Record) 234 + .createdAt, 235 + ); 236 + if (!cutoffDate) { 237 + cutoffDate = latestPost; 238 + } else { 239 + if (latestPost > cutoffDate) { 240 + cutoffDate = latestPost; 241 + } 242 + } 243 + }); 244 + if (cutoffDate) { 245 + console.log("Cutoff date:", cutoffDate); 246 + return cutoffDate; 247 + } else { 248 + return new Date(now); 249 + } 250 + }; 251 + 252 + const filterPostsByDate = (posts: PostsAcc[], cutoffDate: Date) => { 253 + // filter posts for each account that are older than the cutoff date and save the cursor of the last post included 254 + const filteredPosts: PostsAcc[] = posts.map((postAcc) => { 255 + const filtered = postAcc.posts.filter((post) => { 256 + const postDate = new Date( 257 + (post.value as AppBskyFeedPost.Record).createdAt, 258 ); 259 + return postDate >= cutoffDate; 260 + }); 261 + if (filtered.length > 0) { 262 + postAcc.account.currentCursor = filtered[filtered.length - 1].cid; 263 + } 264 + return { 265 + posts: filtered, 266 + account: postAcc.account, 267 + }; 268 + }); 269 + return filteredPosts; 270 + }; 271 + const getNextPosts = async () => { 272 + if (!accountsMetadata.length) { 273 + accountsMetadata = await getAllMetadataFromPds(); 274 + } 275 + 276 + const postsAcc: PostsAcc[] = await Promise.all( 277 + accountsMetadata.map(async (account) => { 278 + const posts = await fetchPostsForUser( 279 + account.did, 280 + account.currentCursor || null, 281 + ); 282 + if (posts) { 283 + return { 284 + posts: posts, 285 + account: account, 286 + }; 287 + } else { 288 + return { 289 + posts: [], 290 + account: account, 291 + }; 292 } 293 + }), 294 ); 295 + const recordsFiltered = postsAcc.filter((postAcc) => 296 + postAcc.posts.length > 0 297 + ); 298 + const cutoffDate = getCutoffDate(recordsFiltered); 299 + const recordsCutoff = filterPostsByDate(recordsFiltered, cutoffDate); 300 + // update the accountMetadata with the new cursor 301 + accountsMetadata = recordsCutoff.map((postAcc) => postAcc.account); 302 + // throw the records in a big single array 303 + let records = recordsCutoff.flatMap((postAcc) => postAcc.posts); 304 + // sort the records by timestamp 305 + records = records.sort((a, b) => { 306 + const aDate = new Date( 307 + (a.value as AppBskyFeedPost.Record).createdAt, 308 + ).getTime(); 309 + const bDate = new Date( 310 + (b.value as AppBskyFeedPost.Record).createdAt, 311 + ).getTime(); 312 + return bDate - aDate; 313 + } 314 + ); 315 + // filter out posts that are in the future 316 + if (!Config.SHOW_FUTURE_POSTS) { 317 const now = Date.now(); 318 + records = records.filter((post) => { 319 + const postDate = new Date( 320 + (post.value as AppBskyFeedPost.Record).createdAt, 321 + ).getTime(); 322 + return postDate <= now; 323 + }); 324 } 325 + // append the new posts to the existing posts 326 + posts = posts.concat( 327 + records.map((record) => { 328 + const account = accountsMetadata.find( 329 + (account) => account.did == processAtUri(record.uri).repo, 330 + ); 331 + if (!account) { 332 + throw new Error(`Account with DID ${processAtUri(record.uri).repo} not found`); 333 + } 334 + return new Post(record, account); 335 + }), 336 + ); 337 + console.log("Fetched posts:", posts); 338 + return posts; 339 + }; 340 341 + const fetchPostsForUser = async (did: At.Did, cursor: string | null) => { 342 + try { 343 + const { data } = await rpc.get("com.atproto.repo.listRecords", { 344 + params: { 345 + repo: did as At.Identifier, 346 + collection: "app.bsky.feed.post", 347 + limit: Config.MAX_POSTS, 348 + cursor: cursor || undefined, 349 + }, 350 + }); 351 + return data.records as ComAtprotoRepoListRecords.Record[]; 352 + } catch (e) { 353 + console.error(`Error fetching posts for ${did}:`, e); 354 + return null; 355 + } 356 }; 357 + 358 + // const fetchAllPosts = async () => { 359 + // const users: AccountMetadata[] = await getAllMetadataFromPds(); 360 + // const postRecords = await Promise.all( 361 + // users.map( 362 + // async (metadata: AccountMetadata) => await fetchPosts(metadata.did), 363 + // ), 364 + // ); 365 + // // Filter out any records that have an error 366 + // const validPostRecords = postRecords.filter((record) => !record.error); 367 + 368 + // const posts: Post[] = validPostRecords.flatMap((userFetch) => 369 + // userFetch.records.map((record) => { 370 + // const user = users.find( 371 + // (user: AccountMetadata) => user.did == userFetch.did, 372 + // ); 373 + // if (!user) { 374 + // throw new Error(`User with DID ${userFetch.did} not found`); 375 + // } 376 + // return new Post(record, user); 377 + // }) 378 + // ); 379 + 380 + // posts.sort((a, b) => b.timestamp - a.timestamp); 381 + 382 + // if (!Config.SHOW_FUTURE_POSTS) { 383 + // // Filter out posts that are in the future 384 + // const now = Date.now(); 385 + // const filteredPosts = posts.filter((post) => post.timestamp <= now); 386 + // return filteredPosts.slice(0, Config.MAX_POSTS); 387 + // } 388 + 389 + // return posts.slice(0, Config.MAX_POSTS); 390 + // }; 391 + export { getAllMetadataFromPds, getNextPosts, Post, posts }; 392 export type { AccountMetadata };