diff options
Diffstat (limited to 'src/state/session/index.tsx')
-rw-r--r-- | src/state/session/index.tsx | 366 |
1 files changed, 70 insertions, 296 deletions
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 0a015d56e..a8bd90ca1 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -2,9 +2,7 @@ import React from 'react' import {AtpSessionEvent, BskyAgent} from '@atproto/api' import {track} from '#/lib/analytics/analytics' -import {networkRetry} from '#/lib/async/retry' -import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' -import {logEvent, tryFetchGates} from '#/lib/statsig/statsig' +import {logEvent} from '#/lib/statsig/statsig' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' import {useCloseAllActiveElements} from '#/state/util' @@ -13,22 +11,15 @@ import {IS_DEV} from '#/env' import {emitSessionDropped} from '../events' import { agentToSessionAccount, - configureModerationForAccount, - configureModerationForGuest, createAgentAndCreateAccount, createAgentAndLogin, - isSessionDeactivated, - isSessionExpired, -} from './util' + createAgentAndResume, +} from './agent' +import {getInitialState, reducer} from './reducer' +export {isSessionDeactivated} from './util' export type {SessionAccount} from '#/state/session/types' -import { - SessionAccount, - SessionApiContext, - SessionStateContext, -} from '#/state/session/types' - -export {isSessionDeactivated} +import {SessionApiContext, SessionStateContext} from '#/state/session/types' const StateContext = React.createContext<SessionStateContext>({ accounts: [], @@ -42,190 +33,16 @@ const ApiContext = React.createContext<SessionApiContext>({ createAccount: async () => {}, login: async () => {}, logout: async () => {}, - initSession: async () => {}, + resumeSession: async () => {}, removeAccount: () => {}, updateCurrentAccount: () => {}, }) -type AgentState = { - readonly agent: BskyAgent - readonly did: string | undefined -} - -type State = { - accounts: SessionStateContext['accounts'] - currentAgentState: AgentState - needsPersist: boolean -} - -type Action = - | { - type: 'received-agent-event' - agent: BskyAgent - accountDid: string - refreshedAccount: SessionAccount | undefined - sessionEvent: AtpSessionEvent - } - | { - type: 'switched-to-account' - newAgent: BskyAgent - newAccount: SessionAccount - } - | { - type: 'updated-current-account' - updatedFields: Partial< - Pick< - SessionAccount, - 'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor' - > - > - } - | { - type: 'removed-account' - accountDid: string - } - | { - type: 'logged-out' - } - | { - type: 'synced-accounts' - syncedAccounts: SessionAccount[] - syncedCurrentDid: string | undefined - } - -function createPublicAgentState() { - configureModerationForGuest() // Side effect but only relevant for tests - return { - agent: new BskyAgent({service: PUBLIC_BSKY_SERVICE}), - did: undefined, - } -} - -function getInitialState(): State { - return { - accounts: persisted.get('session').accounts, - currentAgentState: createPublicAgentState(), - needsPersist: false, - } -} - -function reducer(state: State, action: Action): State { - switch (action.type) { - case 'received-agent-event': { - const {agent, accountDid, refreshedAccount, sessionEvent} = action - if (agent !== state.currentAgentState.agent) { - // Only consider events from the active agent. - return state - } - if (sessionEvent === 'network-error') { - // Don't change stored accounts but kick to the choose account screen. - return { - accounts: state.accounts, - currentAgentState: createPublicAgentState(), - needsPersist: true, - } - } - const existingAccount = state.accounts.find(a => a.did === accountDid) - if ( - !existingAccount || - JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount) - ) { - // Fast path without a state update. - return state - } - return { - accounts: state.accounts.map(a => { - if (a.did === accountDid) { - if (refreshedAccount) { - return refreshedAccount - } else { - return { - ...a, - // If we didn't receive a refreshed account, clear out the tokens. - accessJwt: undefined, - refreshJwt: undefined, - } - } - } else { - return a - } - }), - currentAgentState: refreshedAccount - ? state.currentAgentState - : createPublicAgentState(), // Log out if expired. - needsPersist: true, - } - } - case 'switched-to-account': { - const {newAccount, newAgent} = action - return { - accounts: [ - newAccount, - ...state.accounts.filter(a => a.did !== newAccount.did), - ], - currentAgentState: { - did: newAccount.did, - agent: newAgent, - }, - needsPersist: true, - } - } - case 'updated-current-account': { - const {updatedFields} = action - return { - accounts: state.accounts.map(a => { - if (a.did === state.currentAgentState.did) { - return { - ...a, - ...updatedFields, - } - } else { - return a - } - }), - currentAgentState: state.currentAgentState, - needsPersist: true, - } - } - case 'removed-account': { - const {accountDid} = action - return { - accounts: state.accounts.filter(a => a.did !== accountDid), - currentAgentState: - state.currentAgentState.did === accountDid - ? createPublicAgentState() // Log out if removing the current one. - : state.currentAgentState, - needsPersist: true, - } - } - case 'logged-out': { - return { - accounts: state.accounts.map(a => ({ - ...a, - // Clear tokens for *every* account (this is a hard logout). - refreshJwt: undefined, - accessJwt: undefined, - })), - currentAgentState: createPublicAgentState(), - needsPersist: true, - } - } - case 'synced-accounts': { - const {syncedAccounts, syncedCurrentDid} = action - return { - accounts: syncedAccounts, - currentAgentState: - syncedCurrentDid === state.currentAgentState.did - ? state.currentAgentState - : createPublicAgentState(), // Log out if different user. - needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. - } - } - } -} - export function Provider({children}: React.PropsWithChildren<{}>) { - const [state, dispatch] = React.useReducer(reducer, null, getInitialState) + const cancelPendingTask = useOneTaskAtATime() + const [state, dispatch] = React.useReducer(reducer, null, () => + getInitialState(persisted.get('session').accounts), + ) const onAgentSessionChange = React.useCallback( (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { @@ -245,34 +62,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const createAccount = React.useCallback<SessionApiContext['createAccount']>( - async ({ - service, - email, - password, - handle, - birthDate, - inviteCode, - verificationPhone, - verificationCode, - }) => { + async params => { + const signal = cancelPendingTask() track('Try Create Account') logEvent('account:create:begin', {}) - const {agent, account, fetchingGates} = await createAgentAndCreateAccount( - { - service, - email, - password, - handle, - birthDate, - inviteCode, - verificationPhone, - verificationCode, - }, + const {agent, account} = await createAgentAndCreateAccount( + params, + onAgentSessionChange, ) - agent.setPersistSessionHandler(event => { - onAgentSessionChange(agent, account.did, event) - }) - await fetchingGates + + if (signal.aborted) { + return + } dispatch({ type: 'switched-to-account', newAgent: agent, @@ -281,21 +82,20 @@ export function Provider({children}: React.PropsWithChildren<{}>) { track('Create Account') logEvent('account:create:success', {}) }, - [onAgentSessionChange], + [onAgentSessionChange, cancelPendingTask], ) const login = React.useCallback<SessionApiContext['login']>( - async ({service, identifier, password, authFactorToken}, logContext) => { - const {agent, account, fetchingGates} = await createAgentAndLogin({ - service, - identifier, - password, - authFactorToken, - }) - agent.setPersistSessionHandler(event => { - onAgentSessionChange(agent, account.did, event) - }) - await fetchingGates + async (params, logContext) => { + const signal = cancelPendingTask() + const {agent, account} = await createAgentAndLogin( + params, + onAgentSessionChange, + ) + + if (signal.aborted) { + return + } dispatch({ type: 'switched-to-account', newAgent: agent, @@ -304,88 +104,49 @@ export function Provider({children}: React.PropsWithChildren<{}>) { track('Sign In', {resumedSession: false}) logEvent('account:loggedIn', {logContext, withPassword: true}) }, - [onAgentSessionChange], + [onAgentSessionChange, cancelPendingTask], ) const logout = React.useCallback<SessionApiContext['logout']>( - async logContext => { + logContext => { + cancelPendingTask() dispatch({ type: 'logged-out', }) logEvent('account:loggedOut', {logContext}) }, - [], + [cancelPendingTask], ) - const initSession = React.useCallback<SessionApiContext['initSession']>( - async account => { - const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency') - const agent = new BskyAgent({service: account.service}) - // restore the correct PDS URL if available - if (account.pdsUrl) { - agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl) - } - agent.setPersistSessionHandler(event => { - onAgentSessionChange(agent, account.did, event) - }) - await configureModerationForAccount(agent, account) - - const prevSession = { - accessJwt: account.accessJwt ?? '', - refreshJwt: account.refreshJwt ?? '', - did: account.did, - handle: account.handle, - } - - if (isSessionExpired(account)) { - const freshAccount = await resumeSessionWithFreshAccount() - await fetchingGates - dispatch({ - type: 'switched-to-account', - newAgent: agent, - newAccount: freshAccount, - }) - } else { - agent.session = prevSession - await fetchingGates - dispatch({ - type: 'switched-to-account', - newAgent: agent, - newAccount: account, - }) - if (isSessionDeactivated(account.accessJwt) || account.deactivated) { - // don't attempt to resume - // use will be taken to the deactivated screen - return - } - // Intentionally not awaited to unblock the UI: - resumeSessionWithFreshAccount() - } + const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( + async storedAccount => { + const signal = cancelPendingTask() + const {agent, account} = await createAgentAndResume( + storedAccount, + onAgentSessionChange, + ) - async function resumeSessionWithFreshAccount(): Promise<SessionAccount> { - await networkRetry(1, () => agent.resumeSession(prevSession)) - const sessionAccount = agentToSessionAccount(agent) - /* - * If `agent.resumeSession` fails above, it'll throw. This is just to - * make TypeScript happy. - */ - if (!sessionAccount) { - throw new Error(`session: initSession failed to establish a session`) - } - return sessionAccount + if (signal.aborted) { + return } + dispatch({ + type: 'switched-to-account', + newAgent: agent, + newAccount: account, + }) }, - [onAgentSessionChange], + [onAgentSessionChange, cancelPendingTask], ) const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( account => { + cancelPendingTask() dispatch({ type: 'removed-account', accountDid: account.did, }) }, - [], + [cancelPendingTask], ) const updateCurrentAccount = React.useCallback< @@ -422,14 +183,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) if (syncedAccount && syncedAccount.refreshJwt) { if (syncedAccount.did !== state.currentAgentState.did) { - initSession(syncedAccount) + resumeSession(syncedAccount) } else { // @ts-ignore we checked for `refreshJwt` above state.currentAgentState.agent.session = syncedAccount } } }) - }, [state, initSession]) + }, [state, resumeSession]) const stateContext = React.useMemo( () => ({ @@ -447,7 +208,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { createAccount, login, logout, - initSession, + resumeSession, removeAccount, updateCurrentAccount, }), @@ -455,7 +216,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { createAccount, login, logout, - initSession, + resumeSession, removeAccount, updateCurrentAccount, ], @@ -464,8 +225,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // @ts-ignore if (IS_DEV && isWeb) window.agent = state.currentAgentState.agent + const agent = state.currentAgentState.agent as BskyAgent return ( - <AgentContext.Provider value={state.currentAgentState.agent}> + <AgentContext.Provider value={agent}> <StateContext.Provider value={stateContext}> <ApiContext.Provider value={api}>{children}</ApiContext.Provider> </StateContext.Provider> @@ -473,6 +235,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) } +function useOneTaskAtATime() { + const abortController = React.useRef<AbortController | null>(null) + const cancelPendingTask = React.useCallback(() => { + if (abortController.current) { + abortController.current.abort() + } + abortController.current = new AbortController() + return abortController.current.signal + }, []) + return cancelPendingTask +} + export function useSession() { return React.useContext(StateContext) } |