an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
1import * as ATPAPI from "@atproto/api"; 2import { 3 infiniteQueryOptions, 4 type QueryFunctionContext, 5 queryOptions, 6 useInfiniteQuery, 7 useQuery, 8 type UseQueryResult} from "@tanstack/react-query"; 9import { useAtom } from "jotai"; 10 11import { constellationURLAtom, slingshotURLAtom } from "./atoms"; 12 13export function constructIdentityQuery(didorhandle?: string, slingshoturl?: string) { 14 return queryOptions({ 15 queryKey: ["identity", didorhandle], 16 queryFn: async () => { 17 if (!didorhandle) return undefined as undefined 18 const res = await fetch( 19 `https://${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(didorhandle)}` 20 ); 21 if (!res.ok) throw new Error("Failed to fetch post"); 22 try { 23 return (await res.json()) as { 24 did: string; 25 handle: string; 26 pds: string; 27 signing_key: string; 28 }; 29 } catch (_e) { 30 return undefined; 31 } 32 }, 33 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 34 gcTime: /*0//*/5 * 60 * 1000, 35 }); 36} 37export function useQueryIdentity(didorhandle: string): UseQueryResult< 38 { 39 did: string; 40 handle: string; 41 pds: string; 42 signing_key: string; 43 }, 44 Error 45>; 46export function useQueryIdentity(): UseQueryResult< 47 undefined, 48 Error 49 > 50export function useQueryIdentity(didorhandle?: string): 51 UseQueryResult< 52 { 53 did: string; 54 handle: string; 55 pds: string; 56 signing_key: string; 57 } | undefined, 58 Error 59 > 60export function useQueryIdentity(didorhandle?: string) { 61 const [slingshoturl] = useAtom(slingshotURLAtom) 62 return useQuery(constructIdentityQuery(didorhandle, slingshoturl)); 63} 64 65export function constructPostQuery(uri?: string, slingshoturl?: string) { 66 return queryOptions({ 67 queryKey: ["post", uri], 68 queryFn: async () => { 69 if (!uri) return undefined as undefined 70 const res = await fetch( 71 `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 72 ); 73 let data: any; 74 try { 75 data = await res.json(); 76 } catch { 77 return undefined; 78 } 79 if (res.status === 400) return undefined; 80 if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) { 81 return undefined; // cache “not found” 82 } 83 try { 84 if (!res.ok) throw new Error("Failed to fetch post"); 85 return (data) as { 86 uri: string; 87 cid: string; 88 value: any; 89 }; 90 } catch (_e) { 91 return undefined; 92 } 93 }, 94 retry: (failureCount, error) => { 95 // dont retry 400 errors 96 if ((error as any)?.message?.includes("400")) return false; 97 return failureCount < 2; 98 }, 99 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 100 gcTime: /*0//*/5 * 60 * 1000, 101 }); 102} 103export function useQueryPost(uri: string): UseQueryResult< 104 { 105 uri: string; 106 cid: string; 107 value: ATPAPI.AppBskyFeedPost.Record; 108 }, 109 Error 110>; 111export function useQueryPost(): UseQueryResult< 112 undefined, 113 Error 114 > 115export function useQueryPost(uri?: string): 116 UseQueryResult< 117 { 118 uri: string; 119 cid: string; 120 value: ATPAPI.AppBskyFeedPost.Record; 121 } | undefined, 122 Error 123 > 124export function useQueryPost(uri?: string) { 125 const [slingshoturl] = useAtom(slingshotURLAtom) 126 return useQuery(constructPostQuery(uri, slingshoturl)); 127} 128 129export function constructProfileQuery(uri?: string, slingshoturl?: string) { 130 return queryOptions({ 131 queryKey: ["profile", uri], 132 queryFn: async () => { 133 if (!uri) return undefined as undefined 134 const res = await fetch( 135 `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 136 ); 137 let data: any; 138 try { 139 data = await res.json(); 140 } catch { 141 return undefined; 142 } 143 if (res.status === 400) return undefined; 144 if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) { 145 return undefined; // cache “not found” 146 } 147 try { 148 if (!res.ok) throw new Error("Failed to fetch post"); 149 return (data) as { 150 uri: string; 151 cid: string; 152 value: any; 153 }; 154 } catch (_e) { 155 return undefined; 156 } 157 }, 158 retry: (failureCount, error) => { 159 // dont retry 400 errors 160 if ((error as any)?.message?.includes("400")) return false; 161 return failureCount < 2; 162 }, 163 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 164 gcTime: /*0//*/5 * 60 * 1000, 165 }); 166} 167export function useQueryProfile(uri: string): UseQueryResult< 168 { 169 uri: string; 170 cid: string; 171 value: ATPAPI.AppBskyActorProfile.Record; 172 }, 173 Error 174>; 175export function useQueryProfile(): UseQueryResult< 176 undefined, 177 Error 178>; 179export function useQueryProfile(uri?: string): 180 UseQueryResult< 181 { 182 uri: string; 183 cid: string; 184 value: ATPAPI.AppBskyActorProfile.Record; 185 } | undefined, 186 Error 187 > 188export function useQueryProfile(uri?: string) { 189 const [slingshoturl] = useAtom(slingshotURLAtom) 190 return useQuery(constructProfileQuery(uri, slingshoturl)); 191} 192 193// export function constructConstellationQuery( 194// method: "/links", 195// target: string, 196// collection: string, 197// path: string, 198// cursor?: string 199// ): QueryOptions<linksRecordsResponse, Error>; 200// export function constructConstellationQuery( 201// method: "/links/distinct-dids", 202// target: string, 203// collection: string, 204// path: string, 205// cursor?: string 206// ): QueryOptions<linksDidsResponse, Error>; 207// export function constructConstellationQuery( 208// method: "/links/count", 209// target: string, 210// collection: string, 211// path: string, 212// cursor?: string 213// ): QueryOptions<linksCountResponse, Error>; 214// export function constructConstellationQuery( 215// method: "/links/count/distinct-dids", 216// target: string, 217// collection: string, 218// path: string, 219// cursor?: string 220// ): QueryOptions<linksCountResponse, Error>; 221// export function constructConstellationQuery( 222// method: "/links/all", 223// target: string 224// ): QueryOptions<linksAllResponse, Error>; 225export function constructConstellationQuery(query?:{ 226 constellation: string, 227 method: 228 | "/links" 229 | "/links/distinct-dids" 230 | "/links/count" 231 | "/links/count/distinct-dids" 232 | "/links/all" 233 | "undefined", 234 target: string, 235 collection?: string, 236 path?: string, 237 cursor?: string, 238 dids?: string[] 239} 240) { 241 // : QueryOptions< 242 // | linksRecordsResponse 243 // | linksDidsResponse 244 // | linksCountResponse 245 // | linksAllResponse 246 // | undefined, 247 // Error 248 // > 249 return queryOptions({ 250 queryKey: ["constellation", query?.method, query?.target, query?.collection, query?.path, query?.cursor, query?.dids] as const, 251 queryFn: async () => { 252 if (!query || query.method === "undefined") return undefined as undefined 253 const method = query.method 254 const target = query.target 255 const collection = query?.collection 256 const path = query?.path 257 const cursor = query.cursor 258 const dids = query?.dids 259 const res = await fetch( 260 `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${collection ? `&collection=${encodeURIComponent(collection)}` : ""}${path ? `&path=${encodeURIComponent(path)}` : ""}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}${dids ? dids.map((did) => `&did=${encodeURIComponent(did)}`).join("") : ""}` 261 ); 262 if (!res.ok) throw new Error("Failed to fetch post"); 263 try { 264 switch (method) { 265 case "/links": 266 return (await res.json()) as linksRecordsResponse; 267 case "/links/distinct-dids": 268 return (await res.json()) as linksDidsResponse; 269 case "/links/count": 270 return (await res.json()) as linksCountResponse; 271 case "/links/count/distinct-dids": 272 return (await res.json()) as linksCountResponse; 273 case "/links/all": 274 return (await res.json()) as linksAllResponse; 275 default: 276 return undefined; 277 } 278 } catch (_e) { 279 return undefined; 280 } 281 }, 282 // enforce short lifespan 283 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 284 gcTime: /*0//*/5 * 60 * 1000, 285 }); 286} 287export function useQueryConstellation(query: { 288 method: "/links"; 289 target: string; 290 collection: string; 291 path: string; 292 cursor?: string; 293 dids?: string[]; 294}): UseQueryResult<linksRecordsResponse, Error>; 295export function useQueryConstellation(query: { 296 method: "/links/distinct-dids"; 297 target: string; 298 collection: string; 299 path: string; 300 cursor?: string; 301}): UseQueryResult<linksDidsResponse, Error>; 302export function useQueryConstellation(query: { 303 method: "/links/count"; 304 target: string; 305 collection: string; 306 path: string; 307 cursor?: string; 308}): UseQueryResult<linksCountResponse, Error>; 309export function useQueryConstellation(query: { 310 method: "/links/count/distinct-dids"; 311 target: string; 312 collection: string; 313 path: string; 314 cursor?: string; 315}): UseQueryResult<linksCountResponse, Error>; 316export function useQueryConstellation(query: { 317 method: "/links/all"; 318 target: string; 319}): UseQueryResult<linksAllResponse, Error>; 320export function useQueryConstellation(): undefined; 321export function useQueryConstellation(query: { 322 method: "undefined"; 323 target: string; 324}): undefined; 325export function useQueryConstellation(query?: { 326 method: 327 | "/links" 328 | "/links/distinct-dids" 329 | "/links/count" 330 | "/links/count/distinct-dids" 331 | "/links/all" 332 | "undefined"; 333 target: string; 334 collection?: string; 335 path?: string; 336 cursor?: string; 337 dids?: string[]; 338}): 339 | UseQueryResult< 340 | linksRecordsResponse 341 | linksDidsResponse 342 | linksCountResponse 343 | linksAllResponse 344 | undefined, 345 Error 346 > 347 | undefined { 348 //if (!query) return; 349 const [constellationurl] = useAtom(constellationURLAtom) 350 return useQuery( 351 constructConstellationQuery(query && {constellation: constellationurl, ...query}) 352 ); 353} 354 355type linksRecord = { 356 did: string; 357 collection: string; 358 rkey: string; 359}; 360export type linksRecordsResponse = { 361 total: string; 362 linking_records: linksRecord[]; 363 cursor?: string; 364}; 365type linksDidsResponse = { 366 total: string; 367 linking_dids: string[]; 368 cursor?: string; 369}; 370type linksCountResponse = { 371 total: string; 372}; 373export type linksAllResponse = { 374 links: Record< 375 string, 376 Record< 377 string, 378 { 379 records: number; 380 distinct_dids: number; 381 } 382 > 383 >; 384}; 385 386export function constructFeedSkeletonQuery(options?: { 387 feedUri: string; 388 agent?: ATPAPI.Agent; 389 isAuthed: boolean; 390 pdsUrl?: string; 391 feedServiceDid?: string; 392}) { 393 return queryOptions({ 394 // The query key includes all dependencies to ensure it refetches when they change 395 queryKey: ["feedSkeleton", options?.feedUri, { isAuthed: options?.isAuthed, did: options?.agent?.did }], 396 queryFn: async () => { 397 if (!options) return undefined as undefined 398 const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options; 399 if (isAuthed) { 400 // Authenticated flow 401 if (!agent || !pdsUrl || !feedServiceDid) { 402 throw new Error("Missing required info for authenticated feed fetch."); 403 } 404 const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`; 405 const res = await agent.fetchHandler(url, { 406 method: "GET", 407 headers: { 408 "atproto-proxy": `${feedServiceDid}#bsky_fg`, 409 "Content-Type": "application/json", 410 }, 411 }); 412 if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`); 413 return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 414 } else { 415 // Unauthenticated flow (using a public PDS/AppView) 416 const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}`; 417 const res = await fetch(url); 418 if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`); 419 return (await res.json()) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 420 } 421 }, 422 //enabled: !!feedUri && (isAuthed ? !!agent && !!pdsUrl && !!feedServiceDid : true), 423 }); 424} 425 426export function useQueryFeedSkeleton(options?: { 427 feedUri: string; 428 agent?: ATPAPI.Agent; 429 isAuthed: boolean; 430 pdsUrl?: string; 431 feedServiceDid?: string; 432}) { 433 return useQuery(constructFeedSkeletonQuery(options)); 434} 435 436export function constructPreferencesQuery(agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined) { 437 return queryOptions({ 438 queryKey: ['preferences', agent?.did], 439 queryFn: async () => { 440 if (!agent || !pdsUrl) throw new Error("Agent or PDS URL not available"); 441 const url = `${pdsUrl}/xrpc/app.bsky.actor.getPreferences`; 442 const res = await agent.fetchHandler(url, { method: "GET" }); 443 if (!res.ok) throw new Error("Failed to fetch preferences"); 444 return res.json(); 445 }, 446 }); 447} 448export function useQueryPreferences(options: { 449 agent?: ATPAPI.Agent | undefined, pdsUrl?: string | undefined 450}) { 451 return useQuery(constructPreferencesQuery(options.agent, options.pdsUrl)); 452} 453 454 455 456export function constructArbitraryQuery(uri?: string, slingshoturl?: string) { 457 return queryOptions({ 458 queryKey: ["arbitrary", uri], 459 queryFn: async () => { 460 if (!uri) return undefined as undefined 461 const res = await fetch( 462 `https://${slingshoturl}/xrpc/com.bad-example.repo.getUriRecord?at_uri=${encodeURIComponent(uri)}` 463 ); 464 let data: any; 465 try { 466 data = await res.json(); 467 } catch { 468 return undefined; 469 } 470 if (res.status === 400) return undefined; 471 if (data?.error === "InvalidRequest" && data.message?.includes("Could not find repo")) { 472 return undefined; // cache “not found” 473 } 474 try { 475 if (!res.ok) throw new Error("Failed to fetch post"); 476 return (data) as { 477 uri: string; 478 cid: string; 479 value: any; 480 }; 481 } catch (_e) { 482 return undefined; 483 } 484 }, 485 retry: (failureCount, error) => { 486 // dont retry 400 errors 487 if ((error as any)?.message?.includes("400")) return false; 488 return failureCount < 2; 489 }, 490 staleTime: /*0,//*/ 5 * 60 * 1000, // 5 minutes 491 gcTime: /*0//*/5 * 60 * 1000, 492 }); 493} 494export function useQueryArbitrary(uri: string): UseQueryResult< 495 { 496 uri: string; 497 cid: string; 498 value: any; 499 }, 500 Error 501>; 502export function useQueryArbitrary(): UseQueryResult< 503 undefined, 504 Error 505>; 506export function useQueryArbitrary(uri?: string): UseQueryResult< 507 { 508 uri: string; 509 cid: string; 510 value: any; 511 } | undefined, 512 Error 513>; 514export function useQueryArbitrary(uri?: string) { 515 const [slingshoturl] = useAtom(slingshotURLAtom) 516 return useQuery(constructArbitraryQuery(uri, slingshoturl)); 517} 518 519export function constructFallbackNothingQuery(){ 520 return queryOptions({ 521 queryKey: ["nothing"], 522 queryFn: async () => { 523 return undefined 524 }, 525 }); 526} 527 528type ListRecordsResponse = { 529 cursor?: string; 530 records: { 531 uri: string; 532 cid: string; 533 value: ATPAPI.AppBskyFeedPost.Record; 534 }[]; 535}; 536 537export function constructAuthorFeedQuery(did: string, pdsUrl: string) { 538 return queryOptions({ 539 queryKey: ['authorFeed', did], 540 queryFn: async ({ pageParam }: QueryFunctionContext) => { 541 const limit = 25; 542 543 const cursor = pageParam as string | undefined; 544 const cursorParam = cursor ? `&cursor=${cursor}` : ''; 545 546 const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=app.bsky.feed.post&limit=${limit}${cursorParam}`; 547 548 const res = await fetch(url); 549 if (!res.ok) throw new Error("Failed to fetch author's posts"); 550 551 return res.json() as Promise<ListRecordsResponse>; 552 }, 553 }); 554} 555 556export function useInfiniteQueryAuthorFeed(did: string | undefined, pdsUrl: string | undefined) { 557 const { queryKey, queryFn } = constructAuthorFeedQuery(did!, pdsUrl!); 558 559 return useInfiniteQuery({ 560 queryKey, 561 queryFn, 562 initialPageParam: undefined as never, // ???? what is this shit 563 getNextPageParam: (lastPage) => lastPage.cursor as null | undefined, 564 enabled: !!did && !!pdsUrl, 565 }); 566} 567 568export const ATURI_PAGE_LIMIT = 100; 569 570export interface AturiDirectoryAturisItem { 571 uri: string; 572 cid: string; 573 rkey: string; 574} 575 576export type AturiDirectoryAturis = AturiDirectoryAturisItem[]; 577 578export function constructAturiListQuery(aturilistservice: string, did: string, collection: string, reverse?: boolean) { 579 return queryOptions({ 580 // A unique key for this query, including all parameters that affect the data. 581 queryKey: ["aturiList", did, collection, { reverse }], 582 583 // The function that fetches the data. 584 queryFn: async ({ pageParam }: QueryFunctionContext) => { 585 const cursor = pageParam as string | undefined; 586 587 // Use URLSearchParams for safe and clean URL construction. 588 const params = new URLSearchParams({ 589 did, 590 collection, 591 }); 592 593 if (cursor) { 594 params.set("cursor", cursor); 595 } 596 597 // Add the reverse parameter if it's true 598 if (reverse) { 599 params.set("reverse", "true"); 600 } 601 602 const url = `https://${aturilistservice}/aturis?${params.toString()}`; 603 604 const res = await fetch(url); 605 if (!res.ok) { 606 // You can add more specific error handling here 607 throw new Error(`Failed to fetch AT-URI list for ${did}`); 608 } 609 610 return res.json() as Promise<AturiDirectoryAturis>; 611 }, 612 }); 613} 614 615export function useInfiniteQueryAturiList({aturilistservice, did, collection, reverse}:{aturilistservice: string, did: string | undefined, collection: string | undefined, reverse?: boolean}) { 616 // We only enable the query if both `did` and `collection` are provided. 617 const isEnabled = !!did && !!collection; 618 619 const { queryKey, queryFn } = constructAturiListQuery(aturilistservice, did!, collection!, reverse); 620 621 return useInfiniteQuery({ 622 queryKey, 623 queryFn, 624 initialPageParam: undefined as never, // ???? what is this shit 625 626 // @ts-expect-error i wouldve used as null | undefined, anyways 627 getNextPageParam: (lastPage: AturiDirectoryAturis) => { 628 // If the last page returned no records, we're at the end. 629 if (!lastPage || lastPage.length === 0) { 630 return undefined; 631 } 632 633 // If the number of records is less than our page limit, it must be the last page. 634 if (lastPage.length < ATURI_PAGE_LIMIT) { 635 return undefined; 636 } 637 638 // The cursor for the next page is the `rkey` of the last item we received. 639 const lastItem = lastPage[lastPage.length - 1]; 640 return lastItem.rkey; 641 }, 642 643 enabled: isEnabled, 644 }); 645} 646 647 648type FeedSkeletonPage = ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 649 650export function constructInfiniteFeedSkeletonQuery(options: { 651 feedUri: string; 652 agent?: ATPAPI.Agent; 653 isAuthed: boolean; 654 pdsUrl?: string; 655 feedServiceDid?: string; 656}) { 657 const { feedUri, agent, isAuthed, pdsUrl, feedServiceDid } = options; 658 659 return queryOptions({ 660 queryKey: ["feedSkeleton", feedUri, { isAuthed, did: agent?.did }], 661 662 queryFn: async ({ pageParam }: QueryFunctionContext): Promise<FeedSkeletonPage> => { 663 const cursorParam = pageParam ? `&cursor=${pageParam}` : ""; 664 665 if (isAuthed) { 666 if (!agent || !pdsUrl || !feedServiceDid) { 667 throw new Error("Missing required info for authenticated feed fetch."); 668 } 669 const url = `${pdsUrl}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`; 670 const res = await agent.fetchHandler(url, { 671 method: "GET", 672 headers: { 673 "atproto-proxy": `${feedServiceDid}#bsky_fg`, 674 "Content-Type": "application/json", 675 }, 676 }); 677 if (!res.ok) throw new Error(`Authenticated feed fetch failed: ${res.statusText}`); 678 return (await res.json()) as FeedSkeletonPage; 679 } else { 680 const url = `https://discover.bsky.app/xrpc/app.bsky.feed.getFeedSkeleton?feed=${encodeURIComponent(feedUri)}${cursorParam}`; 681 const res = await fetch(url); 682 if (!res.ok) throw new Error(`Public feed fetch failed: ${res.statusText}`); 683 return (await res.json()) as FeedSkeletonPage; 684 } 685 }, 686 }); 687} 688 689export function useInfiniteQueryFeedSkeleton(options: { 690 feedUri: string; 691 agent?: ATPAPI.Agent; 692 isAuthed: boolean; 693 pdsUrl?: string; 694 feedServiceDid?: string; 695}) { 696 const { queryKey, queryFn } = constructInfiniteFeedSkeletonQuery(options); 697 698 return {...useInfiniteQuery({ 699 queryKey, 700 queryFn, 701 initialPageParam: undefined as never, 702 getNextPageParam: (lastPage) => lastPage.cursor as null | undefined, 703 staleTime: Infinity, 704 refetchOnWindowFocus: false, 705 enabled: !!options.feedUri && (options.isAuthed ? !!options.agent && !!options.pdsUrl && !!options.feedServiceDid : true), 706 }), queryKey: queryKey}; 707} 708 709 710export function yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks(query?: { 711 constellation: string, 712 method: '/links' 713 target?: string 714 collection: string 715 path: string 716}) { 717 console.log( 718 'yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks', 719 query, 720 ) 721 722 return infiniteQueryOptions({ 723 enabled: !!query?.target, 724 queryKey: [ 725 'reddwarf_constellation', 726 query?.method, 727 query?.target, 728 query?.collection, 729 query?.path, 730 ] as const, 731 732 queryFn: async ({pageParam}: {pageParam?: string}) => { 733 if (!query || !query?.target) return undefined 734 735 const method = query.method 736 const target = query.target 737 const collection = query.collection 738 const path = query.path 739 const cursor = pageParam 740 741 const res = await fetch( 742 `https://${query.constellation}${method}?target=${encodeURIComponent(target)}${ 743 collection ? `&collection=${encodeURIComponent(collection)}` : '' 744 }${path ? `&path=${encodeURIComponent(path)}` : ''}${ 745 cursor ? `&cursor=${encodeURIComponent(cursor)}` : '' 746 }`, 747 ) 748 749 if (!res.ok) throw new Error('Failed to fetch') 750 751 return (await res.json()) as linksRecordsResponse 752 }, 753 754 getNextPageParam: lastPage => { 755 return (lastPage as any)?.cursor ?? undefined 756 }, 757 initialPageParam: undefined, 758 staleTime: 5 * 60 * 1000, 759 gcTime: 5 * 60 * 1000, 760 }) 761}