From 8562ff73eb8caf33dbe3e7e65842c4d0c7c37722 Mon Sep 17 00:00:00 2001 From: "whey.party" Date: Thu, 12 Feb 2026 10:37:46 +0700 Subject: [PATCH] AppView-first initial post fetching & polls handling --- README.md | 8 +- policy.ts | 8 +- src/components/PollComponents.tsx | 107 ++++++--- src/components/PostEmbeds.tsx | 22 +- src/components/UniversalPostRenderer.tsx | 239 ++++++++++++++++++-- src/providers/PollMutationQueueProvider.tsx | 9 + src/routes/about.tsx | 5 +- src/routes/settings.tsx | 21 ++ src/utils/atoms.ts | 10 + src/utils/useQuery.ts | 182 +++++++++++++++ 10 files changed, 550 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index a7e3e7e..667b066 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # Red Dwarf -Red Dwarf is a Bluesky client that does not use any AppView servers, instead it gathers the data from [Constellation](https://constellation.microcosm.blue/) and each users' PDS. +Red Dwarf is a Bluesky client that does not depend on Bluesky’s AppView servers. + +It preserves authoritative independence by fetching records directly from each user’s PDS (via [Slingshot](https://slingshot.microcosm.blue/)) +and reconstructing relationships through backlinks (via [Constellation](https://constellation.microcosm.blue/)), +while optionally using AppView as an optimization layer when available. ![screenshot of red dwarf](/public/screenshot.jpg) huge thanks to [Microcosm](https://microcosm.blue/) for making this possible -issue tracker kanban board: [https://github.com/users/rimar1337/projects/1/views/1]https://github.com/users/rimar1337/projects/1/views/1 +issue tracker kanban board: [currently on GitHub Projects](https://github.com/users/rimar1337/projects/1/views/1) ## running dev and build in the `vite.config.ts` file you should change these values diff --git a/policy.ts b/policy.ts index 09a52d4..91baf0e 100644 --- a/policy.ts +++ b/policy.ts @@ -35,9 +35,11 @@ This hosted instance mandates Bluesky Moderation to limit moderation scope and k ## About Red Dwarf -Red Dwarf is a Bluesky client that does not rely on Bluesky API App Servers. -Instead, it uses Microcosm to fetch records directly from each user’s PDS (via Slingshot) -and connect them using backlinks (via Constellation). +Red Dwarf is a Bluesky client that does not depend on Bluesky’s AppView servers. + +It preserves authoritative independence by fetching records directly from each user’s PDS (via Slingshot) +and reconstructing relationships through backlinks (via Constellation), +while optionally using AppView as an optimization layer when available. ## Hosting Your Own Instance diff --git a/src/components/PollComponents.tsx b/src/components/PollComponents.tsx index 7694ca0..efa392a 100644 --- a/src/components/PollComponents.tsx +++ b/src/components/PollComponents.tsx @@ -1,3 +1,4 @@ +//import * as ATPAPI from "@atproto/api" import { useAtom } from "jotai"; import * as React from "react"; @@ -5,16 +6,30 @@ import { usePollData, usePollMutationQueue, } from "~/providers/PollMutationQueueProvider"; -import { useAuth } from "~/providers/UnifiedAuthProvider"; +//import { useAuth } from "~/providers/UnifiedAuthProvider"; import { renderSnack } from "~/routes/__root"; import { imgCDNAtom } from "~/utils/atoms"; import { useQueryArbitrary, useQueryConstellation, useQueryProfile } from "~/utils/useQuery"; -export function PollEmbed({ did, rkey }: { did: string; rkey: string }) { - const { agent } = useAuth(); +import { type embedtryfall } from "./PostEmbeds"; +import { ExternalLinkEmbed } from "./PostEmbeds"; + +export function PollEmbed({ + did, + rkey, + redactedLoading, + embedtryfall +}: { + did: string; + rkey: string; + redactedLoading?: boolean; + embedtryfall?: embedtryfall; +}) { + //const { agent } = useAuth(); const { refreshPollData } = usePollMutationQueue(); const pollUri = `at://${did}/app.reddwarf.embed.poll/${rkey}`; const { data: pollRecord, isLoading, error } = useQueryArbitrary(pollUri); + const dontLoadPolls = embedtryfall && (isLoading || pollRecord === undefined || error !== null) || false const { data: voteCountsA } = useQueryConstellation({ method: "/links/count/distinct-dids", @@ -22,6 +37,7 @@ export function PollEmbed({ did, rkey }: { did: string; rkey: string }) { collection: "app.reddwarf.poll.vote.a", path: ".subject.uri", customkey: "constellation-polls", + enabled: !dontLoadPolls }); const { data: voteCountsB } = useQueryConstellation({ @@ -30,6 +46,7 @@ export function PollEmbed({ did, rkey }: { did: string; rkey: string }) { collection: "app.reddwarf.poll.vote.b", path: ".subject.uri", customkey: "constellation-polls", + enabled: !dontLoadPolls }); const { data: voteCountsC } = useQueryConstellation({ @@ -38,6 +55,7 @@ export function PollEmbed({ did, rkey }: { did: string; rkey: string }) { collection: "app.reddwarf.poll.vote.c", path: ".subject.uri", customkey: "constellation-polls", + enabled: !dontLoadPolls }); const { data: voteCountsD } = useQueryConstellation({ @@ -46,36 +64,41 @@ export function PollEmbed({ did, rkey }: { did: string; rkey: string }) { collection: "app.reddwarf.poll.vote.d", path: ".subject.uri", customkey: "constellation-polls", + enabled: !dontLoadPolls }); - const { data: votersA } = useQueryConstellation({ - method: "/links", - target: pollUri, - collection: "app.reddwarf.poll.vote.a", - path: ".subject.uri", - customkey: "constellation-polls", - }); - const { data: votersB } = useQueryConstellation({ - method: "/links", - target: pollUri, - collection: "app.reddwarf.poll.vote.b", - path: ".subject.uri", - customkey: "constellation-polls", - }); - const { data: votersC } = useQueryConstellation({ - method: "/links", - target: pollUri, - collection: "app.reddwarf.poll.vote.c", - path: ".subject.uri", - customkey: "constellation-polls", - }); - const { data: votersD } = useQueryConstellation({ - method: "/links", - target: pollUri, - collection: "app.reddwarf.poll.vote.d", - path: ".subject.uri", - customkey: "constellation-polls", - }); + // const { data: votersA } = useQueryConstellation({ + // method: "/links", + // target: pollUri, + // collection: "app.reddwarf.poll.vote.a", + // path: ".subject.uri", + // customkey: "constellation-polls", + // enabled: !isLoading + // }); + // const { data: votersB } = useQueryConstellation({ + // method: "/links", + // target: pollUri, + // collection: "app.reddwarf.poll.vote.b", + // path: ".subject.uri", + // customkey: "constellation-polls", + // enabled: !isLoading + // }); + // const { data: votersC } = useQueryConstellation({ + // method: "/links", + // target: pollUri, + // collection: "app.reddwarf.poll.vote.c", + // path: ".subject.uri", + // customkey: "constellation-polls", + // enabled: !isLoading + // }); + // const { data: votersD } = useQueryConstellation({ + // method: "/links", + // target: pollUri, + // collection: "app.reddwarf.poll.vote.d", + // path: ".subject.uri", + // customkey: "constellation-polls", + // enabled: !isLoading + // }); const poll = { ...(pollRecord?.value ?? {}), @@ -99,14 +122,28 @@ export function PollEmbed({ did, rkey }: { did: string; rkey: string }) { d: parseInt((voteCountsD as any)?.total || "0"), }; - const { results, totalVotes, handleVote } = usePollData( + const { results, totalVotes, handleVote, votersA, votersB, votersC, votersD } = usePollData( pollUri, pollRecord?.cid, !!poll.multiple, serverCounts, + !dontLoadPolls ); - - if (isLoading) { + if (dontLoadPolls && embedtryfall) { + const link = embedtryfall.embed.external; + const onOpen = embedtryfall.onOpen + return ( + <> + {/* pass thru confirm
+ embedtryfall = {JSON.stringify(embedtryfall, null, 2)}
+ isLoading = {JSON.stringify(isLoading, null, 2)}
+ pollRecord = {JSON.stringify(pollRecord, null, 2)}
+ error = {JSON.stringify(error, null, 2)}
*/} + + + ) + } + if (isLoading && !embedtryfall) { return (
@@ -128,7 +165,7 @@ export function PollEmbed({ did, rkey }: { did: string; rkey: string }) { return ( <> -
+
diff --git a/src/components/PostEmbeds.tsx b/src/components/PostEmbeds.tsx index 015ef27..3b8b726 100644 --- a/src/components/PostEmbeds.tsx +++ b/src/components/PostEmbeds.tsx @@ -1,3 +1,4 @@ +import * as ATPAPI from "@atproto/api" import { AppBskyEmbedDefs, AppBskyEmbedExternal, @@ -55,7 +56,8 @@ export function PostEmbeds({ nopics, lightboxCallback, constellationLinks, - redactedLoading + redactedLoading, + referral }: { embed?: Embed; moderation?: ModerationDecision; @@ -69,6 +71,7 @@ export function PostEmbeds({ lightboxCallback?: (d: LightboxProps) => void; constellationLinks?: any; redactedLoading?: boolean; + referral?: string[]; }) { function setLightboxIndex(number: number) { navigate({ @@ -548,19 +551,20 @@ export function PostEmbeds({ if (AppBskyEmbedExternal.isView(embed)) { const pollLinks = constellationLinks?.links?.["app.reddwarf.embed.poll"]; const hasPollLink = pollLinks && Object.keys(pollLinks).length > 0; + const isfromappview = referral?.includes("appview") - if (hasPollLink && postid) { + if ((hasPollLink || isfromappview) && postid) { // warning: i gave up and warpped it in a div lmao return (
- +
); } const link = embed.external; return ( - + ); } @@ -579,18 +583,26 @@ export function PostEmbeds({ return
; } +export type embedtryfall = { + embed: ATPAPI.AppBskyEmbedExternal.View, + onOpen?: () => void; +} export function ExternalLinkEmbed({ link, onOpen, style, - redactedLoading + redactedLoading, + referral }: { link: AppBskyEmbedExternal.ViewExternal; onOpen?: () => void; style?: React.CSSProperties; redactedLoading?: boolean; + referral?: string[]; }) { + //const fromappview = referral?.includes("appview") + //const [] const { uri, title, description, thumb } = link; const thumbAspectRatio = 1.91; diff --git a/src/components/UniversalPostRenderer.tsx b/src/components/UniversalPostRenderer.tsx index 3b7360f..29658aa 100644 --- a/src/components/UniversalPostRenderer.tsx +++ b/src/components/UniversalPostRenderer.tsx @@ -27,8 +27,10 @@ import { FollowButton, getLocaleLabel, type LabelWithHydratedLocaleName, Mutual import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; //import type { ContentLabel } from "~/types/moderation"; import { + appviewUrlAtom, composerAtom, constellationURLAtom, + enableAppViewAtom, enableBridgyTextAtom, enableWafrnTextAtom, imgCDNAtom, @@ -41,6 +43,7 @@ import { useQueryIdentity, useQueryPost, useQueryProfile, + useQuerySingularAVPostQuery, yknowIReallyHateThisButWhateverGuardedConstructConstellationInfiniteQueryLinks, } from "~/utils/useQuery"; @@ -96,6 +99,209 @@ export function UniversalPostRendererATURILoader({ filterNoReplies, filterMustHaveMedia, filterMustBeReply, +}: UniversalPostRendererATURILoaderProps) { + const [usesAV] = useAtom(enableAppViewAtom); + if (usesAV) { + return ( + + ) + } + return ( + + ) +} +/* + todo: + - either + - put constellation based reply threading or + - use a getPostThreadV2 once for quick reply threadings (the post thread page always + fetches replies via constellation for complteness) + - do the profile pages too + */ +export function UniversalPostRendererATURILoader_AppView({ + atUri, + onConstellation, + detailed = false, + bottomReplyLine, + topReplyLine, + bottomBorder = true, + feedviewpost = false, + repostedby, + style, + ref, + dataIndexPropPass, + nopics, + concise, + lightboxCallback, + maxReplies, + isQuote, + filterNoReplies, + filterMustHaveMedia, + filterMustBeReply, +}: UniversalPostRendererATURILoaderProps) { + const [avurl] = useAtom(appviewUrlAtom); + const navigate = useNavigate(); + const parsedaturi = new AtUri(atUri); + + const { data, isLoading, isEnabled, isError, error } = useQuerySingularAVPostQuery({ aturi: atUri, avurl: avurl }); + + + const thereply = (data?.record as AppBskyFeedPost.Record)?.reply?.parent + ?.uri; + const feedviewpostreplydid = + thereply && !filterNoReplies ? new AtUri(thereply).host : undefined; + const replyhookvalue = useQueryIdentity( + feedviewpost ? feedviewpostreplydid : undefined, + ); + const feedviewpostreplyhandle = replyhookvalue?.data?.handle; + + const aturirepostbydid = repostedby ? new AtUri(repostedby).host : undefined; + const repostedbyhookvalue = useQueryIdentity( + repostedby ? aturirepostbydid : undefined, + ); + const feedviewpostrepostedbyhandle = repostedbyhookvalue?.data?.handle; + if (!isLoading && data === undefined) { + return ( + + ) + } + return ( + + parsedaturi && + navigate({ + to: "/profile/$did/post/$rkey", + params: { did: parsedaturi.host, rkey: parsedaturi.rkey }, + }) + } + onProfileClick={(e) => { + e.stopPropagation(); + if (parsedaturi) { + navigate({ + to: "/profile/$did", + params: { did: parsedaturi.host }, + }); + } + }} + post={data || { + uri: atUri, + cid: atUri, + author: { + did: parsedaturi.host, + handle: parsedaturi.host, + }, + record: {}, + indexedAt: "", + }} // todo: this is bad. just make it so that UPR allows missing data + uprrrsauthor={{ + ...(data?.author || + { + did: parsedaturi.host, + handle: parsedaturi.host, + }), + "$type": "app.bsky.actor.defs#profileViewDetailed", + }} + salt={atUri} + bottomReplyLine={bottomReplyLine} + topReplyLine={topReplyLine} + bottomBorder={bottomBorder} + feedviewpost={feedviewpost} + feedviewpostreplyhandle={feedviewpostreplyhandle} + repostedby={feedviewpostrepostedbyhandle} + style={style} + ref={ref} + dataIndexPropPass={dataIndexPropPass} + nopics={nopics} + concise={concise} + lightboxCallback={lightboxCallback} + maxReplies={maxReplies} + isQuote={isQuote} + constellationLinks={{}} + /> + ) +} +export function UniversalPostRendererATURILoader_Microcosm({ + atUri, + onConstellation, + detailed = false, + bottomReplyLine, + topReplyLine, + bottomBorder = true, + feedviewpost = false, + repostedby, + style, + ref, + dataIndexPropPass, + nopics, + concise, + lightboxCallback, + maxReplies, + isQuote, + filterNoReplies, + filterMustHaveMedia, + filterMustBeReply, }: UniversalPostRendererATURILoaderProps) { const TEMPLINEAR = true; const parsed = new AtUri(atUri); @@ -607,6 +813,7 @@ export function UniversalPostRenderer({ lightboxCallback, maxReplies, constellationLinks, + referral, }: { post: AppBskyFeedDefs.PostView; uprrrsauthor?: AppBskyActorDefs.ProfileViewDetailed; @@ -631,6 +838,7 @@ export function UniversalPostRenderer({ lightboxCallback?: (d: LightboxProps) => void; maxReplies?: number; constellationLinks?: any; + referral?: string[]; }) { // todo move moderation to one of the UniversalPostRenderer wrapper components, and not the pure renderer component. please. thanks @@ -929,22 +1137,22 @@ export function UniversalPostRenderer({ return null // if feed view post then moderated post isnt important and just remove it from view } return ( -
{ - setMainItem({ post: post }); - onPostClick(e); - } - : () => { - setMainItem({ post: post }); - } - : undefined - }> + : setMainItem + ? onPostClick + ? (e) => { + setMainItem({ post: post }); + onPostClick(e); + } + : () => { + setMainItem({ post: post }); + } + : undefined + }>
) : null} {post.embed && depth > 0 && ( diff --git a/src/providers/PollMutationQueueProvider.tsx b/src/providers/PollMutationQueueProvider.tsx index 694ccc4..26e0722 100644 --- a/src/providers/PollMutationQueueProvider.tsx +++ b/src/providers/PollMutationQueueProvider.tsx @@ -355,6 +355,7 @@ export function usePollData( pollCid: string | undefined, isMultiple: boolean, serverCounts: { a: number; b: number; c: number; d: number }, + enabled?: boolean ) { const { agent } = useAuth(); const myDid = agent?.did; @@ -371,6 +372,7 @@ export function usePollData( collection: "app.reddwarf.poll.vote.a", path: ".subject.uri", customkey: "constellation-polls", + enabled: enabled, }); const { data: votersB } = useQueryConstellation({ method: "/links", @@ -378,6 +380,7 @@ export function usePollData( collection: "app.reddwarf.poll.vote.b", path: ".subject.uri", customkey: "constellation-polls", + enabled: enabled, }); const { data: votersC } = useQueryConstellation({ method: "/links", @@ -385,6 +388,7 @@ export function usePollData( collection: "app.reddwarf.poll.vote.c", path: ".subject.uri", customkey: "constellation-polls", + enabled: enabled, }); const { data: votersD } = useQueryConstellation({ method: "/links", @@ -392,6 +396,7 @@ export function usePollData( collection: "app.reddwarf.poll.vote.d", path: ".subject.uri", customkey: "constellation-polls", + enabled: enabled, }); const handleVote = useCallback( @@ -480,6 +485,10 @@ export function usePollData( stateD.hasVoted, totalVotes: stateA.count + stateB.count + stateC.count + stateD.count, handleVote, + votersA, + votersB, + votersC, + votersD }; }, [ localVotes, diff --git a/src/routes/about.tsx b/src/routes/about.tsx index 5e89150..6cea2de 100644 --- a/src/routes/about.tsx +++ b/src/routes/about.tsx @@ -2,7 +2,7 @@ import { createFileRoute } from '@tanstack/react-router' import { FORCED_LABELER_DIDS, HOST_ABOUT_MARKDOWN, HOST_ADMIN, HOST_DESCRIPTION, HOST_HERO, HOST_LABELMERGE, HOST_SIGNUP_PDS } from '~/../policy'; import { Header } from '~/components/Header'; -import { defaultconstellationURL, defaultImgCDN, defaultLycanURL, defaultslingshotURL, defaultVideoCDN } from '~/utils/atoms'; +import { defaultAppviewURL, defaultconstellationURL, defaultImgCDN, defaultLycanURL, defaultslingshotURL, defaultVideoCDN } from '~/utils/atoms'; import { ProfileSmall } from './__root'; import { NotificationItem } from './notifications'; @@ -219,6 +219,9 @@ function PolicyRenderer(){ Lycan (Personal Search): {defaultLycanURL || "not set"} + + AppView (Bluesky Index): + {defaultAppviewURL || "not set"}
{/* {hostmandate && ()} */} diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx index cb9a98d..5afcb71 100644 --- a/src/routes/settings.tsx +++ b/src/routes/settings.tsx @@ -8,13 +8,16 @@ import { Header } from "~/components/Header"; import Login from "~/components/Login"; import { useAuth } from "~/providers/UnifiedAuthProvider"; import { + appviewUrlAtom, constellationURLAtom, + defaultAppviewURL, defaultconstellationURL, defaulthue, defaultImgCDN, defaultLycanURL, defaultslingshotURL, defaultVideoCDN, + enableAppViewAtom, enableBitesAtom, enableBridgyTextAtom, enableWafrnTextAtom, @@ -34,6 +37,7 @@ export const Route = createFileRoute("/settings")({ export function Settings() { const navigate = useNavigate(); const { agent } = useAuth(); + const [isAppViewEnabled] = useAtom(enableAppViewAtom); return ( <>
+
+ +
+
+ +

Notice: Please restart/refresh the app if changes arent applying correctly

+
); } diff --git a/src/utils/atoms.ts b/src/utils/atoms.ts index 3967b5e..adbe6b5 100644 --- a/src/utils/atoms.ts +++ b/src/utils/atoms.ts @@ -160,6 +160,16 @@ export const enableWafrnTextAtom = atomWithStorage( false ); +export const enableAppViewAtom = atomWithStorage( + "enableAppViewAtom", + true +); +export const defaultAppviewURL = "https://api.bsky.app"; +export const appviewUrlAtom = atomWithStorage( + "AppviewUrl", + defaultAppviewURL +); + // polls state diff --git a/src/utils/useQuery.ts b/src/utils/useQuery.ts index c0b7130..d3f0227 100644 --- a/src/utils/useQuery.ts +++ b/src/utils/useQuery.ts @@ -250,6 +250,7 @@ export function constructConstellationQuery(query?: { cursor?: string; dids?: string[]; customkey?: string; + enabled?: boolean; }) { // : QueryOptions< // | linksRecordsResponse @@ -260,6 +261,7 @@ export function constructConstellationQuery(query?: { // Error // > return queryOptions({ + enabled: query?.enabled, queryKey: [ "constellation", query?.method, @@ -335,6 +337,7 @@ export function useQueryConstellation(query: { cursor?: string; dids?: string[]; customkey?: string; + enabled?: boolean; }): UseQueryResult; export function useQueryConstellation(query: { method: "/links/distinct-dids"; @@ -343,6 +346,7 @@ export function useQueryConstellation(query: { path: string; cursor?: string; customkey?: string; + enabled?: boolean; }): UseQueryResult; export function useQueryConstellation(query: { method: "/links/count"; @@ -351,6 +355,7 @@ export function useQueryConstellation(query: { path: string; cursor?: string; customkey?: string; + enabled?: boolean; }): UseQueryResult; export function useQueryConstellation(query: { method: "/links/count/distinct-dids"; @@ -359,17 +364,20 @@ export function useQueryConstellation(query: { path: string; cursor?: string; customkey?: string; + enabled?: boolean; }): UseQueryResult; export function useQueryConstellation(query: { method: "/links/all"; target: string; customkey?: string; + enabled?: boolean; }): UseQueryResult; export function useQueryConstellation(): undefined; export function useQueryConstellation(query: { method: "undefined"; target: string; customkey?: string; + enabled?: boolean; }): undefined; export function useQueryConstellation(query?: { method: @@ -385,6 +393,7 @@ export function useQueryConstellation(query?: { cursor?: string; dids?: string[]; customkey?: string; + enabled?: boolean; }): | UseQueryResult< | linksRecordsResponse @@ -1384,3 +1393,176 @@ export function constructSingularLabelQuery(options: SingularLabelQuery) { export function useQuerySingularLabelQuery(options: SingularLabelQuery) { return useQuery(constructSingularLabelQuery(options)); } + + +type SingularAVPostQuery = { + aturi: string, + avurl: string, +} +type SingularAVPostResult = ATPAPI.AppBskyFeedDefs.PostView + +type AVPostQueryPostsQueryParams = { + aturis: string[], + avurl: string, +} + +const MAX_URIS_PER_REQUEST = 25; +function chunk(arr: T[], size: number): T[][] { + const result: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)); + } + return result; +} + + +export async function innerAVPostsQueryFn( + options: AVPostQueryPostsQueryParams +): Promise { + const { aturis, avurl } = options; + + if (!aturis?.length) return undefined; + + const batches = chunk(aturis, MAX_URIS_PER_REQUEST); + + const responses = await Promise.all( + batches.map(async (batch) => { + const params = new URLSearchParams(); + batch.forEach((uri) => params.append("uris", uri)); + + const url = `${avurl}/xrpc/app.bsky.feed.getPosts?${params.toString()}`; + + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Labelmerge fetch failed: ${res.status} ${res.statusText}`); + } + + return (await res.json()) as ATPAPI.AppBskyFeedGetPosts.OutputSchema; + }) + ); + + // Merge all posts into one response + const merged: ATPAPI.AppBskyFeedGetPosts.OutputSchema = { + posts: responses.flatMap((r) => r.posts ?? []), + }; + + return merged; +} + +const postquerymerge = create( + /*[], SingularLabelQuery>*/ { + // The fetcher resolves the list of queries(here just a list of user ids as number) to one single api call. + fetcher: async (savpqa: SingularAVPostQuery[]) => { + // Use a shared QueryClient if possible; creating a new one per fetch is usually not needed + + // Deduplicate, but don’t sort + const sarr = Array.from(new Set(savpqa.map((savpq) => savpq.aturi))); + + //const result = await batShitQueryClient.fetchQuery( + // constructLabelMergeQuery({ s: sarr, l: larr }), + //); + const result = await innerAVPostsQueryFn({aturis: sarr, avurl: savpqa.at(-1)?.avurl || savpqa[0].avurl}) + //const qfn = constructLabelMergeQuery({ s: sarr, l: larr }).queryFn + //const result = await (qfn ? qfn() : ()=>{}) + if (!result) return []; + + // Build maps for quick lookup + //const errmap = new Map(); + // const resmap = new Map(); + + // result.posts?.forEach((post) => resmap.set(post.uri, post)); + + // // Map back to the original queries + // const output: Record[] = savpqa.map((savpq) => { + // const key = savpq.aturi; // or just slq.l if you prefer + + // //const err = errmap.get(slq.l); + // const post = resmap.get(key); + + // //if (err) return { [key]: { error: err } }; + // if (post) return { [key]: { labels: label } }; + + // // if result is neither, it means the subject is free of labels + // return { + // [key]: { labels: undefined} + // }; + // // idiot + // // return { + // // [key]: { error: { + // // s: slq.l, + // // e: `!internal-bslm-unknown: ${slq.s}` + // // }} + // // }; + // }); + const output = result.posts; + + return output; + }, + // when we call users.fetch, this will resolve the correct user using the field `id` + resolver: (rslra, savpq) => { + if (rslra.length < 1) { + return undefined; + } + // const result: SingularLabelResult | undefined = slra.find((slr, i) => { + // // find if error first + // const error = slr.error; + // const label = slr.labels; + // if (error) { + // if (slq.l === error.s) { + // return slq; + // } + // } else if (label) { + // // if not error + // if (slq.l === label.src && slq.s === label.uri) { + // return slq; + // } + // // else unhandled not found + // } else { + // return undefined; + // } + // return undefined; + // }); + //const outputMap: Record = Object.assign({}, ...rslra) + //const key = `${slq.l}::${slq.s}`; // or just slq.l if you prefer + const item = rslra.find(obj => obj.uri === savpq.aturi); + const result: SingularAVPostResult | undefined = item//outputMap[key] + return result; + }, + scheduler: windowScheduler(10 * 100), // 1 second + }, +); + +export function constructSingularAVPostQuery(options: SingularAVPostQuery) { + const { aturi, avurl } = options; + + return queryOptions({ + queryKey: ["__volatile","savpq", aturi], + + enabled: !!aturi && !!avurl, + + queryFn: async (): Promise => { + // const result = (await labelmerge.fetch(options).catch(err => {throw { error: err } as SingularLabelResult})) as SingularLabelResult + // if (result.error) { + // throw result.error + // } + // return result; + const result = (await postquerymerge + .fetch(options))as SingularAVPostResult; + // .catch( + // (err) => ({ error: err }) as SingularAVPostResult, + // )) as SingularAVPostResult; + + if (result === undefined) { + throw new Error("what the hell happened") + } + return result; + }, + + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 5 * 60 * 1000, + }); +} + +export function useQuerySingularAVPostQuery(options: SingularAVPostQuery) { + return useQuery(constructSingularAVPostQuery(options)); +} -- 2.51.2