flip types/bsky sources to #/lexicons, add chat client and strict hook contracts

Phase 3 task 2: types/bsky sibling modules (post/profile/starterPack) now
source from generated #/lexicons with interim dual-world widening aliases
(TODO(phase4) markers). SessionBundle gains a chatClient proxied to
did:web:api.bsky.chat#bsky_chat via useChatClient(). usePdsClient() and
useChatClient() no longer fall back to the public appview when logged out -
they return a client that throws NotAuthenticatedError before any network
I/O; useMaybePdsClient()/useMaybeChatClient() cover logged-out-aware
callers. RichText pilot: useRichText.ts on @bsky.app/sdk/richtext with
detectFacets(pdsClient); RichText.tsx display sink accepts both worlds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-16 17:47:04 +03:00
parent e6f73fc110
commit b1a4e1cd16
9 changed files with 361 additions and 54 deletions
+53 -3
View File
@@ -19,7 +19,7 @@ import {AnalyticsContext, useAnalyticsBase, utils} from '#/analytics'
import {IS_WEB} from '#/env'
import {com} from '#/lexicons'
import {emitSessionDropped} from '../events'
import {getPublicLexClient} from './clients'
import {getPublicLexClient, getUnauthenticatedClient} from './clients'
import {type Action, getInitialState, reducer, type State} from './reducer'
import {
buildBundle,
@@ -571,9 +571,59 @@ export function useAppviewClient(): Client {
/**
* The account (PDS) lex {@link Client} for the active account. Writes and record
* mutations go here - requests hit the user's PDS directly (no appview proxy).
* Falls back to the public client when there is no bundle.
*
* Logged-out contract: returns a stable throwing client
* ({@link getUnauthenticatedClient}) that throws `NotAuthenticatedError` on any
* request, BEFORE any network I/O. This is the write path - it must NOT fall
* back to the public appview, so an unauthenticated write fails immediately and
* legibly rather than silently hitting `public.api.bsky.app`. Components may
* safely hold this client while logged out; only calling it throws. A component
* that genuinely branches on auth state should use {@link useMaybePdsClient}.
*/
export function usePdsClient(): Client {
const bundle = useContext(BundleContext)
return bundle?.accountClient ?? getPublicLexClient()
return bundle?.accountClient ?? getUnauthenticatedClient()
}
/**
* The chat lex {@link Client} for the active account. `chat.bsky.*` calls go
* here - proxied to `did:web:api.bsky.chat#bsky_chat`.
*
* Logged-out contract: returns a stable throwing client
* ({@link getUnauthenticatedClient}) that throws `NotAuthenticatedError` on any
* request, BEFORE any network I/O. Chat is meaningless logged out, so this must
* NOT fall back to the public appview. A component that genuinely branches on
* auth state should use {@link useMaybeChatClient}.
*/
export function useChatClient(): Client {
const bundle = useContext(BundleContext)
return bundle?.chatClient ?? getUnauthenticatedClient()
}
/**
* The account (PDS) lex {@link Client} for the active account, or `null` when
* there is no active session (logged out, or used outside the provider).
*
* The escape hatch for the rare component that genuinely renders a logged-out
* branch and must decide whether a write path is available. Prefer
* {@link usePdsClient} for the common case (a write only reachable while
* authenticated); do NOT reach for this hook merely to dodge the throwing
* client's `NotAuthenticatedError`.
*/
export function useMaybePdsClient(): Client | null {
const bundle = useContext(BundleContext)
return bundle?.session ? bundle.accountClient : null
}
/**
* The chat lex {@link Client} for the active account, or `null` when there is
* no active session (logged out, or used outside the provider).
*
* The escape hatch for the rare component that genuinely renders a logged-out
* branch. Prefer {@link useChatClient} for the common case; do NOT reach for
* this hook merely to dodge the throwing client's `NotAuthenticatedError`.
*/
export function useMaybeChatClient(): Client | null {
const bundle = useContext(BundleContext)
return bundle?.session ? bundle.chatClient : null
}