Skip to content

Commit 2dba354

Browse files
committed
initial useQuery proxy implementation
1 parent 433509f commit 2dba354

File tree

2 files changed

+594
-0
lines changed

2 files changed

+594
-0
lines changed
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import {AtUri} from '@atproto/api'
2+
import type {AppBskyFeedPost, AppBskyActorProfile} from '@atproto/api'
3+
4+
const SLINGSHOT_URL = 'https://slingshot.microcosm.blue'
5+
const CONSTELLATION_URL = 'https://constellation.microcosm.blue'
6+
7+
export interface MicrocosmRecord {
8+
uri: string
9+
cid: string
10+
value: any
11+
}
12+
13+
export interface ConstellationCounts {
14+
likeCount: number
15+
repostCount: number
16+
replyCount: number
17+
// might need a saves/bookmark counter
18+
// bookmarkCount: number
19+
}
20+
21+
/**
22+
* Generic helper to fetch from Slingshot proxy
23+
*/
24+
async function fetchFromSlingshot<T>(
25+
endpoint: string,
26+
params: Record<string, string>
27+
): Promise<T | null> {
28+
try {
29+
const queryString = new URLSearchParams(
30+
Object.entries(params).map(([key, value]) => [key, encodeURIComponent(value)])
31+
).toString()
32+
33+
const res = await fetch(`${SLINGSHOT_URL}/xrpc/${endpoint}?${queryString}`)
34+
if (!res.ok) return null
35+
return await res.json()
36+
} catch (e) {
37+
console.error(`Slingshot fetch failed (${endpoint}):`, e)
38+
return null
39+
}
40+
}
41+
42+
/**
43+
* Fetch a record directly from PDS via Slingshot proxy
44+
*/
45+
export async function fetchRecordViaSlingshot(
46+
atUri: string
47+
): Promise<MicrocosmRecord | null> {
48+
return fetchFromSlingshot<MicrocosmRecord>(
49+
'com.bad-example.repo.getUriRecord',
50+
{ at_uri: atUri }
51+
)
52+
}
53+
54+
/**
55+
* Resolve identity (DID/handle) via Slingshot
56+
*/
57+
export async function resolveIdentityViaSlingshot(
58+
identifier: string
59+
): Promise<{did: string; handle: string; pds: string} | null> {
60+
return fetchFromSlingshot(
61+
'com.bad-example.identity.resolveMiniDoc',
62+
{ identifier }
63+
)
64+
}
65+
66+
export async function fetchConstellationCounts(
67+
atUri: string
68+
): Promise<ConstellationCounts> {
69+
try {
70+
const res = await fetch(
71+
`${CONSTELLATION_URL}/links/all?target=${encodeURIComponent(atUri)}`
72+
)
73+
if (!res.ok) throw new Error('Constellation fetch failed')
74+
75+
const data = await res.json()
76+
const links = data.links || {}
77+
78+
return {
79+
likeCount: links?.['app.bsky.feed.like']?.['.subject.uri']?.distinct_dids ?? 0,
80+
repostCount: links?.['app.bsky.feed.repost']?.['.subject.uri']?.distinct_dids ?? 0,
81+
replyCount: links?.['app.bsky.feed.post']?.['.reply.parent.uri']?.records ?? 0,
82+
}
83+
} catch (e) {
84+
console.error('Constellation fetch failed:', e)
85+
return {likeCount: 0, repostCount: 0, replyCount: 0}
86+
}
87+
}
88+
89+
/**
90+
* Detect if error is AppView-related (suspended user, not found, etc.)
91+
*/
92+
export function isAppViewError(error: any): boolean {
93+
if (!error) return false
94+
95+
// Check HTTP status codes
96+
if (error.status === 400 || error.status === 404) return true
97+
98+
// Check error messages
99+
// TODO: see if there is an easy way to source error messages from the appview
100+
const msg = error.message?.toLowerCase() || ''
101+
if (msg.includes('not found')) return true
102+
if (msg.includes('suspended')) return true
103+
if (msg.includes('could not locate')) return true
104+
105+
return false
106+
}
107+
108+
/**
109+
* Build synthetic ProfileViewDetailed from PDS data
110+
*/
111+
export async function buildSyntheticProfileView(
112+
did: string,
113+
handle: string
114+
): Promise<any> {
115+
const profileUri = `at://${did}/app.bsky.actor.profile/self`
116+
const record = await fetchRecordViaSlingshot(profileUri)
117+
118+
return {
119+
$type: 'app.bsky.actor.defs#profileViewDetailed',
120+
did,
121+
handle,
122+
displayName: record?.value?.displayName || handle,
123+
description: record?.value?.description || '',
124+
avatar: record?.value?.avatar
125+
? `https://cdn.bsky.app/img/avatar/plain/${did}/${record.value.avatar.ref.$link}@jpeg`
126+
: undefined,
127+
banner: record?.value?.banner
128+
? `https://cdn.bsky.app/img/banner/plain/${did}/${record.value.banner.ref.$link}@jpeg`
129+
: undefined,
130+
followersCount: undefined, // Not available from PDS
131+
followsCount: undefined, // Not available from PDS
132+
postsCount: undefined, // Not available from PDS
133+
indexedAt: new Date().toISOString(),
134+
viewer: {},
135+
labels: [],
136+
__fallbackMode: true, // Mark as fallback data
137+
}
138+
}
139+
140+
/**
141+
* Build synthetic PostView from PDS + Constellation data
142+
*/
143+
export async function buildSyntheticPostView(
144+
atUri: string,
145+
authorDid: string,
146+
authorHandle: string
147+
): Promise<any> {
148+
const record = await fetchRecordViaSlingshot(atUri)
149+
if (!record) return null
150+
151+
const counts = await fetchConstellationCounts(atUri)
152+
const profileView = await buildSyntheticProfileView(authorDid, authorHandle)
153+
154+
return {
155+
$type: 'app.bsky.feed.defs#postView',
156+
uri: atUri,
157+
cid: record.cid,
158+
author: profileView,
159+
record: record.value,
160+
indexedAt: new Date().toISOString(),
161+
likeCount: counts.likeCount,
162+
repostCount: counts.repostCount,
163+
replyCount: counts.replyCount,
164+
viewer: {},
165+
labels: [],
166+
__fallbackMode: true, // Mark as fallback data
167+
}
168+
}
169+
170+
/**
171+
* Build synthetic feed page from PDS data
172+
* This is used for infinite queries that need paginated results
173+
*/
174+
export async function buildSyntheticFeedPage(
175+
did: string,
176+
pdsUrl: string,
177+
cursor?: string
178+
): Promise<any> {
179+
try {
180+
const limit = 25
181+
const cursorParam = cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''
182+
183+
// Fetch posts directly from PDS using com.atproto.repo.listRecords
184+
const url = `${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=app.bsky.feed.post&limit=${limit}${cursorParam}`
185+
const res = await fetch(url)
186+
187+
if (!res.ok) {
188+
console.error('[Fallback] Failed to fetch author feed from PDS:', res.statusText)
189+
return null
190+
}
191+
192+
const data = await res.json()
193+
194+
// Build FeedViewPost array from records
195+
const feed = await Promise.all(
196+
data.records.map(async (record: any) => {
197+
const postView = await buildSyntheticPostView(
198+
record.uri,
199+
did,
200+
'' // Handle will be resolved in buildSyntheticPostView
201+
)
202+
203+
if (!postView) return null
204+
205+
// Wrap in FeedViewPost format
206+
return {
207+
$type: 'app.bsky.feed.defs#feedViewPost',
208+
post: postView,
209+
feedContext: undefined,
210+
}
211+
})
212+
)
213+
214+
// Filter out null results
215+
const validFeed = feed.filter(item => item !== null)
216+
217+
return {
218+
feed: validFeed,
219+
cursor: data.cursor,
220+
__fallbackMode: true, // Mark as fallback data
221+
}
222+
} catch (e) {
223+
console.error('[Fallback] Failed to build synthetic feed page:', e)
224+
return null
225+
}
226+
}

0 commit comments

Comments
 (0)