More session improvements (#2129)

* More session improvements

* Drop resume session retries from 3 to 1

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
zio/stable
Eric Bailey 2023-12-07 12:25:55 -06:00 committed by GitHub
parent a0b9fd799b
commit 261a935747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 67 additions and 53 deletions

View File

@ -39,7 +39,7 @@ SplashScreen.preventAutoHideAsync()
function InnerApp() { function InnerApp() {
const colorMode = useColorMode() const colorMode = useColorMode()
const {isInitialLoad, currentAccount} = useSession() const {currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
// init // init
@ -53,16 +53,6 @@ function InnerApp() {
resumeSession(account) resumeSession(account)
}, [resumeSession]) }, [resumeSession])
// show nothing prior to init
if (isInitialLoad) {
// TODO add a loading state
return null
}
/*
* Session and initial state should be loaded prior to rendering below.
*/
return ( return (
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:

View File

@ -30,7 +30,7 @@ import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unre
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
function InnerApp() { function InnerApp() {
const {isInitialLoad, currentAccount} = useSession() const {currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const colorMode = useColorMode() const colorMode = useColorMode()
@ -40,16 +40,6 @@ function InnerApp() {
resumeSession(account) resumeSession(account)
}, [resumeSession]) }, [resumeSession])
// show nothing prior to init
if (isInitialLoad) {
// TODO add a loading state
return null
}
/*
* Session and initial state should be loaded prior to rendering below.
*/
return ( return (
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:

View File

@ -28,7 +28,6 @@ export function getAgent() {
export type SessionAccount = persisted.PersistedAccount export type SessionAccount = persisted.PersistedAccount
export type SessionState = { export type SessionState = {
isInitialLoad: boolean
isSwitchingAccounts: boolean isSwitchingAccounts: boolean
accounts: SessionAccount[] accounts: SessionAccount[]
currentAccount: SessionAccount | undefined currentAccount: SessionAccount | undefined
@ -76,7 +75,6 @@ export type ApiContext = {
} }
const StateContext = React.createContext<StateContext>({ const StateContext = React.createContext<StateContext>({
isInitialLoad: true,
isSwitchingAccounts: false, isSwitchingAccounts: false,
accounts: [], accounts: [],
currentAccount: undefined, currentAccount: undefined,
@ -104,31 +102,43 @@ function createPersistSessionHandler(
}) => void, }) => void,
): AtpPersistSessionHandler { ): AtpPersistSessionHandler {
return function persistSession(event, session) { return function persistSession(event, session) {
const expired = !(event === 'create' || event === 'update') const expired = event === 'expired' || event === 'create-failed'
const refreshedAccount: SessionAccount = { const refreshedAccount: SessionAccount = {
service: account.service, service: account.service,
did: session?.did || account.did, did: session?.did || account.did,
handle: session?.handle || account.handle, handle: session?.handle || account.handle,
email: session?.email || account.email, email: session?.email || account.email,
emailConfirmed: session?.emailConfirmed || account.emailConfirmed, emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
refreshJwt: session?.refreshJwt, // undefined when expired or creation fails
accessJwt: session?.accessJwt, // undefined when expired or creation fails /*
* Tokens are undefined if the session expires, or if creation fails for
* any reason e.g. tokens are invalid, network error, etc.
*/
refreshJwt: session?.refreshJwt,
accessJwt: session?.accessJwt,
} }
logger.debug( logger.info(`session: persistSession`, {
`session: BskyAgent.persistSession`, event,
{
expired,
did: refreshedAccount.did, did: refreshedAccount.did,
handle: refreshedAccount.handle, handle: refreshedAccount.handle,
}, })
logger.DebugContext.session,
)
if (expired) { if (expired) {
emitSessionDropped() emitSessionDropped()
} }
/*
* If the session expired, or it was successfully created/updated, we want
* to update/persist the data.
*
* If the session creation failed, it could be a network error, or it could
* be more serious like an invalid token(s). We can't differentiate, so in
* order to allow the user to get a fresh token (if they need it), we need
* to persist this data and wipe their tokens, effectively logging them
* out.
*/
persistSessionCallback({ persistSessionCallback({
expired, expired,
refreshedAccount, refreshedAccount,
@ -140,7 +150,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const isDirty = React.useRef(false) const isDirty = React.useRef(false)
const [state, setState] = React.useState<SessionState>({ const [state, setState] = React.useState<SessionState>({
isInitialLoad: true, // try to resume the session first
isSwitchingAccounts: false, isSwitchingAccounts: false,
accounts: persisted.get('session').accounts, accounts: persisted.get('session').accounts,
currentAccount: undefined, // assume logged out to start currentAccount: undefined, // assume logged out to start
@ -338,7 +347,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} }
} }
} catch (e) { } catch (e) {
console.error('Could not decode jwt.') logger.error(`session: could not decode jwt`)
} }
const prevSession = { const prevSession = {
@ -355,23 +364,53 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
upsertAccount(account) upsertAccount(account)
// Intentionally not awaited to unblock the UI: // Intentionally not awaited to unblock the UI:
resumeSessionWithFreshAccount().then(freshAccount => { resumeSessionWithFreshAccount()
.then(freshAccount => {
if (JSON.stringify(account) !== JSON.stringify(freshAccount)) { if (JSON.stringify(account) !== JSON.stringify(freshAccount)) {
upsertAccount(freshAccount) upsertAccount(freshAccount)
} }
}) })
.catch(e => {
/*
* Note: `agent.persistSession` is also called when this fails, and
* we handle that failure via `createPersistSessionHandler`
*/
logger.info(`session: resumeSessionWithFreshAccount failed`, {
error: e,
})
__globalAgent = PUBLIC_BSKY_AGENT
})
} else { } else {
try {
const freshAccount = await resumeSessionWithFreshAccount() const freshAccount = await resumeSessionWithFreshAccount()
__globalAgent = agent __globalAgent = agent
queryClient.clear() queryClient.clear()
upsertAccount(freshAccount) upsertAccount(freshAccount)
} catch (e) {
/*
* Note: `agent.persistSession` is also called when this fails, and
* we handle that failure via `createPersistSessionHandler`
*/
logger.info(`session: resumeSessionWithFreshAccount failed`, {
error: e,
})
__globalAgent = PUBLIC_BSKY_AGENT
}
} }
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> { async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
await networkRetry(3, () => agent.resumeSession(prevSession)) await networkRetry(1, () => agent.resumeSession(prevSession))
/*
* If `agent.resumeSession` fails above, it'll throw. This is just to
* make TypeScript happy.
*/
if (!agent.session) { if (!agent.session) {
throw new Error(`session: initSession failed to establish a session`) throw new Error(`session: initSession failed to establish a session`)
} }
// ensure changes in handle/email etc are captured on reload // ensure changes in handle/email etc are captured on reload
return { return {
service: agent.service.toString(), service: agent.service.toString(),
@ -395,11 +434,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} }
} catch (e) { } catch (e) {
logger.error(`session: resumeSession failed`, {error: e}) logger.error(`session: resumeSession failed`, {error: e})
} finally {
setState(s => ({
...s,
isInitialLoad: false,
}))
} }
}, },
[initSession], [initSession],