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}