[SDK] Delete the bridge agent and rework the session bundle onto lex clients (#11385)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-13 22:26:22 +03:00
committed by GitHub
parent 59d2bf09d7
commit 54da80dfb0
38 changed files with 812 additions and 1851 deletions
+17 -13
View File
@@ -1,10 +1,10 @@
import {useCallback} from 'react'
import {type HandleString} from '@atproto/syntax'
import {type DidString, type HandleString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent, usePdsClient} from '#/state/session'
import {com} from '#/lexicons'
import {useAppviewClient, usePdsClient} from '#/state/session'
import {app, com} from '#/lexicons'
const handleQueryKeyRoot = 'handle'
const fetchHandleQueryKey = (handleOrDid: string) => [
@@ -16,21 +16,24 @@ const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
export function useFetchHandle() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async (handleOrDid: string) => {
if (handleOrDid.startsWith('did:')) {
const res = await queryClient.fetchQuery({
const data = await queryClient.fetchQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: fetchHandleQueryKey(handleOrDid),
queryFn: () => agent.getProfile({actor: handleOrDid}),
queryFn: () =>
client.call(app.bsky.actor.getProfile, {
actor: handleOrDid as DidString,
}),
})
return res.data.handle
return data.handle
}
return handleOrDid
},
[queryClient, agent],
[queryClient, client],
)
}
@@ -42,7 +45,6 @@ export function useUpdateHandleMutation(opts?: {
return useMutation({
mutationFn: async ({handle}: {handle: string}) => {
// `agent.updateHandle` was a pure alias for this method
await client.call(com.atproto.identity.updateHandle, {
// callers validate the handle before submitting
handle: handle as HandleString,
@@ -59,7 +61,7 @@ export function useUpdateHandleMutation(opts?: {
export function useFetchDid() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async (handleOrDid: string) => {
@@ -69,13 +71,15 @@ export function useFetchDid() {
queryFn: async () => {
let identifier = handleOrDid
if (!identifier.startsWith('did:')) {
const res = await agent.resolveHandle({handle: identifier})
identifier = res.data.did
const data = await client.call(com.atproto.identity.resolveHandle, {
handle: identifier as HandleString,
})
identifier = data.did
}
return identifier
},
})
},
[queryClient, agent],
[queryClient, client],
)
}
+3 -3
View File
@@ -36,7 +36,7 @@ import {moderatePost} from '#/lib/moderation/subjects'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {useAgent, useAppviewClient} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {KnownError} from '#/view/com/posts/PostFeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners'
@@ -151,7 +151,7 @@ export function usePostFeedQuery(
f => f.pinned && f.value === 'following',
) ?? -1
const enableFollowingToDiscoverFallback = followingPinnedIndex === 0
const agent = useAgent()
const {hasSession} = useSession()
const client = useAppviewClient()
const lastRun = useRef<{
data: InfiniteData<FeedPageUnselected>
@@ -214,7 +214,7 @@ export function usePostFeedQuery(
* moderations happen later, which results in some posts being shown and
* some not.
*/
if (!agent.session) {
if (!hasSession) {
assertSomePostsPassModeration(
res.feed,
preferences?.moderationPrefs ||
+46 -50
View File
@@ -1,6 +1,7 @@
import {useCallback} from 'react'
import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {type Client} from '@atproto/lex'
import {type AtUriString, type HandleString} from '@atproto/syntax'
import {deleteLike, deletePost, deleteRepost, like, repost} from '@bsky.app/sdk'
import {
type QueryClient,
@@ -12,16 +13,11 @@ import {
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {type Shadow} from '#/state/cache/types'
import {
useAgent,
useAppviewClient,
usePdsClient,
useSession,
} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {useAnalytics} from '#/analytics'
import {type Metrics, toClout} from '#/analytics/metrics'
import {app} from '#/lexicons'
import {app, com} from '#/lexicons'
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
import {findProfileQueryData} from './profile'
@@ -29,25 +25,15 @@ const RQKEY_ROOT = 'post'
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
export function usePostQuery(uri: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery<AppBskyFeedDefs.PostView>({
queryKey: RQKEY(uri || ''),
queryFn: async () => {
if (!uri) throw new Error('[unreachable] No URI provided')
const urip = new AtUri(uri)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
})
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data.did
}
const res = await agent.getPosts({uris: [urip.toString()]})
if (res.success && res.data.posts[0]) {
return res.data.posts[0]
const post = await fetchPost(client, uri)
if (post) {
return post
}
throw new Error('No data')
@@ -56,6 +42,33 @@ export function usePostQuery(uri: string | undefined) {
})
}
/**
* Read one post by AT-URI, resolving a handle authority first when the URI
* carries one.
*
* The appview still answers with `@atproto/api`-shaped views for the callers of
* these hooks, so the generated view is asserted across at this single
* boundary rather than at every consumer.
*/
async function fetchPost(
client: Client,
uri: string,
): Promise<AppBskyFeedDefs.PostView | undefined> {
const urip = new AtUri(uri)
if (!urip.host.startsWith('did:')) {
const data = await client.call(com.atproto.identity.resolveHandle, {
handle: urip.host as HandleString,
})
urip.host = data.did
}
const data = await client.call(app.bsky.feed.getPosts, {
uris: [urip.toString()],
})
return data.posts[0]
}
export function precachePost(
queryClient: QueryClient,
uri: string,
@@ -66,59 +79,42 @@ export function precachePost(
export function useGetPost() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async ({uri}: {uri: string}) => {
return queryClient.fetchQuery({
queryKey: RQKEY(uri || ''),
async queryFn() {
const urip = new AtUri(uri)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
})
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data.did
}
const res = await agent.getPosts({
uris: [urip.toString()],
})
if (res.success && res.data.posts[0]) {
return res.data.posts[0]
const post = await fetchPost(client, uri)
if (post) {
return post
}
throw new Error('useGetPost: post not found')
},
})
},
[queryClient, agent],
[queryClient, client],
)
}
export function useGetPosts() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async ({uris}: {uris: string[]}) => {
return queryClient.fetchQuery({
queryKey: RQKEY(uris.join(',') || ''),
async queryFn() {
const res = await agent.getPosts({
uris,
const data = await client.call(app.bsky.feed.getPosts, {
uris: uris as AtUriString[],
})
if (res.success) {
return res.data.posts
} else {
throw new Error('useGetPosts failed')
}
// See the note on `fetchPost` about the view shapes.
return data.posts as AppBskyFeedDefs.PostView[]
},
})
},
[queryClient, agent],
[queryClient, client],
)
}
+7 -7
View File
@@ -39,7 +39,7 @@ import {
type UsePreferencesQueryResponse,
} from '#/state/queries/preferences/types'
import {createQueryKey} from '#/state/queries/util'
import {useAgent, usePdsClient} from '#/state/session'
import {useAppviewClient, usePdsClient} from '#/state/session'
import {applyLabelersToClient, saveLabelers} from '#/state/session/moderation'
import {useAgeAssurance} from '#/ageAssurance'
import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util'
@@ -58,7 +58,7 @@ export const preferencesQueryKey = createQueryKey(
export function usePreferencesQuery() {
const client = usePdsClient()
const agent = useAgent()
const appviewClient = useAppviewClient()
const aa = useAgeAssurance()
const query = useQuery({
@@ -86,12 +86,12 @@ export function usePreferencesQuery() {
* from a labeler would not affect server-attached labels until the
* session bundle was rebuilt.
*
* `applyLabelersToClient` writes to the agent, which is what stamps the
* header on the requests the wrapping appview client issues, and it
* drops the Bluesky moderation DID so the globally redacted authority is
* not also listed unredacted.
* The subscriptions go on the appview client, which is what stamps
* `atproto-accept-labelers` on its own requests. The Bluesky moderation
* DID is dropped so the globally redacted authority is not also listed
* unredacted.
*/
applyLabelersToClient(agent, labelerDids)
applyLabelersToClient(appviewClient, labelerDids)
/*
* `BskyPreferences` is now the sdk's own type, so the assembled
+2 -2
View File
@@ -1,5 +1,5 @@
import {useMemo} from 'react'
import {AtpAgent} from '@atproto/api'
import {Client} from '@atproto/lex'
import {interpretLabelValueDefinitions} from '@bsky.app/sdk/moderation'
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
@@ -14,7 +14,7 @@ export function useMyLabelersQuery({
const prefs = usePreferencesQuery()
let dids = Array.from(
new Set(
AtpAgent.appLabelers.concat(
(Client.appLabelers as readonly string[]).concat(
prefs.data?.moderationPrefs.labelers.map(l => l.did) || [],
),
),
+21 -17
View File
@@ -1,9 +1,13 @@
import {type AtpAgent} from '@atproto/api'
import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query'
import {type ResolvedLink, resolveGif, resolveLink} from '#/lib/api/resolve'
import {
type LinkResolvers,
type ResolvedLink,
resolveGif,
resolveLink,
} from '#/lib/api/resolve'
import {STALE} from '#/state/queries/index'
import {useAgent} from '#/state/session'
import {useAppviewClient, useChatClient} from '#/state/session'
import {type Gif} from '#/features/gifPicker/types'
export const RQKEY_LINK_ROOT = 'resolve-link'
@@ -12,24 +16,25 @@ export const RQKEY_LINK = (url: string) => [RQKEY_LINK_ROOT, url]
export const RQKEY_GIF_ROOT = 'resolve-gif'
export const RQKEY_GIF = (url: string) => [RQKEY_GIF_ROOT, url]
export function resolveLinkQueryOptions(agent: AtpAgent, url: string) {
export function resolveLinkQueryOptions(clients: LinkResolvers, url: string) {
return queryOptions({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY_LINK(url),
queryFn: () => resolveLink(agent, url),
queryFn: () => resolveLink(clients, url),
})
}
export function useResolveLinkQuery(url: string) {
const agent = useAgent()
return useQuery(resolveLinkQueryOptions(agent, url))
const appviewClient = useAppviewClient()
const chatClient = useChatClient()
return useQuery(resolveLinkQueryOptions({appviewClient, chatClient}, url))
}
export function fetchResolveLinkQuery(
queryClient: QueryClient,
agent: AtpAgent,
clients: LinkResolvers,
url: string,
) {
return queryClient.fetchQuery(resolveLinkQueryOptions(agent, url))
return queryClient.fetchQuery(resolveLinkQueryOptions(clients, url))
}
export function precacheResolveLinkQuery(
queryClient: QueryClient,
@@ -39,26 +44,25 @@ export function precacheResolveLinkQuery(
queryClient.setQueryData(RQKEY_LINK(url), resolvedLink)
}
/*
* GIF resolution is pure metadata work on a URL the picker already returned -
* it makes no atproto request - so it takes no client.
*/
export function useResolveGifQuery(gif: Gif) {
const agent = useAgent()
return useQuery({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY_GIF(gif.url),
queryFn: async () => {
return await resolveGif(agent, gif)
return await resolveGif(gif)
},
})
}
export function fetchResolveGifQuery(
queryClient: QueryClient,
agent: AtpAgent,
gif: Gif,
) {
export function fetchResolveGifQuery(queryClient: QueryClient, gif: Gif) {
return queryClient.fetchQuery({
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY_GIF(gif.url),
queryFn: async () => {
return await resolveGif(agent, gif)
return await resolveGif(gif)
},
})
}