phase 4: delete the session bridge (SessionAgent, agent.ts, agentToLexClient)

This commit is contained in:
Samuel Newman
2026-07-17 13:12:46 +03:00
parent bab2b621a9
commit ca5b8b6346
14 changed files with 500 additions and 956 deletions
+53 -70
View File
@@ -30,11 +30,9 @@ import {
makeSessionHooks,
type PublicSessionBundle,
sessionAccountToSessionData,
type SessionAgent,
type SessionBundle,
sessionDataToSessionAccount,
} from './session-core'
export {type SessionAgent} from './session-core'
export {isSignupQueued} from './util'
import {addSessionDebugLog} from './logging'
export type {SessionAccount} from '#/state/session/types'
@@ -57,15 +55,10 @@ const StateContext = createContext<SessionStateContext>({
})
StateContext.displayName = 'SessionStateContext'
const AgentContext = createContext<SessionAgent | null>(null)
AgentContext.displayName = 'SessionAgentContext'
/**
* Holds the full {@link SessionBundle} (or the logged-out
* {@link PublicSessionBundle}) for the active account. The three-client hooks
* (`useLexClient`/`useAppviewClient`/`usePdsClient`) read from here, while
* `useAgent()` continues to read the bridge agent from {@link AgentContext}
* (which is just `bundle.agent`).
* (`useLexClient`/`useAppviewClient`/`usePdsClient`) read from here.
*/
const BundleContext = createContext<SessionBundle | PublicSessionBundle | null>(
null,
@@ -133,16 +126,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const state = useSyncExternalStore(store.subscribe, store.getState)
const onboardingDispatch = useOnboardingDispatch()
const onAgentSessionChange = useCallback(
const onSessionChange = useCallback(
(
agent: SessionAgent,
bundle: SessionBundle,
accountDid: string,
sessionEvent: AtpSessionEvent,
) => {
// Snapshot the (mutable) live session data right away.
const refreshedAccount = agent.session
? sessionDataToSessionAccount(agent.session, agent.session.service)
: undefined
const refreshedAccount =
bundle.session && !bundle.session.destroyed
? sessionDataToSessionAccount(
bundle.session.session,
bundle.session.session.service,
)
: undefined
if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {
emitSessionDropped()
}
@@ -150,20 +147,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
* The reducer stores the whole bundle as `currentAgentState.agent` and
* compares `action.agent` by identity to decide whether an expiry/error
* belongs to the active account (background accounts must not be able to
* log the current user out). The hook hands us the SessionAgent that
* fired; map it back to the current bundle when it is the active one, and
* otherwise pass the SessionAgent itself as a distinct, non-matching token
* so the reducer's guard ignores clears for background accounts - matching
* the pre-migration semantics exactly.
* log the current user out). The hook now hands us the bundle that fired,
* so it IS the identity token: a same-bundle event acts on the active
* account, a stale (background) bundle does not match and its clears are
* ignored - matching the pre-migration semantics exactly.
*/
const stored = store.getState().currentAgentState
.agent as unknown as SessionBundle
const eventAgent = (stored.agent === agent
? stored
: agent) as unknown as SessionBundle
store.dispatch({
type: 'received-agent-event',
agent: eventAgent,
agent: bundle,
refreshedAccount,
accountDid,
sessionEvent,
@@ -179,7 +170,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
ax.metric('account:create:begin', {})
const {bundle, account} = await createSessionBundleAndCreateAccount(
params,
onAgentSessionChange,
onSessionChange,
)
if (signal.aborted) {
@@ -195,7 +186,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
})
addSessionDebugLog({type: 'method:end', method: 'createAccount', account})
},
[ax, store, onAgentSessionChange, cancelPendingTask],
[ax, store, onSessionChange, cancelPendingTask],
)
const login = useCallback<SessionApiContext['login']>(
@@ -204,7 +195,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const signal = cancelPendingTask()
const {bundle, account} = await createSessionBundleAndLogin(
params,
onAgentSessionChange,
onSessionChange,
)
if (signal.aborted) {
@@ -222,7 +213,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
)
addSessionDebugLog({type: 'method:end', method: 'login', account})
},
[ax, store, onAgentSessionChange, cancelPendingTask],
[ax, store, onSessionChange, cancelPendingTask],
)
const logoutCurrentAccount = useCallback<
@@ -301,7 +292,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const signal = cancelPendingTask()
const {bundle, account} = await createSessionBundleAndResume(
storedAccount,
onAgentSessionChange,
onSessionChange,
)
if (signal.aborted) {
@@ -318,7 +309,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
onboardingDispatch({type: 'skip'})
}
},
[store, onAgentSessionChange, cancelPendingTask, onboardingDispatch],
[store, onSessionChange, cancelPendingTask, onboardingDispatch],
)
const partialRefreshSession = useCallback<
@@ -330,14 +321,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
* Fetch through the account (PDS) client and dispatch the patch. We do NOT
* mutate the session object anymore (PasswordSession's data is immutable to
* us); the reducer patches only the `accounts` entry, and the email-state
* hook reads from the account rather than `agent.session` (Task 7).
* hook reads from the account rather than the session.
* `client.call` returns the response body directly (no `{data}` wrapper).
*/
const data = await bundle.accountClient.call(com.atproto.server.getSession)
if (signal.aborted) return
store.dispatch({
type: 'partial-refresh-session',
accountDid: bundle.agent.session!.did,
accountDid: bundle.session.did,
patch: {
emailConfirmed: data.emailConfirmed,
emailAuthFactor: data.emailAuthFactor,
@@ -408,20 +399,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} else {
/*
* Same account, new tokens synced from the leader tab. PasswordSession
* is immutable (no in-place session patch like the old
* `agent.sessionManager.session = ...`), so rebuild a fresh bundle
* is immutable (no in-place session patch), so rebuild a fresh bundle
* from the synced tokens WITHOUT a network call (the leader already
* refreshed) and swap it in via `replaced-current-bundle`. The
* bundle-identity effect disposes the previous session once it swaps,
* which strengthens the single-refresher guarantee (the stale-token
* session can no longer refresh).
*/
const prevBundle = state.currentAgentState
.agent as unknown as SessionBundle
const prevBundle = state.currentAgentState.agent as unknown as
| SessionBundle
| PublicSessionBundle
let newBundle!: SessionBundle
const hooks = makeSessionHooks(
onAgentSessionChange,
() => newBundle.agent,
onSessionChange,
() => newBundle,
() => syncedAccount.did,
)
const newSession = new PasswordSession(
@@ -432,9 +423,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
hooks.arm()
addSessionDebugLog({
type: 'agent:patch',
agent: newBundle.agent,
prevSession: prevBundle.agent.session,
nextSession: newBundle.agent.session,
agent: newBundle,
prevSession:
prevBundle.session && !prevBundle.session.destroyed
? prevBundle.session.session
: undefined,
nextSession: newBundle.session.session,
})
store.dispatch({
type: 'replaced-current-bundle',
@@ -444,7 +438,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
}
})
}, [store, state, resumeSession, onAgentSessionChange])
}, [store, state, resumeSession, onSessionChange])
const stateContext = useMemo(
() => ({
@@ -483,11 +477,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const bundle = state.currentAgentState.agent as unknown as
| SessionBundle
| PublicSessionBundle
const agent = bundle.agent
// @ts-expect-error window type is not declared, debug only
// eslint-disable-next-line react-hooks/immutability
if (__DEV__ && IS_WEB) window.agent = agent
if (__DEV__ && IS_WEB) window.bundle = bundle
const currentBundleRef = useRef(bundle)
useEffect(() => {
@@ -497,8 +490,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
currentBundleRef.current = bundle
addSessionDebugLog({
type: 'agent:switch',
prevAgent: prevBundle.agent,
nextAgent: bundle.agent,
prevAgent: prevBundle,
nextAgent: bundle,
})
// We never reuse bundles so let's fully neutralize the previous one.
// This ensures its session won't try to consume any refresh tokens.
@@ -507,22 +500,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}, [bundle])
return (
<AgentContext.Provider value={agent}>
<BundleContext.Provider value={bundle}>
<StateContext.Provider value={stateContext}>
<ApiContext.Provider value={api}>
<AnalyticsContext
metadata={utils.useMeta({
session: utils.accountToSessionMetadata(
stateContext.currentAccount,
),
})}>
{children}
</AnalyticsContext>
</ApiContext.Provider>
</StateContext.Provider>
</BundleContext.Provider>
</AgentContext.Provider>
<BundleContext.Provider value={bundle}>
<StateContext.Provider value={stateContext}>
<ApiContext.Provider value={api}>
<AnalyticsContext
metadata={utils.useMeta({
session: utils.accountToSessionMetadata(
stateContext.currentAccount,
),
})}>
{children}
</AnalyticsContext>
</ApiContext.Provider>
</StateContext.Provider>
</BundleContext.Provider>
)
}
@@ -564,14 +555,6 @@ export function useRequireAuth() {
)
}
export function useAgent(): SessionAgent {
const agent = useContext(AgentContext)
if (!agent) {
throw Error('useAgent() must be below <SessionProvider>.')
}
return agent
}
/**
* Authenticated lex {@link Client} for appview reads. Backed by the active
* bundle's appview client (proxied to the Bluesky appview, with labelers). Its